Text on HTML5 Canvas loop - html

I have code here that displays the days of the week horizontally. How can I make it so that it is displayed in a for loop horizontally but also taking into account the size of the canvas? (the size of the canvas in this case is 600)
canvasObj1 = document.getElementById('myCanvas1');
context = canvasObj1.getContext('2d');
context.fillStyle = "#5D6C7B";
context.font = "12px sans-serif";
context.fillText("Monday", 10, 225);
context.fillText("Tuesday", 70, 225);
context.fillText("Wednesday", 130, 225);
context.fillText("Thursday", 210, 225);
context.fillText("Friday", 275, 225);
context.fillText("Saturday", 330, 225);
context.fillText("Sunday", 400, 225);

Here is one way you can do it:
Online demo
// put week names in an array
var days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
'Friday', 'Saturday', 'Sunday'],
gap = 5, // gap between the names
x = 0, // for drawing text
w = 0, // for measuring total width
i = 0; // generic counter
context.fillStyle = "#5D6C7B";
context.font = "12px sans-serif";
// calc total width incl. gaps
for (; i < days.length; i++) {
w += (context.measureText(days[i]).width + gap);
}
// fine adjust width removing last gap and adding a couple of pixels for space
w = w - gap + 2;
// adjust scale
context.scale(canvas.width / w, 1);
// draw the texts
for (i = 0; i < days.length; i++) {
context.fillText(days[i], x, 225);
x += (context.measureText(days[i]).width + gap);
}
Just remember to reset the scale or use it to draw other elements relative to texts. One other thing you can do is to store the calculated positions to another array so you can use that with for example vertical lines and so forth.
Another way, in case there is space for all text, is to calculate the total width of the text only (not using the gap value), then subtract it from canvas width, divide on 6 to get an average gap value which you then use with the draw loop.

Related

Pixels of Thin Chars are not of the Exact Requested Color

To recover character bounding boxes I draw the full HTML onto HTML canvas in different colors so I can locate them by their pixel color values. Not all pixel colors are those that I assigned to the character, but most are e.g. majority of the pixels of an 'a' drawn with font color rgb(0,0,100) have the color rgb(0,0,100).
However, for pixels of some thin chars such as '-' or '2' of the exponent, the colors are none of those assigned. If I assigned (0,20,71) as the font color, majority of the pixels color values are NOT (0,20,71) but instead (66,81,119). Due to these alternate colors, when I go looking for pixels with color (0,20,71) I find none, and end up without a bounding box for '-' etc.
Are there color choices that minimize use of alternate colors to display a character? If yes then perhaps I could use those colors instead of random color values.
Is there an HTML rendering attribute that I could turn on/off to minimize the use of alternate colors?
Or is there perhaps a simpler way of doing all this? I used to draw those characters one after another, which worked better, but was too slow to be useful.
Note that I don't need the bounding boxes that could be recovered using the font information or getBoundingClientRect as they are not always exact.
Note: This question is cross posted from graphicdesign as it is more appropriate for this forum.
Your problem is probably caused by anti-aliasing of the text. To get a better understanding, let's have a look at
this highly magnified 'a' character in red color (#ff000) as it would be rendered on the canvas.
As you can see the main shape is indeed painted in red color but it's surrounded by lighter tones of red. If we
simply look for a fixed color (e.g. #ff0000) the selection wouldn't contain the surrounding pixels, as it's a
blend between the foreground and the background color - e.g. #ffbfc1 (pink). So if we go further and try to obtain a bounding-box based on red, it would look like this:
One possible fix goes a bit like this:
get the canvas pixel data
check each pixels color - if it's something different than fully transparent, replace it with a fully opaque solid black #000000
write the pixel data back to the canvas
For the 'a' above this will give us something like this:
Now we can simply go over the pixel data once more, look for black pixels and figure out the min/max x and y values to obtain the surrounding area.
Here's a complete code example:
let canvas = document.getElementById('canvas');
let canvas2 = document.getElementById('canvas2');
let ctx = canvas.getContext('2d');
let ctx2 = canvas2.getContext('2d');
ctx.font = '12px Arial';
ctx.fillText('a²+b²=c²', 110, 90);
ctx2.font = '12px Arial';
ctx2.fillText('a²+b²=c²', 110, 90);
let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let color;
for (let a = 0; a < imageData.data.length; a += 4) {
color = imageData.data[a] * imageData.data[a + 1] + imageData.data[a + 2] + imageData.data[a + 3];
if (color > 0) {
imageData.data[a] = 0;
imageData.data[a + 1] = 0;
imageData.data[a + 2] = 0;
imageData.data[a + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
let x = 0;
let y = 0;
let minY = canvas.height;
let maxY = 0;
let minX = canvas.width;
let maxX = 0;
for (let a = 0; a < imageData.data.length; a += 4) {
if (imageData.data[a + 3] == 255) {
y = Math.floor((a / 4) / canvas.width);
x = (a / 4) - (y * canvas.width);
if (y < minY + 1) {
minY = y - 1;
}
if (y > maxY - 1) {
maxY = y + 1;
}
if (x < minX + 1) {
minX = x - 1;
}
if (x > maxX - 1) {
maxX = x + 1;
}
}
}
ctx.strokeStyle = 'red';
ctx.strokeRect(minX, minY, maxX - minX, maxY - minY);
<div style="display:block;"><canvas id="canvas2" style="background-color:grey;"></canvas>
<canvas id="canvas" style="background-color:grey;"></canvas></div>

html canvas clipping noisy edges [duplicate]

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>

Distribute canvas gradient colors evenly given an arbitrary color range

I'm trying to distribute a canvas linear gradient evenly given a random color range. Something like this.
let canvas = document.getElementsByTagName("CANVAS")[0];
let ctx = canvas.getContext("2d");
let gradient = ctx.createLinearGradient(0, 0, 200, 0);
// generate some random colors
let random = Math.round(Math.random() * (10 - 2) + 2)
let colors = Array.from( Array( random ).keys() ).map(function(){
return '#'+Math.floor(Math.random()*16777215).toString(16);
});
// loop through the colors and add color stop with percentage.
colors.forEach( (color, index) => {
let percentage = (index / colors.length) / 1;
console.log(percentage);
gradient.addColorStop(percentage, color);
})
ctx.fillStyle = gradient;
ctx.fillRect(10, 10, 200, 100);
This issue with this is that it's not even. It inevitably ends up with a longer gradient at the end. How can I calculate the percentage to that the gradient is event across the rectangle.
you need to change the formula to
let percentage = index / (colors.length - 1);
because the index goes from 0 to colors.length-1 inclusive.
This way every transition will get the same amount of pixels.

How to show part of element from other side of canvas

How to show part of element outside of canvas from opposite side canvas. Illustration:
You need to draw twice when the shape is outside canvas' boundaries. Draw the main part first, then the same part offset by width so it gives the illusion of showing on the other side.
Manually Draw twice
This draws a shape going from right to left, when the shape is outside the left edge it will be redrawn at the right edge representing the part that is non-visible on the left side. For the opposite way (left to right) the principle is just the same, just use x with canvas' width instead of 0.
var ctx = document.querySelector("canvas").getContext("2d"),
x = 100, // start position
w = 200; // shape width
ctx.fillStyle = "#777";
(function loop() {
ctx.clearRect(0, 0, 300, 150); // clear canvas
ctx.fillRect(x, 0, w, 150); // draw main part/image/shape
if (x < 0) { // should rotate? draw secondary
ctx.fillRect(ctx.canvas.width + x, 0, w, 150); // use canvas width + x (x<0)
}
x -= 7; // animate
if (x <= -w) x = ctx.canvas.width + x; // at some point reset x
requestAnimationFrame(loop)
})();
<canvas></canvas>
Translated Pattern
To simplify this a CanvasPattern can be used. The later version of canvas allows local transforms on the pattern itself, but since this is not currently widely spread I'll show an example using normal transforms and compensated x position:
var ctx = document.querySelector("canvas").getContext("2d"),
pattern,
x = 100, // start position
w = 200; // shape width
// create pattern
ctx.fillStyle = "#777";
ctx.fillRect(x, 0, w, 150); // draw main part/image/shape
pattern = ctx.createPattern(ctx.canvas, "repeat"); // use current canvas as pattern
ctx.fillStyle = pattern; // set pattern as fillStyle
(function loop() {
ctx.setTransform(1,0,0,1,0,0); // reset transforms
ctx.clearRect(0, 0, 300, 150); // clear canvas
ctx.setTransform(1,0,0,1,x,0); // translate absolute x
ctx.fillRect(-x, 0, 300, 150); // fill using pattern, compensate transform
x -= 7; // animate
requestAnimationFrame(loop)
})();
<canvas></canvas>

HTML5 canvas: single stroke around combined regions

HTML5 canvas: I'm looking for a way to draw a single stroke around a combined path.
For example if I have two overlapping circles I don't want to have two overlapping circle strokes, but one single stroke around the combined region of both circles..
Any chance for that?
It can be done by using globalCompositeOperation. There are various ways you can draw the shapes them selves but here is one approach that results in this (for the two rectangle circles in the demo):
Step 1: setup the normal canvas
Step 2: setup an off-screen canvas
Update Not sure how I could miss the obvious, but you can of course just stroke the circles first, then punch a whole with composite mode and a fill - much faster (I guess I had images on my mind when I came up with the offset redraw).
The reason for off-screen canvas is if you have something in the background already on the main canvas. This will be deleted otherwise where we punch the hole. If nothing is there there is no problem drawing this to a single canvas - updated code:
/// some regions
var rect = [ [20, 20, 200, 200], [100, 100, 200,200] ],
/// ox = off-screen context
ox.strokeStyle = '#fff';
ox.lineWidth = 3 * 2; /// x2 as half will be gone when we punch hole
/// stroke outlines
for(; r = rect[i]; i++) {
o = r[2] * 0.5;
ox.beginPath();
ox.arc(r[0] + o, r[1] + o, o, 0, 2 * Math.PI);
ox.stroke();
}
/// punch hole with composite mode and fill
ox.globalCompositeOperation = 'destination-out';
for(i = 0; r = rect[i]; i++) {
o = r[2] * 0.5;
ox.beginPath();
ox.arc(r[0] + o, r[1] + o, o, 0, 2 * Math.PI);
ox.fill();
}
/// draw result to main canvas
/// ctx = main context, ocanvas = off-screen canvas
ctx.drawImage(ocanvas, 0, 0);
(Animated) online demo using this optimized version
I'll leave the old code as it can be used for images that can't be stroked -
Now draw the shapes filled to the off-screen canvas. Draw in the color you want the outline to be in.
/// some regions
var rect = [ [20, 20, 200, 200], [100, 100, 200,200] ],
/// ox = off-screen canvas
ox.fillStyle = '#fff';
/// draw the array with circes
for(; r = rect[i]; i++) {
var o = r[2] * 0.5;
ox.beginPath(); //use this here - arcs are currently buggy
ox.arc(r[0] + o, r[1] + o, o, 0, 2 * Math.PI);
ox.fill(); //.. and here
}
Now draw the cached image of the shapes back to main canvas. the shapes must be drawn with a slight offset in each direction - this step will create the outline:
/// ctx = main context, ocanvas = off-screen canvas
ctx.drawImage(ocanvas, -1, -1);
ctx.drawImage(ocanvas, 1, -1);
ctx.drawImage(ocanvas, 1, -1);
ctx.drawImage(ocanvas, 1, 1);
ctx.drawImage(ocanvas, -1, 1);
ctx.drawImage(ocanvas, 1, 1);
ctx.drawImage(ocanvas, -1, -1);
ctx.drawImage(ocanvas, -1, 1);
And finally we punch a "hole" in the filled shape to make it transparent with an outline using globalCompositeOperation + a final draw in 0 offset position :
ctx.globalCompositeOperation = 'destination-out';
ctx.drawImage(ocanvas, 0, 0);
ONLINE DEMO
To make the border thicker just increase the offset when you draw back the shapes to main canvas.
That's my current solution. Doesn't need a second canvas and is easier to achieve. It still uses the Ken's idea to use globalCompositeOperation:
context.lineWidth = 2;
context.stroke();
var prev = context.globalCompositeOperation;
context.globalCompositeOperation = "destination-out";
context.fill();
context.globalCompositeOperation = prev;