BrianLegg
BrianLegg

Reputation: 1700

How can I create an irregularly shaped HTML Canvas using FabricJS?

I've been attempting to create a triangle shaped canvas for a day or so now and I'm having no luck. The canvas is always square/rectangle. I'm using FabricJS but I can also manipulate the canvas directly if that's an option.

I've attempted using .clipTo(ctx) to clip the canvas as described here: Canvas in different shapes with fabricjs plugin

I've also attempted manipulating the canvas directly as I saw here: https://www.html5canvastutorials.com/tutorials/html5-canvas-custom-shapes/

What I'm trying to accomplish is for a user to drag-drop images onto a triangle shaped canvas so there's no "bleed" of the image outside the triangle shape. I accomplished this easily with a rectangle but I can't figure out how to change the canvas shape. OR if anyone has a "trick" solution that would look like the canvas was a triangle but under the hood remain a square, that would work as well.

Upvotes: 0

Views: 1049

Answers (1)

Blindman67
Blindman67

Reputation: 54026

Using pure API's

I don't use fabric (especially if its just for simple image manipulation) so you will have to locate the appropriate fabric functions to match this answer.

The canvas is always 4 sided. 2D and 3D transforms can change the shape but that also changes the shape of the contained pixels.

You have 2 simple options. There are other ways to do this but they are complex and have compatibility issues.

Visual only

Masking

To get the appearance of irregular shaped canvas you can use a mask (second canvas has mask). Draw the content to the main canvas and then mask that canvas with the mask.

Use the property CanvasRenderingContext2D.globalCompositeOperation to define how the mask is applied.

eg

function createTriangleMask(w, h) {
    const mask = document.createElement("canvas");
    mask.width = w;
    mask.height = h;
    mask.ctx = mask.getContext("2d");
    mask.ctx.beginPath();
    mask.ctx.lineTo(w / 2, 0);
    mask.ctx.lineTo(w    , h);
    mask.ctx.lineTo(0    , h);
    mask.ctx.fill();
    return mask;
}
const mask = createTriangleMask(ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(myImg, 0, 0, ctx.canvas.width, ctx.canvas.height); 
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(mask, 0, 0, ctx.canvas.width, ctx.canvas.height);  
ctx.globalCompositeOperation = "source-over";

Using 2D clip

Or you can use the 2D API CanvasRenderingContext2D.clip to create a clip region and draw the content while the clip is active. Don't forget to pop the 2D state when done with the clip,

function triangleClip(ctx, w, h) {
    ctx.save();
    ctx.beginPath();
    mask.ctx.lineTo(w / 2, 0);
    mask.ctx.lineTo(w    , h);
    mask.ctx.lineTo(0    , h);
    ctx.clip();
}

triangleClip(ctx, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(myImg, 0, 0, ctx.canvas.width, ctx.canvas.height); 
ctx.restore(); // Turn off clip. Must do before calling triangle clip again.

Still rectangular!

This has not changed the canvas shape. It is still a rectangle, just that some pixels are transparent. The DOM still sees a rectangle and user interactions with the canvas will still use the whole rectangular canvas.

CSS clip-path

You can use the style property clip-path to define the shape of a element. This will clip the elements visual content and the elements interactive area. Effectively turning any applicable element to an irregular shaped element.

Using JS Declarative

canvas.style.clipPath = "polygon(50% 0, 100% 100%, 0% 100%)"

Using JS

function clipElement(el, shape) {
    var rule = "polygon(", i = 0, comma = "";
    while (i < shape.length) { 
        rule += comma + shape[i++] + "% " + shape[i++] + "%";
        comma = ",";
    }
    el.style.clipPath = rule + ")";
}

clipElement(canvas, [50, 0, 100, 100, 0, 100]);

Using CSS rule

canvas {
    clip-path: polygon(50% 0, 100% 100%, 0% 100%);
}

With the clipped path in place the canvas will obey its shape via UI

canvas.style.cursor = "pointer";  // Pointer change only inside clipped area
canvas.title = "foo"; // appears only when over clipped area
canvas.addEventListener("mouseover", () => console.log("foo")); // fires when crossing
                                                                // clip boundary

Demo

Creates an animated clip via JS on the canvas element with content rendered once.

There are limitations

  • Note that the CSS defined background color (yellow) and shadow are also clipped. Many other visual properties will also be clipped.

  • Note that JS animation does not update UI events if there are no intervening user iteration.

  • The animation can also be achieved via CSS.

  • Compatibility with fabric is unknown to me, check their documentation.

var clearConsole = 0;
const s = 2 ** 0.5 * 0.25, clipPath = [0.5, 0, 0.5 + s, 0.5 + s,  0.5 - s, 0.5 + s], img = new Image;
img.src = "https://i.sstatic.net/C7qq2.png?s=328&g=1";
img.addEventListener("load",() => canvas.getContext("2d").drawImage(img, 0, 0, 300, 300), {once: true});

requestAnimationFrame(animateLoop);
function clipRotate(el, ang, scale, path) {
    const dx = Math.cos(ang) * scale;
    const dy = Math.sin(ang) * scale;
    var clip = "polygon(", i = 0, comma = "";
    while (i < path.length) {
        const x = path[i++] - 0.5;
        const y = path[i++] - 0.5;
        clip += comma;
        clip += ((x * dx - y * dy + 0.5) * 100) + "% ";
        clip += ((x * dy + y * dx + 0.5) * 100) + "%";
        comma = ",";
    }
    el.style.clipPath = clip + ")";    
}

function animateLoop(time) {
    clipRotate(canvas, time / 1000 * Math.PI, 0.9, clipPath);
    requestAnimationFrame(animateLoop);
    if (clearConsole) {
       clearConsole --;
       !clearConsole && console.clear();
    }
}

canvas.addEventListener("pointerenter", () => (clearConsole = 30, console.log("Pointer over")));
body {
  background-color: #49C;
}
canvas {
    cursor: pointer;
    background-color: yellow;
    box-shadow: 12px 12px 4px rgba(0,0,0,0.8);
}
<canvas id="canvas" width="300" height="300" title="You are over the clipped canvas"></canvas>

Upvotes: 1

Related Questions