Reputation: 1700
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
Reputation: 54026
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.
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";
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.
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.
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
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