Canvas state lost after changing size - javascript

I want to resize a canvas, but when I do, the context gets reset, losing the current fillStyle, transformation matrix, etc. The ctx.save() and ctx.restore() functions didn’t work as I initially expected:
function resizeCanvas(newWidth, newHeight) {
ctx.save();
canvas.width = newWidth; // Resets context, including the save state
canvas.height = newHeight;
ctx.restore(); // context save stack is empty, this does nothing
}
After researching for a while, I can’t seem to find a good way to save and restore the canvas context after resizing. The only way I can think of is manually saving each property, which seems both messy and slow. ctx.save() doesn’t return the saved state either, and I don’t know of a way to access the context’s stack.
Is there a better way, or am I doomed to use something like this:
function resizeCanvas(newWidth, newHeight) {
let fillStyle = ctx.fillStyle;
let strokeStyle = ctx.strokeStyle;
let globalAlpha= ctx.globalAlpha;
let lineWidth = ctx.lineWidth;
// ...
canvas.width = newWidth;
canvas.height = newHeight;
ctx.fillStyle = fillStyle;
ctx.strokeStyle = strokeStyle;
ctx.globalAlpha= globalAlpha;
ctx.lineWidth = lineWidth;
// ...
}

I also found this somewhat annoying and I'm very interested to see if there are other solutions to this. I'd also like to see a reason as to why the state has to be reset and the state stack cleared when the canvas is resized. Really it's unintuitive behavior that you wouldn't expect and even MDN doesn't mention it, so it's probably pretty easy to make this mistake.
The absolute best way is to restructure your code in such a manner that all draw operations can be redone after the canvas is resized since you're gonna have to redraw everything anyway, you might as well be setting all the ctx states after you resize anyway. (i.e. make and utilize a 'draw' function that can be called after you resize the canvas)
If you really want to keep the state then I'd recommend making your own save/restore state stack and perhaps a resize function while you're at it. I'm not gonna judge you and say it's a bad idea...
Let's say I wanted to resize the canvas to the exact width and height of some text before drawing the text.
Normally I would have to set the font for the text first, then measure the text, then resize the canvas, but since resizing the canvas resets the state, I'd then have to set the font again like so:
ctx.font = "48px serif"
width = ctx.measureText('Hello World').width
canvas.width = width
ctx.font = "48px serif"
As an (admittedly over-complicated) workaround, I'd save and restore the state using my custom save-restore functions before and after resizing, respectively.
And yes, I do see the irony in replacing one extra line of code with around 30 or so extra lines of code in this particular example.
let canvas = document.querySelector('canvas')
, ctx = canvas.getContext('2d')
, stack = []
function save(ctx){
let state = {}
for(let property in ctx){
if(property == 'canvas')
continue
if(typeof ctx[property] == 'function')
continue
state[property] = ctx[property]
}
stack.push(state)
}
function restore(ctx){
let state = stack.pop() || {}
for(let property in state){
ctx[property] = state[property]
}
}
function resize(ctx, width, height){
save(ctx)
ctx.canvas.width = width || canvas.width;
ctx.canvas.height = height || canvas.height;
restore(ctx)
}
////////////// EXAMPLE ////////////////
let index = 0
, words = ["Our", "Words", "Are", "Dynamic"];
(function change(){
let font_size = ~~(Math.random() * 150 + 16)
let word = words[index]
index = (index + 1) % words.length
ctx.font = font_size+"px serif"
ctx.textBaseline = "hanging"
ctx.fillStyle = "white"
resize(ctx, ctx.measureText(word).width, font_size)
ctx.fillText(word, 0, 0)
setTimeout(change, 750)
})()
canvas{
background-color : orange
}
<canvas></canvas>

The accepted answer no longer works because some properties are deprecated and hence give runtime error when trying to set deprecated properties.
Here's a fix for Khauri MacClain's answer.
function save(ctx){
let props = ['strokeStyle', 'fillStyle', 'globalAlpha', 'lineWidth',
'lineCap', 'lineJoin', 'miterLimit', 'lineDashOffset', 'shadowOffsetX',
'shadowOffsetY', 'shadowBlur', 'shadowColor', 'globalCompositeOperation',
'font', 'textAlign', 'textBaseline', 'direction', 'imageSmoothingEnabled'];
let state = {}
for(let prop of props){
state[prop] = ctx[prop];
}
return state;
}
function restore(ctx, state){
for(let prop in state){
ctx[prop] = state[prop];
}
}
function resize(ctx, width, height){
let state = save(ctx);
ctx.canvas.width = width || canvas.width;
ctx.canvas.height = height || canvas.height;
restore(ctx, state);
}

I was trying to take a partially-drawn canvas -- generated by a library, and so not really possible to resize before all the draw commands were issued -- and expand it to add some text next to it. There's a deleted answer on this question by #markE that helped me out. See, you can make a new canvas element of arbitrary size, and "draw" the old (smaller) canvas onto it:
const libCanvas = myLib.getCanvas();
const newCanvas = document.createElement("canvas");
const ctx = newCanvas.getContext("2d");
ctx.font = MY_FONT;
newCanvas.width = libCanvas.width + ctx.measureText(myStr).width;
ctx.drawImage(libImage, 0, 0);
ctx.font = MY_FONT; // Changing width resets the context font
ctx.fillText(myStr, libCanvas.width, libCanvas.height / 2);
Now newCanvas is as wide as the old canvas, plus the width of the text I wanted to draw in, and has the contents of the library-generated canvas on the left side, and my label text on the right side.

Related

Why is canvas messing with my image's colors?

I'm developing an app that has a painting feature. The user can paint on an image that is initially made of only pure black and pure white pixels. Later, after the user has finished painting, I need to do some processing on that image based on the colors of each pixel.
However, I realized that by the time I processed the image, the pixels weren't purely black/white anymore, but there were lots of greys in between, even if the user didn't paint anything. I wrote some code to check it and found out there were over 250 different colors on the image, while I was expecting only two (black and white). I suspect canvas is messing with my colors somehow, but I can't figure out why.
I hosted a demo on GitHub, showcasing the problem.
The image
This is the image. It is visibly made of only black and white pixels, but if you want to check by yourself you can use this website. It's source code is available on GitHub and I used it as a reference for my own color counting implementation.
My code
Here is the code where I load the image and count the unique colors. You can get the full source here.
class AppComponent {
/* ... */
// Rendering the image
ngAfterViewInit() {
this.context = this.canvas.nativeElement.getContext('2d');
const image = new Image();
image.src = 'assets/image.png';
image.onload = () => {
if (!this.context) return;
this.context.globalCompositeOperation = 'source-over';
this.context.drawImage(image, 0, 0, this.width, this.height);
};
}
// Counting unique colors
calculate() {
const imageData = this.context?.getImageData(0, 0, this.width, this.height);
const data = imageData?.data || [];
const uniqueColors = new Set();
for (let i = 0; i < data?.length; i += 4) {
const [red, green, blue, alpha] = data.slice(i, i + 4);
const color = `rgba(${red}, ${green}, ${blue}, ${alpha})`;
uniqueColors.add(color);
}
this.uniqueColors = String(uniqueColors.size);
}
This is the implementation from the other site:
function countPixels(data) {
const colorCounts = {};
for(let index = 0; index < data.length; index += 4) {
const rgba = `rgba(${data[index]}, ${data[index + 1]}, ${data[index + 2]}, ${(data[index + 3] / 255)})`;
if (rgba in colorCounts) {
colorCounts[rgba] += 1;
} else {
colorCounts[rgba] = 1;
}
}
return colorCounts;
}
As you can see, besides the implementations being similar, they output very different results - my site says I have 256 unique colors, while the other says there's only two. I also tried to just copy and paste the implementation but I got the same 256. That's why I imagine the problem is in my canvas, but I can't figure out what's going on.
You are scaling your image, and since you didn't tell which interpolation algorithm to use, a default smoothing one is being used.
This will make all the pixels that were on fixed boundaries and should now span on multiple pixels to be "mixed" with their white neighbors and produce shades of gray.
There is an imageSmoothingEnabled property that tells the browser to use a closest-neighbor algorithm, which will improve the situation, but even then you may not have a perfect result:
const canvas = document.querySelector("canvas");
const width = canvas.width = innerWidth;
const height = canvas.height = innerHeight;
const ctx = canvas.getContext("2d");
const img = new Image();
img.crossOrigin = "anonymous";
img.src = "https://raw.githubusercontent.com/ajsaraujo/unique-color-count-mre/master/src/assets/image.png";
img.decode().then(() => {
ctx.imageSmoothingEnabled = false;
ctx.drawImage(img, 0, 0, width, height);
const data = ctx.getImageData(0, 0, width, height).data;
const pixels = new Set(new Uint32Array(data.buffer));
console.log(pixels.size);
});
<canvas></canvas>
So the best would be to not scale your image, or to do so in a computer friendly fashion (using a factor that is a multiple of 2).

Use an existing canvas without losing contents in pIxi.js

I am quite new to Pixi.js so I'm sorry this is a stupid question.
I understand that if I would like to render pixi.js to an existing canvas I have to specify view.
const app = new PIXI.Application({
view: myExistingCanvas,
});
However, I realized that if I write like this, Pixi application actually overwrites my existing canvas and I end up losing all the contents inside "myExistingCanvas".
Could somebody advise me how I can create a pixi application on top of an existing canvas without overwriting?
Using the view property you can pass to the constrctor of a new PIXI.Application, we can tell it to use an existing Canvas. This canvas though doesn't necessarily have to be added to the DOM - it's enough if it exists 'virtually'.
So ultimately we need three Canvas instances - which all should have equal dimensions.
The first canvas would be the existing canvas you've mentioned in your question and act as an off-screen canvas
The second canvas is another empty off-screen canvas, which captures Pixi's output
The third canvas is actually the on-screen canvas which combines the output of the previous two canvases
Now you might wonder how to do this.
To do this we must intercept Pixi's update loop, which we can do by adding a ticker to PIXI.Ticker.shared.
Inside this update loop we need to do the following things:
Update Pixi's animations and call it's renderer
Clear the third (on-screen) canvas
Draw the contents of the first canvas to the third
Draw the contents of the second canvas to the third
Basically that's it - though it might sound a bit abstract.
Here's an example (Just click on 'Run code snippet'):
let offScreenCanvasA = document.createElement("canvas");
let offScreenCanvasB = document.createElement("canvas");
let onScreenCanvas = document.createElement("canvas");
let width = 400;
let height = 300;
offScreenCanvasA.width = width;
offScreenCanvasB.width = width;
onScreenCanvas.width = width;
offScreenCanvasA.height = height;
offScreenCanvasB.height = height;
onScreenCanvas.height = height;
document.body.appendChild(onScreenCanvas);
const app = new PIXI.Application({
view: offScreenCanvasB,
transparent: true,
width: 400,
height: 300
});
const container = new PIXI.Container();
const renderer = PIXI.autoDetectRenderer();
app.stage.addChild(container);
const texture = PIXI.Texture.from('https://picsum.photos/id/237/26/37');
for (let i = 0; i < 25; i++) {
const bunny = new PIXI.Sprite(texture);
bunny.anchor.set(0.5);
bunny.x = (i % 5) * 40;
bunny.y = Math.floor(i / 5) * 40;
container.addChild(bunny);
}
container.x = app.screen.width / 2;
container.y = app.screen.height / 2;
container.pivot.x = container.width / 2;
container.pivot.y = container.height / 2;
let ticker = PIXI.Ticker.shared
ticker.add(function(delta) {
container.rotation -= 0.01;
renderer.render(container);
onScreenCanvas.getContext("2d").clearRect(0, 0, width, height);
onScreenCanvas.getContext("2d").drawImage(offScreenCanvasA, 0, 0, width, height);
onScreenCanvas.getContext("2d").drawImage(offScreenCanvasB, 0, 0, width, height);
});
let image = new Image();
image.onload = function(e) {
offScreenCanvasA.getContext("2d").drawImage(e.target, 0, 0, width, height);
}
image.src = "https://picsum.photos/id/237/400/300";
<script src="https://d157l7jdn8e5sf.cloudfront.net/dev/pixi-legacy.js"></script>
The dog picture in the background is the from the first canvas, and the rotating grid of dogs Pixi's output to the second canvas.

Keep canvas image after resizing [duplicate]

I have created a basic shape in HTML canvas element which works fine.
The problem occurs when I resize the canvas, all the drawing in the canvas disappears. Is this the normal behavior? or is there a function that can be used to stop this?
One way to fix this could be to call drawing function again on canvas resize however this may not be very efficient if there is huge content to be drawn.
What's the best way?
Here is the link to sample code https://gist.github.com/2983915
You need to redraw the scene when you resize.
setting the width or height of a canvas, even if you are setting it to the same value as before, not only clears the canvas but resets the entire canvas context. Any set properties (fillStyle, lineWidth, the clipping region, etc) will also be reset.
If you do not have the ability to redraw the scene from whatever data structures you might have representing the canvas, you can always save the entire canvas itself by drawing it to an in-memory canvas, setting the original width, and drawing the in-memory canvas back to the original canvas.
Here's a really quick example of saving the canvas bitmap and putting it back after a resize:
http://jsfiddle.net/simonsarris/weMbr/
Everytime you resize the canvas it will reset itself to transparant black, as defined in the spec.
You will either have to:
redraw when you resize the canvas, or,
don't resize the canvas
One another way is to use the debounce if you are concerned with the performance.
It doesnt resize or redraw every position you are dragging. But it will resize only when the it is resized.
// Assume canvas is in scope
addEventListener.("resize", debouncedResize );
// debounce timeout handle
var debounceTimeoutHandle;
// The debounce time in ms (1/1000th second)
const DEBOUNCE_TIME = 100;
// Resize function
function debouncedResize () {
clearTimeout(debounceTimeoutHandle); // Clears any pending debounce events
// Schedule a canvas resize
debounceTimeoutHandle = setTimeout(resizeCanvas, DEBOUNCE_TIME);
}
// canvas resize function
function resizeCanvas () { ... resize and redraw ... }
I had the same problem. Try following code
var wrapper = document.getElementById("signature-pad");
var canvas = wrapper.querySelector("canvas");
var ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
It keeps the drawing as it is
One thing that worked for me was to use requestAnimationFrame().
let height = window.innerHeight;
let width = window.innerWidth;
function handleWindowResize() {
height = window.innerHeight;
width = window.innerWidth;
}
function render() {
// Draw your fun shapes here
// ...
// Keep this on the bottom
requestAnimationFrame(render);
}
// Canvas being defined at the top of the file.
function init() {
ctx = canvas.getContext("2d");
render();
}
I had the same problem when I had to resize the canvas to adjust it to the screen.
But I solved it with this code:
var c = document.getElementById('canvas');
ctx = c.getContext('2d');
ctx.fillRect(0,0,20,20);
// Save canvas settings
ctx.save();
// Save canvas context
var dataURL = c.toDataURL('image/jpeg');
// Resize canvas
c.width = 50;
c.height = 50;
// Restore canvas context
var img = document.createElement('img');
img.src = dataURL;
img.onload=function(){
ctx.drawImage(img,20,20);
}
// Restote canvas settings
ctx.restore();
<canvas id=canvas width=40 height=40></canvas>
I also met this problem.but after a experiment, I found that Resizing the canvas element will automatically clear all drawings off the canvas!
just try the code below
<canvas id = 'canvas'></canvas>
<script>
var canvas1 = document.getElementById('canvas')
console.log('canvas size',canvas1.width, canvas1.height)
var ctx = canvas1.getContext('2d')
ctx.font = 'Bold 48px Arial'
var f = ctx.font
canvas1.width = 480
var f1 = ctx.font
alert(f === f1) //false
</script>

I cannot resize the Canvas in JavaScript

Ok, so, I'm working on a project in HTML5 and JavaScript. I'm trying to resize a Canvas, but it won't work. I don't think it's my browser, though, because I am using the latest version of FireFox. I've also researched this issue for a while, and I am confident I'm doing this correctly. So, I don't know why it won't work.
Here's my Code:
var level = 1;
var levelImg = undefined;
var width = 0;
var height = 0;
var cnvs = document.getElementById("cnvs").getContext("2d");
width = window.innerWidth
|| document.documentElement.clientWidth
|| document.body.clientWidth;
height = window.innerHeight
|| document.documentElement.cleintHeight
|| document.body.cleintHeight;
cnvs.width = width;
cnvs.height = height;
window.onload = function Init(){
levelImg = document.getElementById("level" + level);
setInterval("Draw()", 3);
}
function Draw(){
//Clear the Screen
cnvs.clearRect(0, 0, width, height);
//Draw stuff
DrawLevel();
}
function DrawLevel(){
cnvs.fillRect(0, 0, 10, 10);
}
Any help is appreciated. Thanks.
First correct all the typos in you code.
Use the browser console to detect errors.
Difference between canvas and the context
var cnvs = document.getElementById("cnvs").getContext("2d");
cnvs variable is not a canvas but the context for the canvas.
Canvas is the element and context is the object used to write in the canvas.
To access the canvas you need to do this:
var canvas = document.getElementById("cnvs");
var cnvs = canvas.getContext('2d'); //context
Now when you are trying to change the canvas with, you use canvas, not cnvs.
canvas.width = width;
canvas.height = height;
SetInterval expects a function and a number value that represents milliseconds.
"Draw()" is a string, not a function, and 3 is a really small number between each time the browser draws on canvas. It works, but it's very inefficient.
Other point about setInterval. Avoid it by using requestAnimationFrame() instead.
Take a look here: setTimeout or setInterval or requestAnimationFrame
Defining var levelImg = undefined has no utility. It can be replaced by var levelImg;

how to create a canvas dynamically in javascript

I have a canvas that you can draw things with mouse.. When I click the button It has to capture the drawing and add it right under the canvas, and clear the previous one to draw something new..So first canvas has to be static and the other ones has to be created dynamically with the drawing that I draw .. What should I do can anybody help
here is jsfiddle
http://jsfiddle.net/dQppK/378/
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
painting = false,
lastX = 0,
lastY = 0;
You can create a new canvas the same way you’d create any element:
var newCanvas = document.createElement('canvas');
Then you can copy over your old canvas:
newCanvas.width = oldCanvas.width;
newCanvas.height = oldCanvas.height;
oldCanvas.parentNode.replaceChild(newCanvas, oldCanvas);
ctx = newCanvas.getContext('2d');
But if you’re just looking to clear your drawing surface, what’s wrong with clearRect?
ctx.clearRect(0, 0, canvas.width, canvas.height);
Or, in your case, another fillRect. Updated demo
here's the function i use for this, it is part of a library i made and use to ease a few things about canvas.
I just put it on github in case other function might be be of use, i'll have to make a readme later...
https://github.com/gamealchemist/CanvasLib
with namespaceing removed, the code is as follow to insert a canvas :
// insert a canvas on top of the current document.
// If width, height are not provided, use all document width / height
// width / height unit is Css pixel.
// returns the canvas.
insertMainCanvas = function insertMainCanvas (_w,_h) {
if (_w==undefined) { _w = document.documentElement.clientWidth & (~3) ; }
if (_h==undefined) { _h = document.documentElement.clientHeight & (~3) ; }
var mainCanvas = ga.CanvasLib.createCanvas(_w,_h);
if ( !document.body ) {
var aNewBodyElement = document.createElement("body");
document.body = aNewBodyElement;
};
document.body.appendChild(mainCanvas);
return mainCanvas;
}

Categories