Getting crisp pixel-perfect lines using HTML canvas [duplicate] - javascript

I'm playing around with the <canvas> element, drawing lines and such.
I've noticed that my diagonal lines are antialiased. I'd prefer the jaggy look for what I'm doing - is there any way of turning this feature off?

For images there's now context.imageSmoothingEnabled= false.
However, there's nothing that explicitly controls line drawing. You may need to draw your own lines (the hard way) using getImageData and putImageData.

Draw your 1-pixel lines on coordinates like ctx.lineTo(10.5, 10.5). Drawing a one-pixel line over the point (10, 10) means, that this 1 pixel at that position reaches from 9.5 to 10.5 which results in two lines that get drawn on the canvas.
A nice trick to not always need to add the 0.5 to the actual coordinate you want to draw over if you've got a lot of one-pixel lines, is to ctx.translate(0.5, 0.5) your whole canvas at the beginning.

It can be done in Mozilla Firefox. Add this to your code:
contextXYZ.mozImageSmoothingEnabled = false;
In Opera it's currently a feature request, but hopefully it will be added soon.

It must antialias vector graphics
Antialiasing is required for correct plotting of vector graphics that involves non-integer coordinates (0.4, 0.4), which all but very few clients do.
When given non-integer coordinates, the canvas has two options:
Antialias - paint the pixels around the coordinate based on how far the integer coordinate is from non-integer one (ie, the rounding error).
Round - apply some rounding function to the non-integer coordinate (so 1.4 will become 1, for example).
The later strategy will work for static graphics, although for small graphics (a circle with radius of 2) curves will show clear steps rather than a smooth curve.
The real problem is when the graphics is translated (moved) - the jumps between one pixel and another (1.6 => 2, 1.4 => 1), mean that the origin of the shape may jump with relation to the parent container (constantly shifting 1 pixel up/down and left/right).
Some tips
Tip #1: You can soften (or harden) antialiasing by scaling the canvas (say by x) then apply the reciprocal scale (1/x) to the geometries yourself (not using the canvas).
Compare (no scaling):
with (canvas scale: 0.75; manual scale: 1.33):
and (canvas scale: 1.33; manual scale: 0.75):
Tip #2: If a jaggy look is really what you're after, try to draw each shape a few times (without erasing). With each draw, the antialiasing pixels get darker.
Compare. After drawing once:
After drawing thrice:

Try something like canvas { image-rendering: pixelated; }.
This might not work if you're trying to only make one line not antialiased.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillRect(4, 4, 2, 2);
canvas {
image-rendering: pixelated;
width: 100px;
height: 100px; /* Scale 10x */
}
<html>
<head></head>
<body>
<canvas width="10" height="10">Canvas unsupported</canvas>
</body>
</html>
I haven't tested this on many browsers though.

I would draw everything using a custom line algorithm such as Bresenham's line algorithm. Check out this javascript implementation:
http://members.chello.at/easyfilter/canvas.html
I think this will definitely solve your problems.

Adding this:
image-rendering: pixelated; image-rendering: crisp-edges;
to the style attribute of the canvas element helped to draw crisp pixels on the canvas. Discovered via this great article:
https://developer.mozilla.org/en-US/docs/Games/Techniques/Crisp_pixel_art_look

I discovered a better way to disable antialiasing on path / shape rendering using the context's filter property:
The magic / TL;DR:
ctx = canvas.getContext('2d');
// make canvas context render without antialiasing
ctx.filter = "url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9ImZpbHRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj48ZmVDb21wb25lbnRUcmFuc2Zlcj48ZmVGdW5jUiB0eXBlPSJpZGVudGl0eSIvPjxmZUZ1bmNHIHR5cGU9ImlkZW50aXR5Ii8+PGZlRnVuY0IgdHlwZT0iaWRlbnRpdHkiLz48ZmVGdW5jQSB0eXBlPSJkaXNjcmV0ZSIgdGFibGVWYWx1ZXM9IjAgMSIvPjwvZmVDb21wb25lbnRUcmFuc2Zlcj48L2ZpbHRlcj48L3N2Zz4=#filter)";
Demystified:
The data url is a reference to an SVG containing a single filter:
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="filter" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
<feComponentTransfer>
<feFuncR type="identity"/>
<feFuncG type="identity"/>
<feFuncB type="identity"/>
<feFuncA type="discrete" tableValues="0 1"/>
</feComponentTransfer>
</filter>
</svg>
Then at the very end of the url is an id reference to that #filter:
"url(data:image/svg+...Zz4=#filter)";
The SVG filter uses a discrete transform on the alpha channel, selecting only completely transparent or completely opaque on a 50% boundary when rendering. This can be tweaked to add some anti-aliasing back in if needed, e.g.:
...
<feFuncA type="discrete" tableValues="0 0 0.25 0.75 1"/>
...
Cons / Notes / Gotchas
Note, I didn't test this method with images, but I can presume it would affect semi-transparent parts of images. I can also guess that it probably would not prevent antialiasing on images at differing color boundaries. It isn't a 'nearest color' solution but rather a binary transparency solution. It seems to work best with path / shape rendering since alpha is the only channel antialiased with paths.
Also, using a minimum lineWidth of 1 is safe. Thinner lines become sparse or may often disappear completely.
Edit:
I've discovered that, in Firefox, setting filter to a dataurl does not work immediately / synchronously: the dataurl has to 'load' first.
e.g. The following will not work in Firefox:
ctx.filter = "url(data:image/svg+xml;base64,...#filter)";
ctx.beginPath();
ctx.moveTo(10,10);
ctx.lineTo(20,20);
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
ctx.filter = "none";
But waiting till the next JS frame works fine:
ctx.filter = "url(data:image/svg+xml;base64,...#filter)";
setTimeout(() => {
ctx.beginPath();
ctx.moveTo(10,10);
ctx.lineTo(20,20);
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
ctx.filter = "none";
}, 0);

I want to add that I had trouble when downsizing an image and drawing on canvas, it was still using smoothing, even though it wasn't using when upscaling.
I solved using this:
function setpixelated(context){
context['imageSmoothingEnabled'] = false; /* standard */
context['mozImageSmoothingEnabled'] = false; /* Firefox */
context['oImageSmoothingEnabled'] = false; /* Opera */
context['webkitImageSmoothingEnabled'] = false; /* Safari */
context['msImageSmoothingEnabled'] = false; /* IE */
}
You can use this function like this:
var canvas = document.getElementById('mycanvas')
setpixelated(canvas.getContext('2d'))
Maybe this is useful for someone.

ctx.translate(0.5, 0.5);
ctx.lineWidth = .5;
With this combo I can draw nice 1px thin lines.

While we still don't have proper shapeSmoothingEnabled or shapeSmoothingQuality options on the 2D context (I'll advocate for this and hope it makes its way in the near future), we now have ways to approximate a "no-antialiasing" behavior, thanks to SVGFilters, which can be applied to the context through its .filter property.
So, to be clear, it won't deactivate antialiasing per se, but provides a cheap way both in term of implementation and of performances (?, it should be hardware accelerated, which should be better than a home-made Bresenham on the CPU) in order to remove all semi-transparent pixels while drawing, but it may also create some blobs of pixels, and may not preserve the original input color.
For this we can use a <feComponentTransfer> node to grab only fully opaque pixels.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ABEDBE";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "black";
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
// first without filter
ctx.fillText("no filter", 60, 20);
drawArc();
drawTriangle();
// then with filter
ctx.setTransform(1, 0, 0, 1, 120, 0);
ctx.filter = "url(#remove-alpha)";
// and do the same ops
ctx.fillText("no alpha", 60, 20);
drawArc();
drawTriangle();
// to remove the filter
ctx.filter = "none";
function drawArc() {
ctx.beginPath();
ctx.arc(60, 80, 50, 0, Math.PI * 2);
ctx.stroke();
}
function drawTriangle() {
ctx.beginPath();
ctx.moveTo(60, 150);
ctx.lineTo(110, 230);
ctx.lineTo(10, 230);
ctx.closePath();
ctx.stroke();
}
// unrelated
// simply to show a zoomed-in version
const zoomed = document.getElementById("zoomed");
const zCtx = zoomed.getContext("2d");
zCtx.imageSmoothingEnabled = false;
canvas.onmousemove = function drawToZoommed(e) {
const
x = e.pageX - this.offsetLeft,
y = e.pageY - this.offsetTop,
w = this.width,
h = this.height;
zCtx.clearRect(0,0,w,h);
zCtx.drawImage(this, x-w/6,y-h/6,w, h, 0,0,w*3, h*3);
}
<svg width="0" height="0" style="position:absolute;z-index:-1;">
<defs>
<filter id="remove-alpha" x="0" y="0" width="100%" height="100%">
<feComponentTransfer>
<feFuncA type="discrete" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</defs>
</svg>
<canvas id="canvas" width="250" height="250" ></canvas>
<canvas id="zoomed" width="250" height="250" ></canvas>
For the ones that don't like to append an <svg> element in their DOM, and who live in the near future (or with experimental flags on), the CanvasFilter interface we're working on should allow to do this without a DOM (so from Worker too):
if (!("CanvasFilter" in globalThis)) {
throw new Error("Not Supported", "Please enable experimental web platform features, or wait a bit");
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ABEDBE";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "black";
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
// first without filter
ctx.fillText("no filter", 60, 20);
drawArc();
drawTriangle();
// then with filter
ctx.setTransform(1, 0, 0, 1, 120, 0);
ctx.filter = new CanvasFilter([
{
filter: "componentTransfer",
funcA: {
type: "discrete",
tableValues: [ 0, 1 ]
}
}
]);
// and do the same ops
ctx.fillText("no alpha", 60, 20);
drawArc();
drawTriangle();
// to remove the filter
ctx.filter = "none";
function drawArc() {
ctx.beginPath();
ctx.arc(60, 80, 50, 0, Math.PI * 2);
ctx.stroke();
}
function drawTriangle() {
ctx.beginPath();
ctx.moveTo(60, 150);
ctx.lineTo(110, 230);
ctx.lineTo(10, 230);
ctx.closePath();
ctx.stroke();
}
// unrelated
// simply to show a zoomed-in version
const zoomed = document.getElementById("zoomed");
const zCtx = zoomed.getContext("2d");
zCtx.imageSmoothingEnabled = false;
canvas.onmousemove = function drawToZoommed(e) {
const
x = e.pageX - this.offsetLeft,
y = e.pageY - this.offsetTop,
w = this.width,
h = this.height;
zCtx.clearRect(0,0,w,h);
zCtx.drawImage(this, x-w/6,y-h/6,w, h, 0,0,w*3, h*3);
};
<canvas id="canvas" width="250" height="250" ></canvas>
<canvas id="zoomed" width="250" height="250" ></canvas>
Or you can also save the SVG as an external file and set the filter property to path/to/svg_file.svg#remove-alpha.

Notice a very limited trick. If you want to create a 2 colors image, you may draw any shape you want with color #010101 on a background with color #000000. Once this is done, you may test each pixel in the imageData.data[] and set to 0xFF whatever value is not 0x00 :
imageData = context2d.getImageData (0, 0, g.width, g.height);
for (i = 0; i != imageData.data.length; i ++) {
if (imageData.data[i] != 0x00)
imageData.data[i] = 0xFF;
}
context2d.putImageData (imageData, 0, 0);
The result will be a non-antialiased black & white picture. This will not be perfect, since some antialiasing will take place, but this antialiasing will be very limited, the color of the shape being very much like the color of the background.

Here is a basic implementation of Bresenham's algorithm in JavaScript. It's based on the integer-arithmetic version described in this wikipedia article: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
function range(f=0, l) {
var list = [];
const lower = Math.min(f, l);
const higher = Math.max(f, l);
for (var i = lower; i <= higher; i++) {
list.push(i);
}
return list;
}
//Don't ask me.
//https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
function bresenhamLinePoints(start, end) {
let points = [];
if(start.x === end.x) {
return range(f=start.y, l=end.y)
.map(yIdx => {
return {x: start.x, y: yIdx};
});
} else if (start.y === end.y) {
return range(f=start.x, l=end.x)
.map(xIdx => {
return {x: xIdx, y: start.y};
});
}
let dx = Math.abs(end.x - start.x);
let sx = start.x < end.x ? 1 : -1;
let dy = -1*Math.abs(end.y - start.y);
let sy = start.y < end.y ? 1 : - 1;
let err = dx + dy;
let currX = start.x;
let currY = start.y;
while(true) {
points.push({x: currX, y: currY});
if(currX === end.x && currY === end.y) break;
let e2 = 2*err;
if (e2 >= dy) {
err += dy;
currX += sx;
}
if(e2 <= dx) {
err += dx;
currY += sy;
}
}
return points;
}

For those who still looking for answers. here is my solution.
Assumming image is 1 channel gray. I just thresholded after ctx.stroke().
ctx.beginPath();
ctx.moveTo(some_x, some_y);
ctx.lineTo(some_x, some_y);
...
ctx.closePath();
ctx.fill();
ctx.stroke();
let image = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)
for(let x=0; x < ctx.canvas.width; x++) {
for(let y=0; y < ctx.canvas.height; y++) {
if(image.data[x*image.height + y] < 128) {
image.data[x*image.height + y] = 0;
} else {
image.data[x*image.height + y] = 255;
}
}
}
if your image channel is 3 or 4. you need to modify the array index like
x*image.height*number_channel + y*number_channel + channel

Just two notes on StashOfCode's answer:
It only works for a grayscale, opaque canvas (fillRect with white then draw with black, or viceversa)
It may fail when lines are thin (~1px line width)
It's better to do this instead:
Stroke and fill with #FFFFFF, then do this:
imageData.data[i] = (imageData.data[i] >> 7) * 0xFF
That solves it for lines with 1px width.
Other than that, StashOfCode's solution is perfect because it doesn't require to write your own rasterization functions (think not only lines but beziers, circular arcs, filled polygons with holes, etc...)

According to MDN docs, Scaling for high resolution displays, "You may find that canvas items appear blurry on higher-resolution displays. While many solutions may exist, a simple first step is to scale the canvas size up and down simultaneously, using its attributes, styling, and its context's scale."
Ignoring the apparent paradox in their statement, this worked in my case, sharpening edges which had previously been unacceptably fuzzy.
// Get the DPR and size of the canvas
const dpr = window.devicePixelRatio;
const rect = canvas.getBoundingClientRect();
// Set the "actual" size of the canvas
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
// Scale the context to ensure correct drawing operations
ctx.scale(dpr, dpr);
// Set the "drawn" size of the canvas
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;

Related

Use drawImage to scale a canvas above

I'm trying to make a pixel editor with 2 canvas. The first canvas displays a second canvas which contains the pixels. The first canvas uses drawImage to position and scale the second canvas.
When the second canvas is scaled smaller than it's original size, it starts to glitch.
Here is the canvas displayed at it's original size. When I zoom in, the second canvas get bigger and everything works perfectly.
However when I zoom out, the grid and the background (transparency) act very strangely.
To draw the second canvas on the first canvas, I use the function
ctx.drawImage(drawCanvas, offset.x, offset.y, width * pixelSize, height * pixelSize);
I have read that scaling in multiple iterations might give a better quality with images but I am not sure about a canvas.
I could fully redraw the second canvas in a lower resolution when the user zooms out, but it is a bit heavy on the cpu.
Is there any better solution that I don't know of?
Your problem comes from anti-aliasing.
Pixels aren't sub-divisible, and when you ask the computer to draw something outside of the pixel boundaries, it will try its best to render something that usually looks good to eyes, by mixing the colors so that what should have been a black 0.1 pixel line will become a light-gray pixel for instance.
This generally works good, particularly with pictures of the real word, or complex shapes like circles. However with grids... That's not so great as you experienced it.
Your case is dealing with two different cases, and you will have to deal with hem separately.
In the canvas 2D API (and a lot of 2D APIs) stroke do bleed from both sides of the coordinates you did set it. So when drawing lines of 1px wide, you need to account for a 0.5px offset to be sure it won't get rendered as two gray pixels. For more info about this, see this answer. You are probably using such a stroke for the grid.
fill on the other hand only covers the inside of the shape, so if you fill a rectangle, you need to not offset its coords from the px boundaries. This is required for the checkerboard.
Now, for boh these drawings, the best is probably to use patterns. You only need to draw a small version of it, and then the pattern will repeat it automatically, saving a lot of computation.
Scaling of a pattern can be done by calling the transform methods of the 2D context. We can even take advantage of the closest-neighbor algorithm to avoid antialising when drawing this pattern by setting the imageSmoothingEnabled property to false.
However for our grid, we may want to keep the lineWidth constant. For this we will need to generate a new pattern at every draw call.
// An helper function to create CanvasPatterns
// returns a 2DContext on which a simple `finalize` method is attached
// method which does return a CanvasPattern from the underlying canvas
function patternMaker(width, height) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.finalize = (repetition = "repeat") => ctx.createPattern(canvas, repetition);
return ctx;
}
// The checkerboard can be generated only once
const checkerboard_patt_maker = patternMaker(2, 2);
checkerboard_patt_maker.fillStyle = "#CCC";
checkerboard_patt_maker.fillRect(0,0,1,1);
checkerboard_patt_maker.fillRect(1,1,1,1);
const checkerboard_patt = checkerboard_patt_maker.finalize();
// An helper function to create grid patterns
// Since we want a constant lineWidth, no matter the zoom level
function makeGridPattern(width, height) {
width = Math.round(width);
height = Math.round(height);
const grid_patt_maker = patternMaker(width, height);
grid_patt_maker.lineWidth = 1;
// apply the 0.5 offset only if we are on integer coords
// for instance a <3,3> pattern wouldn't need any offset, 1.5 is already perfect
const x = width/2 % 1 ? width/2 : width/2 + 0.5;
const y = height/2 % 1 ? height/2 : height/2 + 0.5;
grid_patt_maker.moveTo(x, 0);
grid_patt_maker.lineTo(x, height);
grid_patt_maker.moveTo(0, y);
grid_patt_maker.lineTo(width, y);
grid_patt_maker.stroke();
return grid_patt_maker.finalize();
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const checkerboard_input = document.getElementById('checkerboard_input');
const grid_input = document.getElementById('grid_input');
const connector = document.getElementById('connector');
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const checkerboard_zoom = checkerboard_input.value;
const grid_zoom = grid_input.value;
// we generate a new pattern for the grid, so the lineWidth is always 1
const grid_patt = makeGridPattern(grid_zoom, grid_zoom);
// draw once the rectangle covering the whole canvas
// with normal transforms
ctx.beginPath();
ctx.rect(0, 0, canvas.width, canvas.height);
// the checkerboard
ctx.fillStyle = checkerboard_patt;
// our path is already drawn, we can control only the fill
ctx.scale(checkerboard_zoom, checkerboard_zoom);
// avoid antialiasing when painting our pattern (similar to rounding the zoom level)
ctx.imageSmoothingEnabled = false;
ctx.fill();
// done, reset to normal
ctx.imageSmoothingEnabled = true;
ctx.setTransform(1, 0, 0, 1, 0, 0);
// paint the grid
ctx.fillStyle = grid_patt;
// because our grid is drawn in the middle of the pattern
ctx.translate(Math.round(grid_zoom/2), Math.round(grid_zoom/2));
ctx.fill();
// reset
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
draw();
checkerboard_input.oninput = grid_input.oninput = function(e) {
if(connector.checked) {
checkerboard_input.value = grid_input.value = this.value;
}
draw();
};
connector.oninput = e => checkerboard_input.oninput();
<label>checkerboard-layer zoom<input id="checkerboard_input" type="range" min="2" max="50" step="0.1"></label><br>
<label>grid-layer zoom<input id="grid_input" type="range" min="2" max="50" step="1"></label><br>
<label>connect both zooms<input id="connector" type="checkbox"></label>
<canvas id="canvas"></canvas>

JS canvas white lines when scaling

Using JavaScript I am displaying an array on an html 5 canvas. The program uses c.fillRect() for each value in the array. Everything looks normal until I scale it using c.scale(). After being scaled white lines are visible between the squares. I do know their white because that is the color of the background (When the background changes their color changes too).
Since the squares are 5 units apart I tried setting their width to 5.5 instead of 5; this only remove the white lines when zoom in far enough, but when zooming out the white lines were still there.
This is my code (unnecessary parts removed):
function loop()
{
c.resetTransform();
c.fillStyle = "white";
c.fillRect(0, 0, c.canvas.width, c.canvas.height);
c.scale(scale, scale);
c.translate(xViewportOffset, yViewportOffset);
...
for(var x = 0; x < array.length; x++)
{
for(var y = 0; y < array[x].length; y++)
{
...
c.fillStyle = 'rgb(' + r + ',' + g + ',' + b + ')';
c.fillRect(0 + x * 5, 200 + y * 5, 5, 5);
}
}
...
}
No scaling:
Zoomed in:
Zoomed out:
(the pattern changes depending on the amount of zoom)
Thanks for any help and if any other information is needed please let me know.
Update:
I am using Google Chrome
Version 71.0.3578.98 (Official Build) (64-bit)
This is probably because you are using non-integer values to set the context's scale and/or translate.
Doing so, your rects are not on pixel boundaries anymore but on floating values.
Let's make a simple example:
Two pixels, one at coords (x,y) (11,10) the other at coords (12,10).
At default scale, both pixels should be neighbors.
Now, if we apply a scale of 1.3, the real pixel-coords of the first square will be at (14.3,13) and the ones of the second one at (15.6,13).
None of these coords can hold a single pixel, so browsers will apply antialiasing, which consist in smoothing your color with the background color to give the impression of smaller pixels. This is what makes your grids.
const ctx = small.getContext('2d');
ctx.scale(1.3, 1.3);
ctx.fillRect(2,10,10,10);
ctx.fillRect(12,10,10,10);
const mag = magnifier.getContext('2d');
mag.scale(10,10);
mag.imageSmoothingEnabled = false;
mag.drawImage(small, 0,-10);
/* it is actually transparent, not just more white */
body:hover{background:yellow}
<canvas id="small" width="50" height="50"></canvas><br>
<canvas id="magnifier" width="300" height="300"></canvas>
To avoid this, several solutions, all dependent on what you are doing exactly.
In your case, it seems you'd win a lot by working on an ImageData which would allow you to replace all these fillRect calls to simpler and faster pixel manipulation.
By using a small ImageData, the size of your matrix, you can replace each rect to a single pixel. Then you just need to put this matrix on your canvas and redraw the canvas over itself at the correct scale after disabling the imageSmootingEnabled flag, which allows us to disable antialiasing for drawImage and CanvasPatterns only.
// the original matrix will be 20x20 squares
const width = 20;
const height = 20;
const ctx = canvas.getContext('2d');
// create an ImageData the size of our matrix
const img = ctx.createImageData(width, height);
// wrap it inside an Uint32Array so that we can work on it faster
const pixels = new Uint32Array(img.data.buffer);
// we could have worked directly with the Uint8 version
// but our loop would have needed to iterate 4 pixels every time
// just to draw a radial-gradient
const rad = width / 2;
// iterate over every pixels
for(let x=0; x<width; x++) {
for(let y=0; y<height; y++) {
// make a radial-gradient
const dist = Math.min(Math.hypot(rad - x, rad - y), rad);
const color = 0xFF * ((rad - dist) / rad) + 0xFF000000;
pixels[(y * width) + x] = color;
}
}
// here we are still at 50x50 pixels
ctx.putImageData(img, 0, 0);
// in case we had transparency, this composite mode will ensure
// that only what we draw after is kept on the canvas
ctx.globalCompositeOperation = "copy";
// remove anti-aliasing for drawImage
ctx.imageSmoothingEnabled = false;
// make it bigger
ctx.scale(30,30);
// draw the canvas over itself
ctx.drawImage(canvas, 0,0);
// In case we draw again, reset all to defaults
ctx.setTransform(1,0,0,1,0,0);
ctx.globalCompositeOperation = "source-over";
body:hover{background:yellow}
<canvas id="canvas" width="600" height="600"></canvas>

Edges on arc, using Canvas

I am experimenting with drawing using javascript and the canvas element..my goal now is to draw a circle and gradually increase the opacity; I have this code:
http://codepen.io/anon/pen/zrVvOQ
Which seems to work, but the circle has rough edges; I found I need to clear the canvas each time the frame is redrawn, but the attempts I have made have not quite worked...any suggestions on how to?
window.onload = function draw(){
var frame1 = document.getElementById('frame1');
if (frame1.getContext){
var ctx = frame1.getContext('2d');
var centerX = frame1.width / 2;
var centerY = frame1.height / 2;
var radius = 50;
var alpha = 1.0;
/*call function over and over */
var requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
var rendergreen = function()
{
var opacityValue = 0;
opacityValue += 0.03;
ctx.fillStyle = 'rgba(68,107,62, ' + opacityValue + ')';
animate();
ctx.arc(50, centerY, radius, 0, 2 * Math.PI, false);
ctx.clip();
ctx.fill();
ctx.closePath;
function animate() {
if (opacityValue < 1) {
opacityValue += 0.3;
} else {
opacityValue = 1;
}
}
requestAnimationFrame(rendergreen);
}
rendergreen();
}
}
You say the circle has "rough edges". That's pixellation and is inherent in using canvas to draw, which is a bit-mapped style of graphics. That means that you essentially can't get higher resolution than a single pixel. Contrast that with svg which is vector-based. An svg image can be magnified a thousand times and still have a smooth edge. I've shown an svg circle next to the canvas circle so that you can see the difference. It becomes much more apparent if you zoom in with your browser. There are pro's and con's to using canvas vs svg, too much to go into here, but it's worth looking into if you're really concerned.
In terms of changing the opacity of the circle, you've got several problems with your approach. With the way you've written it, you're actually not changing the opacity. Instead, you're drawing the same very transparent circle many times over top of each other so that by the end it looks opaque, giving the impression that you are gradually increasing the transparency of a single circle. Notice that you're setting your opacity to zero in each drawing iteration, then incrementing it to 0.05 (note that there are differences in the code in your question versus in the codepen that you linked to...I'm referring to the codepen version), then drawing it (so it will always be drawn at opacity 0.05), then further changing the value of the variable opacityValue which is never used in the drawing. The example below shows a relatively simple example of what I think you were trying to achieve. Note that I've deliberately made the 'clearRect' too small so that you can see how not clearing the canvas each time allows semi-transparent drawings to "pile up". This also allows you to see that the blockiness gets worse if you overlay many semi-transparent images. e.g. Compare the left and right sides of the canvas circle. The part of the circle that is cleared every time ends up looking smoother because of anti-aliasing, but the overlaid images have the smoothing effects of anti-aliasing effectively destroyed.
window.onload = function draw() {
var frame1 = document.getElementById('frame1');
if (frame1.getContext) {
var ctx = frame1.getContext('2d');
var opacityValue = 0;
var render = function() {
ctx.clearRect(0, 0, 80, 80); // deliberately set too small
ctx.beginPath();
opacityValue += 0.01;
ctx.fillStyle = 'rgba(68,107,62, ' + opacityValue + ')';
ctx.arc(60, 60, 50, 0, 2 * Math.PI, false);
ctx.fill();
ctx.closePath;
requestAnimationFrame(render);
}
render();
}
}
<canvas id="frame1" width="120" height="120"></canvas>
<svg width="120" height="120">
<circle cx="60" cy="60" r="50" fill="#446B3E"></circle>
</svg>

HTML5 Canvas alpha transparency doesn't work in firefox for curves when window is big

I'm drawing a curve on an HTML5 canvas and am using alpha transparency to create a glow effect, by drawing a thicker version of the curve underneath with an alpha of less than 1, then drawing a thinner version of the curve on top (and I'm doing this with several levels of recursion).
Okay here's the problem. It works exactly the way I want it to in Chrome, giving a beautiful glow effect. But in Firefox, the alpha doesn't render properly if my browser dimensions are bigger than around 300px in height (yes that sounds crazy but it is actually what it is doing for some reason). If I resize my browser to be extremely tiny, then all the sudden the alpha works and I get my awesome glow. Once I make the window a reasonable size, the alpha no longer works so instead of a glowing line I just get a really thick line. :( Code is below.
HTML:
<body>
<canvas id="viewport">
<script type="text/javascript" src="scripts/render.js"></script>
</body>
CSS:
* {
background-color:#000000;
padding:0px;
margin:0px;
width:100%;
height:100%;
overflow:hidden;
}
#viewport {
border:0px;
}
Javascript:
window.viewport = document.getElementById("viewport");
window.context = viewport.getContext("2d");
window.xFactor = 1;
window.yFactor = 1;
function initializeViewport() {
maximizeViewport();
setFactors();
}
function maximizeViewport() {
viewport.width = window.innerWidth;
viewport.height = window.innerHeight;
}
function setFactors() {
xFactor = window.innerWidth / 100;
yFactor = window.innerHeight / 100;
}
function absX(x) {
return Math.floor(x * xFactor);
}
function absY(y) {
return Math.floor(y * yFactor);
}
function drawQuadraticCurve(startX, startY, controlX, controlY, endX, endY, lineWidth, gradient, alpha, glowiness, glowLevel) {
glowLevel = (typeof glowLevel === 'undefined') ? 0 : glowLevel;
// Draw the glow first
if (glowLevel < glowiness) {
drawQuadraticCurve(startX, startY, controlX, controlY, endX, endY, lineWidth + Math.sqrt(glowLevel), gradient, alpha*0.65, glowiness, glowLevel + 1);
}
// Then draw the curve
context.beginPath();
context.moveTo(absX(startX), absY(startY));
context.quadraticCurveTo(absX(controlX), absY(controlY), absX(endX), absY(endY));
context.lineWidth = lineWidth;
context.strokeStyle = gradient;
context.globalAlpha = alpha;
context.shadowColor = "#FFFFFF";
context.shadowBlur = 0;
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;
context.stroke();
}
function createRadialGradient(colors, innerX, innerY, innerR, outerX, outerY, outerR) {
var gradient = context.createRadialGradient(absX(innerX),absY(innerY),Math.min(absX(innerR/2), absY(innerR/2)),absX(outerX),absY(outerY),Math.min(absX(outerR/2), absY(outerR/2)));
var gradientLength = colors.length;
for (i=0; i<gradientLength; i++) {
gradient.addColorStop(colors[i][0], colors[i][1]);
}
return gradient;
}
initializeViewport();
drawQuadraticCurve(80,65,20,70,70,10, 1,createRadialGradient([[0,"#FFFFFF"],[0.7,"#33CCFF"],[1,"#9944FF"]],50,50,1,50,50,90),1,8,0);
Screenshot of it working in Chrome: http://i.imgur.com/brVT2i6.png
Screenshot of it NOT working in Firefox: http://i.imgur.com/63Z4PJY.png
Screenshot of it working in Firefox after I've resized the window to be ridiculously small: http://i.imgur.com/d9AihEu.png
First working solution gets an upvote and a green checkmark! Yay!
Here is a glowing quadratic curve made up of small, individual line segments--each segment being a different color. A shadowColor equal to the segment color causes the glow. The rendering is compatible across browsers (including FF).
(You can control the linewidth and the glow strength)
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
// variables to define colors -- use hsl instead of rgb
var hue=10;
var hueShift=4;
// define the quadratic curve
var startPt={x:350,y:100};
var controlPt={x:0,y:250};
var endPt={x:350,y:400};
// variables defining the starting & ending point of
// the current line segment.
var newXY=startPt;
var oldXY=startPt;
// the current interval along the quadratic curve
// (used to calc an x,y along the curve)
// (t is kind-of like a percentage along the curve--kind of but not)
var t=0;
// the unshadowed linewidth
ctx.lineWidth=1;
// the shadow to apply around the line
ctx.shadowBlur=7;
// round the endcaps to visually blend the line segments
ctx.lineCap='round';
// start with a black-filled canvas
ctx.fillStyle='black';
ctx.fillRect(0,0,cw,ch);
// start the animation
requestAnimationFrame(animate);
function animate(time){
// calculate a new x,y along the curve
var T=t/100;
var newXY=getQuadraticBezierXYatT(startPt,controlPt,endPt,T);
// change the color for this segment
hue=(hue+hueShift)%360;
// draw this line segment with a shadow-glow
glowLine(oldXY,newXY,hue);
// set old=new in preparation for the next loop
oldXY=newXY;
// request another animation loop intil reaching 100
if(++t<100){
requestAnimationFrame(animate);
}
}
function glowLine(oldXY,newXY,hue){
// calculate the hsl color given the new hue
var hsl="hsl(" + (hue % 360) + ",99%,50%)";
// draw a glowing line segment
// (==a line segment with a shadow of the same color as the line segment)
ctx.beginPath();
ctx.moveTo(oldXY.x,oldXY.y);
ctx.lineTo(newXY.x,newXY.y);
ctx.fillStyle= hsl
ctx.strokeStyle=hsl;
ctx.shadowColor=hsl;
// overdraw the line segment so it really stands out
for(var i=0;i<6;i++){
ctx.stroke();
}
}
// calculate an [x,y] along a quadratic curve given an interval T
function getQuadraticBezierXYatT(startPt,controlPt,endPt,T) {
var x = Math.pow(1-T,2) * startPt.x + 2 * (1-T) * T * controlPt.x + Math.pow(T,2) * endPt.x;
var y = Math.pow(1-T,2) * startPt.y + 2 * (1-T) * T * controlPt.y + Math.pow(T,2) * endPt.y;
return( {x:x,y:y} );
}
body{ background-color:ivory; padding:10px; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=500 height=500></canvas>
This is really a comment, but it wouldn't fit in the space allocated to a comment. :-)
I've consulted the All-Knowing-Oracle of Html5 Canvas--the w3.org.
If you assign a zero shadowBlur (as you do) the spec says there should be no shadow applied.
That means that FF with the larger canvas size is correctly applying the w3 standard (not drawing any shadow) and both Chrome & FF(smaller version) are incorrectly applying a shadow when it should not.
http://www.w3.org/TR/2dcontext/
Shadows are only drawn if the opacity component of the alpha component
of the color of shadowColor is non-zero and either the shadowBlur is
non-zero, or the shadowOffsetX is non-zero, or the shadowOffsetY is
non-zero.
Therefore, to have cross-browser compatibility, you mustn't rely on quirks in the rendering when shadowBlur=0. You must create your glow in another way within the "rules".

Can I turn off antialiasing on an HTML <canvas> element?

I'm playing around with the <canvas> element, drawing lines and such.
I've noticed that my diagonal lines are antialiased. I'd prefer the jaggy look for what I'm doing - is there any way of turning this feature off?
For images there's now context.imageSmoothingEnabled= false.
However, there's nothing that explicitly controls line drawing. You may need to draw your own lines (the hard way) using getImageData and putImageData.
Draw your 1-pixel lines on coordinates like ctx.lineTo(10.5, 10.5). Drawing a one-pixel line over the point (10, 10) means, that this 1 pixel at that position reaches from 9.5 to 10.5 which results in two lines that get drawn on the canvas.
A nice trick to not always need to add the 0.5 to the actual coordinate you want to draw over if you've got a lot of one-pixel lines, is to ctx.translate(0.5, 0.5) your whole canvas at the beginning.
It can be done in Mozilla Firefox. Add this to your code:
contextXYZ.mozImageSmoothingEnabled = false;
In Opera it's currently a feature request, but hopefully it will be added soon.
It must antialias vector graphics
Antialiasing is required for correct plotting of vector graphics that involves non-integer coordinates (0.4, 0.4), which all but very few clients do.
When given non-integer coordinates, the canvas has two options:
Antialias - paint the pixels around the coordinate based on how far the integer coordinate is from non-integer one (ie, the rounding error).
Round - apply some rounding function to the non-integer coordinate (so 1.4 will become 1, for example).
The later strategy will work for static graphics, although for small graphics (a circle with radius of 2) curves will show clear steps rather than a smooth curve.
The real problem is when the graphics is translated (moved) - the jumps between one pixel and another (1.6 => 2, 1.4 => 1), mean that the origin of the shape may jump with relation to the parent container (constantly shifting 1 pixel up/down and left/right).
Some tips
Tip #1: You can soften (or harden) antialiasing by scaling the canvas (say by x) then apply the reciprocal scale (1/x) to the geometries yourself (not using the canvas).
Compare (no scaling):
with (canvas scale: 0.75; manual scale: 1.33):
and (canvas scale: 1.33; manual scale: 0.75):
Tip #2: If a jaggy look is really what you're after, try to draw each shape a few times (without erasing). With each draw, the antialiasing pixels get darker.
Compare. After drawing once:
After drawing thrice:
Try something like canvas { image-rendering: pixelated; }.
This might not work if you're trying to only make one line not antialiased.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillRect(4, 4, 2, 2);
canvas {
image-rendering: pixelated;
width: 100px;
height: 100px; /* Scale 10x */
}
<html>
<head></head>
<body>
<canvas width="10" height="10">Canvas unsupported</canvas>
</body>
</html>
I haven't tested this on many browsers though.
I would draw everything using a custom line algorithm such as Bresenham's line algorithm. Check out this javascript implementation:
http://members.chello.at/easyfilter/canvas.html
I think this will definitely solve your problems.
Adding this:
image-rendering: pixelated; image-rendering: crisp-edges;
to the style attribute of the canvas element helped to draw crisp pixels on the canvas. Discovered via this great article:
https://developer.mozilla.org/en-US/docs/Games/Techniques/Crisp_pixel_art_look
I discovered a better way to disable antialiasing on path / shape rendering using the context's filter property:
The magic / TL;DR:
ctx = canvas.getContext('2d');
// make canvas context render without antialiasing
ctx.filter = "url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9ImZpbHRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj48ZmVDb21wb25lbnRUcmFuc2Zlcj48ZmVGdW5jUiB0eXBlPSJpZGVudGl0eSIvPjxmZUZ1bmNHIHR5cGU9ImlkZW50aXR5Ii8+PGZlRnVuY0IgdHlwZT0iaWRlbnRpdHkiLz48ZmVGdW5jQSB0eXBlPSJkaXNjcmV0ZSIgdGFibGVWYWx1ZXM9IjAgMSIvPjwvZmVDb21wb25lbnRUcmFuc2Zlcj48L2ZpbHRlcj48L3N2Zz4=#filter)";
Demystified:
The data url is a reference to an SVG containing a single filter:
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="filter" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
<feComponentTransfer>
<feFuncR type="identity"/>
<feFuncG type="identity"/>
<feFuncB type="identity"/>
<feFuncA type="discrete" tableValues="0 1"/>
</feComponentTransfer>
</filter>
</svg>
Then at the very end of the url is an id reference to that #filter:
"url(data:image/svg+...Zz4=#filter)";
The SVG filter uses a discrete transform on the alpha channel, selecting only completely transparent or completely opaque on a 50% boundary when rendering. This can be tweaked to add some anti-aliasing back in if needed, e.g.:
...
<feFuncA type="discrete" tableValues="0 0 0.25 0.75 1"/>
...
Cons / Notes / Gotchas
Note, I didn't test this method with images, but I can presume it would affect semi-transparent parts of images. I can also guess that it probably would not prevent antialiasing on images at differing color boundaries. It isn't a 'nearest color' solution but rather a binary transparency solution. It seems to work best with path / shape rendering since alpha is the only channel antialiased with paths.
Also, using a minimum lineWidth of 1 is safe. Thinner lines become sparse or may often disappear completely.
Edit:
I've discovered that, in Firefox, setting filter to a dataurl does not work immediately / synchronously: the dataurl has to 'load' first.
e.g. The following will not work in Firefox:
ctx.filter = "url(data:image/svg+xml;base64,...#filter)";
ctx.beginPath();
ctx.moveTo(10,10);
ctx.lineTo(20,20);
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
ctx.filter = "none";
But waiting till the next JS frame works fine:
ctx.filter = "url(data:image/svg+xml;base64,...#filter)";
setTimeout(() => {
ctx.beginPath();
ctx.moveTo(10,10);
ctx.lineTo(20,20);
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
ctx.filter = "none";
}, 0);
I want to add that I had trouble when downsizing an image and drawing on canvas, it was still using smoothing, even though it wasn't using when upscaling.
I solved using this:
function setpixelated(context){
context['imageSmoothingEnabled'] = false; /* standard */
context['mozImageSmoothingEnabled'] = false; /* Firefox */
context['oImageSmoothingEnabled'] = false; /* Opera */
context['webkitImageSmoothingEnabled'] = false; /* Safari */
context['msImageSmoothingEnabled'] = false; /* IE */
}
You can use this function like this:
var canvas = document.getElementById('mycanvas')
setpixelated(canvas.getContext('2d'))
Maybe this is useful for someone.
ctx.translate(0.5, 0.5);
ctx.lineWidth = .5;
With this combo I can draw nice 1px thin lines.
While we still don't have proper shapeSmoothingEnabled or shapeSmoothingQuality options on the 2D context (I'll advocate for this and hope it makes its way in the near future), we now have ways to approximate a "no-antialiasing" behavior, thanks to SVGFilters, which can be applied to the context through its .filter property.
So, to be clear, it won't deactivate antialiasing per se, but provides a cheap way both in term of implementation and of performances (?, it should be hardware accelerated, which should be better than a home-made Bresenham on the CPU) in order to remove all semi-transparent pixels while drawing, but it may also create some blobs of pixels, and may not preserve the original input color.
For this we can use a <feComponentTransfer> node to grab only fully opaque pixels.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ABEDBE";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "black";
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
// first without filter
ctx.fillText("no filter", 60, 20);
drawArc();
drawTriangle();
// then with filter
ctx.setTransform(1, 0, 0, 1, 120, 0);
ctx.filter = "url(#remove-alpha)";
// and do the same ops
ctx.fillText("no alpha", 60, 20);
drawArc();
drawTriangle();
// to remove the filter
ctx.filter = "none";
function drawArc() {
ctx.beginPath();
ctx.arc(60, 80, 50, 0, Math.PI * 2);
ctx.stroke();
}
function drawTriangle() {
ctx.beginPath();
ctx.moveTo(60, 150);
ctx.lineTo(110, 230);
ctx.lineTo(10, 230);
ctx.closePath();
ctx.stroke();
}
// unrelated
// simply to show a zoomed-in version
const zoomed = document.getElementById("zoomed");
const zCtx = zoomed.getContext("2d");
zCtx.imageSmoothingEnabled = false;
canvas.onmousemove = function drawToZoommed(e) {
const
x = e.pageX - this.offsetLeft,
y = e.pageY - this.offsetTop,
w = this.width,
h = this.height;
zCtx.clearRect(0,0,w,h);
zCtx.drawImage(this, x-w/6,y-h/6,w, h, 0,0,w*3, h*3);
}
<svg width="0" height="0" style="position:absolute;z-index:-1;">
<defs>
<filter id="remove-alpha" x="0" y="0" width="100%" height="100%">
<feComponentTransfer>
<feFuncA type="discrete" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</defs>
</svg>
<canvas id="canvas" width="250" height="250" ></canvas>
<canvas id="zoomed" width="250" height="250" ></canvas>
For the ones that don't like to append an <svg> element in their DOM, and who live in the near future (or with experimental flags on), the CanvasFilter interface we're working on should allow to do this without a DOM (so from Worker too):
if (!("CanvasFilter" in globalThis)) {
throw new Error("Not Supported", "Please enable experimental web platform features, or wait a bit");
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ABEDBE";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "black";
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
// first without filter
ctx.fillText("no filter", 60, 20);
drawArc();
drawTriangle();
// then with filter
ctx.setTransform(1, 0, 0, 1, 120, 0);
ctx.filter = new CanvasFilter([
{
filter: "componentTransfer",
funcA: {
type: "discrete",
tableValues: [ 0, 1 ]
}
}
]);
// and do the same ops
ctx.fillText("no alpha", 60, 20);
drawArc();
drawTriangle();
// to remove the filter
ctx.filter = "none";
function drawArc() {
ctx.beginPath();
ctx.arc(60, 80, 50, 0, Math.PI * 2);
ctx.stroke();
}
function drawTriangle() {
ctx.beginPath();
ctx.moveTo(60, 150);
ctx.lineTo(110, 230);
ctx.lineTo(10, 230);
ctx.closePath();
ctx.stroke();
}
// unrelated
// simply to show a zoomed-in version
const zoomed = document.getElementById("zoomed");
const zCtx = zoomed.getContext("2d");
zCtx.imageSmoothingEnabled = false;
canvas.onmousemove = function drawToZoommed(e) {
const
x = e.pageX - this.offsetLeft,
y = e.pageY - this.offsetTop,
w = this.width,
h = this.height;
zCtx.clearRect(0,0,w,h);
zCtx.drawImage(this, x-w/6,y-h/6,w, h, 0,0,w*3, h*3);
};
<canvas id="canvas" width="250" height="250" ></canvas>
<canvas id="zoomed" width="250" height="250" ></canvas>
Or you can also save the SVG as an external file and set the filter property to path/to/svg_file.svg#remove-alpha.
Notice a very limited trick. If you want to create a 2 colors image, you may draw any shape you want with color #010101 on a background with color #000000. Once this is done, you may test each pixel in the imageData.data[] and set to 0xFF whatever value is not 0x00 :
imageData = context2d.getImageData (0, 0, g.width, g.height);
for (i = 0; i != imageData.data.length; i ++) {
if (imageData.data[i] != 0x00)
imageData.data[i] = 0xFF;
}
context2d.putImageData (imageData, 0, 0);
The result will be a non-antialiased black & white picture. This will not be perfect, since some antialiasing will take place, but this antialiasing will be very limited, the color of the shape being very much like the color of the background.
Here is a basic implementation of Bresenham's algorithm in JavaScript. It's based on the integer-arithmetic version described in this wikipedia article: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
function range(f=0, l) {
var list = [];
const lower = Math.min(f, l);
const higher = Math.max(f, l);
for (var i = lower; i <= higher; i++) {
list.push(i);
}
return list;
}
//Don't ask me.
//https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
function bresenhamLinePoints(start, end) {
let points = [];
if(start.x === end.x) {
return range(f=start.y, l=end.y)
.map(yIdx => {
return {x: start.x, y: yIdx};
});
} else if (start.y === end.y) {
return range(f=start.x, l=end.x)
.map(xIdx => {
return {x: xIdx, y: start.y};
});
}
let dx = Math.abs(end.x - start.x);
let sx = start.x < end.x ? 1 : -1;
let dy = -1*Math.abs(end.y - start.y);
let sy = start.y < end.y ? 1 : - 1;
let err = dx + dy;
let currX = start.x;
let currY = start.y;
while(true) {
points.push({x: currX, y: currY});
if(currX === end.x && currY === end.y) break;
let e2 = 2*err;
if (e2 >= dy) {
err += dy;
currX += sx;
}
if(e2 <= dx) {
err += dx;
currY += sy;
}
}
return points;
}
For those who still looking for answers. here is my solution.
Assumming image is 1 channel gray. I just thresholded after ctx.stroke().
ctx.beginPath();
ctx.moveTo(some_x, some_y);
ctx.lineTo(some_x, some_y);
...
ctx.closePath();
ctx.fill();
ctx.stroke();
let image = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)
for(let x=0; x < ctx.canvas.width; x++) {
for(let y=0; y < ctx.canvas.height; y++) {
if(image.data[x*image.height + y] < 128) {
image.data[x*image.height + y] = 0;
} else {
image.data[x*image.height + y] = 255;
}
}
}
if your image channel is 3 or 4. you need to modify the array index like
x*image.height*number_channel + y*number_channel + channel
Just two notes on StashOfCode's answer:
It only works for a grayscale, opaque canvas (fillRect with white then draw with black, or viceversa)
It may fail when lines are thin (~1px line width)
It's better to do this instead:
Stroke and fill with #FFFFFF, then do this:
imageData.data[i] = (imageData.data[i] >> 7) * 0xFF
That solves it for lines with 1px width.
Other than that, StashOfCode's solution is perfect because it doesn't require to write your own rasterization functions (think not only lines but beziers, circular arcs, filled polygons with holes, etc...)
According to MDN docs, Scaling for high resolution displays, "You may find that canvas items appear blurry on higher-resolution displays. While many solutions may exist, a simple first step is to scale the canvas size up and down simultaneously, using its attributes, styling, and its context's scale."
Ignoring the apparent paradox in their statement, this worked in my case, sharpening edges which had previously been unacceptably fuzzy.
// Get the DPR and size of the canvas
const dpr = window.devicePixelRatio;
const rect = canvas.getBoundingClientRect();
// Set the "actual" size of the canvas
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
// Scale the context to ensure correct drawing operations
ctx.scale(dpr, dpr);
// Set the "drawn" size of the canvas
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;

Categories