Is there any way to draw a rectangle whose composing lines have width thinner than 1 pixel?
This code works perfectly, as expected:
// context is a HTML5 canvas 2D context
context.lineWidth = 1;
context.strokeStyle = "black";
context.rect(0, 0, 20, 20);
context.stroke();
It draws a nice rectangle.
But, if I try to draw a rectangle with thinner lines:
// See line width
context.lineWidth = 0.5;
context.strokeStyle = "black";
context.rect(0, 0, 20, 20);
context.stroke();
It still draws a rectangle whose borders have 1 pixel width.
I'm dealing with the canvas object here, and not CSS, where you have ways to "simulate" this.
Although it doesn't make much sense, you can acheive that with using a regular 1-pixel line with a 50% scaled canvas (but again it's a 1-pixel rendition, read below). See this snippet:
var canvas = document.querySelector('canvas');
var context = canvas.getContext('2d');
function scale() {
context.scale(0.5, 0.5);
draw();
}
function draw() {
context.beginPath();
context.moveTo(100, 150);
context.lineTo(450, 50);
context.stroke();
}
draw()
<canvas width="400" height="150"></canvas>
<button onclick="scale()">Scale down</button>
But again, I wonder how you expect the half-pixel line to look on your screen, antialiasing?
Right :) I suppose I was thinking on some way of drawing thinner lines, like, for example, when you use CSS styles. I've looked around and I don't think I can use alternate units.
There's no way to make something that's smaller than the smallest component unit, in our case a pixel. You can mimic the thinner look by transparency, or opacity, or even some sort of antialiasing (which again relies on transparency or the colouring of the neighbouring pixels), but not by trying to go below one pixel.
I agree, there is a sub-pixel rendering mode in browsers, for example, when you work with percentages, but in the end, the browser just renders full pixels with some of the modification I've described above.
And you know if you could render unit smaller than pixels, you'd technically have infinite resolutions on displays. I wish it was possible. :)
Related
Suppose I have a 400x200 canvas.
I want to work in a coordinate system where (0, 0) is in the exact middle of the canvas and positive y means up and positive x means right.
So, I set the transform as follows:
var ctx = document.getElementById("canvas").getContext("2d");
ctx.setTransform(1, 0, 0, -1, 200, 100);
ctx.fillRect(-20, -20, 40, 40);
<canvas id="canvas" style="width: 400px; height: 200px"></canvas>
So when I fill the rectangle as in the snippet above, I would expect to see a square centered in the middle of the canvas element. However, when running the above snippet (in latest Chrome) the square is demonstrably not centered. Why is this? Am I misunderstanding something about the transform matrix? If so, how can I achieve my goal?
The size for the canvas element isn't set properly which means the canvas defaults to 150 pixels in height, which is then stretched out using CSS. This gives the illusion of the object being offset.
To properly set canvas size use its attributes instead of CSS:
<canvas id="canvas" width=400 height=200></canvas>
Also be aware of that the Y-axis is now flipped upside-down so any text and images are drawn upside-down as well. These will need local transformation to be drawn correctly.
The problem seems to be that the dimensions in style are ignored and default to the standard dimensions 300×150. So set them properly with
<canvas id="canvas" width="400" height="200"></canvas>
Independent of that, it can be a good idea to not rely on hard-coded dimensions, especially if you are going to use a local coordinate system anyway.
My snippet modifications:
Use the actual canvas dimensions and scale so that the local coordinates still at least contain the square (-100,100)×(-100,100).
Add a coordinate cross before setting the transformation, that shows that even when the canvas is "wrong", the square is at the coordinate origin.
After the transformation, add a circle at positive y position to show that "up" is really up.
var cnv = document.getElementById("canvas");
var w = cnv.width, h = cnv.height;
var ctx = cnv.getContext("2d");
ctx.beginPath();
ctx.moveTo(0,h/2); ctx.lineTo(w,h/2);
ctx.moveTo(w/2,0); ctx.lineTo(w/2,h);
ctx.closePath(); ctx.stroke();
var scale = Math.min(w,h)/200.0;
ctx.setTransform(scale, 0, 0, -scale, w/2, h/2);
ctx.moveTo(0,50); ctx.arc(0,50,10,0,2*Math.PI); ctx.stroke();
ctx.fillRect(-20, -20, 40, 40);
<canvas id="canvas" width="450" height="250"></canvas>
I would like to understand & learn a solution for the issue I am experiencing when I am filling two shapes/paths with a shared border, i.e. after the fill is called for all the shapes there is still a tiny gap present between them.
The snippet presents the code doing the drawing of the involved shapes/paths:
ctx.beginPath();
ctx.moveTo(pBottom.x, pBottom.y);
ctx.lineTo(0, 0);
ctx.lineTo(pTop.x, pTop.y);
ctx.lineTo(pTopFront.x, pTopFront.y);
ctx.lineTo(pBottomFront.x, pBottomFront.y);
ctx.fillStyle = gradientTopSide;
ctx.fill();
ctx.fillStyle = gradientBottomSide;
ctx.fill();
ctx.beginPath();
ctx.moveTo(pBottom.x, pBottom.y);
ctx.arc(0, 0, radiusBackArc, (angleBottomBack) * Math.PI / 180, (angleTopBack) * Math.PI / 180);
ctx.lineTo(0, 0);
ctx.fillStyle = gradientBackArc;
ctx.fill();
The gaps are visible to the left (by the blue circle), to the top of it and to the bottom. They are where the fills of radial and linear gradients meet.
I wanted play a bit with canvas to create a simple light/torch effect and these lines are ruining my fun. Certainly cause I do not know how to fill them in a nice way without ruining the gradient effect.
Please find the JSFiddle presenting the issue.
When you deal with gradients involving transparency you'll run into overlaps where alpha channel values will multiply as well as sub-pixeling, and rounding errors for the points and possibly also for gradient calculations.
Chances are that the calculations you do in the code has to be rounded off properly. If they aren't you will have canvas do sub-pixeling of those pixels and it's hard to maintain a concise result, in particular when alpha is involved. It's the quirks of canvas and this way of drawing gradient lines. Canvas does not properly "bend" (join) in the corners either so you get overlapping drawings - not much we can do about that part.
I see two ways to solve this is a simple manner:
Use an image which you draw in instead of building a shape - this is fast and the result just as good unless you need different sizes (if animated, that doesn't matter so much though). I would personally go with this option (and you'll get more artistic freedom as well).
Draw the shape with a different technique:
I'll demonstrate the latter here, and as a bonus you can drop a lot of code. If you need transparency than you should go with option 1. Also, it ain't perfect either as we still have to rely on canvas' way of doing this, but it could be perhaps an improved replacement when it comes to the overlapping issues:
Use a single closed path of the shape
Modify current shape by removing the last lineTo() and replace it with closePath()
Important: change lineJoin to round
We create a loop where we draw the shape overlapping, a and for each iteration:
Change solid color based on iteration
Scale slightly
Move slightly
I made a simple version of this here but I'll leave it to you to figure out the more satisfying details of values if you choose to use it.
var d2r = Math.PI / 180, i = 0, iterations = 14;
ctx.lineJoin = "round";
ctx.lineWidth = 3;
for(; i < iterations; i++) {
ctx.beginPath();
ctx.moveTo(pBottom.x, pBottom.y);
ctx.arc(0, 0, radiusBackArc, angleBottomBack * d2r, angleTopBack * d2r);
ctx.lineTo(pTopFront.x, pTopFront.y);
ctx.arc(0, 0, radiusFrontArc, angleTopFront * d2r, angleBottomFront * d2r);
// don't set endpoint=startpoint, instead just close the shape
ctx.closePath();
// using HSL will allow us to easily set a gradient ligthness:
ctx.strokeStyle = "hsl(0, 0%, " + (i/iterations*100) + "%)";
ctx.stroke();
ctx.translate(i*0.1,-i*0.02);
ctx.scale(0.99, 0.98);
}
Modified fiddle here
So I'm creating a simple userscript for me - I want to draw a play button over shaded version of any GIF image on the site to have them only played when I want to.
I have inspired myself here. To make the whole thing nice, I'm drawing a green circle over the animation. This is my intended effect (I really wonder if once can make some shapes in GIMP):
And this is what I currently have:
The Cleese ironic face animation comes from this GIF portal
I made a fiddle of my current GIF pauser userscript.
The critical code for printing the circle:
//Assume these variables
var ctx = canvas context (2d)
var w = canvas width
var h = canvas height
//The code:
ctx.beginPath();
//I'm making sure the circle will always fit - therefore Math.min
ctx.arc(w/2, h/2, Math.min(60, w/2-20, h/2-20), 0, 2 * Math.PI, false);
ctx.fillStyle = 'rgba(0,180,0,0.5)';
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = 'red';
ctx.stroke();
ctx.closePath();
I hope it's clear that the triangle must always fit into the circle (size of which may vary). It must have the vertical line a little bit shorter then the other two, which must be of equal length.
I'm totally not interested in any existing GIF pausing libraries. Keep in mind that the question is about the triangle, not the GIFs.
Like this? http://jsfiddle.net/32ECU/
//Draw a triangle in it
ctx.strokeStyle = 'white';
ctx.beginPath();
ctx.moveTo(w/3, h/3);
ctx.lineTo(w/3, h-h/3);
ctx.lineTo(w-w/4, h/2);
ctx.lineTo(w/3, h/3);
ctx.fillStyle = 'white';
ctx.fill();
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.
I just experimented a bit with Javascript and HTML5 canvas and when I saw it in my browser (chrome) I realised that it's not very pretty rendered. After that I saw it in the Internet Explorer and there it looks even more crawful. I made a little Example: http://ios.xomz.de/
I just declared the canvas object in the html code
<canvas id="mycanvas" width="1000px" height="600px"/>
and rendered into it with
var canvas = document.getElementById("mycanvas");
var context = canvas.getContext("2d");
context.beginPath();
context.rect(200, 200, 600, 200);
context.lineWidth = 5;
context.strokeStyle = 'black';
context.stroke();
context.font = "40pt arial";
context.fillStyle = "black";
context.fillText("Hello World!", 220, 380);
for example.
Can you explain why the rendering isn't good ?
Do not use "px", also I'd recommend not using a self-closing tag:
<canvas id="mycanvas" width="1000" height="600"></canvas>
http://jsfiddle.net/c2KeD/
This problem is related to the way objects are drawn on a float based grid (especially vertical and horizontal lines and thus rects).
See there for an explanation and a schema : http://canop.org/blog/?p=220
Depending on the size of your objects, you need to use integer or mid-integer coordinates and sizes for your shapes, the goal being to fill complete pixels in both dimensions.
For example :
use a mid-integer for a thin line (one pixel width)
use an integer coordinate for a 2 pixels wide line
(and extend the logic for rects)
In your case, with a line width of 5, you have sharper rects by using this :
context.rect(200.5, 200.5, 600, 200);
Demonstration here : http://jsfiddle.net/dystroy/TyNBB/