Accelerated canvas context translation: bug, or my mistake? - html5-canvas

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...

Related

How to transform the canvas using DOMMatrix?

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.

p5js only drawing what's needed

I'm looking for a way to limit what gets done in the draw loop.
I have a system where when I click, it add's a rect.
This rect then starts spawning circles that move.
since the rect does not change location, it isn't ideal to redraw it in every frame.
Is there a way to put the rects on a different layer of sorts, or is there another mechanism that I can use to limit the rect-drawing without impeding the circle-drawing?
I've tried with createGraphic to make a background with the rects, but I can't make the 'foreground' where the circles reside to be transparant.
Curious about this I tried myself. My idea was simply grabbing the canvas and interacting with it regardless of p5.js.
My result was that the draw... in this case ctx.fillRect did not render on screen.
However the fillStyle was changed.
Canvas is surprisingly efficient as well as WebGL and can handle the performance usually... unless you are rendering hundreds(mobile) to thousands(laptop/desktop) of objects.
I would have liked to have a better outcome but I think it was worthwhile posting what I had tried and my outcome nonetheless.
//P5 Setup
function setup(){
createCanvas(1500, 750);
background('rgba(0, 0, 0, 0.3)');
stroke(255);
fill(255)
doNonP5Drawing();
}
//Render
function draw(){
background(0);
frame();
}
function doNonP5Drawing(){
let canvas = document.querySelector('canvas'),
ctx = canvas.getContext('2d');
ctx.fillStyle="red";
ctx.fillRect(canvas.innerWidth/2 - 100,canvas.innerHeight/2 - 100,200,200);
}

Running background in P5

Im trying to make a side-scroller game and I got stuck on the running background part. I've looked for solutions and I've discovered some , but they were using javascript not the p5 library.
I started from the tutorials found on The Coding Train , and looked over all the examples and references on their site.
Although I could avoid this by using something else, just for the sake of it being here in case someone gets stuck on the same issue, could anyone offer a solution to this in p5? Disclaimer: Im a total noob p5.js.
later edit : By running background i mean moving Background image in a loop from left to right
Honestly, from the discussion we had in the comments, it sounds like you're overthinking it.
The general approach to animation (that tutorial is for Processing, but the principles apply to P5.js as well) is as follows:
Step 1: Create a set of variables that represent the state of your scene.
Step 2: Use those variables to draw your scene every frame.
Step 3: Change those variables over time to make your scene move.
You already know what to do: load an image that contains your background, then draw that image, and move it a little bit each frame.
You've said you want to call the background() function instead of the image() function, which doesn't make a ton of sense. The background() function is not any more efficient than the image() function. In fact, the background() function just calls the image() function for you!
From the P5.js source:
p5.prototype.background = function() {
if (arguments[0] instanceof p5.Image) {
this.image(arguments[0], 0, 0, this.width, this.height);
} else {
this._renderer.background.apply(this._renderer, arguments);
}
return this;
};
P5.js simply checks whether the argument is an image, and if so, calls the image() function for you. So it doesn't really make sense to say that using the image() function is "less efficient" than using the background() function.
Taking a step back, you should really avoid thinking about these kinds of micro-optimizations until you A: understand the problem and B: actually have a problem. Don't make assumptions about "efficiency" until you've actually measured your code for performance.
Anyway, back to your question. You also said that you're loading the image twice, which you shouldn't have to do. You can just load the image once (make sure you do that in the setup() function and not the draw() function, and then draw that image twice:
var img;
function preload() {
img = loadImage("image.jpg");
}
function setup() {
image(img, 0, 0);
image(img, 100, 100);
}
And since you can draw two images, you'd then just draw them next to each other. Here's an example using colored rectangles to show the approach more clearly:
var offsetX = 0;
function setup() {
createCanvas(200, 200);
}
function draw() {
background(0);
fill(0, 255, 0);
rect(offsetX, 0, width, height);
fill(0, 0, 255);
rect(offsetX + width, 0, width, height);
offsetX--;
if(offsetX <= -width){
offsetX = 0;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.14/p5.js"></script>
There are other ways to do it, like creating an image that contains the wrapping itself. But the general approach is pretty much the same.
If you're still stuck, please try to break your problem down into smaller pieces like I've done here. For example, notice that I created a simple sketch that deals with images, and another simple sketch that deals with moving rectangles. Then if you get stuck, please post a MCVE in a new question post and we'll go from there. Good luck.
Maybe it is a late answer.. but you can make the environment 3D and then move the camera.
Docs: https://p5js.org/reference/#/p5/camera
Example:
function setup() {
createCanvas(windowWidth - 200, windowHeight - 200, WEBGL);
background(175);
frameRate(30);
}
function draw() {
background(175);
//move the camera Xaxis when mouse is moved
let camX = map(mouseX, 0, width, 0,width);
camera(camX, 0, (height/2.0) / tan(PI*30.0 / 180.0), camX, 0, 0, 0, 1, 0);
normalMaterial();
noStroke();
ambientLight(251,45,43);
box(100, 100, 50);
ang += 0.3;
rotateY(ang * 0.03);
}
Keep calm and Happy Coding!

Better way to draw a quadratic Curve in KineticJS?

I am writing an app in which I have to draw a lot of draggable quadratic curves.
I am using Kinetic.Shape for this (KineticJS 4.4.3).
Since the performance is not great I tried to analyze and optimize the code and found out that the drawFunc function is executed twice.
Look at the attached Demo Code.
var stage = new Kinetic.Stage({
container: 'kinetic',
width: 400,
height: 300
});
var curveLayer = new Kinetic.Layer();
var line = new Kinetic.Shape({
drawFunc: function (canvas) {
console.log("drawFunc executed");
var context = canvas.getContext();
context.beginPath();
context.moveTo(10, 10);
context.quadraticCurveTo(95, 100, 200, 10);
canvas.stroke(this);
},
strokeWidth: 10
});
curveLayer.add(line);
stage.add(curveLayer);
If you run the script there will be 2 times "drawFunc executed" in the console.
I do not understand why and I ask myself if there is any better way to do it.
The Link to the Fiddle:
http://jsfiddle.net/solitud/ZpU4J/9/
The Link to my project:
http://modulargrid.net/e/patches/view/92
KineticJS always creates a non-visible buffer canvas that it uses for drags, etc.
You are seeing drawFunc execute once for that buffer canvas and once for your visible canvas.
No way to eliminate that 2X drawing.
Looking at your project link, I'm guessing that the user creates connections into various receptors by visually dragging the plugs. No way to make that more efficient.
But, once any connection is complete, you can greatly speed redrawing of that connector by caching it to an image: connector1.toImage( ... );
Redrawing your image-cached "connectors" create much less performance penalty than redrawing quad-beziers.

HTML Canvas clip area - Context restore?

Am trying to set a "dirty zone" on my canvas to prevent the repainting of unmoved items (background image, static items, etc.)
i.e. only the background painted behind a moving player needs to be redrawn
EDIT: As suggested, here's the jsfiddle of it
http://jsfiddle.net/7kbzj/3/
The "update" method doesn't work out there, so it's moveSprite() you can get run by clicking the "move sprite" link... Basically, the clipping zone shouldmove by 10px to the right each time you click. Clipping mask stays at initial position, only the re-paint occurs. Weird o_O
So as I init my canvas, once the background is painted, set I use the ctx.save() method:
function init() {
canvas = document.getElementById('kCanvas');
ctx = canvas.getContext('2d');
ctx.fillStyle = "rgb(0,128,0)";
ctx.fillRect (0,0,320,240);
ctx.save();
setInterval(function () { update(); }, tpf);
}
In order to see the clipping works, I draw a different color background (blue one) in the area that I wanted clipped... the result is bad, only the first clipped area is painted blue :(
function update() {
setDirtyArea(x,y,w+1,h)
ctx.fillStyle = "rgb(0,0,128)";
ctx.fillRect (0,0,320,240);
x++;
// paint image
ctx.clearRect(x,y,w,h);
ctx.drawImage(imageObj, x, y);
}
function setDirtyArea(x,y,w,h) {
ctx.restore();
// define new dirty zone
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.clip();
}
I'd love to se the blue zone propagate itself towards the right of the screen... please help, I don't understand what's wrong!
Thanks,
J.
You need to wrap the actual drawing and clipping of the box with the save and restore methods. and include the closePath method. I have modified your fiddle to work the way I believe you are trying to make it.
http://jsfiddle.net/jaredwilli/7kbzj/7/
ctx.save(); // save the context
// define new dirty zone
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.clip();
ctx.restore(); // restore the context
I also have learned that using save and restore can get really complex, and confusing to know which context your in. It came up with a pretty huge canvas app im working on, and i found that indenting your canvas code helps immensely. Especially the save/restores. I have even decided it should be considered a best practice, so the more people who know and do it the better.
Hope this helps.

Resources