How to make a canvas animation scale (HTML5) - javascript

I have an animation/canvas drawing where the pixels are hard coded and not relative. How do I make it so that the canvas and the drawing/animation scales with the browser size?
I thought of making the pixel values relative to the width and height of either the browser or the canvas but I don't know if it's a good idea.
Any tips?
P.S: I didn't post the code because it is almost 1000 lines long but if required I could post part of it.

Scale to fit, fill, or stretch
Almost every device has a different display aspect ratio, and then each setup will use only part, or all of the available screen to display the window.
The only real choice you have is how to adapt to fit the available space.
As the previous answer points out you can get the available size with innerWidth, innerHeight.
So lets assume you have that as width and height.
var width = innerWidth; // or canvas width
var height = innerHeight; //
Default resolution
I am assuming you app has a fixed aspect and an optimal resolution. To make it simple to adapt your app you need to keep the optimal resolution stored in code.
var defaultWidth = ?
var defaultHeight = ?
To adapt to displays you will need to get a scale that you set at the start of the app and adjust when the display size changes. You will also need the origin. the point where coordinate 0,0 is as for some solutions that will move.
These values are all relative to the current display that app is rendering on. They will take you native coordinates and make them conform to the device pixels.
var displaySetting = {
scaleX : ?,
scaleY : ?,
originX : ?,
originY : ?,
}
Fit, fill, or stretch
The 3 most basic options.
Stretch to fit. You stretch the rendering to fit the display but lose the aspect.
Scale to fit. You scale the rendering so that you maintain the aspect and all of the display is visible. Thought depending on the device you may have empty space on the sides or top and bottom.
Scale to fill. You scale the rendering so that it fills the device screen but you may end up clipping some or the rendering on the sides or top and bottom.
.
function getDisplayTransform(type, displaySetting){
displaySetting = displaySetting || {};
// first get scales
if(type === "stretch"){
displaySetting.scaleX = width / defaultWidth;
displaySetting.scaleY = height / defaultHeight;
} else if (type === "fit") {
displaySetting.scaleX = Math.min(width / defaultWidth, height / defaultHeight);
displaySetting.scaleY = displaySetting.scaleX;
} else { // type to fill
displaySetting.scaleX = Math.max(width / defaultWidth, height / defaultHeight);
displaySetting.scaleY = displaySetting.scaleX;
}
// now that the scale is set get the location of the origin. which is
displaySetting.originX = width / 2 - defaultWidth * 0.5 * displaySetting.scaleX;
displaySetting.originY = height / 2 - defaultHeight * 0.5 * displaySetting.scaleY;
// Note that for scale to fill the origin may be of the display or canvas
return displaySetting;
}
So now you just have to set the new coordinate system
ctx.setTransform(
displaySetting.scaleX,0,
0,displaySetting.scaleY,
displaySetting.originX,displaySetting.originY
);
So to fit with space to spare use getDisplayTransform("fit",displaySetting); or to fill use getDisplayTransform("fill",displaySetting); which will have some clipping top & bottom or left & right.
Once the transform is set you render as normal. You don't have to change the coordinates in your code, Line widths, font sizes, shadow offsets all remain the same as far as you code is concerned.

Set the canvas width and height to window.innerWidth and window.innerHeight respectively. So, the code would be like (in JS):
var canvas = document.getElementById('my_canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
Above code will only change the canvas size according to window width and height. To set the drawing/animation scale, you need to set its position/size dynamically (respect to the window width/height), not hard coded.

Related

Zooming HTML <canvas> so it's not blurry

I have a bit of code that copies from an off-screen source to the on-screen destination.
let destCanvas = document.getElementById("render");
let ctx = destCanvas.getContext('2d');
let srcCanvas = calcCanvas();
ctx.drawImage(srcCanvas,
0,0,srcCanvas.width,srcCanvas.height,
0,destCanvas.height,destCanvas.width,destCanvas.height);
The problem is that, if the user zooms in the browser (Ctrl +), the result is blurry. Each px gets larger and the source pixels have to be stretched over more physical pixels (but the same number of px) in the destination. If anti-aliasing is turned off, then the result is blocky, and that's not what's desired either.
What feels like the correct solution is to render the off-screen canvas at a higher resolution when the browser is zoomed in, but keep the px at a one-to-one ratio, so that devicePixelRatio is always 1. Of course, the on-screen canvas would need to be enlarged too. Zoom events can be detected as resize events, but there seems to be no way to replace them with the desired behavior. If this were possible, then the user could zoom in and the result would be large and crisp instead of large and blurry.
Since it's impossible (?) to disable browser zoom, is there a way to do drawImage() so that it works with physical pixels instead of px? It wouldn't be difficult to render the off-screen canvas based on devicePixelRatio, but it seems that the destination canvas is always treated as though it's made up of abstract px instead of physical pixels. Higher resolution source data doesn't help.
Some follow-up... It turns out that canvas.style.width and canvas.width can be used to get close to a solution. style.width sets the width on the monitor, in px as a unit of measurement, while width specifies the number of px (in the sense of abstract pixels) to use for the canvas. For a fixed style.width, you can have an arbitrary number of abstract pixels. So, as the user zooms in, increase the number of abstract pixels, while keeping the size in measurement px constant:
function adjustCanvas() {
let canvas = document.getElementById("render");
// Fix the measured size, to fill the window in this example.
canvas.style.width = document.documentElement.clientWidth + "px";
canvas.style.height = document.documentElement.clientHeight + "px";
// Adjust the number of pixels so that the number of pixels per unit
// length (actual physical length) remains constant.
canvas.width = document.documentElement.clientWidth * window.devicePixelRatio;
canvas.height = document.documentElement.clientHeight * window.devicePixelRatio;
}
This isn't quite pixel-perfect rendering, but it's closer than letting the browser manage it. It is still slightly blurry, probably
because there is some remaining mismatch between physical pixels and the CSS notion of an abstract px.

Resizing canvas to fit within container

Having some trouble trying to get my Canvas/Stage to resize and fit correctly within the parent container. I've found other similar posts and while the answers did help me with obtaining the new screen size it still doesn't want to fit within the container and instead goes right to the edges of the screen (which is expected from the examples), going over the parent container with the left/right content.
I've tried many different ways but still cannot find the correct way where it, doesn't go outside the parent container and the scale X/Y doesn't throw off the position of the shape within the canvas. The most obvious way for me was to set a constraint to the actual canvas/stage via CSS but that ends up throwing off the scaling making the shape appear outside the canvas.
In the code link, I included my hacky approach that I was trying to do and while it looks correct in some cases with smaller device screens, the canvas width will still go outside the parent container. I'm not sure if I'm overthinking this a lot or if there is a much simpler way to approach my issue, any help or tips would be great.
Live CodeSandbox: https://codesandbox.io/s/white-surf-yc2vh
Desired Result Image: https://imgur.com/a/w0oXxG0
I think what you want / need is to resize the canvas so that it fits into the container, while keeping the aspect ratio which you have specified.
You can use this function to resize some rectangle (rect) so that it fits into some other rectangle (container):
// fit a rect into some container, keeping the aspect ratio of the rect
export const fitRectIntoContainer = (rectWidth, rectHeight, containerWidth, containerHeight) => {
const widthRatio = containerWidth / rectWidth; // ration container width to rect width
const heightRatio = containerHeight / rectHeight; // ration container height to rect height
const ratio = Math.min( widthRatio, heightRatio ); // take the smaller ratio
// new rect width and height, scaled by the same ratio
return {
width: rectWidth * ratio,
height: rectHeight * ratio,
};
};
Then you can resize the canvas so that it fits into the container:
const canvasSize = fitRectIntoContainer( 700, 600, size.width, size.height );
const canvasWidth = canvasSize.width;
const canvasHeight = canvasSize.height;
Then I guess you don't need any scaling anymore (so scaleX and scaleY can be both 1, or undefined)

Fabric js transform and zoom canvas to fit all objects in viewport

I'm trying to made a collaboration tool based on fabricJs. Because I want canvas to be responsive I get some problems with pan/zoom functionality
Canvas view on large monitor
Problem is when someone on large monitor put some objects on corners for example and another user open this canvas on laptop or small monitor. In this case there is objects outside of current user viewport
Canvas view on small monitor
My idea is to add a button and functionality to fit all objects inside view with zoom and viewportTransform (I don't want to move objects in center of canvas because after save I will get different results from origin).
For this first I create a group with
let group = new fabric.Group(_this.canvas.getObjects()); and then I calculate zoom with
canvas.height / group.height and I look zoom to be calculated properly.
After that I don't know how to calculate where I need to zoom canvas. I try with getBoundingRect, calcViewportBoundaries and other function but can`t get correct zoom point.
JSFiddle - https://jsfiddle.net/znru0yx6/4/
I know this is 2.5 years after you asked. But this might help others who stumble across this (as i did) and give you some closure as well.
I've tried to include as many comments as possible as i always prefer to see the explanation alongside each line, rather than separately.
window.zoomToContents = function(canvas){
//first check if there are any elemnts to zoom to
if (canvas.getObjects().length < 1) {
return;
}
// reset zoom so pan actions work as expected
canvas.setZoom(1);
//group all the objects
const group = new fabric.Group(canvas.getObjects());
//find the centre of the group on the canvas
const x = (group.left + (group.width / 2)) - (canvas.width / 2);
const y = (group.top + (group.height / 2)) - (canvas.height / 2);
//and pan to it
canvas.absolutePan({x:x, y:y});
//now we need to decide whether width or height should determine the scaling
//e.g. a portrait box in a landscape canvas (height) needs scaling differently to a portrait box in a portrait canvas (could be height or width)
//or a landscape box in a portrait canvas (width)
//work out the distance between the edges of the group and the canvas
const heightDist = canvas.getHeight() - group.height;
const widthDist = canvas.getWidth() - group.width;
let groupDimension = 0;
let canvasDimension = 0;
//the smaller the number then that's the side we need to use as a reference to scale
//either group is inside the canvas (positive number) so the edge closest to the limits of the canvas will be used as the reference scale (smallest positive difference)
//or the group extends outside the canvas so the edge that extends further will be the reference (large negative number)
//either way, we want the smallest number
if (heightDist < widthDist) {
//height is the reference so need the height to scale to be nearly the height of the canvas
groupDimension = group.height;
canvasDimension = canvas.getHeight();
} else {
//width is the reference so need the width to scale to be nearly the width of the canvas
groupDimension = group.width;
canvasDimension = canvas.getWidth();
}
//work out how to scale the group to match the canvas size (then only make it zoom 80% of the way)
const zoom = (canvasDimension / groupDimension) * 0.8;
//we've already panned the canvas to the centre of the group, so now zomm using teh centre of teh canvas as teh reference point
canvas.zoomToPoint({ x: canvas.width / 2, y: canvas.height / 2 }, zoom);
}

How can i get high resolution image from smaller canvas?

I am using one canvas in my web app and it's actual height and width are 500px. I am showing this canvas on screen as 500px square but i want image exported from this canvas as 1600px square. I have tried below code with no luck.
canvas.width = 1600;
canvas.style.width = 500;
Any help will be appreciated.
You can have the canvas display at 500px while still having a resolution of 1600px. Display size and resolution are independent. For resolution you set the canvas width and height properties. For display size you set the canvas style width and height properties.
// create a canvas or get it from the page
var canvas = document.createElement("canvas");
// set the resolution (number of pixels)
canvas.width = canvas.height = 1600;
// set the display size
canvas.style.width = canvas.style.height = "500px";
// get the rendering context
var ctx = canvas.getContext("2d");
To get the rendering to match the display size you need to scale up all rendering. You can do this by setting the transform scale to the canvas resolution divided by the display size
var scale = 1600 / 500; // get the scale that matches display size
ctx.setTransform(scale,0,0,scale,0,0);
Now when you render to the canvas you use the screen size coordinates.
ctx.fillRect(0,0,500,500); // fill all of the canvas.
ctx.fillStyle = "red"; // draw a red circle 100 display pixels in size.
ctx.beginPath();
ctx.arc(250,250,100,0,Math.PI * 2);
ctx.fill();
When you then save the canvas, what ever method you use as long as it is not screen capture the saved canvas will be 1600 by 1600 and all the rendering will be correctly positions and proportional
HTML
<canvas width="1600px" height="1600px" > </canvas>
CSS
canvas{
position :absolute;
transform:scale(0.3125);
left:-500px; //adjust
top:-350px; //adjust
}
Use transform:scale() to adjust size of your canvas
Now 1600 * 1600 will be the actual size of your canvas, so you can directly export images from your canvas
But in view it show as 500px * 500px beacuse it's scaled down, it dose not affect the image quality while exporting
Honest answer: you can't.
If you did, then you'd have found a way to losslessly compress data with less than 1/9th of the original size, and without any encoding, which is unarguably impossible.
What you can do is scale it up in a way that it at least doesn't get blurry. To do that, you need the final image to be an integer multiple of the previous canvas, so the browser won't apply anti-aliasing. Or if you want to use your own copying formula with putImageData that would get rid of anti-aliasing, you'll still get various incongruences and it would be very slow
In your case, the closest you could get is 1500x1500 ( 3*500x3*500 ). If your point was to process an image, you're not in luck, but if you just want to display something good enough, you can resort to various other tricks such as centering the canvas and using properties like box-shadow to make it clear that it's separate from the rest of the screen

Scale div into div like background image

I'm working on a web app and now facing a rather tricky task.
I have an image that I have to add overlay for.
Let's say I have an image of a car. I need to add values, like outside and inside temperature, on top of the car image.
It would be easy to add fixed pixel offsets for those temperature labels, but the image need to be scalable to every screen height and width-wise. I can't think of easy way to scale div into div exactly as "background-size:contain;" does for images.
Could someone point me to right tracks before I write complex javascript scaling logic?
ww = window width
wh = window height
ow = Original Width of your image
oh = Original Height of your image
nw = New Width of your image
nh = New Height of your image
mt = Margin top to adjust image to the screen vertically
ox = overlay location x coordinate according to original size of the image
oy = overlay location y coordinate according to original size of the image
nox = New ox
noy = new oy
sf = scaling factor
based on screen size you'll have a scaling factor for your image.
sf = ww / ow -> we find our scaling factor
nh = oh * sf -> then calculate our new image height
mt = (oh - nh) / 2 -> amount we need to shift image up to center screen
nox = ox * sf -> new overlay x coordinate
noy = (oy * sf) - mt -> new overlay y coordinate
edit
if image is too wide then this logic needs to be adjusted to shift image horizontally not vertically
edit 2
If you're targeting modern browsers only, then you can use CSS transform: scale(sx[, sy]); . This will re-size your DIV and all it's content proportionally Use transform: scale() with transform-origin: 0 0; to re-size from top left corner.
If you are using a SVG image this would be easy:
http://css-tricks.com/svg-text-typographic-designs/
Here is two examples with CSS:
http://codepen.io/chriscoyier/pen/zxqdev
http://maketea.co.uk/2013/12/16/smooth-text-overlays-with-css-transforms.html
I'm never tested CSS3 object-fit , but I think it's very useful for your solution:
https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit
https://dev.opera.com/articles/css3-object-fit-object-position/

Categories