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.
Related
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.
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.
I would like to clip an animated canvas as background to a <h1 />. The requirements are:
Use an actual <h1 /> tag instead of rendered heading in canvas.
Use of images to be rendered with ctx.drawImage().
h1 {
color: transparant;
background-image: <some-canvas>;
background-clip: text;
}
There are several approaches I've tried so far with varying success:
Creating a regular canvas and setting it as background of the <h1 />-tag using -webkit-canvas and -moz-element. This approach ticked all of my requirements but unfortunatly -webkit-canvas was deprecated along with document.getCSSCanvasContext("2d") in Chromium. Safari is the only working browser.
Using the CSS Paint API (Houdini). Using a requestAnimationFrame() to update a css var I can keep ticking the animation and do the animation I would like to implement. However, in Chromium, passing in images has to be done using a workaround (instead of creating a property of type image, images have to be passed in using background-image, making me unable to use the background-image to tell CSS Paint to use my worklet as background. The spec is not entirely implemented.
Creating an inline svg as background-image: url("data:image/svg+xml;utf8,<svg><foreignObject><canvas id='..'/></foreignObject></svg>"); and trying to update that canvas using requestAnimationFrame. Does not work at all.
Are there any other methods I could try?
If it doesn't have to be a background-image, you could put the canvas element inside the h1 element, match the canvas width and height to that of the h1 element, give it a lower z-index than the text in the h1 element so that it appears to be behind the text, acting like a background.
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
/*
Set the dimensions of the canvas to
match the parent h1 element
----------------------------------------------------------*/
setCanvasSize();
// (and for demonstrative purposes, scale on window resize)
window.addEventListener('resize', setCanvasSize, false);
function setCanvasSize () {
var _w = canvas.parentNode.clientWidth;
var _h = canvas.parentNode.clientHeight;
canvas.width = _w;
canvas.height = _h;
canvas.style.width = "'" + _w + "px'";
canvas.style.height = "'" + _h + "px'";
}
/*--------------------------------------------------------*/
/*--------------------------------------------------------
All this code below is just a ball animation borrowed from MDN
to illustrate what I'm suggesting.
Source: MDN
https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Advanced_animations
*/
var raf;
var running = false;
var ball = {
x: 100,
y: 100,
vx: 5,
vy: 1,
radius: 50,
color: 'blue',
draw: function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
}
};
function clear() {
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.fillRect(0,0,canvas.width,canvas.height);
}
function draw() {
clear();
ball.draw();
ball.x += ball.vx;
ball.y += ball.vy;
if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
ball.vy = -ball.vy;
}
if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
ball.vx = -ball.vx;
}
raf = window.requestAnimationFrame(draw);
}
if (!running) {
raf = window.requestAnimationFrame(draw);
running = true;
}
ball.draw();
/*--------------------------------------------------------*/
h1 {
/* Important for positioning the span element */
position: relative;
/* Cosmetic/demonstrative */
border: 2px solid red;
height: 100%;
text-align: center;
color: red;
font-size: 32px;
font-family: Roboto,sans-serif;
margin: 0 auto;
}
h1 span {
/* Vertically and horizontally center the text */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
/*
Text will appear above canvas as long as its
z-index > canvas' z-index
*/
z-index: 2;
}
h1 canvas {
/* this will make the canvas appear behind the text */
position: relative;
z-index: 1;
}
<h1>
<span>Lorem Ipsum etc.</span>
<canvas id="canvas"></canvas>
</h1>
Just a thought anyway, maybe you can try it out and expand on the idea to suit your needs. I'm not 100% sure what you're working on so apologies if it's not helpful.
Main Issue:
l essentially want to figure the issue with my event listener as it is aligning with the canvas object, which is the image '', in the middle of the canvas however, the Y areas below it are still clickable and the X areas on the right of it are still clickable.
l would like to eliminate this issue, which l believe is being caused by my IF statement and the DRAWIMAGE conditions, in relation to my canvas. There is a reproducible demo, fullscreen/expand it to see.
Another issue:
Another thing to note, which would be much appreciated, is the canvas object not truly sticking in one position on the canvas when you resize the browser window. It simply moves off into a different direction even though it should be stuck in one area of the canvas no matter what size my browser's window is - meaning that the canvas object somehow needs to dynamically resize along with how my browser resize + the event listener needs to see it. Again this would be highly appreciated as l really want to understand the error of my logic as l might using the wrong coordinate system,i don't really know :/
var canvas = document.getElementById("c");
var ctx = canvas.getContext("2d");
var the_button = document.getElementById("the_button");
var the_background = document.getElementById("the_background");
var button_imageX = 600;
var button_imageY = 390;
window.onload = function() {
ctx.drawImage(the_background, 0, 0, canvas.width, canvas.height);
drawButton();
}
initialize();
function initialize() {
window.addEventListener('resize', resizeCanvas, false);
resizeCanvas();
}
function drawButton() {
/* l belive this is partly responsible aswell for the issue */
ctx.drawImage(the_button, button_imageX, canvas.height - button_imageY, 170, 100);
}
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(the_background, 0, 0, canvas.width, canvas.height);
drawButton();
}
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
redraw();
}
the_button_function = (paramater1) => {
/* Problem lies here in the IF statement aswell, that's my guess */
if ((paramater1.x > (canvas.width - button_imageX)) && (paramater1.x < canvas.width) && (paramater1.y > (canvas.height - button_imageY)) && (paramater1.y < canvas.height)) {
alert("<Button>")
}
}
canvas.addEventListener('click', (e) => the_button_function(e));
html,
body {
width: 100%;
height: 100%;
margin: 0px;
border: 0;
overflow: hidden;
display: block;
}
<html>
<canvas id="c"></canvas>
<img style="display: none;" id="the_button" src="https://i.imgur.com/wO7Wc2w.png" />
<img style="display: none;" id="the_background" src="https://img.freepik.com/free-photo/hand-painted-watercolor-background-with-sky-clouds-shape_24972-1095.jpg?size=626&ext=jpg" />
</html>
To stop clicks responding to the right of and below the button, take into account the width and height of the button!
Fixing this, and knowing where the image was previously drawn on the canvas should fix the second issue. To debug the problem the snippet code below replaces
button_imageX with button_imageLeft - how many pixels from canvas left to draw the image.
button_imageY with button_imageBottom - how many pixels from canvas bottom to draw the top of the image. (This seemed to be how the posted code was positioning the button in the y direction.)
[image_bottonLeft and image_buttonBottom values were modified for seeing results on Stack Overflow.]
And introduced
button_offsetX - x position of where the left hand side of the button was last drawn
button_offsetY - y position of where the top of the button was last drawn
button_imageWidth and button_imageHeight values for button height and width, replacing hard coded values function drawButton.
var canvas = document.getElementById("c");
var ctx = canvas.getContext("2d");
var the_button = document.getElementById("the_button");
var the_background = document.getElementById("the_background");
var button_imageLeft = 100;
var button_imageBottom = 150;
var button_imageWidth = 170;
var button_imageHeight = 100;
var button_offsetX, button_offsetY;
window.onload = function() {
ctx.drawImage(the_background, 0, 0, canvas.width, canvas.height);
drawButton();
}
initialize();
function initialize() {
window.addEventListener('resize', resizeCanvas, false);
resizeCanvas();
}
function drawButton() {
button_offsetX = button_imageLeft;
button_offsetY = canvas.height - button_imageBottom;
ctx.drawImage(the_button, button_offsetX, button_offsetY, button_imageWidth, button_imageHeight);
}
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(the_background, 0, 0, canvas.width, canvas.height);
drawButton();
}
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
redraw();
}
the_button_function = (event) => {
const {x,y} = event;
if( (x >= button_offsetX && x < (button_offsetX+button_imageWidth))
&& (y >= button_offsetY && y < (button_offsetY+button_imageHeight))) {
alert("<Button>")
}
}
canvas.addEventListener('click', (e) => the_button_function(e));
html,
body {
width: 100%;
height: 100%;
margin: 0px;
border: 0;
overflow: hidden;
display: block;
}
<canvas id="c"></canvas>
<img style="display: none;" id="the_button" src="https://i.imgur.com/wO7Wc2w.png" />
<img style="display: none;" id="the_background" src="https://img.freepik.com/free-photo/hand-painted-watercolor-background-with-sky-clouds-shape_24972-1095.jpg?size=626&ext=jpg" />
When change the window size is not going to fill canvas with the gradient...
window.addEventListener('resize', resizeCanvas, false);
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
draw();
}
resizeCanvas();
http://codepen.io/tworog/pen/jVoYrz
Please help.
You don't need javascript to accomplish this, you can just use css. (I made the canvas blue in the following example to prove it's filling the entire page)
canvas {
background: cornflowerblue;
}
html, body, canvas {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
}
<canvas></canvas>
The canvas size is already being updated.
In order to get rid of the gradient you need to change Line 48, so that size of the rectangle matches the canvas.
ctx.fillRect(0,0,canvas.width,canvas.height);