HTML Canvas blurry in Safari but not Chrome after translate3d - javascript

Running into an interesting canvas bug: after translating a canvas, the pixels appear blurry on Safari but not Chrome.
I've tried just about every image-rendering and imageSmoothing trick to no avail.
Here's a codepen where I've been able to reproduce the issue: https://codepen.io/plillian/pen/RwQegyR
Is this a just Safari bug? Or is there a way to force nearest neighbor in Safari as well?

Yes this is a Safari bug, you may want to let them know about it. For what it's worth, it's still an issue in the latest Technology Preview (Safari 15.4, WebKit 17614.1.14.10.6) where it's not even able to render every frame on time and will just "blink".
As for a workaround, the only one I can think of would be to do this all on the canvas directly, you can easily make this resizing of an ImageData by first converting it to an ImageBitmap and use drawImage().
Though to implement the scrolling behavior we'll have a bit of work to do.
One way is to use a placeholder <div> and make it act as-if we did transform our <canvas>. This way we can still use the native scrolling behavior and simply update the arguments to drawImage().
We then can stick the canvas on the top left corner of the viewport, and set it to the size of the viewport, overcoming the issue of possibly having a too big canvas.
(async () => {
const SIZE = 1024;
const X = -208.97398878415459;
const Y = 47.03519866364394;
const scale = 80;
const viewport = document.getElementById('viewport');
const wrapper = document.getElementById('wrapper');
const placeholder = document.getElementById('placeholder');
const canvas = document.getElementById('cvs');
placeholder.style.width = SIZE + 'px';
placeholder.style.height = SIZE + 'px';
const c = canvas.getContext('2d');
const pixels = new Uint8ClampedArray(4 * SIZE * SIZE);
for (let xi = 0; xi < SIZE; xi++) {
for (let yi = 0; yi < SIZE; yi++) {
const idx = (xi + yi * SIZE) * 4;
pixels[idx] = (xi << 6) % 255;
pixels[idx + 1] = (yi << 6) % 255;
pixels[idx + 3] = 255;
}
}
const pixelData = new ImageData(pixels, SIZE, SIZE);
// Convert to an ImageBitmap for ease of resizing and cropping
const bmp = await createImageBitmap(pixelData);
// We resize the canvas bitmap based on the size of the viewport
// While respecting the actual dPR (gimme crisp pixels!)
// Thanks to gman for the reminder of how to suppport all early impl.
// https://stackoverflow.com/a/65435847/3702797
const observer = new ResizeObserver(([entry]) => {
let width;
let height;
const dPR = devicePixelRatio;
if (entry.devicePixelContentBoxSize) {
width = entry.devicePixelContentBoxSize[0].inlineSize;
height = entry.devicePixelContentBoxSize[0].blockSize;
} else if (entry.contentBoxSize) {
if ( entry.contentBoxSize[0]) {
width = entry.contentBoxSize[0].inlineSize * dPR;
height = entry.contentBoxSize[0].blockSize * dPR;
} else {
width = entry.contentBoxSize.inlineSize * dPR;
height = entry.contentBoxSize.blockSize * dPR;
}
} else {
width = entry.contentRect.width * dPR;
height = entry.contentRect.height * dPR;
}
canvas.width = width;
canvas.height = height;
canvas.style.width = (width / dPR) + 'px';
canvas.style.height = (height / dPR) + 'px';
c.scale(dPR, dPR);
c.imageSmoothingEnabled = false;
});
// observe the scrollbox size changes
try {
observer.observe(viewport, { box: 'device-pixel-content-box' });
}
catch(err) {
observer.observe(viewport, { box: 'content-box' });
}
function getDrawImageArgs(nodetranslate) {
const { width, height } = canvas;
const { scrollLeft, scrollTop } = viewport;
const mat = new DOMMatrix(nodetranslate).inverse();
const source = mat.transformPoint({ x: scrollLeft, y: scrollTop });
const sourceWidth = canvas.width;
const sourceHeight = canvas.height;
return [source.x, source.y, sourceWidth, sourceHeight, 0, 0, canvas.width * scale, canvas.height * scale];
}
function animate() {
const nodetranslate = `translate3D(${X}px, ${Y}px, 0px) scale(${scale})`;
wrapper.style.transform = nodetranslate;
c.clearRect(0, 0, canvas.width, canvas.height);
c.drawImage(bmp, ...getDrawImageArgs(nodetranslate));
requestAnimationFrame(animate);
}
animate();
})().catch(console.error)
body { margin: 0 }
#viewport {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
overflow: auto;
}
.sticker {
position: sticky;
top: 0;
left: 0;
height: 0px;
width: 0px;
overflow: visible;
line-height: 0;
z-index: 1;
}
canvas {
position: absolute;
}
#wrapper {
transform-origin: 0 0;
position: absolute;
}
#placeholder {
display: inline-block;
}
<script>
// Because Safari wouldn't be Safari without all its little bugs...
// See https://stackoverflow.com/a/35503829/3702797
(()=>{if(function(){const e=document.createElement("canvas").getContext("2d");e.fillRect(0,0,40,40),e.drawImage(e.canvas,-40,-40,80,80,50,50,20,20);var a=e.getImageData(50,50,30,30),r=new Uint32Array(a.data.buffer),n=(e,t)=>r[t*a.width+e];return[[9,9],[20,9],[9,20],[20,20]].some(([e,t])=>0!==n(e,t))||[[10,10],[19,10],[10,19],[19,19]].some(([e,t])=>0===n(e,t))}()){const e=CanvasRenderingContext2D.prototype,i=e.drawImage;i?e.drawImage=function(e,t,a){if(!(9===arguments.length))return i.apply(this,[...arguments]);var r,n=function(e,t,a,r,n,i,o,h,m){var{width:s,height:d}=function(t){var e=e=>{e=globalThis[e];return e&&t instanceof e};{if(e("HTMLImageElement"))return{width:t.naturalWidth,height:t.naturalHeight};if(e("HTMLVideoElement"))return{width:t.videoWidth,height:t.videoHeight};if(e("SVGImageElement"))throw new TypeError("SVGImageElement isn't yet supported as source image.","UnsupportedError");return e("HTMLCanvasElement")||e("ImageBitmap")?t:void 0}}(e);r<0&&(t+=r,r=Math.abs(r));n<0&&(a+=n,n=Math.abs(n));h<0&&(i+=h,h=Math.abs(h));m<0&&(o+=m,m=Math.abs(m));var g=Math.max(t,0),u=Math.min(t+r,s),s=Math.max(a,0),d=Math.min(a+n,d),r=h/r,n=m/n;return[e,g,s,u-g,d-s,t<0?i-t*r:i,a<0?o-a*n:o,(u-g)*r,(d-s)*n]}(...arguments);return r=n,[3,4,7,8].some(e=>!r[e])?void 0:i.apply(this,n)}:console.error("This script requires a basic implementation of drawImage")}})();
</script>
<div id="viewport">
<div class="sticker">
<!-- <canvas> isn't a void element, it must have a closing tag -->
<!-- We place it in a "sticky" element, outside of the one that gets transformed -->
<canvas id="cvs"></canvas>
</div>
<div id="wrapper">
<div id="placeholder"><!--
We'll use it as an easy way to measure what part of the canvas we should draw
based on the current scroll position.
--></div>
<div>
</div>
You can inspect the <canvas> element and see it's actually only as big as the viewport and not some 81920x81920px.

Related

HTML5 Canvas pixellation issue on Safari (blurry) [duplicate]

Running into an interesting canvas bug: after translating a canvas, the pixels appear blurry on Safari but not Chrome.
I've tried just about every image-rendering and imageSmoothing trick to no avail.
Here's a codepen where I've been able to reproduce the issue: https://codepen.io/plillian/pen/RwQegyR
Is this a just Safari bug? Or is there a way to force nearest neighbor in Safari as well?
Yes this is a Safari bug, you may want to let them know about it. For what it's worth, it's still an issue in the latest Technology Preview (Safari 15.4, WebKit 17614.1.14.10.6) where it's not even able to render every frame on time and will just "blink".
As for a workaround, the only one I can think of would be to do this all on the canvas directly, you can easily make this resizing of an ImageData by first converting it to an ImageBitmap and use drawImage().
Though to implement the scrolling behavior we'll have a bit of work to do.
One way is to use a placeholder <div> and make it act as-if we did transform our <canvas>. This way we can still use the native scrolling behavior and simply update the arguments to drawImage().
We then can stick the canvas on the top left corner of the viewport, and set it to the size of the viewport, overcoming the issue of possibly having a too big canvas.
(async () => {
const SIZE = 1024;
const X = -208.97398878415459;
const Y = 47.03519866364394;
const scale = 80;
const viewport = document.getElementById('viewport');
const wrapper = document.getElementById('wrapper');
const placeholder = document.getElementById('placeholder');
const canvas = document.getElementById('cvs');
placeholder.style.width = SIZE + 'px';
placeholder.style.height = SIZE + 'px';
const c = canvas.getContext('2d');
const pixels = new Uint8ClampedArray(4 * SIZE * SIZE);
for (let xi = 0; xi < SIZE; xi++) {
for (let yi = 0; yi < SIZE; yi++) {
const idx = (xi + yi * SIZE) * 4;
pixels[idx] = (xi << 6) % 255;
pixels[idx + 1] = (yi << 6) % 255;
pixels[idx + 3] = 255;
}
}
const pixelData = new ImageData(pixels, SIZE, SIZE);
// Convert to an ImageBitmap for ease of resizing and cropping
const bmp = await createImageBitmap(pixelData);
// We resize the canvas bitmap based on the size of the viewport
// While respecting the actual dPR (gimme crisp pixels!)
// Thanks to gman for the reminder of how to suppport all early impl.
// https://stackoverflow.com/a/65435847/3702797
const observer = new ResizeObserver(([entry]) => {
let width;
let height;
const dPR = devicePixelRatio;
if (entry.devicePixelContentBoxSize) {
width = entry.devicePixelContentBoxSize[0].inlineSize;
height = entry.devicePixelContentBoxSize[0].blockSize;
} else if (entry.contentBoxSize) {
if ( entry.contentBoxSize[0]) {
width = entry.contentBoxSize[0].inlineSize * dPR;
height = entry.contentBoxSize[0].blockSize * dPR;
} else {
width = entry.contentBoxSize.inlineSize * dPR;
height = entry.contentBoxSize.blockSize * dPR;
}
} else {
width = entry.contentRect.width * dPR;
height = entry.contentRect.height * dPR;
}
canvas.width = width;
canvas.height = height;
canvas.style.width = (width / dPR) + 'px';
canvas.style.height = (height / dPR) + 'px';
c.scale(dPR, dPR);
c.imageSmoothingEnabled = false;
});
// observe the scrollbox size changes
try {
observer.observe(viewport, { box: 'device-pixel-content-box' });
}
catch(err) {
observer.observe(viewport, { box: 'content-box' });
}
function getDrawImageArgs(nodetranslate) {
const { width, height } = canvas;
const { scrollLeft, scrollTop } = viewport;
const mat = new DOMMatrix(nodetranslate).inverse();
const source = mat.transformPoint({ x: scrollLeft, y: scrollTop });
const sourceWidth = canvas.width;
const sourceHeight = canvas.height;
return [source.x, source.y, sourceWidth, sourceHeight, 0, 0, canvas.width * scale, canvas.height * scale];
}
function animate() {
const nodetranslate = `translate3D(${X}px, ${Y}px, 0px) scale(${scale})`;
wrapper.style.transform = nodetranslate;
c.clearRect(0, 0, canvas.width, canvas.height);
c.drawImage(bmp, ...getDrawImageArgs(nodetranslate));
requestAnimationFrame(animate);
}
animate();
})().catch(console.error)
body { margin: 0 }
#viewport {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
overflow: auto;
}
.sticker {
position: sticky;
top: 0;
left: 0;
height: 0px;
width: 0px;
overflow: visible;
line-height: 0;
z-index: 1;
}
canvas {
position: absolute;
}
#wrapper {
transform-origin: 0 0;
position: absolute;
}
#placeholder {
display: inline-block;
}
<script>
// Because Safari wouldn't be Safari without all its little bugs...
// See https://stackoverflow.com/a/35503829/3702797
(()=>{if(function(){const e=document.createElement("canvas").getContext("2d");e.fillRect(0,0,40,40),e.drawImage(e.canvas,-40,-40,80,80,50,50,20,20);var a=e.getImageData(50,50,30,30),r=new Uint32Array(a.data.buffer),n=(e,t)=>r[t*a.width+e];return[[9,9],[20,9],[9,20],[20,20]].some(([e,t])=>0!==n(e,t))||[[10,10],[19,10],[10,19],[19,19]].some(([e,t])=>0===n(e,t))}()){const e=CanvasRenderingContext2D.prototype,i=e.drawImage;i?e.drawImage=function(e,t,a){if(!(9===arguments.length))return i.apply(this,[...arguments]);var r,n=function(e,t,a,r,n,i,o,h,m){var{width:s,height:d}=function(t){var e=e=>{e=globalThis[e];return e&&t instanceof e};{if(e("HTMLImageElement"))return{width:t.naturalWidth,height:t.naturalHeight};if(e("HTMLVideoElement"))return{width:t.videoWidth,height:t.videoHeight};if(e("SVGImageElement"))throw new TypeError("SVGImageElement isn't yet supported as source image.","UnsupportedError");return e("HTMLCanvasElement")||e("ImageBitmap")?t:void 0}}(e);r<0&&(t+=r,r=Math.abs(r));n<0&&(a+=n,n=Math.abs(n));h<0&&(i+=h,h=Math.abs(h));m<0&&(o+=m,m=Math.abs(m));var g=Math.max(t,0),u=Math.min(t+r,s),s=Math.max(a,0),d=Math.min(a+n,d),r=h/r,n=m/n;return[e,g,s,u-g,d-s,t<0?i-t*r:i,a<0?o-a*n:o,(u-g)*r,(d-s)*n]}(...arguments);return r=n,[3,4,7,8].some(e=>!r[e])?void 0:i.apply(this,n)}:console.error("This script requires a basic implementation of drawImage")}})();
</script>
<div id="viewport">
<div class="sticker">
<!-- <canvas> isn't a void element, it must have a closing tag -->
<!-- We place it in a "sticky" element, outside of the one that gets transformed -->
<canvas id="cvs"></canvas>
</div>
<div id="wrapper">
<div id="placeholder"><!--
We'll use it as an easy way to measure what part of the canvas we should draw
based on the current scroll position.
--></div>
<div>
</div>
You can inspect the <canvas> element and see it's actually only as big as the viewport and not some 81920x81920px.

Tilt element pane on mousemove

I'm trying to redo the animation that I saw on a site where an image changes it's x and y values with the movement of the mouse. The problem is that the origin of the mouse is in the top left corner and I'd want it to be in the middle.
To understand better, here's how the mouse axis values work :
Now here's how I'd want it to be:
sorry for the bad quality of my drawings, hope you understand my point from those ^^
PS: I'm having a problem while trying to transform the x y values at the same time and I don't know why.
Here's what I wrote in JavaScript :
document.onmousemove = function(e){
var x = e.clientX;
var y = e.clientY;
document.getElementById("img").style.transform = "rotateX("+x*0.005+"deg)";
document.getElementById("img").style.transform = "rotateY("+y*0.005+"deg)";
}
The exact 3D effect you're up to is called "tilting".
Long story short, it uses CSS transform's rotateX() and rotateY() on a child element inside a perspective: 1000px parent. The values passed for the rotation are calculated from the mouse/pointer coordinates inside the parent Element and transformed to a respective degree value.
Here's a quick simplified remake example of the original script:
const el = (sel, par) => (par || document).querySelector(sel);
const elWrap = el("#wrap");
const elTilt = el("#tilt");
const settings = {
reverse: 0, // Reverse tilt: 1, 0
max: 35, // Max tilt: 35
perspective: 1000, // Parent perspective px: 1000
scale: 1, // Tilt element scale factor: 1.0
axis: "", // Limit axis. "y", "x"
};
elWrap.style.perspective = `${settings.perspective}px`;
const tilt = (evt) => {
const bcr = elWrap.getBoundingClientRect();
const x = Math.min(1, Math.max(0, (evt.clientX - bcr.left) / bcr.width));
const y = Math.min(1, Math.max(0, (evt.clientY - bcr.top) / bcr.height));
const reverse = settings.reverse ? -1 : 1;
const tiltX = reverse * (settings.max / 2 - x * settings.max);
const tiltY = reverse * (y * settings.max - settings.max / 2);
elTilt.style.transform = `
rotateX(${settings.axis === "x" ? 0 : tiltY}deg)
rotateY(${settings.axis === "y" ? 0 : tiltX}deg)
scale(${settings.scale})
`;
}
elWrap.addEventListener("pointermove", tilt);
/*QuickReset*/ * {margin:0; box-sizing: border-box;}
html, body { min-height: 100vh; }
#wrap {
height: 100vh;
display: flex;
background: no-repeat url("https://i.stack.imgur.com/AuRxH.jpg") 50% 50% / cover;
}
#tilt {
outline: 1px solid red;
height: 80vh;
width: 80vw;
margin: auto;
background: no-repeat url("https://i.stack.imgur.com/wda9r.png") 50% 50% / contain;
}
<div id="wrap"><div id="tilt"></div></div>
Regarding your code:
Avoid using on* event handlers (like onmousemove). Use EventTarget.addEventListener() instead — unless you're creating brand new Elements from in-memory. Any additionally added on* listener will override the previous one. Bad programming habit and error prone.
You cannot use style.transform twice (or more) on an element, since the latter one will override any previous - and the transforms will not interpolate. Instead, use all the desired transforms in one go, using Transform Matrix or by concatenating the desired transform property functions like : .style.transform = "rotateX() rotateY() scale()" etc.
Disclaimer: The images used in the above example from the original problem's reference website https://cosmicpvp.com might be subject to copyright. Here are used for illustrative and educative purpose only.
You can find out how wide / tall the screen is:
const width = window.innerWidth;
const height = window.innerHeight;
So you can find the centre of the screen:
const windowCenterX = width / 2;
const windowCenterY = height / 2;
And transform your mouse coordinates appropriately:
const transformedX = x - windowCenterX;
const transformedY = y - windowCenterY;
Small demo:
const coords = document.querySelector("#coords");
document.querySelector("#area").addEventListener("mousemove", (event)=>{
const x = event.clientX;
const y = event.clientY;
const width = window.innerWidth;
const height = window.innerHeight;
const windowCenterX = width / 2;
const windowCenterY = height / 2;
const transformedX = x - windowCenterX;
const transformedY = y - windowCenterY;
coords.textContent = `x: ${transformedX}, y: ${transformedY}`;
});
body, html, #area {
margin: 0;
width: 100%;
height: 100%;
}
#area {
background-color: #eee;
}
#coords {
position: absolute;
left: 10px;
top: 10px;
}
<div id="area"></div>
<div id="coords"></div>
I think I would use the bounding rect of the image to determine the center based on the image itself rather than the screen... something like this (using CSSVars to handle the transform)
const img = document.getElementById('fakeimg')
addEventListener('pointermove', handler)
function handler(e) {
const rect = img.getBoundingClientRect()
const x1 = (rect.x + rect.width / 2)
const y1 = (rect.y + rect.height / 2)
const x2 = e.clientX
const y2 = e.clientY
let angle = Math.atan2(y2 - y1, x2 - x1) * (180 / Math.PI) + 90
angle = angle < 0 ?
360 + angle :
angle
img.style.setProperty('--rotate', angle);
}
*,
*::before,
*::after {
box-sizeing: border-box;
}
html,
body {
height: 100%;
margin: 0
}
body {
display: grid;
place-items: center;
}
[id=fakeimg] {
width: 80vmin;
background: red;
aspect-ratio: 16 / 9;
--rotation: calc(var(--rotate) * 1deg);
transform: rotate(var(--rotation));
}
<div id="fakeimg"></div>

Text is vertically stretched on my canvas, making it unreadable

I have a canvas that I am drawing on top of a OpenLayers map. The canvas is colored in gradient colors and now I am trying to add text on top of it.
However the text seems to be stretched making it completely unreadable.
function createSpeedBar(min, max, speeds) {
//List of integer speeds
var fullSpeeds = [];
//List of unique speed values (no duplicates)
var uniqueSpeeds = [];
//Filling fullSpeeds using minimum and maximum values
for (i = min; i <= max; i++) {
fullSpeeds.push(i);
}
//Filling uniqueSpeeds with unique values
$.each(speeds, function (i, el) {
if ($.inArray(el, uniqueSpeeds) === -1) uniqueSpeeds.push(el);
});
//Sorting uniqueSpeeds (low to high)
uniqueSpeeds = uniqueSpeeds.sort(function (a, b) {
return a - b;
});
//Getting canvas element
var canvas = document.getElementById("speedList");
var context = canvas.getContext("2d");
context.rect(0, 0, canvas.width, canvas.height);
var grd = context.createLinearGradient(0, 0, 0, 170);
var hslMin = 0;
var hslMax = 240;
//Defining hslColors using uniqueSpeeds
var hslValues = uniqueSpeeds.map(function (value) {
if ($.inArray(value, fullSpeeds)){
return {
h: Math.ceil(((value - min) / (max - min)) * (hslMax - hslMin) + hslMin),
s: 100,
l: 50,
full: true,
speed: value
};
} else {
return {
h: Math.ceil(((value - min) / (max - min)) * (hslMax - hslMin) + hslMin),
s: 100,
l: 50,
full: false
};
};
});
var count = 1;
var length = hslValues.length;
//Gradient coloring using hslColors
hslValues.forEach(function (value) {
var color = 'hsl(' + value.h + ',' + value.s + '%,' + value.l + '%)';
grd.addColorStop(count / length, color)
count += 1;
});
context.fillStyle = grd;
context.fill();
//Setting up coloring and drawing of text
count = 1
var height = canvas.height;
var width = canvas.width;
var elementHeight = height / length;
//Drawing text on canvas
hslValues.forEach(function (value) {
context.font = "12px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillStyle = "black";
if (value.full === true) {
context.fillText(value.speed, width / 2, ((elementHeight / 2) + elementHeight * count));
}
count += 1;
});
}
As it might be clear I am trying to create a bar displaying the intensities of the speed on the map where I have colored some markers. However the text on the canvas comes out like this:
Right now I have made the height of the canvas inherit the height of the map which is 500. The width of the canvas is set manually using css:
html
<div id="map" class="map">
<canvas id="speedList"></canvas>
</div>
css
#map {
position: relative;
height: 500px;
}
#speedList {
position: absolute;
right: 10px;
top: 0;
bottom: 0;
width: 20px;
z-index: 1000;
height: inherit;
margin: 0;
padding: 0;
}
I am currently working on a Fiddle, but it takes a little time to reproduce, and I bet the issue is not that big, so hopefully someone knows how to fix it before I finish the Fiddle.
The main problem here is the CSS adjustment of the width and height of the canvas element.
If it helps to understand the problem, think of <canvas> the same way you would think of <img/>, if you take an img, and give it a width and height of 50 x 500, it would stretch too.
The fix is to ensure that you set the width an height of the canvas element itself, before it processes it's content, like this:
<canvas id="speedList" width="20" height="500"></canvas>
You then also need to make sure your remove the width and height properties inside your CSS, like this:
#map {
position: relative;
height: 500px;
}
#speedList {
position: absolute;
right: 10px;
top: 0;
bottom: 0;
z-index: 1000;
margin: 0;
padding: 0;
}

Some bugs in code using Canvas

I have a scratch card simulator.
The user should be able to click and drag in order to reveal the text underneath.
I have 2 bugs with this implementaion:
a) Sometimes the scratch card clears itself as soon as the cursor enters the canvas from left or right. It should only clear itself when most of the card has been scratched off. Currently, it only works if the user moves their cursor in from the top border, going downwards.
b) Sometimes the scratch card will not work at all, or the scratching will be offset from the cursor, but only when the browser window is smaller than the document size (e.g., browser window is 300px wide but the body has a min-width of 900px or something and the user had to scroll the card into view).
(function () {
"use strict";
var container = document.getElementById('cbox-canvas'),
arrow = document.getElementById('cbox-arrow'),
textOne = document.getElementById('cbox-text-1'),
textTwo = document.getElementById('cbox-text-2'),
boxOne = document.getElementById('cbox-box-1'),
boxTwo = document.getElementById('cbox-box-2'),
cnv = container.getElementsByTagName('canvas'),
imageCover;
function createCanvas(parent, width, height) {
var canvas = {};
canvas.node = document.createElement('canvas');
canvas.context = canvas.node.getContext('2d');
canvas.node.width = width || 100;
canvas.node.height = height || 100;
parent.appendChild(canvas.node);
return canvas;
}
function init(container, width, height, fillColor) {
var canvas = createCanvas(container, width, height),
ctx = canvas.context;
// define a custom fillCircle method
ctx.fillCircle = function (x, y, radius, fillColor) {
//this.fillStyle = fillColor;
this.shadowBlur = 15;
this.shadowOffsetX = 0;
this.shadowOffsetY = 0;
this.shadowColor = fillColor;
this.beginPath();
this.moveTo(x, y);
this.arc(x, y, radius, 0, Math.PI * 2, false);
this.fill();
this.stroke();
};
ctx.clearTo = function (fillColor) {
var imageObj = new Image();
imageObj.onload = function () {
ctx.drawImage(imageObj, 0, 0);
};
imageObj.src = fillColor;
};
ctx.clearTo(fillColor || "#ddd");
// bind mouse events
canvas.node.onmousemove = function (e) {
var canvasRect = container.getBoundingClientRect(),
x = e.pageX - canvasRect.left,
y = e.pageY - canvasRect.top,
radius = 30,
calc = 0;
fillColor = '#ff0000';
ctx.globalCompositeOperation = 'destination-out';
ctx.fillCircle(x, y, radius, fillColor);
calc += x;
if (calc > 330 || calc < 6) {
container.removeChild(cnv[0]);
arrow.className += " slide-it";
textOne.className += " reveal-it";
textTwo.className += " fade-in";
boxOne.className += " fade-in-two";
boxTwo.className += " fade-in-one";
}
};
container.onmousemove = function (e) {
var canvasRect = container.getBoundingClientRect(),
mouseX = e.pageX || e.clientX,
mouseY = e.pageY || e.clientY,
relMouseX = mouseX - canvasRect.left,
relMouseY = mouseY - canvasRect.top,
leftLimit = 37,
topLimit = 37,
rightLimit = 25,
bottomLimit = 44,
x = e.pageX - canvasRect.left,
y = e.pageY - canvasRect.top,
radius = 25;
fillColor = '#ff0000';
if (relMouseX < leftLimit) {
relMouseX = leftLimit;
}
if (relMouseY < topLimit) {
relMouseY = topLimit;
}
if (relMouseX > width - rightLimit) {
relMouseX = width - rightLimit;
}
if (relMouseY > height - bottomLimit) {
relMouseY = height - bottomLimit;
}
if (!canvas.isDrawing) {
return;
}
ctx.globalCompositeOperation = 'destination-out';
ctx.fillCircle(x, y, radius, fillColor);
};
}
imageCover = "images/scratch.png";
init(container, 369, 371, imageCover);
}());
https://jsfiddle.net/p05kg0vq/
Problems
There are several issues here:
You are loading the clear image for each time which happens asynchronously and it may not appear in time.
The same method, clearTo(), also takes a fill-style color, but tries to load it as image
You are listening to mouse move events two places which is not needed
For improvement: You're listening to mouse move events on the canvas. This is not wrong, but it will be more fluid using the window object
You're using pageX/Y to calculate mouse position. These are relative to page not client. getBoundingClientRect() is relative to client.
Not sure how you intend to calculate coverage
There are additional room for refactoring.
Solutions
Load image once globally and use the object as argument instead of the URL.
Differentiate between an image and color string. To do this check if argument is a string, if so, set fillStyle and use fillRect() to clear. If not use drawImage().
Remove the event from the container. It's not needed and will conflict with the second listener.
Use window.onmousemove instead (not required, but a better option in this case as it will move the cursor completely outside canvas - optionally use a parent node that is wider - it's up to you...).
Calculate using clientX and clientY instead and always.
Extract the ImageData and count pixels with no alpha data (=0). Then divide this count on total number of pixels to get percentage of coverage. This is fast (I'll show how below).
Left to OP to improve :)
So, lets modify the structure a bit. This is not optimal, but meant to get you started. Load the image once and globally (or within the parent scope so the object is accessible).
// preload image once
var imageCover = "//i.imgur.com/b4m1M1n.png"; // needed cors for demo
var imageObj = new Image();
imageObj.onload = go;
imageObj.crossOrigin = ""; // for demo, for getImageData to work
imageObj.src = imageCover;
function go() {
/* ... inner code not shown ... */
init(container, 369, 371, imageObj);
};
Then rewrite clearTo() to accept both image and fill style. Notice this may break browser optimizations as there are two different types involved, but in this case it likely doesn't matter:
ctx.clearTo = function(fillColor) {
if (typeof fillColor === "string") { // is a string?
ctx.fillStyle = fillColor; // set as fill style
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
else { // assumes an image if not string
ctx.drawImage(fillColor, 0, 0);
}
};
ctx.clearTo(fillColor || "#ddd");
Then move onmousemove to window object and use clientX/clientY:
window.onmousemove = function(e) {
var canvasRect = container.getBoundingClientRect(),
x = e.clientX - canvasRect.left, // use clientX/Y (pageXY is unofficial)
y = e.clientY - canvasRect.top,
/* ... */
Within the same code block provide a function calculate real-time coverage of canvas:
// calc converage and clean if < 20%
if (calcCover(ctx) < 0.2) {
// end, reveal, etc.
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
console.log("DONE");
};
The function used here does:
function calcCover(ctx) {
var w = ctx.canvas.width, // just to cache width/height
h = ctx.canvas.height,
// convert Uint8ClampedArray to Uint32Array, no memory loss
// but faster
data32 = new Uint32Array(ctx.getImageData(0,0,w,h).data.buffer),
count = w * h; // total number of pixels
// iterate, check for alpha-channel (0xAABBGGRR, little-endian format)
// for the Uint32Array data.
for(var i = 0; i < data32.length; i++) if (!(data32[i] & 0xff000000)) count--;
// convert to a percentage (or rather normalize)
return count / (w*h);
};
Now we are good to go:
Modified fiddle
Looking at your code I could only suggest a full rewrite
;(function () {
"use strict";
// Generic functions and constants
const PI2 = Math.PI * 2;
function applyStyle (ctx, style) { Object.keys(style).forEach(key => ctx[key] = style[key] ) }
function ease (val, power) { return val < 0 ? 0 : val > 1 ? 1 : Math.pow(val, power) }
// General settings
const settings = {
width : 369,
height : 371,
coveragedMin : 0.2, // when to uncover all out of 1
coverColor : "#ddd", // colour to show on canvas while main image is loading. (not needed but to keep with you code)
mouseEvents : "mouseup,mousedown,mousemove".split(","), // list of mouse events to listen to
coverImage : loadImage("https://image.ibb.co/f8TNS5/scratch.png"), // the scratch image
container : document.getElementById('cbox-canvas'), // the container
drawStyle : { // the draw style of the revealing mouse moves. Note that this adds radius to the context but should not matter
radius : 20,
shadowBlur : 15,
shadowOffsetX : 0,
shadowOffsetY : 0,
shadowColor : "black",
fillStyle : "black",
globalCompositeOperation : "destination-out",
},
startAnim (){ // specific to this scratch reveal animations
document.getElementById("cbox-arrow").className = "cbox-arrow slide-it";
document.getElementById("cbox-text-1").className = "cbox-text-1 reveal-it";
document.getElementById("cbox-box-1").className = "cbox-box-1 fade-in-two";
document.getElementById("cbox-box-2").className = "cbox-box-2 fade-in-one";
document.getElementById("cbox-text-2").className = "cbox-text-2 fade-in";
},
coverageArray : (() => {const buf = new Uint8Array(64); buf.fill(1); return buf }) (), // array to is used to determine coverage
}
var update = true; // when true update canvas render
const mouse = { x : 0, y : 0, button : false}; // Mouse state
function mouseEvent (e) { // handles all mouse events
const canvasRect = settings.container.getBoundingClientRect();
mouse.x = e.pageX - canvasRect.left - scrollX;
mouse.y = e.pageY - canvasRect.top - scrollY;
if (e.type === "mousedown") { mouse.button = true }
else if (e.type === "mouseup") { mouse.button = false }
update = true; // flags that there needs to be a re render
}
function fillCircle (ctx, x, y, style) { // Draws a circle on context ctx, at location x,y using style
applyStyle(ctx, style);
ctx.beginPath();
ctx.arc(x, y, style.radius, 0, Math.PI * 2);
ctx.fill();
}
function setCoverage (array,x,y){ // Clears the coverage array, coordinates x,y are normalised 0-1
var i = array.length - 1; // and returns coverage as a value 0 no coverage to 1 full cover
const size = Math.sqrt(array.length) | 0;
array[(x * size) | 0 + ((y * size) | 0) * size] = 0;
var count = 0;
while(i-- > 0){ count += array[i] };
return count / array.length;
}
function loadImage (url) { // Loads an image and sets a property indicating if its has been rendered
const image = new Image();
image.src = url;
image.rendered = false;
return image;
}
function createCanvas (width, height) { // Creates a canvas of size width and height, set property ctx to the 2D context
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.ctx = canvas.getContext("2d");
return canvas;
}
(function (settings) { // Start the app
const canvas = createCanvas(settings.width, settings.height);
settings.container.appendChild(canvas);
const ctx = canvas.ctx;
settings.mouseEvents.forEach(eventName => addEventListener(eventName, mouseEvent)); // start the mouse
var reveal = false; // when true reveal the prize (yep suckers) ???
var fade = 1; // fades out the canvas
ctx.fillStyle = settings.coverColor; // cover while waiting for image to load
ctx.fillRect(0, 0, canvas.width, canvas.height); // image will not yet have loaded so cover image
(function mainLoop () { // main animation loop will play unt ill canvas faded out
if (settings.coverImage.complete && !settings.coverImage.rendered) { // wait till image has loaded
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(settings.coverImage, 0, 0, settings.width, settings.height);
settings.coverImage.rendered = true;
const swipeEl = document.getElementById("swipe-area");
swipeEl.className = "swipe-area loaded";
swipeEl.title = "Use your mouse to reveal your PRIZE :P";
}
if (update) { // only if needed render canvas
if (settings.coverImage.rendered) {
mouse.button && fillCircle(ctx, mouse.x, mouse.y, settings.drawStyle);
setCoverage(settings.coverageArray, mouse.x / settings.width, mouse.y / settings.height) < settings.coveragedMin && (reveal = true);
update = false;
}
if (reveal) {
fade -= 0.05;
canvas.style.opacity = ease(fade,2);
update = true; // need continuous update for animation
}
}
if (reveal && fade <= 0) { // scratching all done remove canvas, mouse events and start any animations. Do not call requestAnimationFrame as all done.
const swipeEl = document.getElementById("swipe-area");
swipeEl.style.cursor = "pointer";
swipeEl.title = "Click here to collect your $$$$";
settings.container.removeChild(canvas);
settings.mouseEvents.forEach(eventName => removeEventListener(eventName, mouseEvent));
settings.startAnim();
// All done. All objects should now have no references (important to remove mouse and requestAnimation frame) and any other functions
// that can hold a closure
} else {
requestAnimationFrame(mainLoop);
}
} () );
} (settings) );
} () );
/*SWIPE*/
.swipe-area {
position: absolute;
width: 369px;
height: 371px;
left: 10px;
top: 10px;
z-index: 15;
background-size: 100%;
}
.preload {
cursor : wait;
background: url('https://image.ibb.co/f8TNS5/scratch.png') no-repeat;
background-size: 100%;
}
.loaded {
cursor : url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAbCAYAAABm409WAAABkklEQVRIibXWPWtUQRjF8d/GbDRFomghZFNorREMiIUgpEgRSFJFuyAWNoovTQIqdvZp8lXCBb+ApZ9AApb2lmFS3Jns3XVm9ya7+8DDwMA9/3vOM/eFfC0W9qdSodHTFz46FkIQfv4S8GdaoCQY6AMaoIkgoapqodQJkgCTQgbE/4ecTOwkC+hDTi76qpAiYBAysHc6M0gIM3MxOPjLQkYCcpBLA4aPagHyNwGqasYurgRo6SIH6UzVRVX1OwKWcAMLmBsFOGvrYgiwhvtYxU3MTxRVxsVvbGEDD3Ab10qAzriomoCj4wsX+9jDM/RiXNmax+NxTsRvRwNwgI94GSNbUhj+Qszx7SgnCdCYwzd8xms8wS2FgSfAOr4348jF1BD/isMY1XrUyAK60d4aXuCH/NFMfYgvcf3UJqK56KKnHtge3uDfkPAHvI/rO7yKNzR2yKkWsYKHeI5NbGMHu3HdiXubeIpHUbz1b0+K62688F6he7iD5TZ3Plwd9dHt4nqhu+oHq/U7KQfpqOeT66LwOUobOkEQCJp1AAAAAElFTkSuQmCC') 9 20, pointer;
background: url('https://image.ibb.co/j4j7uk/sc_bg.png') no-repeat;
background-size: 100%;
}
.anim-container {
position: absolute;
width: 360px;
height: 366px;
right: 5px;
top: 5px;
z-index: -1;
background-size: 100%;
overflow: hidden;
}
.cbox-arrow {
position: absolute;
left: 56px;
top: 0;
z-index: -1;
width: 260px;
height: 264px;
background: url('https://image.ibb.co/fXKwn5/arrow.png') no-repeat;
background-size: 100%;
-webkit-animation: 10s slide;
}
.cbox-text-1 {
position: absolute;
left: 72px;
top: 100px;
z-index: -1;
width: 230px;
height: 65px;
background: url('https://image.ibb.co/d6YYZk/test1.png') no-repeat;
background-size: 100%;
opacity: 1;
}
.cbox-text-2 {
position: absolute;
left: 72px;
top: 100px;
z-index: -1;
width: 230px;
height: 65px;
background: url('https://image.ibb.co/bCaQfQ/test2.png') no-repeat;
background-size: 100%;
opacity: 0;
}
.cbox-box-1 {
position: absolute;
left: 55px;
top: 167px;
z-index: -1;
width: 257px;
height: 65px;
background: url('https://image.ibb.co/fG7hS5/box1.png') no-repeat;
background-size: 100%;
opacity: 0;
}
.cbox-box-2 {
position: absolute;
left: 135px;
top: 124px;
z-index: -1;
width: 99px;
height: 127px;
background: url('https://image.ibb.co/dOSSuk/box2.png') no-repeat;
background-size: 100%;
opacity: 0;
}
.hidden { display: none;}
/* unknowns */
.newslisting, #sidebar1Bottom { background: #ffffff !important; }
.tmx header { height: 269px;}
/*Animations, you can add agent prescripts, though we should never have to do that */
.fade-in { animation: 1.5s 2.5s fade;}
.fade-in-one { animation: 2.5s 5s fade;}
.fade-in-two { animation: 2.5s 7.5s fade-alt forwards;}
.fade-in-three { animation: 5s 15s fade;}
.reveal-it { animation: 2.5s reveal forwards;}
.slide-it { animation: 5s slide-in forwards;}
#keyframes fade-alt {0% { opacity: 0; } 10% {opacity : 1;} 100% { opacity: 1;} }
#keyframes fade { 0% { opacity: 0; } 10% {opacity: 1;} 90% {opacity: 1;} 100% {opacity: 0;} }
#keyframes reveal {0% { opacity: 1;} 80% { opacity: 1;} 100% { opacity: 0;} }
#keyframes slide-in {0% { top: 5px; } 80% { top: 5px; } 100% { top: -150px;} }
<div class="swipe-area preload" id="swipe-area" title="Just a moment as we asses your gullibility!">
<div id="cbox-canvas">
<div class="anim-container">
<div id="cbox-arrow" class="hidden"></div>
<div id="cbox-text-1" class="hidden"></div>
<div id="cbox-box-1" class="hidden"></div>
<div id="cbox-box-2" class="hidden"></div>
<div id="cbox-text-2" class="hidden"></div>
</div>
</div>
</div>

Resize HTML5 canvas to fit window

How can I automatically scale the HTML5 <canvas> element to fit the page?
For example, I can get a <div> to scale by setting the height and width properties to 100%, but a <canvas> won't scale, will it?
I believe I have found an elegant solution to this:
JavaScript
/* important! for alignment, you should make things
* relative to the canvas' current width/height.
*/
function draw() {
var ctx = (a canvas context);
ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight;
//...drawing code...
}
CSS
html, body {
width: 100%;
height: 100%;
margin: 0;
}
Hasn't had any large negative performance impact for me, so far.
The following solution worked for me the best. Since I'm relatively new to coding, I like to have visual confirmation that something is working the way I expect it to. I found it at the following site:
http://htmlcheats.com/html/resize-the-html5-canvas-dyamically/
Here's the code:
(function() {
var
// Obtain a reference to the canvas element using its id.
htmlCanvas = document.getElementById('c'),
// Obtain a graphics context on the canvas element for drawing.
context = htmlCanvas.getContext('2d');
// Start listening to resize events and draw canvas.
initialize();
function initialize() {
// Register an event listener to call the resizeCanvas() function
// each time the window is resized.
window.addEventListener('resize', resizeCanvas, false);
// Draw canvas border for the first time.
resizeCanvas();
}
// Display custom canvas. In this case it's a blue, 5 pixel
// border that resizes along with the browser window.
function redraw() {
context.strokeStyle = 'blue';
context.lineWidth = '5';
context.strokeRect(0, 0, window.innerWidth, window.innerHeight);
}
// Runs each time the DOM window resize event fires.
// Resets the canvas dimensions to match window,
// then draws the new borders accordingly.
function resizeCanvas() {
htmlCanvas.width = window.innerWidth;
htmlCanvas.height = window.innerHeight;
redraw();
}
})();
html,
body {
width: 100%;
height: 100%;
margin: 0px;
border: 0;
overflow: hidden;
/* Disable scrollbars */
display: block;
/* No floating content on sides */
}
<canvas id='c' style='position:absolute; left:0px; top:0px;'></canvas>
The blue border shows you the edge of the resizing canvas, and is always along the edge of the window, visible on all 4 sides, which was NOT the case for some of the other above answers. Hope it helps.
Basically what you have to do is to bind the onresize event to your body, once you catch the event you just need to resize the canvas using window.innerWidth and window.innerHeight.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Canvas Resize</title>
<script type="text/javascript">
function resize_canvas(){
canvas = document.getElementById("canvas");
if (canvas.width < window.innerWidth)
{
canvas.width = window.innerWidth;
}
if (canvas.height < window.innerHeight)
{
canvas.height = window.innerHeight;
}
}
</script>
</head>
<body onresize="resize_canvas()">
<canvas id="canvas">Your browser doesn't support canvas</canvas>
</body>
</html>
Setting the canvas coordinate space width and height based on the browser client's dimensions requires you to resize and redraw whenever the browser is resized.
A less convoluted solution is to maintain the drawable dimensions in Javascript variables, but set the canvas dimensions based on the screen.width, screen.height dimensions. Use CSS to fit:
#containingDiv {
overflow: hidden;
}
#myCanvas {
position: absolute;
top: 0px;
left: 0px;
}
The browser window generally won't ever be larger than the screen itself (except where the screen resolution is misreported, as it could be with non-matching dual monitors), so the background won't show and pixel proportions won't vary. The canvas pixels will be directly proportional to the screen resolution unless you use CSS to scale the canvas.
A pure CSS approach adding to solution of #jerseyboy above.
Works in Firefox (tested in v29), Chrome (tested in v34) and Internet Explorer (tested in v11).
<!DOCTYPE html>
<html>
<head>
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0;
}
canvas {
background-color: #ccc;
display: block;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<canvas id="canvas" width="500" height="500"></canvas>
<script>
var canvas = document.getElementById('canvas');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
ctx.fillRect(25,25,100,100);
ctx.clearRect(45,45,60,60);
ctx.strokeRect(50,50,50,50);
}
</script>
</body>
</html>
Link to the example: http://temporaer.net/open/so/140502_canvas-fit-to-window.html
But take care, as #jerseyboy states in his comment:
Rescaling canvas with CSS is troublesome. At least on Chrome and
Safari, mouse/touch event positions will not correspond 1:1 with
canvas pixel positions, and you'll have to transform the coordinate
systems.
function resize() {
var canvas = document.getElementById('game');
var canvasRatio = canvas.height / canvas.width;
var windowRatio = window.innerHeight / window.innerWidth;
var width;
var height;
if (windowRatio < canvasRatio) {
height = window.innerHeight;
width = height / canvasRatio;
} else {
width = window.innerWidth;
height = width * canvasRatio;
}
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
};
window.addEventListener('resize', resize, false);
Set initial size.
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
Update size on window resize.
function windowResize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
window.addEventListener('resize', windowResize);
2022 answer
The recommended way in 2022 to check if an element resized is to use ResizeObserver
const observer = new ResizeObserver(myResizeTheCanvasFn);
observer.observe(someCanvasElement);
It's better than window.addEventListener('resize', myResizeTheCanvasFn) or onresize = myResizeTheCanvasFn because it handles EVERY case of the canvas resizing, even when it's not related to the window resizing.
Similarly it makes no sense to use window.innerWidth, window.innerHeight. You want the size of the canvas itself, not the size of the window. That way, no matter where you put the canvas you'll get the correct size for the situation and won't have to re-write your sizing code.
As for getting the canvas to fill the window
html, body {
height: 100%;
}
canvas {
width: 100%;
height: 100%;
display: block; /* this is IMPORTANT! */
}
The reason you need display: block is because by default the canvas is inline which means it includes extra space at the end. Without display: block you'll get a scrollbar. Many people fix the scrollbar issue by adding overflow: hidden to the body of the document but that's just hiding the fact that the canvas's CSS was not set correctly. It's better to fix the bug (set the canvas to display: block than to hide the bug with overflow: hidden
Full example
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const observer = new ResizeObserver((entries) => {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
});
observer.observe(canvas)
// not import but draw something just to showcase
const hsla = (h, s, l, a) => `hsla(${h * 360}, ${s * 100}%, ${l * 100}%, ${a})`;
function render(time) {
const {width, height} = canvas;
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.translate(width / 2, height / 2);
ctx.rotate(time * 0.0001);
const range = Math.max(width, height) * 0.8;
const size = 64 + Math.sin(time * 0.001) * 50;
for (let i = 0; i < range; i += size) {
ctx.fillStyle = hsla(i / range * 0.3 + time * 0.0001, 1, 0.5, 1);
ctx.fillRect( i, -range, size, range * 2);
ctx.fillRect(-i, -range, size, range * 2);
}
ctx.restore();
requestAnimationFrame(render)
}
requestAnimationFrame(render)
html, body {
height: 100%;
margin: 0;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
<canvas></canvas>
Note: there are other issues related to resizing the canvas. Specifically if you want to deal with different devicePixelRatio settings. See this article for more.
Unless you want the canvas to upscale your image data automatically (that's what James Black's answer talks about, but it won't look pretty), you have to resize it yourself and redraw the image. Centering a canvas
If your div completely filled the webpage then you can fill up that div and so have a canvas that fills up the div.
You may find this interesting, as you may need to use a css to use percentage, but, it depends on which browser you are using, and how much it is in agreement with the spec:
http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#the-canvas-element
The intrinsic dimensions of the canvas
element equal the size of the
coordinate space, with the numbers
interpreted in CSS pixels. However,
the element can be sized arbitrarily
by a style sheet. During rendering,
the image is scaled to fit this layout
size.
You may need to get the offsetWidth and height of the div, or get the window height/width and set that as the pixel value.
CSS
body { margin: 0; }
canvas { display: block; }
JavaScript
window.addEventListener("load", function()
{
var canvas = document.createElement('canvas'); document.body.appendChild(canvas);
var context = canvas.getContext('2d');
function draw()
{
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.moveTo(0, 0); context.lineTo(canvas.width, canvas.height);
context.moveTo(canvas.width, 0); context.lineTo(0, canvas.height);
context.stroke();
}
function resize()
{
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
draw();
}
window.addEventListener("resize", resize);
resize();
});
If you're interested in preserving aspect ratios and doing so in pure CSS (given the aspect ratio) you can do something like below. The key is the padding-bottom on the ::content element that sizes the container element. This is sized relative to its parent's width, which is 100% by default. The ratio specified here has to match up with the ratio of the sizes on the canvas element.
// Javascript
var canvas = document.querySelector('canvas'),
context = canvas.getContext('2d');
context.fillStyle = '#ff0000';
context.fillRect(500, 200, 200, 200);
context.fillStyle = '#000000';
context.font = '30px serif';
context.fillText('This is some text that should not be distorted, just scaled', 10, 40);
/*CSS*/
.container {
position: relative;
background-color: green;
}
.container::after {
content: ' ';
display: block;
padding: 0 0 50%;
}
.wrapper {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
}
canvas {
width: 100%;
height: 100%;
}
<!-- HTML -->
<div class=container>
<div class=wrapper>
<canvas width=1200 height=600></canvas>
</div>
</div>
Using jQuery you can track the window resize and change the width of your canvas using jQuery as well.
Something like that
$( window ).resize(function() {
$("#myCanvas").width($( window ).width())
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="myCanvas" width="200" height="100" style="border:1px solid #000000;">
Here's a tiny, complete Code Snippet that combines all the answers. Press: "Run Code Snippet" then press "Full Page" and resize the window to see it in action:
function refresh(referenceWidth, referenceHeight, drawFunction) {
const myCanvas = document.getElementById("myCanvas");
myCanvas.width = myCanvas.clientWidth;
myCanvas.height = myCanvas.clientHeight;
const ratio = Math.min(
myCanvas.width / referenceWidth,
myCanvas.height / referenceHeight
);
const ctx = myCanvas.getContext("2d");
ctx.scale(ratio, ratio);
drawFunction(ctx, ratio);
window.requestAnimationFrame(() => {
refresh(referenceWidth, referenceHeight, drawFunction);
});
}
//100, 100 is the "reference" size. Choose whatever you want.
refresh(100, 100, (ctx, ratio) => {
//Custom drawing code! Draw whatever you want here.
const referenceLineWidth = 1;
ctx.lineWidth = referenceLineWidth / ratio;
ctx.beginPath();
ctx.strokeStyle = "blue";
ctx.arc(50, 50, 49, 0, 2 * Math.PI);
ctx.stroke();
});
div {
width: 90vw;
height: 90vh;
}
canvas {
width: 100%;
height: 100%;
object-fit: contain;
}
<div>
<canvas id="myCanvas"></canvas>
</div>
This snippet uses canvas.clientWidth and canvas.clientHeight rather than window.innerWidth and window.innerHeight to make the snippet run inside a complex layout correctly. However, it works for full window too if you just put it in a div that uses full window. It's more flexible this way.
The snippet uses the newish window.requestAnimationFrame to repeatedly resize the canvas every frame. If you can't use this, use setTimeout instead. Also, this is inefficient. To make it more efficient, store the clientWidth and clientHeight and only recalculate and redraw when clientWidth and clientHeight change.
The idea of a "reference" resolution lets you write all of your draw commands using one resolution... and it will automatically adjust to the client size without you having to change the drawing code.
The snippet is self explanatory, but if you prefer it explained in English: https://medium.com/#doomgoober/resizing-canvas-vector-graphics-without-aliasing-7a1f9e684e4d
A bare minimum setup
HTML
<canvas></canvas>
CSS
html,
body {
height: 100%;
margin: 0;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
JavaScript
const canvas = document.querySelector('canvas');
const resizeObserver = new ResizeObserver(() => {
canvas.width = Math.round(canvas.clientWidth * devicePixelRatio);
canvas.height = Math.round(canvas.clientHeight * devicePixelRatio);
});
resizeObserver.observe(canvas);
For WebGL
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');
const resizeObserver = new ResizeObserver(() => {
canvas.width = Math.round(canvas.clientWidth * devicePixelRatio);
canvas.height = Math.round(canvas.clientHeight * devicePixelRatio);
gl.viewport(0, 0, canvas.width, canvas.height);
});
resizeObserver.observe(canvas);
Notice that we should take device pixel ratio into account, especially for HD-DPI display.
I think this is what should we exactly do: http://www.html5rocks.com/en/tutorials/casestudies/gopherwoord-studios-resizing-html5-games/
function resizeGame() {
var gameArea = document.getElementById('gameArea');
var widthToHeight = 4 / 3;
var newWidth = window.innerWidth;
var newHeight = window.innerHeight;
var newWidthToHeight = newWidth / newHeight;
if (newWidthToHeight > widthToHeight) {
newWidth = newHeight * widthToHeight;
gameArea.style.height = newHeight + 'px';
gameArea.style.width = newWidth + 'px';
} else {
newHeight = newWidth / widthToHeight;
gameArea.style.width = newWidth + 'px';
gameArea.style.height = newHeight + 'px';
}
gameArea.style.marginTop = (-newHeight / 2) + 'px';
gameArea.style.marginLeft = (-newWidth / 2) + 'px';
var gameCanvas = document.getElementById('gameCanvas');
gameCanvas.width = newWidth;
gameCanvas.height = newHeight;
}
window.addEventListener('resize', resizeGame, false);
window.addEventListener('orientationchange', resizeGame, false);
(function() {
// get viewport size
getViewportSize = function() {
return {
height: window.innerHeight,
width: window.innerWidth
};
};
// update canvas size
updateSizes = function() {
var viewportSize = getViewportSize();
$('#myCanvas').width(viewportSize.width).height(viewportSize.height);
$('#myCanvas').attr('width', viewportSize.width).attr('height', viewportSize.height);
};
// run on load
updateSizes();
// handle window resizing
$(window).on('resize', function() {
updateSizes();
});
}());
This worked for me.
Pseudocode:
// screen width and height
scr = {w:document.documentElement.clientWidth,h:document.documentElement.clientHeight}
canvas.width = scr.w
canvas.height = scr.h
Also, like devyn said, you can replace "document.documentElement.client" with "inner" for both the width and height:
**document.documentElement.client**Width
**inner**Width
**document.documentElement.client**Height
**inner**Height
and it still works.

Categories