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.
Related
I need to detect screen resolution. The problem is, if scale setting in Windows 10 is not 100%, it affects screen.width / screen.height values.
I could use pixelDeviceRatio for correct them, but here is another problem: if the page is zoomed, it also affects pixelDeviceRatio.
For example, I have Windows scale setting is 150%, and page zoom is 67%. My screen resolution is 1920x1080. In this case, screen.width is 1280, and pixelDeviceRatio is 1.0 (because Windows scale and page zoom level compensate each other). How can I detect the real screen resolution?
Upd.: I'm working on HTML5 game. I need to resize it to actual screen resolution when going to fullscreen mode.
Upd. 2: I have <canvas> element and call its requestFullscreen method to switch into fullscreen mode. Then I have to set width and height of the canvas. If I set it, for example, less than actual screen resolution (1280 instead of 1920), empty margins around the canvas appears.
This is an XY problem, OP doesn't want the screen resolution, but the one of the document.
Note for future readers.
There are chances that if you also are trying to get the actual screen resolution, you are yourself in some kind of an XY problem.
If what you want is sharp pixels, getting the actual screen resolution won't help you much.
Taking OP's example of a browser zoom of 75% itself scaled by the OS zoom at 150%, we get for a single white pixel,
1 white pixel x 0.75 => impossible to render. => anti-aliasing with surrounding pixels. So if we say that surrounding pixels were black, then we've now got a grayish pixel.
And this was only at the browser level... We still have to pass the OS zoom algorithm.
1 grayish pixel x 1.5 => impossible to render => anti-aliasing with surrounding pixels. And yet a new pixel, even farther from our original white.
The best we can do here is to set our canvas bitmap's size to the one reported by the browser. This way, only the OS zoom would kick in.
Note: You may want to try to detect high density screens a.k.a retina®, which are actually able to render pixels at sub-px level, in this case, you might try using window.devicePixelRatio as a general scaling factor, but remember to round it, since a floating scale would mean more antialiasing (no high density screen will ever use 2.3 pixels to render one px).
So to detect this document size, is an easy task, with multiple different ways of doing:
If you deal with hard-coded sizes in windowed mode, you can simply check for window.innerWidth and window.innerHeight when entering fullScreen:
Unfortunately, StackSnippets® iframe do not allow fullscreen mode...
// listen for the resize event
window.onresize = function() {
// if we are in fullscreen mode
if (document.fullscreenElement === canvas) {
// simply size our canvas to the reported innerWidth/innerHeight.
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
draw();
}
// in the fulscreenchange event
document.onfullscreenchange = function() {
// if we are exiting the fullscreen mode
if (!document.fullscreenElement) {
// set back to hard-coded values
W = canvas.width = 500
H = canvas.height = 200;
}
draw();
}
Fiddle.
If you want something more dynamic, you could also make use of CSS to let you know the actual size. Indeed, you can resize your canvas through CSS, as long as you remember to set its bitmap's size to the displayed size.
// listen only for the resize event
window.onresize = function() {
// set the canvas size to its own rendered size
W = canvas.width = canvas.offsetWidth;
H = canvas.height = canvas.offsetHeight;
draw();
}
/* target only when in fullscreen mode */
canvas::fullscreen {
width: 100vw;
height: 100vh;
}
/* windowed mode */
canvas {
width: 50vw;
height: 50vh;
}
Fiddle.
You can try this:
var width = screen.width
var height = screen.height
width = width*window.devicePixelRatio;
height = height*window.devicePixelRatio;
alert("width : "+width);
alert("height : "+height);
Currently there is no JavaScript api to detect os scale factor.
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.
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
I am using a jquery plugin that utilities canvas to draw up spot motion animations.(http://arena.palamago.com.ar/spotMotion/)
I know that in this instance i can use animated GIF images, but the image types i use in future will be requiring higher quality and transparency.
If you look at the jsfiddle below you will see the images are not sharp, i am on a retina display and they look even worse, the original image is 800px. Canvas is not scaling the images high enough fo some unknown reason. I am fairly new to canvas and have seen a few methods for up scaling but have had no luck in getting a better result.
I looked at canvas width and canvas style width
canvas.width = "200";
canvas.height = "200"; // allow 40 pixels for status bar on iOS
canvas.style.width = "100px";
canvas.style.height = "100px";
I also looked at css image rendering techniques
canvas { image-rendering:optimizeQuality;}
http://jsfiddle.net/TsAzP/1/
Another attempt but i just cant seem to intergrate it with this plugin.
function enhanceContext(canvas, context) {
var ratio = window.devicePixelRatio || 1,
width = canvas.width,
height = canvas.height;
if (ratio > 1) {
canvas.width = width * ratio;
canvas.height = height * ratio;
canvas.style.width = width + "px";
canvas.style.height = height + "px";
context.scale(ratio, ratio);
}
}
I have seen some very complicated methods with people writing up-scaling algorithms, i just dont understand how to put it together. If anyone knows how to improve image quality please spare me some time.
Thank you
Problem
The cause is that the source image is too large to reduce in size in a single down-scale.
The browser typically uses bi-linear interpolation over bi-cubic interpolation when it comes to the canvas element.
Bi-linear interpolation analyses 2x2 pixels while bi-cubic uses 4x4 (in down-sampling functioning as a low-pass filter to create an average pixel). If the image size reduction is too steep there is simply not enough pixels to consider for averaging and the result will be in part "choppy" or pixelated.
Solution
To solve you can do one of the following steps:
Prepare the image(s) at a smaller size in an image editor (for example Photoshop) and scale the image to the destination size you want to use (ref. retina display).
Process the image on client before drawing it by creating an off-screen canvas and scale down the image in 2-3 steps.
The first step could be a better solution for a variety of reasons such as:
Processing of the image (for size) does not happen on client
Quality in resulting image (easier to post-process)
Bandwidth reduction (less data to transfer)
Faster processing of the image (in use) on client
Saves on battery (less processing involved)
As for the second step: There's too much code in the fiddle (TL; TR), but the principle is as follows (and it's not so complicated):
/// create two temporary canvas elements
var ocanvas = document.createElement('canvas'), /// off-screen canvas
tcanvas = document.createElement('canvas'), /// temp canvas
octx = ocanvas.getContext('2d'),
tctx = ocanvas.getContext('2d');
Then we do a first step-down scaling of the image - for this example we will do it twice which is the minimum needed. You might need a third step if the size difference is huge (you can normally calculate this by using a function of log etc., but I'll leave that out of the um, "equation" here):
/// use temp canvas (tcanvas) to scale for the first step
tcanvas.width = img.width * 0.5; /// 50% allow good result with bi-linear
tcanvas.height = img.height * 0.5;
/// draw image into canvas
tctx.drawImage(img, 0, 0, tcanvas.width, tcanvas.height);
The next step is just as simple as the above but with an absolute size:
/// set destination size
ocanvas.width = 200;
ocanvas.height = 200;
/// draw temp canvas into canvas
octx.drawImage(tcanvas, 0, 0, ocanvas.width, ocanvas.height);
You can now use ocanvas in your solution instead of img.
We use two canvases as we want to use the final ocanvas to replace img directly later at the proper size. If we used one canvas we would have to resize it in the final step which mean the canvas would be cleared.
If you do need a third step then you can reuse one of the canvases.
The added advantage here is that the browser won't need to scale anything when animating which reduces the load on the CPU/GPU.
I suggest also doing this down-scaling inside a function so that the temporary canvas references (except the one you need to use pf course, which you need to return) can be easily discarded by the browser after use (GC/memory wise).
Hope this helps!
I'm using context-blender to apply a multiply effect on the first 192 pixels of the html background-image with a fixed color to achieve a transparency effect on the header of the page.
On the html I have 2 canvas. One for the part of the image to apply the multiply effect and one for the color.
On the javascript, after setting the color of the color-canvas and the width of both canvas to the window.innerWidth I'm getting the background image with:
imageObj.src = $('html').css('background-image').replace(/^url|[\(\)]/g, '');
Now comes the problem. I want to draw a cropped image to the image to the image-canvas so I can apply the multiply effect. I'm trying to do the following:
imageObj.onload = function(){
// getting the background-image height
var imageHeight = window.innerWidth * imageObj.height / imageObj.width;
// get the corresponding pixels of the source image that correspond to the first 192 pixels of the background-image
var croppedHeight = 192 * imageObj.height / imageHeight;
// draw the image to the canvas
imageCanvas.drawImage(imageObj, 0, 0, imageObj.width, croppedHeight, 0, 0, window.innerWidth, 192);
// apply the multiply effect
colorCanvas.blendOnto( imageCanvas, 'multiply');
}
But I'm doing something wrong getting the cropped height.
Ex: For an 1536x1152 image and a 1293x679 browser container, the value I'm getting for the source cropped height is 230 but to get the correct crop I need to use something around 296.
Edit:
I'm using background-size: cover on the css to create the background-image
Edit2:
I created a fiddle to illustrate the problem. If you uncomment the line //cHeight *= magicConstant; the cropped image looks a lot better but things stop making sense. I removed the multiply effect on the fiddler but that's not required to reproduce the problem. I also noticed that the behavior changed if I remove the second canvas from the URL.
Btw, this behavior happened with google chrome, but I think the same thing happens on safari and firefox.
OK, I've fixed it. Man was that hard! Mainly because you forgot to set the imageCanvas' canvas height. It also didn't help that the image has a white border. I spent a hell of a lot of time trying to figure out where the padding was coming from.
So to start, for the case of the fiddle, in function doBlending(), set imageCanvas.canvas.height = height;
Then the calculations in crop() need to cover 2 possibilities. Is the image being scaled for height and truncated on the left or scaled for width and truncated on the bottom? I'm not going to write both for you, but here's the one for the case where it is scaled for height:
function crop(imageObj, imageCanvas, colorCanvas) {
// Assumes bg image is scaled for hight
var scale = imageObj.height / window.innerHeight;
var targetHeight = imageCanvas.canvas.height;
var targetWidth = window.innerWidth;
imageCanvas.drawImage(imageObj,
0, 0, targetWidth * scale, targetHeight * scale,
0, 0, targetWidth, targetHeight);
}
I really have no idea where you came up with the scaling factors in your example. The image is going to be scaled by multiplying both the x and y dimensions by some scale factor. That's how you preserve the aspect ratio. The scale factor will be the larger of the one to make the height of the image match the height of the window and the one to make the width of the image match the width of the window.
I think it may not be valid for you to be using window inner dimensions here. Since cover will maintain the aspect ratio of the background image it means that both of its dimensions may not be fully displayed. So if you are trying to transform between aspect ratios to determine where to clip, you would have to account for the fact that the image may flow out of the window borders.