Zooming on HTML5 <canvas> and no pixelation for text? - javascript

Let's put some text on a HTML5 <canvas> with
var canvas = document.getElementById('myCanvas'),
ctx = canvas.getContext('2d'),
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.font = '14px sans-serif';
ctx.fillText('Bonjour', 10, 10);
When zooming the canvas on text, one can see pixelation.
Is there a way of zooming on a canvas without having pixelation on text ?

When you fillText on the canvas, it stops being letters and starts being a letter-shaped collection of pixels. When you zoom in on it, the pixels become bigger. That's how a canvas works.
When you want the text to scale as a vector-based font and not as pixels, don't draw them on the canvas. You could create <span> HTML elements instead and place them on top of the canvas using CSS positioning. That way the rendering engine will render the fonts in a higher resolution when you zoom in and they will stay sharp. But anything you draw on the canvas will zoom accordingly.
Alternatively, you could override the browsers zoom feature and create your own zooming algorithm, but this will be some work.
When the user zooms in or out of the window, the window.onresize event handler is triggered. You can use this trigger to adjust the width and the height of the canvas css styling accordingly (not the properties of the canvas. That's the internal rendering resolution. Change the width and height attributes of the style which is the resolution it is scaled to on the website).
Now you effectively disabled the users web browser from resizing the canvas, and also have a place where you can react on the scaling input events. You can use this to adjust the context.scale of your canvas to change the size of everything you draw, including fonts.
Here is an example:
<!DOCTYPE html>
<html>
<head>
<script type="application/javascript">
"use strict"
var canvas;
var context;
function redraw() {
// clears the canvas and draws a text label
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.font = "60pt sans-serif";
context.fillText("Hello World!", 100, 100);
}
function adjustSize() {
var width = window.innerWidth;
var height = window.innerHeight;
// resize the canvas to fill the whole screen
var style = canvas.style;
style.width = width + "px";
style.height = height + "px";
// backup the old current scaling factor
context.save();
// change the scaling according to the new zoom factor
context.scale(1000 / width, 1000 / height);
// redraw the canvas
redraw();
// restore the original scaling (important because multiple calls to scale are relative to the current scale factor)
context.restore();
}
window.onload = function() {
canvas = document.getElementById("myCanvas");
context = canvas.getContext("2d");
adjustSize();
}
window.onresize = adjustSize;
</script>
</head>
<body>
<canvas id ="myCanvas" width = 1000 height = 1000 ></canvas>
</body>
</html>

If you only need to scale text you can simply scale the font size.
A couple of notes on that however: fonts, or typefaces, are not just straight forward to scale meaning you will not get a smooth progress. This is because fonts are often optimized for certain sizes so the sizes in between so to speak are a result of the previous and next size. This can make the font look like it's moving around a little when scaled up and is normal and expected.
The approach here uses a simply size scale. If you need an absolute smooth scale for animation purposes you will have to use a very different technique.
The simple way is:
ctx.font = (fontSize * scale).toFixed(0) + 'px sans-serif';
An online demo here.
For animation purposes you would need to do the following:
Render a bigger size to an off-screen canvas which is then used to draw the different sizes
When the difference is too big and you get problems with interpolation you will have to render several of these cached text images at key sizes so you can switch between them when scaling factor exceeds a certain threshold.
In this demo you can see that at small sizes the pixels gets a bit "clumpy" but otherwise is much smoother than a pure text approach.
This is because the browser uses bi-linear interpolation rather than bi-cubic with canvas (this may or may not change in the future) so it's not able to interpolate properly when the difference gets to big (see below for solution with this issue).
The opposite happens at big sizes as the text gets blurry also due to interpolation.
This is where we would have to switch to a smaller (or bigger) cached version which we then scale within a certain range before we again switch.
The demo is simplified to show only a single cached version. You can see halfway through that this works fine. The principle would be in a full solution (sizes being just examples):
(Update Here is a demo of a switched image during scale).
-- Cached image (100px)
-- Draw cached image above scaled based on zoom between 51-100 pixels
-- Cached image (50px) generated from 100px version / 2
-- Draw cached image above scaled based on zoom between 26-50 pixels
-- Cached image (25px) generated from 50px version / 2
-- Draw cached image above scaled based on zoom between 1-25 pixels
Then use a "sweet spot" (which you find by experiment a little) to toggle between the cached versions before drawing them to screen.
var ctx = canvas.getContext('2d'),
scale = 1, /// initial scale
initialFactor = 6, /// fixed reduction scale of cached image
sweetSpot = 1, /// threshold to switch the cached images
/// create two off-screen canvases
ocanvas = document.createElement('canvas'),
octx = ocanvas.getContext('2d'),
ocanvas2 = document.createElement('canvas'),
octx2 = ocanvas2.getContext('2d');
ocanvas.width = 800;
ocanvas.height = 150;
ocanvas2.width = 400; /// 50% here, but maybe 75% in your case
ocanvas2.height = 75; /// experiment to find ideal size..
/// draw a big version of text to first off-screen canvas
octx.textBaseline = 'top';
octx.font = '140px sans-serif';
octx.fillText('Cached text on canvas', 10, 10);
/// draw a reduced version of that to second (50%)
octx2.drawImage(ocanvas, 0, 0, 400, 75);
Now we only need to check the sweet spot value to find out when to switch between these versions:
function draw() {
/// calc dimensions
var w = ocanvas.width / initialFactor * scale,
h = ocanvas.height / initialFactor * scale;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (scale >= sweetSpot) {
ctx.drawImage(ocanvas, 10, 10, w, h); /// use cached image 1
} else {
ctx.drawImage(ocanvas2, 10, 10, w, h); /// use cached image 2
}
}
So why not just draw the second cached image with a font? You can do that but then you are back to the issue with fonts being optimized for certain sizes and it would generate a small jump when scaling. If you can live with that then use this as it will provide a little better quality (specially at small sizes). If you need smooth animation you will have to reduce a larger cached version in order to keep the size 100% proportional.
You can see this answer on how to get a large image resized without interpolation problems.
Hope this helps.

Related

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

Low Quality Images in spot motion canvas animation

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!

Print canvas contents

var print = document.createElement('button');
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
canvas.width = 300;
canvas.height = 100;
ctx.fillStyle = '#000';
ctx.font = '15px sans-serif';
ctx.fillText('Fill Text, 18px, sans-serif', 10, 20);
print.innerHTML = 'Print';
document.body.appendChild(print);
document.body.appendChild(canvas);
print.addEventListener('click', function () {
window.print();
});
http://jsfiddle.net/vpetrychuk/LWup5/.
As you can see text in the canvas displays ok, but after clicking "Print" button (and saving page as PDF) output image becomes ugly.
Any chance to print the canvas contents without blur?
You need to make the actual canvas at print size then scale it on screen using CSS rules.
The browser will always use the internal bitmap size first and adjust that to the print or screen. If the bitmap is then of high resolution you will get better result on the print.
But mind you though, you will need to scale every coordinate and size when you print to the canvas. You will also need to prioritize screen versus print as one of them will look worse (if you prioritize print it will not look super on screen and vica verse).
Here is a modified example of your canvas which is now equivalent of 300 DPI (versus default 96 DPI). You can see it looks about the same on screen but will be much sharper when you print it.
/// conversion factor for scale, we want 300 DPI in this example
var dpiFactor = 300 / 96,
width = 400,
height = 100;
/// set canvas size representing 300 DPI
canvas.width = width * dpiFactor;
canvas.height = height * dpiFactor;
/// scale all content to fit the 96 DPI display (DPI doesn't really matter here)
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
/// scale all sizes incl. font size
ctx.font = (15 * dpiFactor).toFixed(0) + 'px sans-serif';
/// scale all positions
ctx.fillText('Fill Text, 18px, sans-serif', 10 * dpiFactor, 20 * dpiFactor);
Simply use wrapper functions to do all the math for you:
function fillText(txt, x, y) {
ctx.fillText(txt, x * dpiFactor, y * dpiFactor);
}
You can try to detect when printing is going to happen (e.g. using Webkit window.matchMedia) but the actual printing is done using a scaling that is not under your control so creating a perfect 1x1 scaled canvas for sharp graphics is impossible.
You can replace the canvas with an high-enough resolution version when you detect printing, but the exact real size of the output is unknown. It's like if the user has a zoomed view of the page... even if canvas has been drawn sharply the browser can zoom it making the result blurry anyway.
Probably for text the best quality option is using a regular text div overimposed on the canvas instead of using fillText (of course this won't work for all use cases).
I had in css width: 100% for canvas, which caused wrong scaling
Fixed by changing from 100% to 210mm
A better solution is to use the canvas.toDataURL() method and then set the resulting string to the src of an img. When doing this, generate the canvas through Javascript and never actually append it to the body of the page.
Daniel's answer works great. Printing images instead of canvases is better. You don't need to write any JS hacks.

How redraw on canvas resize without blurring?

When the canvas element is resized (via the style changing) I also want to scale the canvas' drawn image as well. I cannot just change the height/width as this causes the canvas to clear itself, so I do:
Create a temporary canvas element
Draw the current canvas' image onto that temporary canvas
Resize the current canvas
Draw the temp canvas' image back to the current canvas but scaled to the new size
This results in some blurring - very noticeable after many resizes (example: when dragging to resize). How would I do this without any blurring?
EDIT: Turning off image smoothing (context.webkitImageSmoothingEnabled = false;) does not fix the problem, it simply makes it redraw it more and more jagged until the image looks nothing like the original after a number of resizes.
Called on resize event:
var tmpCanvas = null;
//Make a temporary canvas
tmpCanvas = document.createElement( "canvas" );
//Set its size to be equal
tmpCanvas.height = originalCanvas.height;
tmpCanvas.width = originalCanvas.width;
//Draw our current canvas onto it
tmpCanvas.getContext( "2d" ).drawImage( originalCanvas, 0, 0 );
//Set new dimensions
originalCanvas.width = originalCanvas.offsetWidth;
originalCanvas.height = originalCanvas.offsetHeight;
var originalContext = originalCanvas.getContext( "2d" );
//Set background and colors
originalContext.fillStyle = "#ffffff";
originalContext.strokeStyle = "#000000";
//Set paintbrush
originalContext.lineWidth = 4;
originalContext.lineCap = "round";
//Fill background as white
originalContext.fillRect( 0, 0, originalCanvas.width, originalCanvas.height );
//We have a saved signature
if ( SignatureCanvas.hasSignature === true )
{
//Draw it back but scaled (results in blurred image)
originalContext.drawImage( tmpCanvas, 0, 0, tmpCanvas.width, tmpCanvas.height, 0, 0, originalCanvas.width, originalCanvas.height );
/**
* This results in a blurred image as well
//Draw it back but scaled
originalContext.scale( originalCanvas.width / tmpCanvas.width, originalCanvas.height / tmpCanvas.height );
originalContext.drawImage( tmpCanvas, 0, 0, tmpCanvas.width, tmpCanvas.height, 0, 0, tmpCanvas.width, tmpCanvas.height );
*/
}
Is there a way to get the strokes and "scale" all those points and redraw?
Instead of taking the rendered image from the original canvas, actually redraw the image. By that, I mean execute the same logic you executed against the original canvas, but with the points involved scaled to the new size.
If you can, think about using SVG instead. It scales well by its nature.
Edit: Another option I've thought of is to simply use a gigantic canvas to start with. Sizing down tends to look better than sizing up, especially with smoothing on.
Edit II: The original answer was irrelevant, though the comment I had made is relevant, and am now promoting it and editing it to be an answer...and the answer I had given was not all that great anyway **.
Of course if you scale up raster graphics, that is, from an image with a smaller pixel dimensions of pixels, create an image with higher pixel dimensions, you are going to get blurred images. By scaling up, you're making a low resolution picture high resolution, but without the high resolution details.
There's absolutely no way around that blurriness unless you make multiple additional assumptions about your raster image like the only gray you'd see is at an image edge, or corners can only occur at apparent inflection points where the angle between the tangents of the joined curves must be 100 degrees or less. Essentially, you'd have to give additional information so that your higher resolution image can have detail "filled in". It's not all that terribly different from reverse engineering an SVG from a raster.
So, you appear to want to emulate is scaling vector graphics, in which the only solution is to save the commands, draw a SVG, or draw to a bigger canvas like Stuart Branham suggested.
** I had originally proposed that invoking drawImage would distort the pixels even if it were not scaled, and that it would be better to work with the actual pixel data. If that's true, I can't find proof, but that's irrelevant, as he wanted his image scaled up, without blurring...which is impossible, as I just mentioned.

html5 image cropping

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.

Categories