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

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.

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.stack.imgur.com/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>

Related

HTML5 canvas rotation not centered at origin [duplicate]

Hi I want to rotate this shape around its center when I move my mouse, but currently it's rotating around (0, 0). How to change my code?
Source code (also see jsfiddle):
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
class Circle {
constructor(options) {
this.cx = options.x;
this.cy = options.y;
this.radius = options.radius;
this.color = options.color;
this.angle = 0;
this.toAngle = this.angle;
this.binding();
}
binding() {
const self = this;
window.addEventListener('mousemove', (e) => {
self.update(e.clientX, e.clientY);
});
}
update(nx, ny) {
this.toAngle = Math.atan2(ny - this.cy, nx - this.cx);
}
render() {
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.save();
ctx.beginPath();
ctx.lineWidth = 1;
if (this.toAngle !== this.angle) {
ctx.rotate(this.toAngle - this.angle);
}
ctx.strokeStyle = this.color;
ctx.arc(this.cx, this.cy, this.radius, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.fillStyle = 'black';
ctx.fillRect(this.cx - this.radius / 4, this.cy - this.radius / 4, 20, 20);
ctx.closePath();
ctx.restore();
}
}
var rotatingCircle = new Circle({
x: 150,
y: 100,
radius: 40,
color: 'black'
});
function animate() {
rotatingCircle.render();
requestAnimationFrame(animate);
}
animate();
All good answers, well frustratingly no... they fail to mention that the solution only works if the current transform is at it default. They fail to mention how to get back to the default state and save and restore states.
To get the default transformation state
ctx.setTransform(1,0,0,1,0,0);
To save and restore all states
ctx.save();
ctx.transform(10,0,0,2,200,100); // set some transform state
ctx.globalAlpha = 0.4;
ctx.restore(); // each save must be followed by a restore at some point
and they can be nested
ctx.save(); // save default state
ctx.globalAlpha = 0.4;
ctx.save(); // save state with alpha = 0.4
ctx.transform(10,0,0,2,200,100); // set some transform state
ctx.restore(); // restore to alpha at 0.4
ctx.restore(); // restore to default.
setTransform completely replaces the current transformation. while transform, scale, rotate, translate, multiply the existing transform with the appropriate transform. This is handy if you have an object attached to another, and want the transformation of the first to apply to the second, and additional transforms to the second but not to the first.
ctx.rotate(Math.PI /2); // Rotates everything 90 clockwise
ctx.rotate(Math.PI /2); // Rotates everything another 90 clockwise so that
// everything is 180 from default
ctx.translate(1,1); // move diagonally down by 1. Origin is now at 1,1
ctx.translate(1,1); // move diagonally down by 1. Origin is now at 2,2
ctx.translate(1,1); // move diagonally down by 1. Origin is now at 3,3
ctx.translate(1,1); // move diagonally down by 1. Origin is now at 4,4
ctx.scale(2,2); // scale by 2 everything twice as big
ctx.scale(2,2); // scale by 2 everything four times as big
And an alternative that does not require the default transform state of ctx
// scaleX, scaleY are scales along axis x,y
// posX, posY is position of center point
// rotate is in radians clockwise with 0 representing the x axis across the screen
// image is an image to draw.
ctx.setTransform(scaleX,0,0,scaleY, posX, posY);
ctx.rotate(rotate);
ctx.drawImage(image,-image.width / 2, -image.height / 2);
Or if not a image but a object
ctx.setTransform(scaleX,0,0,scaleY, posX, posY);
ctx.rotate(rotate);
ctx.translate(-object.width / 2, -object.height / 2);
You need to:
first translate to the point of rotation (pivot)
then rotate
then either:
A: draw in at (0,0) using (-width/2, -height/2) as relative coordinate (for centered drawings)
B: translate back and use the object's absolute position and subtract relative coordinates for centered drawing
Modified code:
ctx.beginPath();
ctx.lineWidth = 1;
ctx.translate(this.cx, this.cy); // translate to pivot
if (this.toAngle !== this.angle) {
ctx.rotate(this.toAngle - this.angle);
}
ctx.strokeStyle = this.color;
ctx.arc(0, 0, this.radius, 0, Math.PI * 2); // render at pivot
ctx.closePath(); // must come before stroke() btw.
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = 'black';
ctx.fillRect(-this.radius / 4, -this.radius / 4, 20, 20); // render at pivot
Modified Fiddle
Bonus tip: you're currently using save()/restore() calls to maintain the transformation matrix. Another way could be to set the matrix using absolute values initially replacing the save()/restore() - so instead of the first translate():
ctx.setTranform(1,0,0,1,this.cx, this.cy); // translate to pivot
You can also set things like styles on an individual basis for each. Regardless, it doesn't change the core solution though.
You have to first translate to the circle centre, make the rotation and then translate back
Do this before rendering the circle and the square
ctx.translate(this.cx, this.cy);
ctx.rotate(this.toAngle - this.angle);
ctx.translate(-this.cx, -this.cy);
jsfiddle below:
https://jsfiddle.net/1st8Lbu8/2/

Are HTML canvas clip paths inclusive or exclusive?

I've been working on a Typescript based touch screen client for our CQC home automation platform, and ran across something odd. There are lots of places where various graphical elements are layered over images. When it's time to update some area of the screen, I set a clip area and update.
But I always ended up with a line around everything, which was the color of the underlying color fill behind the image. I of course blamed myself. But, in the end, instead of committing suicide, I did a little test program.
It seems to indicate that drawImage() does NOT include the clip path boundary, while a color fill does. So blitting over the part of the images that underlies the area I'm updating doesn't completely fill the target area, leaving a line around the area.
After that simple program demonstrated the problem, I went back and for image updates I inflated the clip area by one, but left it alone for everything else, and now it's all working. I tested this in Chrome and Edge, just to make sure it wasn't some bug, and they both act exactly the same.
Strangely, I've never see any statement in the docs about whether clip paths are intended to be exclusive or inclusive of the boundary, but surely it shouldn't be one way for one type of primitive and another way for others, right?
function drawRect(ctx, x, y, w, h) {
ctx.moveTo(x, y);
ctx.lineTo(x + w, y);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x, y + h);
ctx.lineTo(x, y);
}
function init()
{
var canvas = document.getElementById("output");
canvas.style.width = 480;
canvas.style.height = 480;
canvas.width = 480;
canvas.height = 480;
var drawCtx = canvas.getContext("2d");
drawCtx.translate(0.5, 0.5);
var img = new Image();
img.src = "test.jpg";
img.onload = function() {
// DRaw the image
drawCtx.drawImage(img, 0, 0);
// SEt a clip path
drawCtx.beginPath();
drawRect(drawCtx, 10, 10, 200, 200);
drawCtx.clip();
// Fill the whole area, which fills the clip area
drawCtx.fillStyle = "black";
drawCtx.fillRect(0, 0, 480, 480);
// Draw the image again, which should fill the area
drawCtx.drawImage(img, 0, 0);
// But it ends up with a black line around it
}
}
window.addEventListener("load", init, false);
I think they behave same.
Clip region are not inclusive of the border, but they can use anti aliasing.
Chrome was not using this techinque and was giving jagged lines on clipping. ( probably they changed recently ).
The thin black border is the side effect of a compositing operation.
The clip region is across a pixel. so the fillRect will draw black everywhere, but the border will be 50% black and 50% transparent, compositing with the first image draw.
The second draw image get clpped, at the border with 50% opacity to simulate the half pixel. at this point at the clip border you have:
image 100%
black fill 50%
image 50%
This will make a small dark border appear.
function drawRect(ctx, x, y, w, h) {
ctx.moveTo(x, y);
ctx.lineTo(x, y + h);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x + w, y);
ctx.closePath();
}
function init()
{
var canvas = document.getElementById("output");
canvas.style.width = 480;
canvas.style.height = 480;
canvas.width = 480;
canvas.height = 480;
var drawCtx = canvas.getContext("2d");
drawCtx.translate(0.5, 0.5);
var img = new Image();
img.src = "http://fabricjs.com/assets/printio.png";
img.onload = function() {
// DRaw the image
drawCtx.drawImage(img, 0, 0);
// SEt a clip path
drawCtx.beginPath();
drawRect(drawCtx, 10, 10, 200, 200);
drawCtx.clip();
// Fill the whole area, which fills the clip area
drawCtx.fillStyle = "black";
drawCtx.fillRect(0, 0, 480, 480);
// Draw the image again, which should fill the area
drawCtx.drawImage(img, 0, 0);
// But it ends up with a black line around it
}
}
init();
<canvas id="output" />

How to show part of element from other side of canvas

How to show part of element outside of canvas from opposite side canvas. Illustration:
You need to draw twice when the shape is outside canvas' boundaries. Draw the main part first, then the same part offset by width so it gives the illusion of showing on the other side.
Manually Draw twice
This draws a shape going from right to left, when the shape is outside the left edge it will be redrawn at the right edge representing the part that is non-visible on the left side. For the opposite way (left to right) the principle is just the same, just use x with canvas' width instead of 0.
var ctx = document.querySelector("canvas").getContext("2d"),
x = 100, // start position
w = 200; // shape width
ctx.fillStyle = "#777";
(function loop() {
ctx.clearRect(0, 0, 300, 150); // clear canvas
ctx.fillRect(x, 0, w, 150); // draw main part/image/shape
if (x < 0) { // should rotate? draw secondary
ctx.fillRect(ctx.canvas.width + x, 0, w, 150); // use canvas width + x (x<0)
}
x -= 7; // animate
if (x <= -w) x = ctx.canvas.width + x; // at some point reset x
requestAnimationFrame(loop)
})();
<canvas></canvas>
Translated Pattern
To simplify this a CanvasPattern can be used. The later version of canvas allows local transforms on the pattern itself, but since this is not currently widely spread I'll show an example using normal transforms and compensated x position:
var ctx = document.querySelector("canvas").getContext("2d"),
pattern,
x = 100, // start position
w = 200; // shape width
// create pattern
ctx.fillStyle = "#777";
ctx.fillRect(x, 0, w, 150); // draw main part/image/shape
pattern = ctx.createPattern(ctx.canvas, "repeat"); // use current canvas as pattern
ctx.fillStyle = pattern; // set pattern as fillStyle
(function loop() {
ctx.setTransform(1,0,0,1,0,0); // reset transforms
ctx.clearRect(0, 0, 300, 150); // clear canvas
ctx.setTransform(1,0,0,1,x,0); // translate absolute x
ctx.fillRect(-x, 0, 300, 150); // fill using pattern, compensate transform
x -= 7; // animate
requestAnimationFrame(loop)
})();
<canvas></canvas>

adding up down motion to a line in canvas html5

I am trying to add a normal up and down motion to my two lines drawn in html5 using canvas . but i am unable to..
Kindly suggest me a way to overcome this and is there any better way to do this.
I am adding the function here
function create_blades()
{
canvas_context.moveTo(60+x_pos,80);
canvas_context.lineTo(125+x_pos,40+y_factor);
canvas_context.lineWidth = 3;
canvas_context.stroke();
canvas_context.moveTo(60+x_pos,110);
canvas_context.lineTo(125+x_pos,60-y_factor);
canvas_context.lineWidth = 3;
canvas_context.stroke();
//motion();
}
You are not describing the motion so here is a generic approach to animate lines (or any objects).
Update positions
Check boundaries/criterias
Clear canvas
Redraw shape
Loop
Here is a live example.
var xpos = 0, // holds current position (here, actually delta to fixed pos)
ypos = 0,
dltY = 2, // delta values
dltX = 2;
(function loop() {
xpos += dltX; // add delta values
ypos += dltY;
// check boundaries, here canvas edges
if (xpos < 0 || xpos > canvas.width) dltX = -dltX;
if (ypos < 0 || ypos > canvas.height) dltY = -dltY;
// redraw graphics
create_blades(xpos, ypos);
// loop
requestAnimationFrame(loop);
})();
Then the draw function:
function create_blades(x_pos, y_factor) {
// clear canvas
canvas_context.clearRect(0, 0, canvas.width, canvas.height);
canvas_context.beginPath(); // need this first
canvas_context.moveTo(60 + x_pos, 80);
canvas_context.lineTo(125 + x_pos, 40 + y_factor);
//canvas_context.lineWidth = 3; // not needed here
//canvas_context.stroke();
canvas_context.moveTo(60 + x_pos, 110);
canvas_context.lineTo(125 + x_pos, 60 - y_factor);
canvas_context.lineWidth = 3;
canvas_context.stroke();
//motion();
}
As you are using fixed values in the draw position the passed arguments with positions will of course just add to that, but you should be able to see the principle in how you move the shape.

HTML5 canvas: single stroke around combined regions

HTML5 canvas: I'm looking for a way to draw a single stroke around a combined path.
For example if I have two overlapping circles I don't want to have two overlapping circle strokes, but one single stroke around the combined region of both circles..
Any chance for that?
It can be done by using globalCompositeOperation. There are various ways you can draw the shapes them selves but here is one approach that results in this (for the two rectangle circles in the demo):
Step 1: setup the normal canvas
Step 2: setup an off-screen canvas
Update Not sure how I could miss the obvious, but you can of course just stroke the circles first, then punch a whole with composite mode and a fill - much faster (I guess I had images on my mind when I came up with the offset redraw).
The reason for off-screen canvas is if you have something in the background already on the main canvas. This will be deleted otherwise where we punch the hole. If nothing is there there is no problem drawing this to a single canvas - updated code:
/// some regions
var rect = [ [20, 20, 200, 200], [100, 100, 200,200] ],
/// ox = off-screen context
ox.strokeStyle = '#fff';
ox.lineWidth = 3 * 2; /// x2 as half will be gone when we punch hole
/// stroke outlines
for(; r = rect[i]; i++) {
o = r[2] * 0.5;
ox.beginPath();
ox.arc(r[0] + o, r[1] + o, o, 0, 2 * Math.PI);
ox.stroke();
}
/// punch hole with composite mode and fill
ox.globalCompositeOperation = 'destination-out';
for(i = 0; r = rect[i]; i++) {
o = r[2] * 0.5;
ox.beginPath();
ox.arc(r[0] + o, r[1] + o, o, 0, 2 * Math.PI);
ox.fill();
}
/// draw result to main canvas
/// ctx = main context, ocanvas = off-screen canvas
ctx.drawImage(ocanvas, 0, 0);
(Animated) online demo using this optimized version
I'll leave the old code as it can be used for images that can't be stroked -
Now draw the shapes filled to the off-screen canvas. Draw in the color you want the outline to be in.
/// some regions
var rect = [ [20, 20, 200, 200], [100, 100, 200,200] ],
/// ox = off-screen canvas
ox.fillStyle = '#fff';
/// draw the array with circes
for(; r = rect[i]; i++) {
var o = r[2] * 0.5;
ox.beginPath(); //use this here - arcs are currently buggy
ox.arc(r[0] + o, r[1] + o, o, 0, 2 * Math.PI);
ox.fill(); //.. and here
}
Now draw the cached image of the shapes back to main canvas. the shapes must be drawn with a slight offset in each direction - this step will create the outline:
/// ctx = main context, ocanvas = off-screen canvas
ctx.drawImage(ocanvas, -1, -1);
ctx.drawImage(ocanvas, 1, -1);
ctx.drawImage(ocanvas, 1, -1);
ctx.drawImage(ocanvas, 1, 1);
ctx.drawImage(ocanvas, -1, 1);
ctx.drawImage(ocanvas, 1, 1);
ctx.drawImage(ocanvas, -1, -1);
ctx.drawImage(ocanvas, -1, 1);
And finally we punch a "hole" in the filled shape to make it transparent with an outline using globalCompositeOperation + a final draw in 0 offset position :
ctx.globalCompositeOperation = 'destination-out';
ctx.drawImage(ocanvas, 0, 0);
ONLINE DEMO
To make the border thicker just increase the offset when you draw back the shapes to main canvas.
That's my current solution. Doesn't need a second canvas and is easier to achieve. It still uses the Ken's idea to use globalCompositeOperation:
context.lineWidth = 2;
context.stroke();
var prev = context.globalCompositeOperation;
context.globalCompositeOperation = "destination-out";
context.fill();
context.globalCompositeOperation = prev;