I'm trying to draw the following gradient image in canvas, but there's a problem in the right bottom.
Desired effect:
Current output:
I'm probably missing something really simple here.
function color(r, g, b) {
var args = Array.prototype.slice.call(arguments);
if (args.length == 1) {
args.push(args[0]);
args.push(args[0]);
} else if (args.length != 3 && args.length != 4) {
return;
}
return "rgb(" + args.join() + ")";
}
function drawPixel(x, y, fill) {
var fill = fill || "black";
context.beginPath();
context.rect(x, y, 1, 1);
context.fillStyle = fill;
context.fill();
context.closePath();
}
var canvas = document.getElementById("primary");
var context = canvas.getContext("2d");
canvas.width = 256;
canvas.height = 256;
for (var x = 0; x < canvas.width; x++) {
for (var y = 0; y < canvas.height; y++) {
var r = 255 - y;
var g = 255 - x - y;
var b = 255 - x - y;
drawPixel(x, y, color(r, g, b));
}
}
#primary {
display: block;
border: 1px solid gray;
}
<canvas id="primary"></canvas>
JSFiddle
Using gradients.
You can get the GPU to do most of the processing for you.The 2D composite operation multiply effectively multiplies two colours for each pixel. So for each channel and each pixel colChanDest = Math.floor(colChanDest * (colChanSrc / 255)) is done via the massively parallel processing power of the GPU, rather than a lowly shared thread running on a single core (JavaScript execution context).
The two gradients
One is the background White to black from top to bottom
var gradB = ctx.createLinearGradient(0,0,0,255);
gradB.addColorStop(0,"white");
gradB.addColorStop(1,"black");
The other is the Hue that fades from transparent to opaque from left to right
var swatchHue
var col = "rgba(0,0,0,0)"
var gradC = ctx.createLinearGradient(0,0,255,0);
gradC.addColorStop(0,``hsla(${hueValue},100%,50%,0)``);
gradC.addColorStop(1,``hsla(${hueValue},100%,50%,1)``);
Note the above strings quote are not rendering correctly on SO so I just doubled them to show, use a single quote as done in the demo snippet.
Rendering
Then layer the two, background (gray scale) first, then with composite operation "multiply"
ctx.fillStyle = gradB;
ctx.fillRect(0,0,255,255);
ctx.fillStyle = gradC;
ctx.globalCompositeOperation = "multiply";
ctx.fillRect(0,0,255,255);
ctx.globalCompositeOperation = "source-over";
Only works for Hue
It is important that the color (hue) is a pure colour value, you can not use a random rgb value. If you have a selected rgb value you need to extract the hue value from the rgb.
The following function will convert a RGB value to a HSL colour
function rgbToLSH(red, green, blue, result = {}){
value hue, sat, lum, min, max, dif, r, g, b;
r = red/255;
g = green/255;
b = blue/255;
min = Math.min(r,g,b);
max = Math.max(r,g,b);
lum = (min+max)/2;
if(min === max){
hue = 0;
sat = 0;
}else{
dif = max - min;
sat = lum > 0.5 ? dif / (2 - max - min) : dif / (max + min);
switch (max) {
case r:
hue = (g - b) / dif;
break;
case g:
hue = 2 + ((b - r) / dif);
break;
case b:
hue = 4 + ((r - g) / dif);
break;
}
hue *= 60;
if (hue < 0) {
hue += 360;
}
}
result.lum = lum * 255;
result.sat = sat * 255;
result.hue = hue;
return result;
}
Put it all together
The example renders a swatch for a random red, green, blue value every 3 second.
Note that this example uses Balel so that it will work on IE
var canvas = document.createElement("canvas");
canvas.width = canvas.height = 255;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
function drawSwatch(r, g, b) {
var col = rgbToLSH(r, g, b);
var gradB = ctx.createLinearGradient(0, 0, 0, 255);
gradB.addColorStop(0, "white");
gradB.addColorStop(1, "black");
var gradC = ctx.createLinearGradient(0, 0, 255, 0);
gradC.addColorStop(0, `hsla(${Math.floor(col.hue)},100%,50%,0)`);
gradC.addColorStop(1, `hsla(${Math.floor(col.hue)},100%,50%,1)`);
ctx.fillStyle = gradB;
ctx.fillRect(0, 0, 255, 255);
ctx.fillStyle = gradC;
ctx.globalCompositeOperation = "multiply";
ctx.fillRect(0, 0, 255, 255);
ctx.globalCompositeOperation = "source-over";
}
function rgbToLSH(red, green, blue, result = {}) {
var hue, sat, lum, min, max, dif, r, g, b;
r = red / 255;
g = green / 255;
b = blue / 255;
min = Math.min(r, g, b);
max = Math.max(r, g, b);
lum = (min + max) / 2;
if (min === max) {
hue = 0;
sat = 0;
} else {
dif = max - min;
sat = lum > 0.5 ? dif / (2 - max - min) : dif / (max + min);
switch (max) {
case r:
hue = (g - b) / dif;
break;
case g:
hue = 2 + ((b - r) / dif);
break;
case b:
hue = 4 + ((r - g) / dif);
break;
}
hue *= 60;
if (hue < 0) {
hue += 360;
}
}
result.lum = lum * 255;
result.sat = sat * 255;
result.hue = hue;
return result;
}
function drawRandomSwatch() {
drawSwatch(Math.random() * 255, Math.random() * 255, Math.random() * 255);
setTimeout(drawRandomSwatch, 3000);
}
drawRandomSwatch();
To calculate the colour from the x and y coordinates you need the calculated Hue then the saturation and value to get the hsv colour (NOTE hsl and hsv are different colour models)
// saturation and value are clamped to prevent rounding errors creating wrong colour
var rgbArray = hsv_to_rgb(
hue, // as used to create the swatch
Math.max(0, Math.min(1, x / 255)),
Math.max(0, Math.min(1, 1 - y / 255))
);
Function to get r,g,b values for h,s,v colour.
/* Function taken from datGUI.js
Web site https://workshop.chromeexperiments.com/examples/gui/#1--Basic-Usage
// h 0-360, s 0-1, and v 0-1
*/
function hsv_to_rgb(h, s, v) {
var hi = Math.floor(h / 60) % 6;
var f = h / 60 - Math.floor(h / 60);
var p = v * (1.0 - s);
var q = v * (1.0 - f * s);
var t = v * (1.0 - (1.0 - f) * s);
var c = [
[v, t, p],
[q, v, p],
[p, v, t],
[p, q, v],
[t, p, v],
[v, p, q]
][hi];
return {
r: c[0] * 255,
g: c[1] * 255,
b: c[2] * 255
};
}
I had to do this with OpenGL, and Blindman67's answer was the only resource I found.
In the end, I did it by drawing 3 rectangles on top of each other.
All white
Transparent red to opaque red, horizontally
Transparent black to opaque black, vertically
Update: In the previous example, I've only created the gradient for red. I can also use the same method to create green and blue gradients after a little modification, but I can't use it to create gradients for random hues. Red, Green, and Blue are easy because while the one channel is 255, other two have the same value. For a random hue, e.g. 140°, that is not the case. H=140translates to rgb(0,255,85). Red and Blue can't have equal values. This requires a different and a more complicated calculation.
Blindman67's answer solves this problem. Using built-in gradients, you can easily create gradients for any random hue:
jsfiddle. But being a very curious person, I wanted to do it the hard way anyway, and this is it:
(Compared to Blindman67's, it's very slow...)
JSFiddle
function drawPixel(x, y, fillArray) {
fill = "rgb(" + fillArray.join() + ")" || "black";
context.beginPath();
context.rect(x, y, 1, 1);
context.fillStyle = fill;
context.fill();
}
var canvas = document.getElementById("primary");
var context = canvas.getContext("2d");
var grad1 = [ [255, 255, 255], [0, 0, 0] ]; // brightness
fillPrimary([255, 0, 0]); // initial hue = 0 (red)
$("#secondary").on("input", function() {
var hue = parseInt(this.value, 10);
var clr = hsl2rgb(hue, 100, 50);
fillPrimary(clr);
});
function fillPrimary(rgb) {
var grad2 = [ [255, 255, 255], rgb ]; // saturation
for (var x = 0; x < canvas.width; x++) {
for (var y = 0; y < canvas.height; y++) {
var grad1Change = [
grad1[0][0] - grad1[1][0],
grad1[0][1] - grad1[1][1],
grad1[0][2] - grad1[1][2],
];
var currentGrad1Color = [
grad1[0][0] - (grad1Change[0] * y / 255),
grad1[0][1] - (grad1Change[1] * y / 255),
grad1[0][2] - (grad1Change[2] * y / 255)
];
var grad2Change = [
grad2[0][0] - grad2[1][0],
grad2[0][1] - grad2[1][1],
grad2[0][2] - grad2[1][2],
];
var currentGrad2Color = [
grad2[0][0] - (grad2Change[0] * x / 255),
grad2[0][1] - (grad2Change[1] * x / 255),
grad2[0][2] - (grad2Change[2] * x / 255)
];
var multiplied = [
Math.floor(currentGrad1Color[0] * currentGrad2Color[0] / 255),
Math.floor(currentGrad1Color[1] * currentGrad2Color[1] / 255),
Math.floor(currentGrad1Color[2] * currentGrad2Color[2] / 255),
];
drawPixel(x, y, multiplied);
}
}
}
function hsl2rgb(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
var r, g, b;
if (s == 0) {
r = g = b = l;
} else {
var hue2rgb = function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [
Math.round(r * 255),
Math.round(g * 255),
Math.round(b * 255),
];
}
#primary {
display: block;
border: 1px solid gray;
}
#secondary {
width: 256px;
height: 15px;
margin-top: 15px;
outline: 0;
display: block;
border: 1px solid gray;
box-sizing: border-box;
-webkit-appearance: none;
background-image: linear-gradient(to right, red 0%, yellow 16.66%, lime 33.33%, cyan 50%, blue 66.66%, violet 83.33%, red 100%);
}
#secondary::-webkit-slider-thumb {
-webkit-appearance: none;
height: 25px;
width: 10px;
border-radius: 10px;
background-color: rgb(230, 230, 230);
border: 1px solid gray;
box-shadow: inset 0 0 2px rgba(255, 255, 255, 1), 0 0 2px rgba(255, 255, 255, 1);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="primary" width="256" height="256"></canvas>
<input type="range" min="0" max="360" step="1" value="0" id="secondary" />
Okay, so I've figured out what the problem is. While the vertical range is always between [0,255], horizontal range is between [0,r]. So g and b can't be greater than r (Duh!).
function color(r, g, b) {
var args = Array.prototype.slice.call(arguments);
if (args.length == 1) {
args.push(args[0]);
args.push(args[0]);
} else if (args.length != 3 && args.length != 4) {
return;
}
return "rgb(" + args.join() + ")";
}
function drawPixel(x, y, fill) {
var fill = fill || "black";
context.beginPath();
context.rect(x, y, 1, 1);
context.fillStyle = fill;
context.fill();
context.closePath();
}
var canvas = document.getElementById("primary");
var context = canvas.getContext("2d");
canvas.width = 256;
canvas.height = 256;
for (var x = 0; x < canvas.width; x++) {
for (var y = 0; y < canvas.height; y++) {
var r = 255 - y;
var g = b = r - Math.floor((x / 255) * r); // tada!
drawPixel(x, y, color(r, g, b));
}
}
#primary {
display: block;
border: 1px solid gray;
}
<canvas id="primary"></canvas>
Related
I am drawing an ellipse in a "canvas" element with the following lines of code:
let centerX = 250, centerY = 250;
let widthEllipse = 75;
let heightEllipse = 50;
context.beginPath();
context.lineWidth = 1;
context.ellipse(centerX, centerY, heightEllipse, widthEllipse, Math.PI / 4, 0, 2 * Math.PI);
context.stroke();
As a result, I get this drawing:
How can I calculate all the pixels on the circumference of an ellipse?
For example, to make such calculations for a circle, I used the following formulas:
for (let y = 0; y < r*2; y++) {
P1=(x0-sqrt(r^2-(y-y0)^2), y);
P2=(x0+sqrt(r^2-(y-y0)^2), y;
}
Scan lines. Axis Aligned ellipse
The first example is simple and only handles axis aligned ellipses.
Call is scanEllipse(x, y, xRadius, yRadius);
const ctx = can.getContext("2d");
scanEllipse(102, 64, 100, 30);
scanEllipse(256, 64, 40, 60);
function scanEllipse(x, y, h, v) {
const hSqr = h * h;
const scale = h / v;
var i = -v;
while (i <= v) {
var ii = i * scale;
var p1 = (hSqr - ii * ii) ** 0.5;
ctx.fillStyle = i % 2 ? "#F00" : "#000";
ctx.fillRect(x - p1, y + i, p1 * 2, 1);
i++;
}
}
canvas { border: 1px solid black; }
<canvas id="can" width="300" height="128"></canvas>
Scan lines. Rotated ellipse
This gets messy. To make sure it covers as many cases as possible I animated the function. I could not see any glitches but there may be some cases (very big ellipse or ellipses with very large eccentricity) where the floating point error may cause artifacts.
Call is scanEllipse(x, y, xRadius, yRadius, ang); ang is in radians.
const ctx = can.getContext("2d");
const quadRoots = (a, b, c) => { // find quadratic roots
if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : [] }
b /= a;
var d = b * b - 4 * (c / a);
if (d > 0) {
d = d ** 0.5;
return [0.5 * (-b + d), 0.5 * (-b - d)]
}
return d === 0 ? [0.5 * -b] : [];
}
function drawHLine(x, y, w) {
ctx.fillStyle = y % 2 ? "#F00" : "#000";
ctx.fillRect(x, y, w, 1);
}
function scanEllipse(x, y, h, v, a) {
const C = Math.cos(a), C2 = C * C;
const S = Math.sin(a), S2 = S * S;
const v2 = v * v, h2 = h * h;
const A = v2 * C2 + h2 * S2;
var i = 0, a, b, scan = true;
function atY(y) {
const B = 2 * y * C * S * (v2 - h2);
const c = y * y *(v2 * S2 + h2 * C2 )- h2 * v2;
return quadRoots(A, B, c);
}
while (scan) {
[a, b] = atY(i);
if (a !== undefined && b !== undefined) {
drawHLine(x + a, y + i, b - a);
if (i > 0) {
[a, b] = atY(-i);
drawHLine(x + a, y - i, b - a);
}
} else { scan = false; }
i++;
}
}
requestAnimationFrame(renderLoop);
function renderLoop(time) {
ctx.clearRect(0, 0, can.width, can.height);
const h = Math.sin(time * 0.001) * 45 + 50;
const v = Math.sin(time * 0.00333) * 35 + 40;
scanEllipse(100, 100, h, v, time * 0.00077);
requestAnimationFrame(renderLoop);
}
canvas { border: 1px solid black; }
<canvas id="can" width="200" height="200"></canvas>
Scan lines. Rotated ellipse edge only
Addresses just the outside pixels. (to fit the rules of good pixel art line work).
The function uses the same method as above by uses each previous row to workout which pixels are edge pixels.
There is plenty of room for optimization and the animation is slowed just a little to let use see the pixels.
Not that this version calculates the left and right edge using the center of each pixel row (eg y + 0.5). Using the top or bottom of the row makes for a lesser quality ellipse IMHO.
Call is scanEllipse(x, y, xRadius, yRadius, ang); ang is in radians.
const ctx = can.getContext("2d");
const quadRoots = (a, b, c) => { // find quadratic roots
if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : [] }
b /= a;
var d = b * b - 4 * (c / a);
if (d > 0) {
d = d ** 0.5;
return [0.5 * (-b + d), 0.5 * (-b - d)]
}
return d === 0 ? [0.5 * -b] : [];
}
function drawHLine(x, y, w) {
ctx.fillStyle = y % 2 ? "#F00" : "#000";
ctx.fillRect(x, y, w, 1);
}
function scanEllipse(x, y, h, v, a) {
const C = Math.cos(a), C2 = C * C;
const S = Math.sin(a), S2 = S * S;
const v2 = v * v, h2 = h * h;
const A = v2 * C2 + h2 * S2;
var i = 0, a1, b1, a2, b2, scan = true;
var pa1, pb1, pa2, pb2; // p for previous
function atY(y) {
const B = 2 * y * C * S * (v2 - h2);
const c = y * y *(v2 * S2 + h2 * C2 )- h2 * v2;
return quadRoots(A, B, c);
}
const max = Math.max, min = Math.min;
const addPx = (x, y) => ctx.fillRect(x, y, 1, 1);
const addEdgeLine = (x1, x2, y) => {
[x1, x2] = [min(x1, x2) | 0, max(x1, x2) | 0];
if (x1 == x2) { addPx(x1++, y); }
while (x1 < x2) {addPx(x1++, y);}
}
while (scan) {
[a1, b1] = atY(i - 0.5);
if (a1 !== undefined && b1 !== undefined) {
[a2, b2] = atY(-i +0.5);
if (pa1) {
addEdgeLine(pa1 + x, a1 + x, y + i - 1);
addEdgeLine(pb1 + x, b1 + x, y + i - 1);
if (i > 1) {
addEdgeLine(pa2 + x, a2 + x, y - i + 1);
addEdgeLine(pb2 + x, b2 + x, y - i + 1);
}
pa2 = a2;
pb2 = b2;
} else {
pa2 = min(a1,a2);
pb2 = max(b1,b2);
}
pa1 = a1;
pb1 = b1;
} else {
// add last row (top bottom)
if (pa1) {
addEdgeLine(pa1 + x, pb1 + x, y + i - 1);
addEdgeLine(pa2 + x, pb2 + x, y - i + 1);
}
scan = false;
}
i++;
}
}
requestAnimationFrame(renderLoop);
var tick = 0;
function renderLoop(time) {
if (tick++ % 4 === 0) {
time /= 4;
ctx.clearRect(0, 0, can.width, can.height);
const h = Math.sin(time * 0.001) ** 2 * 34 + 10;
const v = Math.sin(time * 0.00333) ** 2 * 35 + 10;
scanEllipse(50, 50, h, v, time * 0.00077);
}
requestAnimationFrame(renderLoop);
}
canvas {
border: 1px solid black;
width: 400px;
height: 400px;
image-rendering: pixelated;
}
<canvas id="can" width="100" height="100"></canvas>
I'm having difficulties replicating the pyramid below on the canvas.
I'm struggling with the math portion on how to draw a new ball on each new line. Here is my code so far.
<canvas id="testCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>
<script>
// Access canvas element and its context
const canvas = document.getElementById('testCanvas');
const context = canvas.getContext("2d");
const x = canvas.width;
const y = canvas.height;
const radius = 10;
const diamater = radius * 2;
const numOfRows = canvas.width / diamater;
function ball(x, y) {
context.arc(x, y, radius, 0, 2 * Math.PI, true);
context.fillStyle = "#FF0000"; // red
context.fill();
}
function draw() {
for (let i = 0; i < numOfRows; i++) {
for (let j = 0; j < i + 1; j++) {
ball(
//Pos X
(x / 2),
//Pos Y
diamater * (i + 1)
);
}
}
ball(x / 2, y);
context.restore();
}
draw();
</script>
I've been stuck on this problem for a while. I appreciate any assistance you can provide.
Thank you.
I noticed that the circle do not touch. I am not sure if you need or want them to but as this presented an interesting problem I create this answer.
Distance between stacked circles.
The distance between rows can be calculated using the right triangle as shown in the following image
Where R is the radius of the circle and D is the distance between rows.
D = ((R + R) ** 2 - R ** 2) ** 0.5;
With that we can get the number of rows we can fit given a radius as
S = (H - R * 2) / D;
Where H is the height of the canvas and S is the number of rows.
Example
Given a radius fits as many rows as possible into the give canvas height.
const ctx = canvas.getContext("2d");
const W = canvas.width, H = canvas.height, CENTER = W / 2;
const cols = ["#E80", "#0B0"];
draw();
function fillPath(path, x, y, color) {
ctx.fillStyle = color;
ctx.setTransform(1, 0, 0, 1, x, y);
ctx.fill(path);
}
function draw() {
const R = 10;
const D = ((R * 2) ** 2 - R ** 2) ** 0.5;
const S = (H - R * 2) / D | 0;
const TOP = R + (H - (R * 2 + D * S)) / 2; // center horizontal
const circle = new Path2D();
circle.arc(0, 0, R, 0, Math.PI * 2);
var y = 0, x;
while (y <= S) {
x = 0;
const LEFT = CENTER - (y * R);
while (x <= y) {
fillPath(circle, LEFT + (x++) * R * 2, TOP + y * D, cols[y % 2]);
}
y ++;
}
}
canvas {
border:1px solid #ddd;
}
<canvas id="canvas" width="300" height="180"></canvas>
Radius to fit n rows of stacked circles
Or if you have the height H and the number of rows S you want to fit. As shown in next image.
We want to find R given H and S we rearrange for H and solve the resulting quadratic with
ss = S * S - 2 * S + 1;
a = 4 / ss;
b = -4 * H / ss;
c = H * H / ss;
R = (-b-(b*b - 4 * a * c) ** 0.5) / (2 * a); // the radius
Example
Given the number of rows (number input) calculates the radius that will fit that number of rows
const ctx = canvas.getContext("2d");
const W = canvas.width, H = canvas.height, CENTER = W / 2;
rowsIn.addEventListener("input", draw)
const cols = ["#DD0", "#0A0"];
draw();
function fillPath(path, x, y, color) {
ctx.fillStyle = color;
ctx.setTransform(1, 0, 0, 1, x, y);
ctx.fill(path);
}
function draw() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0,0,W,H);
const S = Number(rowsIn.value);
const ss = S * S - 2 * S + 1;
const a = 4 / ss - 3, b = -4 * H / ss, c = H * H / ss;
const R = (- b - ((b * b - 4 * a * c) ** 0.5)) / (2 * a); // the radius
const TOP = R;
const D = ((R * 2) ** 2 - R ** 2) ** 0.5;
//const S = (H - R * 2) / D;
const circle = new Path2D();
circle.arc(0, 0, R, 0, Math.PI * 2);
var y = 0, x;
while (y < S) {
x = 0;
const LEFT = CENTER - (y * R);
while (x <= y) {
fillPath(circle, LEFT + (x++) * R * 2, TOP + y * D, cols[y % 2]);
}
y ++;
}
}
canvas {
border:1px solid #ddd;
}
<canvas id="canvas" width="300" height="180"></canvas>
<input type="number" id="rowsIn" min="3" max="12" value="3">Rows
How you can approach this problem is by breaking it down into one step at a time.
On (1)st row draw 1 circle
On (2)nd row draw 2 circles
On (3)rd row draw 3 circles
And so on...
Then you have to figure out where to draw each circle. That also you can break down into steps.
1st-row 1st circle in the center (width)
2nd-row 1st circle in the center minus diameter
2nd-row 2nd circle in the center plus diameter
and so on.
Doing this way you will find a pattern to convert into 2 for loops.
Something like this:
//1st row 1st circle
ball(w/2,radius * 1, red);
//2nd row 1st circle
ball(w/2 - radius,radius * 3, blue);
//2nd row 2nd circle
ball(w/2 + radius,radius * 3, blue);
The code below shows each step how each ball is drawn. I have also done few corrections to take care of the numberOfRows.
const canvas = document.getElementById('testCanvas');
const context = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const radius = 10;
const diamater = radius * 2;
const numOfRows = Math.min(h / diamater, w / diamater);
const red = "#FF0000";
const blue = "#0000FF";
var k = 1;
function ball(x, y, color) {
setTimeout(function() {
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI, true);
context.fillStyle = color;
context.fill();
}, (k++) * 250);
}
for (var i = 1; i <= numOfRows; i++) {
for (var j = 1; j <= i; j++) {
var y = (i * radius * 2) - radius;
var x = (w / 2) - ((i * radius) + radius) + (j * diamater);
ball(x, y, i % 2 ? red : blue);
}
}
<canvas id="testCanvas"
width="300" height="180"
style="border:1px solid #d3d3d3;"></canvas>
How could I change my canvas so that it's smooth (I think it's called anti-aliased) like the CSS version?
// https://codepen.io/private_akongkj/pen/RwRMzqY?editors=1010 came from https://codepen.io/bantic/pen/zNKopG?editors=0010 and https://medium.com/#bantic/hand-coding-a-color-wheel-with-canvas-78256c9d7d43
function xy2polar(x, y) {
const r = Math.sqrt(x * x + y * y);
const phi = Math.atan2(y, x);
return [r, phi];
}
// rad in [-π, π] range
// return degree in [0, 360] range
function rad2deg(rad) {
return ((rad + Math.PI) / (2 * Math.PI)) * 360;
}
function createColorWheel(canvas) {
console.log('createColorWheel(canvas)', canvas);
const ctx = canvas.getContext('2d');
const radius = canvas.width / 2;
const image = ctx.createImageData(2 * radius, 2 * radius);
const data = image.data;
for (let x = -radius; x < radius; x++) {
for (let y = -radius; y < radius; y++) {
const [r, phi] = xy2polar(x, y);
if (r > radius) {
// skip all (x,y) coordinates that are outside of the circle
continue;
}
const deg = rad2deg(phi);
// Figure out the starting index of this pixel in the image data array.
const rowLength = 2 * radius;
const adjustedX = x + radius; // convert x from [-50, 50] to [0, 100] (the coordinates of the image data array)
const adjustedY = y + radius; // convert y from [-50, 50] to [0, 100] (the coordinates of the image data array)
const pixelWidth = 4; // each pixel requires 4 slots in the data array
const index = (adjustedX + adjustedY * rowLength) * pixelWidth;
const hue = deg;
const saturation = 1.0;
const value = 1.0;
const [red, green, blue] = hsv2rgb(hue, saturation, value);
const alpha = 255;
data[index] = red;
data[index + 1] = green;
data[index + 2] = blue;
data[index + 3] = alpha;
}
}
ctx.putImageData(image, 0, 0);
}
// hue in range [0, 360]
// saturation, value in range [0,1]
// return [r,g,b] each in range [0,255]
// See: https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV
function hsv2rgb(hue, saturation, value) {
const chroma = value * saturation;
const hue1 = hue / 60;
const x = chroma * (1 - Math.abs((hue1 % 2) - 1));
let r1, g1, b1;
if (hue1 >= 0 && hue1 <= 1) {
[r1, g1, b1] = [chroma, x, 0];
} else if (hue1 >= 1 && hue1 <= 2) {
[r1, g1, b1] = [x, chroma, 0];
} else if (hue1 >= 2 && hue1 <= 3) {
[r1, g1, b1] = [0, chroma, x];
} else if (hue1 >= 3 && hue1 <= 4) {
[r1, g1, b1] = [0, x, chroma];
} else if (hue1 >= 4 && hue1 <= 5) {
[r1, g1, b1] = [x, 0, chroma];
} else if (hue1 >= 5 && hue1 <= 6) {
[r1, g1, b1] = [chroma, 0, x];
}
const m = value - chroma;
const [r, g, b] = [r1 + m, g1 + m, b1 + m];
// Change r,g,b values from [0,1] to [0,255]
return [255 * r, 255 * g, 255 * b];
}
createColorWheel(document.getElementById('canvas'));
#wheel {
width: 100px;
height: 100px;
border-radius: 50%;
background: conic-gradient(rgb(255, 0, 0), rgb(255, 255, 0), rgb(0, 255, 0), rgb(0, 255, 255), rgb(0, 0, 255), rgb(255, 0, 255), rgb(255, 0, 0));
transform: rotate(270deg);
display: inline-block;
}
<div id="wheel"></div><canvas id="canvas" width="100" height="100"></canvas>
Composite your aliased circle with an anti-aliased one:
// https://codepen.io/private_akongkj/pen/RwRMzqY?editors=1010 came from https://codepen.io/bantic/pen/zNKopG?editors=0010 and https://medium.com/#bantic/hand-coding-a-color-wheel-with-canvas-78256c9d7d43
function xy2polar(x, y) {
const r = Math.sqrt(x * x + y * y);
const phi = Math.atan2(y, x);
return [r, phi];
}
// rad in [-π, π] range
// return degree in [0, 360] range
function rad2deg(rad) {
return ((rad + Math.PI) / (2 * Math.PI)) * 360;
}
function createColorWheel(canvas) {
console.log('createColorWheel(canvas)', canvas);
const ctx = canvas.getContext('2d');
const radius = canvas.width / 2;
const image = ctx.createImageData(2 * radius, 2 * radius);
const data = image.data;
for (let x = -radius; x < radius; x++) {
for (let y = -radius; y < radius; y++) {
const [r, phi] = xy2polar(x, y);
if (r > radius) {
// skip all (x,y) coordinates that are outside of the circle
continue;
}
const deg = rad2deg(phi);
// Figure out the starting index of this pixel in the image data array.
const rowLength = 2 * radius;
const adjustedX = x + radius; // convert x from [-50, 50] to [0, 100] (the coordinates of the image data array)
const adjustedY = y + radius; // convert y from [-50, 50] to [0, 100] (the coordinates of the image data array)
const pixelWidth = 4; // each pixel requires 4 slots in the data array
const index = (adjustedX + adjustedY * rowLength) * pixelWidth;
const hue = deg;
const saturation = 1.0;
const value = 1.0;
const [red, green, blue] = hsv2rgb(hue, saturation, value);
const alpha = 255;
data[index] = red;
data[index + 1] = green;
data[index + 2] = blue;
data[index + 3] = alpha;
}
}
ctx.putImageData(image, 0, 0);
// apply antialias
ctx.beginPath();
ctx.arc(radius+0.5, radius+0.5, radius-0.5, 0, Math.PI*2);
ctx.globalCompositeOperation = "destination-in";
ctx.fill();
// revert to defaults
ctx.globalCompositeOperation = "source-over";
}
// hue in range [0, 360]
// saturation, value in range [0,1]
// return [r,g,b] each in range [0,255]
// See: https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV
function hsv2rgb(hue, saturation, value) {
const chroma = value * saturation;
const hue1 = hue / 60;
const x = chroma * (1 - Math.abs((hue1 % 2) - 1));
let r1, g1, b1;
if (hue1 >= 0 && hue1 <= 1) {
[r1, g1, b1] = [chroma, x, 0];
} else if (hue1 >= 1 && hue1 <= 2) {
[r1, g1, b1] = [x, chroma, 0];
} else if (hue1 >= 2 && hue1 <= 3) {
[r1, g1, b1] = [0, chroma, x];
} else if (hue1 >= 3 && hue1 <= 4) {
[r1, g1, b1] = [0, x, chroma];
} else if (hue1 >= 4 && hue1 <= 5) {
[r1, g1, b1] = [x, 0, chroma];
} else if (hue1 >= 5 && hue1 <= 6) {
[r1, g1, b1] = [chroma, 0, x];
}
const m = value - chroma;
const [r, g, b] = [r1 + m, g1 + m, b1 + m];
// Change r,g,b values from [0,1] to [0,255]
return [255 * r, 255 * g, 255 * b];
}
createColorWheel(document.getElementById('canvas'));
#wheel {
width: 100px;
height: 100px;
border-radius: 50%;
background: conic-gradient(rgb(255, 0, 0), rgb(255, 255, 0), rgb(0, 255, 0), rgb(0, 255, 255), rgb(0, 0, 255), rgb(255, 0, 255), rgb(255, 0, 0));
transform: rotate(270deg);
display: inline-block;
}
<div id="wheel"></div><canvas id="canvas" width="100" height="100"></canvas>
First off, sorry about the bad title, I couldn't think of a better way to describe what I was trying to do. I have an HTML canvas, which, for argument's sake, is x pixels wide and y pixels tall. I have been trying to write a code that takes the location in the array of the canvas' image data of two pixels that are on lines z and z + 1 and fill in all the pixels in the higher row between the two pixels a certain color. I'm afraid I may not have made much sense, so here's a diagram:
Sorry about the poor graphics, but assume each rectangle is a pixel. The program should take in the first value for each of the black pixels (each is stored as r,g,b,a, the program gets the location of the r in the array representing the canvas' image data), and stores the r value for the lower pixel as bottomPixel and the higher one as topPixel. In this case, bottomPixel = 124 and topPixel = 112 It should use this to fill all pixels between the two base pixels a certain color. For example, using the previous pixel locations, the red pixels in the following picture should be colored in, but the blue one should not.
Here is the code I have: (Assume that the canvas has an Id "Canvas" and is 6px wide by 10px tall)
var cnvs = document.getElementById("Canvas");
cnvs.height = 10; //This height and width is here as an example.
cnvs.width = 6;
var cont = cnvs.getContext("2d");
var environment = cont.getImageData(0,0,6,10);
var bottomPixel = 124;//Not neccesarily 124 or 112, just example values
var topPixel = 112;
if ( bottomPixel - topPixel > 6*4 ) //If bottomPixel is to the right of topPixel
{
for ( var i = 0 ; i < ((bottomPixel-6*4)-topPixel)/4 ; i++ )
{
var index = topPixel + i * 4;
environment.data[index] = 0;
environment.data[index + 1 ] = 255;
environment.data[index + 2 ] = 0;
environment.data[index + 3 ] = 255;
}
}
if ( bottomPixel - topPixel > 6*4 ) //If bottomPixel is to the left of topPixel
{
for ( var i = 0 ; i < (topPixel-(bottomPixel-6*4))/4; i++ )
{
var index = topPixel - i * 4;
environment.data[index] = 0;
environment.data[index + 1 ] = 255;
environment.data[index + 2 ] = 0;
environment.data[index + 3 ] = 255;
}
}
I'd like to know why my code isn't doing what I previously described. If anything here needs clarification, please leave a comment. Thanks!
This is a method that works on the point coordinates and uses a the setPixel function to modify imageData. I'm using blue for start and black for end. You'll need to adjust for your exact condition but you can use setPixel to allow for direct x and y edits on the imageData.
update
I've included an alternate line method and your line method. There is also an animation that will help you find errors.
function ptIndex(p, w) {
return ((p.x|0) + ((p.y|0) * w)) * 4;
}
function setPixel(p, w, d, rgba) {
var i = ptIndex(p, w);
d[i] = rgba.r;
d[i + 1] = rgba.g;
d[i + 2] = rgba.b;
d[i + 3] = rgba.a;
}
function yourLine(p1, p2, w, d, rgba) {
var cnvs = document.getElementById("Canvas");
var bottomPixel = ptIndex(p1, w);
var topPixel = ptIndex(p2, w)
if (bottomPixel - topPixel > w * 4) //If bottomPixel is to the right of topPixel
{
for (var i = 0; i < ((bottomPixel - w * 4) - topPixel) / 4; i++) {
var index = topPixel + i * 4;
d[index] = rgba.r;
d[index + 1] = rgba.g;
d[index + 2] = rgba.b;
d[index + 3] = rgba.a
}
}
if (bottomPixel - topPixel > w * 4) //If bottomPixel is to the left of topPixel
{
for (var i = 0; i < (topPixel - (bottomPixel - w * 4)) / 4; i++) {
var index = topPixel - i * 4;
d[index] = rgba.r;
d[index + 1] = rgba.g;
d[index + 2] = rgba.b;
d[index + 3] = rgba.a
}
}
}
function drawRandPoints() {
var cnvs = document.getElementById("Canvas");
var cont = cnvs.getContext("2d");
// ghost last draw
cont.fillStyle = "white";
cont.fillRect(0, 0, cnvs.width, cnvs.height);
// get image data
var environment = cont.getImageData(0, 0, cnvs.width, cnvs.height);
var d = environment.data, w = cnvs.width;
// create colors
var black = {
r: 0,
g: 0,
b: 0,
a: 255
};
var red = {
r: 255,
g: 0,
b: 0,
a: 255
};
var blue = {
r: 0,
g: 0,
b: 255,
a: 255
};
var frames = 0;
var p1 = {x: ((cnvs.width / 2|0)), y: 0, sx: 1, sy:0};
var p2 = {x: cnvs.width, y: ((cnvs.height / 2)|0), sx: -1, sy: 0};
function step(p) {
if (p.x > cnvs.width) {
p.x = cnvs.width;
p.sx = 0;
p.sy = 1;
}
if (p.y > cnvs.height) {
p.y = cnvs.height;
p.sy = 0;
p.sx = -1;
}
if (p.x < 0) {
p.x = 0;
p.sx = 0;
p.sy = -1;
}
if (p.y < 0) {
p.y = 0;
p.sy = 0;
p.sx = 1;
}
}
function ani() {
cont.fillStyle = "white";
cont.fillRect(0, 0, cnvs.width, cnvs.height);
environment = cont.getImageData(0, 0, cnvs.width, cnvs.height);
d = environment.data;
step(p1);
step(p2);
var p3 = {
x: cnvs.width - p1.x,
y: cnvs.height - p2.y
};
var p4 = {
x: cnvs.width - p2.x,
y: cnvs.height - p1.y
};
yourLine(p1, p2, w, d, {r:0,g:255,b:0,a:255});
myDrawLine(p1, p2, w, d, red);
drawLineNoAliasing(p3, p4, w, d, blue);
setPixel(p1, w, d, black);
setPixel(p2, w, d, black);
frames %= 12;
p1.x += p1.sx;
p1.y += p1.sy;
p2.x += p2.sx;
p2.y += p2.sy;
// Put the pixel data on the canvas.
cont.putImageData(environment, 0, 0);
requestAnimationFrame(ani);
}
ani();
}
function myDrawLine(p1, p2, w, d, rgba) {
// Get the max length between x or y
var lenX = Math.abs(p1.x - p2.x);
var lenY = Math.abs(p1.y - p2.y);
var len = Math.sqrt(Math.pow(lenX,2) + Math.pow(lenY,2));
// Calculate the step increment between points
var stepX = lenX / len;
var stepY = lenY / len;
// If the first x or y is greater then make step negetive.
if (p2.x < p1.x) stepX *= -1;
if (p2.y < p1.y) stepY *= -1;
// Start at the first point
var x = p1.x;
var y = p1.y;
for (var i = 0; i < len; i++) {
x += stepX;
y += stepY;
// Make a point from new x and y
var p = {
x: x,
y: y
};
// Draw pixel on data
setPixel(p, w, d, rgba);
// reached goal (removes extra pixel)
if (Math.abs(p.x - p2.x) <= 1 && Math.abs(p.y - p2.y) <= 1) {
break;
}
}
// Draw start and end pixels. (might draw over line start and end)
setPixel(p1, w, d, rgba);
setPixel(p2, w, d, rgba);
}
// alternate from http://stackoverflow.com/questions/4261090/html5-canvas-and-anti-aliasing answer
// some helper functions
// finds the distance between points
function DBP(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}
// finds the angle of (x,y) on a plane from the origin
function getAngle(x, y) {
return Math.atan(y / (x == 0 ? 0.01 : x)) + (x < 0 ? Math.PI : 0);
}
// the function
function drawLineNoAliasing(p1, p2, w, d, rgba) {
var dist = DBP(p1.x, p1.y, p2.x, p2.y); // length of line
var ang = getAngle(p2.x - p1.x, p2.y - p1.y); // angle of line
var cos = Math.cos(ang);
var sin = Math.sin(ang);
for (var i = 0; i < dist; i++) {
// for each point along the line
var pt = {
x: p1.x + cos * i,
y: p1.y + sin * i
};
setPixel(pt, w, d, rgba);
}
}
// end alt
drawRandPoints();
#Canvas {
border: 1px solid red image-rendering: optimizeSpeed;
/* Older versions of FF */
image-rendering: -moz-crisp-edges;
/* FF 6.0+ */
image-rendering: -webkit-optimize-contrast;
/* Safari */
image-rendering: -o-crisp-edges;
/* OS X & Windows Opera (12.02+) */
image-rendering: pixelated;
/* Awesome future-browsers */
image-rendering: optimize-contrast;
/* CSS3 Proposed */
-ms-interpolation-mode: nearest-neighbor;
/* IE */
}
<canvas id="Canvas" width="128" height="64" style="width:320px"></canvas>
I have an image where I need to change it's background color, but keep the "effects" on it (on the image the black dots, white lines etc.)
Here's the orginal image:
I managed to change the color, but also I keep removing those "effects". Preview:
Here's the code:
//let's say I want it to be red
var r = 255;
var g = 0;
var b = 0;
var imgElement = document.getElementById('img');
var canvas = document.getElementById('canvas');
canvas.width = imgElement.width;
canvas.height = imgElement.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(imgElement, 0, 0);
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
if (data[i + 3] !== 0) {
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
data[i + 3] = data[i + 3];
}
}
ctx.putImageData(imageData, 0, 0);
<img src="foo" id="img" />
<canvas id="canvas"></canvas>
How to prevent that?
For modern browsers except Internet Explorer, you can use compositing to change the hue of your original image while leaving the saturation & lightness unchanged. This will "recolor" your original image while leaving the contours intact.
Example code that works in modern browsers except Internet Explorer
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var img=new Image();
img.onload=start;
img.src="https://dl.dropboxusercontent.com/u/139992952/multple/M449a.png";
function start(){
// create an overlay with solid #00d9c6 color
var tempCanvas=document.createElement('canvas');
var tempctx=tempCanvas.getContext('2d');
canvas.width=tempCanvas.width=img.width;
canvas.height=tempCanvas.height=img.height;
tempctx.drawImage(img,0,0);
tempctx.globalCompositeOperation='source-atop';
tempctx.fillStyle='#00d9c6';
tempctx.fillRect(0,0,tempCanvas.width,tempCanvas.height);
//
canvas.width=img.width;
canvas.height=img.height;
// use compositing to change the hue of the original image
ctx.drawImage(img,0,0);
ctx.globalCompositeOperation='hue';
ctx.drawImage(tempCanvas,0,0);
// always clean up: reset compositing to its default
ctx.globalCompositeOperation='source-over';
}
#canvas{border:1px solid red; }
<canvas id="canvas" width=300 height=300></canvas>
Since Internet Explorer does not support Blend Compositing, you will have to do it manually.
Read the RGBA value of each pixel.
Convert that RGBA value to HSL.
Shift the hue value (the "H" in HSL) by the difference between your blue hue and your desired green hue.
Convert the hue-shifted HSL value to RGBA value.
Write the hue-shifted RGBA value back to the pixel.
Here's example code of manually shifting the hue:
Important note: This manual method works by manipulating pixels with .getImageData. Therefore you must make sure the original image is hosted on the same domain as the webpage. Otherwise, the canvas will become tainted for security reasons and you will not be able to use .getImageData.
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var img = new Image();
img.crossOrigin = "anonymous";
img.onload = start;
img.src = "https://dl.dropboxusercontent.com/u/139992952/multple/marioStanding.png";
function start() {
ctx.drawImage(img, 0, 0);
ctx.drawImage(img, 150, 0);
// shift blueish colors to greenish colors
recolorPants(-.33);
}
function recolorPants(colorshift) {
var imgData = ctx.getImageData(150, 0, canvas.width, canvas.height);
var data = imgData.data;
for (var i = 0; i < data.length; i += 4) {
red = data[i + 0];
green = data[i + 1];
blue = data[i + 2];
alpha = data[i + 3];
// skip transparent/semiTransparent pixels
if (alpha < 200) {
continue;
}
var hsl = rgbToHsl(red, green, blue);
var hue = hsl.h * 360;
// change blueish pixels to the new color
if (hue > 200 && hue < 300) {
var newRgb = hslToRgb(hsl.h + colorshift, hsl.s, hsl.l);
data[i + 0] = newRgb.r;
data[i + 1] = newRgb.g;
data[i + 2] = newRgb.b;
data[i + 3] = 255;
}
}
ctx.putImageData(imgData, 150, 0);
}
function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return ({
h: h,
s: s,
l: l,
});
}
function hslToRgb(h, s, l) {
var r, g, b;
if (s == 0) {
r = g = b = l; // achromatic
} else {
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return ({
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
});
}
<p>Example shifting color Hue with .getImageData</p>
<p>(Original: left, Recolored: right)</p>
<canvas id="canvas" width=300 height=300></canvas>
You need to convert each pixel to the LSH colour space (lightness/luminance, hue, saturation). Then you set the Hue to the colour you want and keep the calculated luminance and saturation, then convert back to RGB and set the imageData to the new RGB value.
I have added my own code for conversions. There may be faster versions out there.
// returns RGB in an array on 3 numbers 0-255
var lshToRGB = function(ll,ss,hh){ //ll 0-255,ss 0-255, hh 0-360
var l = ll/255;
var s = ss/255;
var hhh = (hh/255)*360;
var C = (1 - Math.abs(2*l - 1)) * s;
var X = C*(1 - Math.abs(((hhh / 60)%2) - 1));
var m = l - C/2;
if(hhh < 60){
var r = C;
var g = X;
var b = 0;
}else
if(hhh < 120){
var r = X;
var g = C;
var b = 0;
}else
if(hhh < 180){
var r = 0;
var g = C;
var b = X;
}else
if(hhh < 240){
var r = 0;
var g = X;
var b = C;
}else
if(hhh < 300){
var r = X;
var g = 0;
var b = C;
}else{
var r = C;
var g = 0;
var b = X;
}
r += m;
g += m;
b += m;
// is there a need to clamp these ????)
r = Math.round(Math.min(255,Math.max(0,r*255)));
g = Math.round(Math.min(255,Math.max(0,g*255)));
b = Math.round(Math.min(255,Math.max(0,b*255)));
return [r,g,b];
}
// returns array of 3 numbers 0-255,0-255,0-360
var rgbToLSH = function(rr,gg,bb){ // could do without the conversion from 360 to 255 on hue
var r,
g,
b,
h,
s,
l,
min,
max,
d;
r = rr / 255;
g = gg / 255;
b = bb / 255;
max = Math.max(r, g, b);
min = Math.min(r, g, b);
l = (max + min) / 2;
if (max == min) {
h = 0;
s = 0; // achromatic
} else {
d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d;
break;
case g:
h = 2 + ((b - r) / d);
break;
case b:
h = 4 + ((r - g) / d);
break;
}
h *= 60;
if (h < 0) {
h += 360;
}
h = Math.round(h);
}
return [
Math.min(Math.round(l*255),255),
Math.min(Math.round(s*255),255),
Math.min(Math.round((h/360)*255),255)
];
}