How to transform the canvas using DOMMatrix? - html5-canvas

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.

Related

Three.js - How to prevent a light from shining on an object

Is there a way to prevent a directional light from illuminating a specific object? I guess this would also apply to a spotlight. The reason for this is I would like to use two directional lights, but with one light shining on an object to give it 'self shadowing', and do not want that light it to interfere with another object.
Currently I have a single directional light declared as such :
function addpointlight()
{
var SHADOW_MAP_WIDTH = 4096, SHADOW_MAP_HEIGHT = 2048;
//Enabling this this light just creates serious artifacts on the obj I am trying to shadow.
//var newlight=new THREE.DirectionalLight(0xeeeeee,0.7);
//newlight.position.set( 0, 100, 300 );
//newlight.castShadow=false;
//scene.add(newlight);
// create a directional light
pointLight = new THREE.DirectionalLight(0xeeeeee,0.80);
//pointLight.onlyShadow=true;
pointLight.position.set( 0, 100, 300 );
pointLight.shadowCameraVisible=true;
pointLight.shadowCameraNear = 10;
pointLight.shadowCameraFar = 1500;
pointLight.shadowCameraFov = 90;
pointLight.castShadow=true;
var d = 4;
pointLight.shadowCameraLeft = -d;
pointLight.shadowCameraRight = d;
pointLight.shadowCameraTop = -d;
pointLight.shadowCameraBottom = d;
pointLight.shadowBias = 0.00;
pointLight.shadowDarkness = 0.7;
pointLight.shadowMapWidth = SHADOW_MAP_WIDTH;
pointLight.shadowMapHeight = SHADOW_MAP_HEIGHT;
// add to the scene
scene.add(pointLight);
}
This light wraps nicely around the object I want to self shadow, eliminating shadow artifacts. It moves with a moving object that it is creating a shadow on using this :-
pointLight.position.set(obj.position.x+40,obj.position.y+5,obj.position.z+300);
pointLight.target=obj;
So I'd like to create a second directional light that only affects the other objects, not this one, tnd this one's light must not affect other objects.
I'd create a fiddle, but the models I am testing with together with the textures make it a rather large fiddle in terms of bandwidth.
The three.js version in r70.
You want to limit the objects that a light affects. Until the time at which three.js supports "layers", where a light will only affect objects in its own layer(s), you may be able to achieve what you want with a work-around: two separate scenes and two render passes.
renderer.autoClear = false;
...
renderer.clear();
renderer.render( scene1, camera );
renderer.render( scene2, camera );
If you have transparent objects, they will have to be in the second scene. Also, an object can only be in one scene, so you will have to duplicate a light if you want it in both.
three.js r.70

Accelerated canvas context translation: bug, or my mistake?

I found an odd behavior while working on my pet game. I wanted to draw few objects on canvas, some of them required image / icon to be rotated. It is quite common use case, usual solution is to make use of context's rotate method. Going with the blow I used also translate to nicely and consistently put images in desired place.
This worked fine, until I tried Chrome on Windows laptop, with hardware acceleration enabled. Images started to blink and teleport across the screen. I found that it is related to acceleration (turning off accelerated graphics fixes problem) and my best guess is that when updating frame the renderer assumes that drawing calls are independent and can be executed in parallel. When context transforms take place it is not the case because context state changes.
Example of undesired behavior: having a canvas element with ID 'screen' try the following:
var canvas = document.getElementById("screen"),
ctx = canvas.getContext("2d");
var drawrect = function () {
ctx.fillStyle = this.color;
ctx.translate(this.x+10, this.y+10);
ctx.rotate(this.rotation);
ctx.fillRect(-10, -10, 20, 20);
ctx.rotate(-this.rotation);
ctx.translate(-this.x-10, -this.y-10);
};
var red = {
x: 22,
y: 22,
rotation: 0,
color: "#ff0000",
draw: drawrect
};
var blu = {
x: 22,
y: 111,
rotation: 0,
color: "#0000ff",
draw: drawrect
};
function main_loop() {
ctx.clearRect(0, 0, 450, 450);
frameId = requestAnimationFrame(main_loop);
red.draw();
red.x += 1;
red.rotation +=0.1;
blu.draw();
blu.x += 1;
blu.rotation -=0.1;
}
main_loop();
Working example: http://jsfiddle.net/1u2d7uhr/7/ (tested on Chrome, Chromium, Firefox; accelerated Chrome glitches, others do not)
I was able to 'fix' this by removing translations and rendering rotating elements to separate canvas, which is then (after rotations) drawn onto the main one. This seems hackish to me though.
Is it code error on my part?
In this case what is the right way to render rotations (perhaps with this question I should go do codereview, but I'm not sure if this is the case)?
Or is it buggy behavior on browser side? I understand the logic behind it but it can be very much surprising (and cause some confusion) to developers. Or am I only one...

Make environment map scale when moving from the object

I use CubeCamera to build a simple reflection model. The setup can be seen on the picture below.
If the camera is close enough to the cube - the reflection looks fine. However, if i move away from the objects - the reflection just gets bigger. See the picture below.
This is not the way i want it. I'd like the reflection to proportionally get smaller. I tried to play with different settings, then I thought this could be achieved using a proper shader program (just squish the cube texture, kind of), so i've tried to mess with the existing PhongShader, but no luck there, i'm too newbie to this.
Also, i've noticed that if i change the width and height of the cubeCamera.renderTarget, i.e.
cubeCamera.renderTarget.width = cubeCamera.renderTarget.height = 150;
i can get the proper dimensions of the reflection, but its position on the surface is wrong. It's visible from the angle presented on the picture below, but not visible if i place the camera straight. Looks like the texture needs to be centered.
The actual code is pretty straightforward:
var cubeCamera = new THREE.CubeCamera(1, 520, 512);
cubeCamera.position.set(0, 1, 0);
cubeCamera.renderTarget.format = THREE.RGBAFormat;
scene.add(cubeCamera);
var reflectorObj = new THREE.Mesh(
new THREE.CubeGeometry(20, 20, 20),
new THREE.MeshPhongMaterial({
envMap: cubeCamera.renderTarget,
reflectivity: 0.3
})
);
reflectorObj.position.set(0, 0, 0);
scene.add(reflectorObj);
var reflectionObj = new THREE.Mesh(
new THREE.SphereGeometry(5),
new THREE.MeshBasicMaterial({
color: 0x00ff00
})
);
reflectionObj.position.set(0, -5, 20);
scene.add(reflectionObj);
function animate () {
reflectorObj.visible = false;
cubeCamera.updateCubeMap(renderer, scene);
reflectorObj.visible = true;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
Appreciate any help!
Environment mapping in three.js is based on the assumption that the object being reflected is "infinitely" far away from the reflective surface.
The reflected ray used in the environment map look-up does not emanate from the surface of the reflective material, but from the CubeCamera's center. This approximation is OK, as long as the reflected object is sufficiently far away. In your case it is not.
You can read more about this topic in this tutorial.
three.js r.58

Dynamic text or texture on curved object

I'm a newcomer to three.js and am looking for what approaches are possible to achieve an effect like this:
For a cola can like object as in the image below (minus condensation), I want to change independent bits of text on the surface of the can based on user interaction. The variants of text are fairly arbitrary, too many for pre-baked full can textures. For instance I might want to:
change "Euro 2012" to arbitrary text
change the nutritional stats on the back of the can
show or hide one of the individual music notes
I'm sure it's possible, just looking for what concepts I need to employ. Is it difficult to have multiple textures on the same object? Or to generate arbitrary text and position it on an object and wrap it to the shape of the object?
Any pointers helpful!
You can use image created in a separate canvas as a Three.js texture. Instead of trying to mix and blend multiple textures in Three.js (possible, but tricky and limited control), I think the best solution would be to create the dynamic texture in 2D, totally out of Three.js then just apply the full texture to the can.
You can create your canvas image manually or using canvas image manipulation library of your choice (some possibilities: https://docs.google.com/spreadsheet/ccc?key=0Aqj_mVmuz3Y8dHNhUVFDYlRaaXlyX0xYSTVnalV5ZlE#gid=0 ). Or you can have your template as SVG and modify that (should be quite simple), render that to canvas, then use it as texture.
Using canvas as a texture is very simple:
var canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
var context = canvas.getContext('2d');
// drawing something here....
context.font = "Bold 20px Helvetica";
context.lineWidth = 4;
context.strokeStyle = 'rgba(255,255,255,.8)';
context.fillStyle = "rgba(0,0,0,1)";
context.strokeText("Testing", 4, 22);
context.fillText("Testing", 4, 22);
var texture = new THREE.Texture(canvas);
texture.needsUpdate = true;

Multiple clipping areas on Fabric.js canvas

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.

Resources