The canvas context.arc() method draws distorted arcs when the context is scaled up. It looks like the arcs are (poorly) approximated with a Bézier curve. Works correctly in Firefox. Untested in IE.
I observed this problem some time ago, but recently it seems to have become much worse (I'm not sure when).
I found a number of canvas issues on StackOverflow, but not this one. If you know it to be a manifestation of an already-reported issue, please forward a link. I've already reported it via Chrome's Help/Report Issue mechanism.
Before I write my own, does anyone have a workaround? ...perhaps an overloaded or alternative 'arc' method?
The following demo is viewable here: http://keveney.com/chrome_arc_bug.html
paint_canvas();
// simulate circle with line segments
//
function regular_polygon(ctx, segments, cx, cy, r) {
var i, a;
ctx.moveTo(cx + r, cy);
for (i = 0; i < segments; i++) {
a = (Math.PI * 2) * i / segments;
ctx.lineTo(cx + r * Math.cos(a), cy + r * Math.sin(a));
}
ctx.closePath();
ctx.stroke();
}
function paint_canvas() {
var ctx;
// draw unscaled circle using canvas 'arc' method
//
ctx = document.getElementById('canv').getContext('2d');
ctx.beginPath();
ctx.strokeStyle = "#000";
ctx.lineWidth = 1.25;
ctx.arc(250, 250, 200, 0, 2 * Math.PI, false);
ctx.stroke();
// draw enclosing polygons
//
ctx.beginPath();
ctx.strokeStyle = "#c00";
regular_polygon(ctx, 36, 250, 250, 215);
regular_polygon(ctx, 36, 250, 250, 185);
// the same but scaled up from smaller units
//
ctx = document.getElementById('canv2').getContext('2d');
ctx.beginPath();
ctx.strokeStyle = "#000";
ctx.scale(100, 100);
ctx.lineWidth = 1.25 / 100;
ctx.arc(2.5, 2.5, 2, 0, 2 * Math.PI, false);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = "#c00";
regular_polygon(ctx, 36, 2.5, 2.5, 2.15);
regular_polygon(ctx, 36, 2.5, 2.5, 1.85);
}
body {
background-color: #F4F4F4;
width: 800px;
margin-left: auto;
margin-right: auto;
}
canvas {
background-color: #FFFFFF;
}
<p>Chrome arc scaling bug</p>
<canvas id="canv" height=500 width=500></canvas>
<canvas id="canv2" height=500 width=500></canvas>
<p>expected: Both images should be identical.</p>
<p>actual: Arc in second image is badly distorted.</p>
<p>Issue reported 6/17/2015.</p>
<p>tested with 43.0.2357.124 (64-bit)</p>
<p>This issue was observed some time ago, but has gotten worse in recent releases of Chrome. Not tested on Internet Explorer. If you find a convenient solution,
please notify Matt Keveney, matt#keveney.com</p>
This effect stems from an approximation of a circle with a small radius, it looks more like a square than a circle.
If you knowingly will make this kind of circles, I'd recommend making a function that draws a circle with a radius which will generate a good approximation of a circle that will scale well (I chose a radius of 10 in my example below), then adjust the parameters to achieve the wanted circle.
function drawSmallArc(x,y,r,scale) {
var adjust = 10/r;
ctx.save();
ctx.beginPath();
ctx.strokeStyle = "#00f";
ctx.scale(scale/adjust, scale/adjust);
ctx.lineWidth = 1.25 / scale * adjust;
ctx.arc(x*adjust, y*adjust,r*adjust,0,2 * Math.PI, false);
ctx.stroke();
ctx.restore();
}
In action below
var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
//Two referense circles.
ctx.beginPath();
ctx.strokeStyle = "#0f0"; //green
ctx.lineWidth = 1.25;
ctx.arc(250, 250, 180, 0, 2 * Math.PI, false);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = "#0f0"; //green
ctx.lineWidth = 1.25;
ctx.arc(250, 250, 220, 0, 2 * Math.PI, false);
ctx.stroke();
//Red circle using OP's original circle
ctx.save();
ctx.beginPath();
ctx.strokeStyle = "#f00"; //Red
ctx.lineWidth = 1.25 / 100;
ctx.scale(100,100);
ctx.arc(2.5, 2.5, 2, 0, 2 * Math.PI, false);
ctx.stroke();
ctx.restore();
//blue circle with better approximation of circle.
drawSmallArc(2.5,2.5,2,100);
function drawSmallArc(x,y,r,scale) {
var adjust = 10/r;
ctx.save();
ctx.beginPath();
ctx.strokeStyle = "#00f";
ctx.scale(scale/adjust, scale/adjust);
ctx.lineWidth = 1.25 / scale * adjust;
ctx.arc(x*adjust, y*adjust,r*adjust,0,2 * Math.PI, false);
ctx.stroke();
ctx.restore();
}
<canvas id="canvas" height=500 width=500></canvas>
I have worked around the problem for now, using a high-count polygon. Mine is not a fully-compatible drop-in replacement for arc, so it will not be repeated here. It is much like the function used in the above sample code to render the red reference polygons.
I remain interested in a better solution, or a Chrome update that fixes the problem, in case anyone finds it.
Related
I am trying to draw image with rounded shape and the text on the bottom. But when I use ctx.clip() to round image corners, text dissappears.
Text with rectangle image works fine but I need images cornes to be rounded.
ctx.save();
ctx.beginPath();
ctx.fillStyle = `rgb(100, 100, 100, 0.8)`;
ctx.arc(x, y, z / 2, 0, 2 * Math.PI, false);
ctx.clip();
ctx.drawImage(img.img, x - 10, y - 10, 20, 20);
ctx.fill();
ctx.font = `${3.5}px Arial`;
ctx.fillStyle = "black";
ctx.textAlign = "center";
ctx.fillText('TEXT', x, y + 13.5);
Without a workable code it’s hard to t/s. I think you just need to restore after ctx.fill() though.
ctx.save();
ctx.beginPath();
ctx.fillStyle = `rgb(100, 100, 100, 0.8)`;
ctx.arc(x, y, z / 2, 0, 2 * Math.PI, false);
ctx.clip();
ctx.drawImage(img.img, x - 10, y - 10, 20, 20);
ctx.fill();
ctx.restore(); //add this
ctx.font = `${3.5}px Arial`;
ctx.fillStyle = "black";
ctx.textAlign = "center";
ctx.fillText('TEXT', x, y + 13.5);
I've created a program to generate planet sprites. I'm doing that by creating a circular path, running ctx.clip() to keep all the following layers inside of the circle, then drawing a black and transparent texture layer, then a randomly colored rectangle over the full canvas, then a shadow and glow on top of it all. The issue is that a colored line also appears under the circle after clipping, and I'm not sure why. I need this removed.
Here is a fiddle. The last line sets the code to loop every half second: https://jsfiddle.net/tzkwmzqu/4/
I am not sure I do understand your problem, but I will assume that you are talking about the anti-aliasing problem.
Currently, you are drawing a lot over your clipped area.
At each draw, new anti-aliasing artifacts will come to smooth the latest drawing. At the end, what should have been semi-transparent pixels are now fully opaque ones.
In the other hand, with globalCompositeOperation like 'destination-in', you need only one drawing to make the compositing (~clipping). So you don't accumulate artifacts. But even if you did, gCO is global and since it takes transparency into account, the accumulation would be less important.
var ctx1 = clip.getContext('2d');
var ctx2 = gCO.getContext('2d');
var ctx3 = gCO2.getContext('2d');
ctx1.beginPath();
ctx1.arc(150, 150, 150, 0, Math.PI*2)
ctx1.clip();
// drawing multiple times on this clipped area will increase artifacts
ctx1.fillRect(0,0,300, 150);
ctx1.fillRect(0,0,300, 150);
ctx1.fillRect(0,0,300, 150);
ctx1.fillRect(0,0,300, 150);
ctx2.beginPath();
ctx2.arc(150, 150, 150, 0, Math.PI*2)
ctx2.fillRect(0,0,300, 150);
ctx2.globalCompositeOperation = 'destination-in';
//With gCO you only draw once, but even if you did draw multiple times, there would still be less artifacts
ctx2.fill();
ctx2.fill();
ctx2.fill();
ctx2.fill();
ctx2.globalCompositeOperation = 'source-over';
ctx3.beginPath();
ctx3.arc(150, 150, 150, 0, Math.PI*2)
ctx3.fillRect(0,0,300, 150);
ctx3.globalCompositeOperation = 'destination-in';
// only one drawing needed:
ctx3.fill();
ctx3.globalCompositeOperation = 'source-over';
ctx1.fillStyle = ctx2.fillStyle = ctx3.fillStyle = "white";
ctx1.fillText('clipping', 120, 100);
ctx2.fillText('compositing', 120, 100);
ctx3.fillText('single compositing', 120, 100);
canvas{
border: 1px solid;
}
<canvas id="clip"></canvas><canvas id="gCO"></canvas><canvas id="gCO2"></canvas>
A few unrelated notes about your code :
closePath does not mark the end of your path declaration, only a new beginPath() call does. ctx.fillStyle = 'transparent'; ctx.fill() won't do anything. Only putImageData, clearRect methods and globalCompositeOperation + drawing method can produce transparent pixels.
So here is all the above in one snippet :
/* Load images */
var texture = new Image();
texture.src = "http://i.imgur.com/0qMwa8p.png";
var shadow = new Image();
shadow.src = "http://i.imgur.com/pX3HVFY.png";
/* Create the canvas and context references */
var canvas = document.getElementById("game");
canvas.style.width = (canvas.width = 512) + "px";
canvas.style.height = (canvas.height = 512) + "px";
var ctx = canvas.getContext("2d");
/* render */
function render() {
/* Size of planets */
var scale = Math.random() + 1
// We don't need to save/restore the canvas state now,
// simply remember to set the gCO back to 'source-over'
// here it done at the end of the function
/* Clear canvas for redraw */
ctx.clearRect(0, 0, canvas.width, canvas.height);
/* Place texture onto planet */
ctx.globalAlpha = Math.random() * .5 + .5;
ctx.drawImage(texture, (Math.round(Math.random() * 256) - 128 * scale), (Math.round(Math.random() * 256) - 128 * scale), texture.naturalWidth * scale, texture.naturalHeight * scale)
/* Color Planet */
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "multiply";
var color = "hsl(" + Math.random() * 256 + ", 100%, 50%)"
ctx.fillStyle = color;
ctx.fillRect(0, 0, canvas.width, canvas.height)
/* Give planet its shine and shadow */
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(shadow, Math.round(Math.random() * 200 - 128 * scale), Math.round(Math.random() * 200 - 128 * scale), shadow.naturalWidth * scale, shadow.naturalHeight * scale)
// instead of clipping, use gCO
ctx.globalCompositeOperation = 'destination-in';
ctx.beginPath();
ctx.arc(256, 256, 128 * scale, 0, 2 * Math.PI);
ctx.fill();
// reset gCO
ctx.globalCompositeOperation = 'source-over';
}
render()
window.interval = setInterval(render, 500)
#game {
border: 1px solid black;
background-color: black;
}
<canvas id="game"></canvas>
I order to build a HTML 5 datacenter floor plan, I would like to create a polygon filled with a grid. This grid must not be a picture pattern as I would like to be able to zoom or rotate the floor plan without having pixelization.
I would like to be able to create this kind of output :
How can I do that ?
There are multiple ways, like
using a clipping region
var ctx = c.getContext('2d');
drawShape();
ctx.stroke();
ctx.save(); // so we can remove the clipping
ctx.clip();
drawGrid();
ctx.restore(); // remove the clipping
function drawShape() {
ctx.beginPath();
var pts = [
20, 20,
80, 20,
90, 50,
120, 90,
30, 80,
20,20
];
for(var i=0;i<pts.length;i+=2){
ctx.lineTo(pts[i], pts[i+1]);
}
}
function drawGrid() {
ctx.beginPath();
for(var x=-.5; x<c.width; x+=20) {
ctx.moveTo(x, 0);
ctx.lineTo(x, c.height);
}
for(var y=-.5; y<c.height; y+=20) {
ctx.moveTo(0, y);
ctx.lineTo(c.width, y);
}
ctx.stroke();
}
<canvas id="c"></canvas>
using compositing
var ctx = c.getContext('2d');
drawGrid();
ctx.globalCompositeOperation = 'destination-in';
drawShape();
ctx.fill();
ctx.globalCompositeOperation = 'source-over';
ctx.stroke();
function drawShape() {
ctx.beginPath();
var pts = [
20, 20,
80, 20,
90, 50,
120, 90,
30, 80,
20,20
];
for(var i=0;i<pts.length;i+=2){
ctx.lineTo(pts[i], pts[i+1]);
}
}
function drawGrid() {
ctx.beginPath();
for(var x=-.5; x<c.width; x+=20) {
ctx.moveTo(x, 0);
ctx.lineTo(x, c.height);
}
for(var y=-.5; y<c.height; y+=20) {
ctx.moveTo(0, y);
ctx.lineTo(c.width, y);
}
ctx.stroke();
}
<canvas id="c"></canvas>
But in your case, a regular grid, it might actually be better to use a pattern.
Indeed, you'd have to only draw one cell every time you change the scale of your grid, for translations, this can be done internally.
So I didn't do the performance tests myself, and thus encourage you to double check it's worth it, but theoretically, it might be faster and esaier to manage than redrawing the grid every time.
var ctx = c.getContext('2d');
var pat_ctx = document.createElement('canvas').getContext('2d');
var cell_size = 20;
// just a basic drawing example
// first we generate the grid as a pattern
ctx.fillStyle = generatePattern(cell_size, cell_size);
drawShape();
ctx.stroke();
// we move the pattern by half a cell because we actually drawn only a cross
ctx.translate(-cell_size / 2, -cell_size / 2);
ctx.fill();
// make the grid follow the mouse
// without having to redraw ourself the grid
onmousemove = function(e) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, c.width, c.height);
drawShape();
ctx.stroke();
// move the grid
ctx.translate(e.clientX - cell_size / 2, e.clientY - -cell_size / 2);
ctx.fill();
}
// click to zoom (+shift to zoom out)
onclick = function(e) {
if (e.shiftKey) cell_size--;
else cell_size++;
ctx.fillStyle = generatePattern(cell_size, cell_size);
onmousemove(e);
}
// dimply draws a cross
function generatePattern(w, h) {
var canvas = pat_ctx.canvas;
canvas.width = w;
canvas.height = h;
pat_ctx.moveTo(w / 2, 0);
pat_ctx.lineTo(w / 2, h);
pat_ctx.moveTo(0, h / 2);
pat_ctx.lineTo(w, h / 2);
pat_ctx.stroke();
return pat_ctx.createPattern(canvas, 'repeat');
}
function drawShape() {
ctx.beginPath();
var pts = [
20, 20,
80, 20,
90, 50,
120, 90,
30, 80,
20, 20
];
for (var i = 0; i < pts.length; i += 2) {
ctx.lineTo(pts[i], pts[i + 1]);
}
}
<canvas id="c"></canvas>
I'm looking into this clock. I have no experience with the js that comes with this file.
Here is the demo
Here is all the code
How can I edit the face of the clock?
I'm looking at this and I would like more control over what the hands look like. Can someone tell me what makes them come to a point. How could I make them just a thick line?
// draw hour
ctx.save();
var theta = (hour - 3) * 2 * Math.PI / 12;
ctx.rotate(theta);
ctx.beginPath();
ctx.moveTo(-15, -5);
ctx.lineTo(-15, 5);
ctx.lineTo(clockRadius * 0.5, 1);
ctx.lineTo(clockRadius * 0.5, -1);
ctx.fill();
ctx.restore();
// draw minute
ctx.save();
var theta = (minute - 15) * 2 * Math.PI / 60;
ctx.rotate(theta);
ctx.beginPath();
ctx.moveTo(-15, -4);
ctx.lineTo(-15, 4);
ctx.lineTo(clockRadius * 0.8, 1);
ctx.lineTo(clockRadius * 0.8, -1);
ctx.fill();
ctx.restore();
// draw second
ctx.save();
var theta = (seconds - 15) * 2 * Math.PI / 60;
ctx.rotate(theta);
ctx.beginPath();
ctx.moveTo(-15, -3);
ctx.lineTo(-15, 3);
ctx.lineTo(clockRadius * 0.9, 1);
ctx.lineTo(clockRadius * 0.9, -1);
ctx.fillStyle = '#0f0';
ctx.fill();
ctx.restore();
ctx.restore();
}
Thanks!
Look at what each of the calls to ctx do to make the hand:
ctx.rotate(theta); // Rotates the canvas according to the hand position.
ctx.beginPath(); // Start drawing a path.
ctx.moveTo(-15, -4); // Set the "brush"/"pen" at center left corner.
ctx.lineTo(-15, 4); // Draw a line to position center right corner.
ctx.lineTo(clockRadius * 0.8, 1); // Draw a line to the edge, right corner.
ctx.lineTo(clockRadius * 0.8, -1); // Draw a line to the edge, left corner.
ctx.fill(); // Fill the polygon we just drew.
ctx.restore(); // Rotate the canvas back.
So, for example, changing the 1 and -1 values to 4 and -4 would make a thick rectangular face.
More learnin' can be aquired here: http://www.html5canvastutorials.com/tutorials/html5-canvas-lines/
I'm drawing a simple progress indicator using canvas. When the element is drawn for the first time it looks all nice and anti-aliased, but when drawn a second time, it loses it's anti-aliasing. Anyone know what could be going on here?
function drawProgress(id, percent) {
var selected = $(safeID(id)).is('.selected');
var canvas = $(safeID("CANVAS_" + id));
var ctx = $(canvas)[0].getContext('2d');
ctx.clearRect();
if ( selected ) {
ctx.fillStyle = "#ffffff";
ctx.strokeStyle = "#ffffff";
}
else {
ctx.fillStyle = "#99a7ca";
ctx.strokeStyle = "#99a7ca";
}
ctx.beginPath();
ctx.arc(canvas.width()/2.0, canvas.height()/2.0, canvas.width()/2.0-1, 0, Math.PI, false);
ctx.fill();
ctx.beginPath();
ctx.arc(canvas.width()/2.0, canvas.height()/2.0, canvas.width()/2.0-1, 0, Math.PI*2.0, false);
ctx.stroke();
}
You need to specify dimensions to clearRect.