For making Photo Collage Maker, I use fabric js which has an object-based clipping feature. This feature is great but the image inside that clipping region cannot be scaled, moved or rotated. I want a fixed position clipping region and the image can be positioned inside the fixed clipping area as the user want.
I googled and find very near solution.
var canvas = new fabric.Canvas('c');
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.rect(10,10,150,150);
ctx.rect(180,10,200,200);
ctx.closePath();
ctx.stroke();
ctx.clip();
Multiple Clipping Areas on fabric js canvas
where the image of one clipping region has appeared in another clipping region. How can I avoid this or is there another way of accomplishing this using fabric js.
This can be accomplished with Fabric using the clipTo property, but you have to 'reverse' the transformations (scale and rotation), in the clipTo function.
When you use the clipTo property in Fabric, the scaling and rotation are applied after the clipping, which means that the clipping is scaled and rotated with the image. You have to counter this by applying the exact reverse of the transformations in the clipTo property function.
My solution involves having a Fabric.Rect serve as the 'placeholder' for the clip region (this has advantages because you can use Fabric to move the object around and thus the clip region.
Please note that my solution uses the Lo-Dash utility library, particularly for _.bind() (see code for context).
Example Fiddle
Breakdown
1. Initialize Fabric
First, we want our canvas, of course:
var canvas = new fabric.Canvas('c');
2. Clip Region
var clipRect1 = new fabric.Rect({
originX: 'left',
originY: 'top',
left: 180,
top: 10,
width: 200,
height: 200,
fill: 'none',
stroke: 'black',
strokeWidth: 2,
selectable: false
});
We give these Rect objects a name property, clipFor, so the clipTo functions can find the one by which they want to be clipped:
clipRect1.set({
clipFor: 'pug'
});
canvas.add(clipRect1);
There doesn't have to be an actual object for the clip region, but it makes it easier to manage, as you're able to move it around using Fabric.
3. Clipping Function
We define the function which will be used by the images' clipTo properties separately to avoid code duplication:
Since the angle property of the Image object is stored in degrees, we'll use this to convert it to radians.
function degToRad(degrees) {
return degrees * (Math.PI / 180);
}
findByClipName() is a convenience function, which is using Lo-Dash, to find the with the clipFor property for the Image object to be clipped (for example, in the image below, name will be 'pug'):
function findByClipName(name) {
return _(canvas.getObjects()).where({
clipFor: name
}).first()
}
And this is the part that does the work:
var clipByName = function (ctx) {
var clipRect = findByClipName(this.clipName);
var scaleXTo1 = (1 / this.scaleX);
var scaleYTo1 = (1 / this.scaleY);
ctx.save();
ctx.translate(0,0);
ctx.rotate(degToRad(this.angle * -1));
ctx.scale(scaleXTo1, scaleYTo1);
ctx.beginPath();
ctx.rect(
clipRect.left - this.left,
clipRect.top - this.top,
clipRect.width,
clipRect.height
);
ctx.closePath();
ctx.restore();
}
NOTE: See below for an explanation of the use of this in the function above.
4. fabric.Image object using clipByName()
Finally, the image can be instantiated and made to use the clipByName function like this:
var pugImg = new Image();
pugImg.onload = function (img) {
var pug = new fabric.Image(pugImg, {
angle: 45,
width: 500,
height: 500,
left: 230,
top: 170,
scaleX: 0.3,
scaleY: 0.3,
clipName: 'pug',
clipTo: function(ctx) {
return _.bind(clipByName, pug)(ctx)
}
});
canvas.add(pug);
};
pugImg.src = 'https://fabricjs.com/lib/pug.jpg';
What does _.bind() do?
Note that the reference is wrapped in the _.bind() function.
I'm using _.bind() for the following two reasons:
We need to pass a reference Image object to clipByName()
The clipTo property is passed the canvas context, not the object.
Basically, _.bind() lets you create a version of the function that uses the object you specify as the this context.
Sources
https://lodash.com/docs#bind
https://fabricjs.com/docs/fabric.Object.html#clipTo
https://html5.litten.com/understanding-save-and-restore-for-the-canvas-context/
I have tweaked the solution by #natchiketa as the positioning of the clip region was not positioning correctly and was all wonky upon rotation. But all seems to be good now. Check out this modified fiddle:
https://jsfiddle.net/PromInc/ZxYCP/
The only real changes were made in the clibByName function of step 3 of the code provided by #natchiketa. This is the updated function:
var clipByName = function (ctx) {
this.setCoords();
var clipRect = findByClipName(this.clipName);
var scaleXTo1 = (1 / this.scaleX);
var scaleYTo1 = (1 / this.scaleY);
ctx.save();
var ctxLeft = -( this.width / 2 ) + clipRect.strokeWidth;
var ctxTop = -( this.height / 2 ) + clipRect.strokeWidth;
var ctxWidth = clipRect.width - clipRect.strokeWidth + 1;
var ctxHeight = clipRect.height - clipRect.strokeWidth + 1;
ctx.translate( ctxLeft, ctxTop );
ctx.rotate(degToRad(this.angle * -1));
ctx.scale(scaleXTo1, scaleYTo1);
ctx.beginPath();
ctx.rect(
clipRect.left - this.oCoords.tl.x,
clipRect.top - this.oCoords.tl.y,
ctxWidth,
ctxHeight
);
ctx.closePath();
ctx.restore();
}
Two minor catches I found:
Adding a stroke to the clipping object seems to throw things off by a few pixels. I tried to compensate for the positioning, but then upon rotation, it would add 2 pixels to the bottom and right sides. So, I've opted to just remove it completely.
Once in a while when you rotate the image, it will end up with a 1px spacing on random sides in the clipping.
Update to #Promlnc answer.
You need to replace the order of context transformations in order to perform proper clipping.
translation
scaling
rotation
Otherwise, you will get wrongly clipped object - when you scale without keeping aspect ratio (changing only one dimension).
Code (69-72):
ctx.translate( ctxLeft, ctxTop );
ctx.rotate(degToRad(this.angle * -1));
ctx.scale(scaleXTo1, scaleYTo1);
Replace to:
ctx.translate( ctxLeft, ctxTop );
ctx.scale(scaleXTo1, scaleYTo1);
ctx.rotate(degToRad(this.angle * -1));
See this:
https://jsfiddle.net/ZxYCP/185/
Proper result:
UPDATE 1:
I have developed a feature to clip by polygon:
https://jsfiddle.net/ZxYCP/198/
This can be done much more easily. Fabric provides render method to clip by the context of another object.
Checkout this fiddle. I saw this on a comment here.
obj.clipTo = function(ctx) {
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
clippingRect.render(ctx);
ctx.restore();
};
As I tested all fiddles above they have one bug. It is when you will flip X and Y values together, clipping boundaries will be wrong. Also, in order not doing all calculations for placing images into the right position, you need to specify originX='center' and originY='center' for them.
Here is a clipping function update to original code from #natchiketa
var clipByName = function (ctx) {
var clipRect = findByClipName(this.clipName);
var scaleXTo1 = (1 / this.scaleX);
var scaleYTo1 = (1 / this.scaleY);
ctx.save();
ctx.translate(0,0);
//logic for correct scaling
if (this.getFlipY() && !this.getFlipX()){
ctx.scale(scaleXTo1, -scaleYTo1);
} else if (this.getFlipX() && !this.getFlipY()){
ctx.scale(-scaleXTo1, scaleYTo1);
} else if (this.getFlipX() && this.getFlipY()){
ctx.scale(-scaleXTo1, -scaleYTo1);
} else {
ctx.scale(scaleXTo1, scaleYTo1);
}
//IMPORTANT!!! do rotation after scaling
ctx.rotate(degToRad(this.angle * -1));
ctx.beginPath();
ctx.rect(
clipRect.left - this.left,
clipRect.top - this.top,
clipRect.width,
clipRect.height
);
ctx.closePath();
ctx.restore();
}
Please check the updated fiddle
With the latest update on fabric 1.6.0-rc.1, you are able to skew the image by hold shift and drag the middle axis.
I have trouble with how to reverse the skew so that the clipping area stays the same. I have tried the following code to try to reverse it back, but didn't work.
var skewXReverse = - this.skewX;
var skewYReverse = - this.skewY;
ctx.translate( ctxLeft, ctxTop );
ctx.scale(scaleXTo1, scaleYTo1);
ctx.transform(1, skewXReverse, skewYReverse, 1, 0, 0);
ctx.rotate(degToRad(this.angle * -1));
Demo: https://jsfiddle.net/uimos/bntepzLL/5/
Update to previous guys answers.
ctx.rect(
clipRect.oCoords.tl.x - this.oCoords.tl.x - clipRect.strokeWidth,
clipRect.oCoords.tl.y - this.oCoords.tl.y - clipRect.strokeWidth,
clipRect.oCoords.tr.x - clipRect.oCoords.tl.x,
clipRect.oCoords.bl.y - clipRect.oCoords.tl.y
);
Now we are able to scale the clipping area without a doubt.
Related
I'm reading through the MDN documentation for canvas and above the transformations section, it says "The methods listed below remain for historical and compatibility reasons as DOMMatrix objects are used in most parts of the API nowadays and will be used in the future instead." This seems to suggest that transform methods (such as .rotate() and .scale()) used directly aginst the CanvasRenderingContext2D are obsolete. However, I don't see any clear explanation as to what the new mechanism is for doing things like rotating and scaling the entire canvas using the DOMMatrix mechanism. How can this be done and is there any decent documentation for it? Even MDN's own canvas tutorial still calls transform methods against the canvas rendering context!
These methods aren't obsolete, you are still safe to use them and this paragraph is I believe misleading. I'll think on it but I may end up removing it from MDN since we've got no intention of removing these methods.
And while this will be implementation dependent, I know that at least in Chromium both don't end up in the same path internally, and I wouldn't be surprised that using a DOMMatrix object would be somehow slower than using the relative transforms. There are also cases where using a DOMMatrix object just makes your code more complex to read and maintain.
So you'd better not drop the tranform methods just because someone wrote that line in this article.
Anyway, DOMMatrix objects are convenient and there are definitely cases where you'll want them. To do so, apply the transforms on that object, and then apply it through context.setTransform(matrix). This will set the context's current transform matrix (CTM) to the one represented by the DOMMatrix object, and disregard whatever was set as CTM before.
So for instance to translate your context:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 50, 50); // untransformed
const mat = new DOMMatrix();
// mat.translate() would return a new DOMMatrix without modifying this one
mat.translateSelf(120, 50);
// set the context CTM to our DOMMatrix
ctx.setTransform(mat);
ctx.fillStyle = "green";
ctx.fillRect(0, 0, 50, 50); // transformed
<canvas></canvas>
However beware there is a huge bug in the DOMMatrix API: the rotation angle has been wrongfully defined as degrees. This is basically the only place in almost all the Web-APIs that a JS angle is defined as degrees instead of being defined as radians. So we have to do stoopid conversions there and scratch our head every time we see our rotation didn't work...
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const angle = Math.PI * 1.8; // radians
ctx.translate(150, 75);
ctx.rotate(angle);
ctx.translate(-50, -50);
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, 50, 50); // default ctx.rotate();
const mat = new DOMMatrix();
mat.translateSelf(150, 75);
mat.rotateSelf(angle); // this should have been in degrees!
mat.translateSelf(-50, -50);
ctx.setTransform(mat);
ctx.fillStyle = "green";
ctx.fillRect(0, 0, 50, 50); // that's not what we expected
<canvas></canvas>
Also, to make only relative updates to the current transform matrix (CTM), you'd have to either keep your DOMMatrix object around in your code, or to retrieve it from the context's .getTransform() method.
Once you got the context's CTM, you can either apply relative transforms using the DOMMatrix.\[...\]Self methods, or even multiply this DOMMatrix object with another one.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.translate(150, 75); // not gonna disappear
const identity = new DOMMatrix();
const anim = () => {
const mat = ctx.getTransform();
ctx.setTransform(identity); // to clear the context, reset to identity
// after you got the previous CTM
ctx.clearRect(0, 0, canvas.width, canvas.height);
mat.rotateSelf(1); // one degree
ctx.setTransform(mat);
ctx.fillRect(-25, -25, 50, 50);
requestAnimationFrame(anim);
};
requestAnimationFrame(anim);
<canvas></canvas>
Finally, note that while the DOMMatrix interface does support 3D transforms, the canvas 2D API still doesn't support non-affine transformations. You still won't have perspective even when passing a 3D transform.
In Three.js there is a GridHelper that allows you to create a plane grid easily, just like this:
var grid = new THREE.GridHelper(size, divisions, colorCenterLine, colorGrid);
You can see a result example here.
What I want to do is to create a box/cube grid, exactly like this:
I need to have a cube, because later I'll want to know, for example, if an object it's inside the cube (dynamically).
I wasn't able to find a helper that do what I need, so my idea is to take the GridHelper source code and use a BoxBufferGeometry instead of a BufferGeometry, but I don't even know if that's possible. I want to add that I do not have much knowledge in the field of 3D graphics, I'm just starting.
I'd love to hear you thoughts about this: I'm going in the right direction? How would you approach this problem?
I've finally used the approach that #WestLangley proposed:
var createBoxGrid = function (base, height, translateY, divisions, color) {
boxGrid = new THREE.Group();
boxGrid.name = "BoxGrid";
var box3 = new THREE.Box3(new THREE.Vector3(-base / 2, 0, -base / 2), new THREE.Vector3(base / 2, height, base / 2));
var box3Helper = new THREE.Box3Helper(box3, color);
var gridHelper = new THREE.GridHelper(base, divisions, color, color);
boxGrid.add(box3Helper);
boxGrid.add(gridHelper);
boxGrid.translateY(translateY);
return boxGrid;
};
I'm working on a ThreeJS project where there is a text tunnel, created by generating text objects with TextGeometry, and moving them along the z axis continuously. The objects are generated dynamically. I use 30 at a time with a circular buffer.
For some reason, I get a good FPS (around 60 or more), but it varies a lot. There are drops in FPS every 5-10 seconds or so. I've tested the code in several conditions (i.e. not using other elements like other objects, video capture, cam movement, etc.) and I am pretty sure the moving text objects are the cause of drop in FPS.
Ideas?
ThreeJS update function:
function update()
{
bookidx = bookidx % book.length;
// FLOATING TEXT
if (stepsFloating++ % floatingWordsRate == 0 && makeFloatingWords){
addFloatingText(book[bookidx], scene);
}
else{
floatingWords.move();
}
// TUNNEL TEXT
// move sentences forward
for (t in tunnelWords.tokens){
tunnelWords.tokens[t][1].position.z += 120;
}
// get another sentence
if (tunnelWords.tokens[0][1].position.z > -2000){
addTunnelText(book[bookidx++], scene);
}
// KEYBOARD
keyboardUpdate();
// CONTROLS AND STATS
controls.update();
stats.update();
}
This creates the object (I intentionally use a high bevel thickness for the effect, decreasing it or not using it at all did not make a difference:
function makeTextMeshForTunnel(word){
var textGeom = new THREE.TextGeometry( word ,
{
size: 22, height: 1, curveSegments: 1,
font: "helvetiker", weight: "normal", style: "normal",
bevelThickness: 900, bevelSize: 2, bevelEnabled: true,
material: 1, extrudeMaterial: 0
});
// font: helvetiker, gentilis, droid sans, droid serif, optimer
// weight: normal, bold
var textMesh = new THREE.Mesh(textGeom, tunnel_textMaterial );
textGeom.computeBoundingBox();
var textWidth = textGeom.boundingBox.max.x - textGeom.boundingBox.min.x;
textMesh.position.set( -0.5 * textWidth, 0, -3000 );
textMesh.rotation.x = TUNNEL_TEXT_X_ROTATION;
return textMesh;
}
This helps, seems textGeometry is more costly than other geometries, this helps to understand the problem with suggested solutions:
https://github.com/mrdoob/three.js/issues/1825
It's a pretty self-explanatory question.
I am curious as to why there is no built-in function to create gradients. The only way I found to "fake it" is to create a series of lines or rectangles each with a unique color calculated with b.lerpColor.
I saw that the InDesign Object Model has of course the gradient class but I don't know how to access it using basiljs.
Maybe if someone could show me? Many thanks.
Check out this reference http://jongware.mit.edu/idcs6js/pc_Gradient.html
And try it like this:
#includepath "~/Documents/;%USERPROFILE%Documents";
#include "basiljs/bundle/basil.js";
function draw() {
var d = b.doc();
var r = b.rect(0, 0, b.width, b.height);
var myGrad = d.gradients.add({
name: "Col " + (parseInt(Math.random() * 10000)),
type: GradientType.linear
});
myGrad.gradientStops[0].properties = {
stopColor: d.colors.item(2),
location: Math.random() * 50
};
myGrad.gradientStops[1].properties = {
stopColor: d.colors.item(4),
location: 50 + Math.random() * 50
};
r.fillColor = myGrad;
// to set the fill of the gradient use the following line
r.gradientFillAngle = 50;//b.random(-180,180);
}
b.go();
The script creates a new gradient swatch every time you run it.
edit:added gradientFillAngle
Take a look here.
gradientFillAngle number r/w The angle of a linear gradient applied to the fill of the Rectangle. (Range: -180 to 180)
I'm trying to take a users mouse/touch drawn line and then have it alpha fade out the result using a tween. The problem is when cap and joint style are set to rounded then joint point fades behind the rest of the line. It looks fine when set to miter or bevel.
What I want is a smooth solid fade of the shape. Any ideas?
Fiddle: http://jsfiddle.net/mcfarljw/ZNGK2/
Function for drawing the line based on user input:
function handleMouseMove(event) {
var midPt = new createjs.Point(oldPt.x + stage.mouseX >> 1, oldPt.y + stage.mouseY >> 1);
drawingCanvas.graphics.setStrokeStyle(stroke, 'round', 'round').beginStroke(color).moveTo(midPt.x, midPt.y).curveTo(oldPt.x, oldPt.y, oldMidPt.x, oldMidPt.y);
oldPt.x = stage.mouseX;
oldPt.y = stage.mouseY;
oldMidPt.x = midPt.x;
oldMidPt.y = midPt.y;
stage.update();
}
Tween applied to the shape after line is finished:
createjs.Tween.get(drawingCanvas).to({
alpha: 0
}, 2000).call(function() {
drawingCanvas.alpha = 1;
drawingCanvas.graphics.clear();
});
You'll want to cache the whole shape before fading it out. See the updates I have made to the fiddle. Mainly, take a look at line 52 on the handleMouseUp event.
drawingCanvas.cache(0, 0, 800, 800);
Then, when your fade is complete. Make sure to uncache before showing the object again. Otherwise your graphics.clear() won't work.
drawingCanvas.uncache();