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>
I'm trying to make this one https://massmoca.org/event/walldrawing340/
in Javascript code, using p5.js, but I have no clue how to fill these shapes with lines. Is there any other possibility, like making canvas that is circle or something like that, or I just have to make each shape seperately?
For now I was doing shape by shape, but making triangle and trapezoid is rough...
var sketch = function (p) {
with(p) {
let h,
w,
space;
p.setup = function() {
createCanvas(900, 400);
h = height / 2;
w = width / 3;
space = 10;
noLoop();
};
p.draw = function() {
drawBackground('red', 'blue', 0, 0);
shape('Circle', 'red', 'blue', 0, 0);
drawBackground('yellow', 'red', w, 0);
shape('Square', 'yellow', 'red', w, 0);
drawBackground('blue', 'yellow', 2 * w, 0);
shape('Triangle', 'blue', 'red', 2 * w, 0)
drawBackground('red', 'yellow', 0, h);
shape('Rectangle', 'red', 'blue', 0, h)
drawBackground('yellow', 'blue', w, h);
shape('Trapezoid', 'yellow', 'red', w, h);
drawBackground('blue', 'red', 2 * w, h);
};
function drawBackground(bColor, lColor, x, y) {
fill(bColor)
noStroke();
rect(x, y, w, h)
stroke(lColor);
strokeWeight(1);
for (let i = 0; i < h / space; i++) {
line(0 + x, i * space + y + 10, w + x, i * space + y + 10);
}
}
function shape(shape, bColor, lColor, x, y) {
fill(bColor)
noStroke();
let w1;
switch (shape) {
case 'Circle':
circle(x + w / 2, y + h / 2, h - space * 6);
stroke(lColor);
strokeWeight(1);
for (let i = 0; i < w / space; i++) {
for (let j = 0; j < h; j++) {
pX = i * space + x;
pY = 0 + y + j;
if (pow(x + w / 2 - pX, 2)
+ pow(pY - (y + h / 2), 2) <= pow(h - space * 6 * 2 - 10, 2)) {
point(pX, pY);
}
}
}
break;
case 'Square':
w1 = w - (h - space * 6);
rect(x + w1 / 2, y + space * 3, h - space * 6, h - space * 6);
stroke(lColor);
strokeWeight(1);
for (let i = 0; i < 15; i++) {
for (let j = 0; j < h - space * 6; j++) {
point(x + w1 / 2 + i * space, y + space * 3 + j)
}
}
break;
case 'Triangle':
w1 = w - (h - space * 6);
triangle(x + w1 / 2, h - space * 3 + y, x + w / 2, y + space * 3, x + w1 / 2 + h - space * 6, h - space * 3 + y)
for (let i = 0; i < w / space; i++) {
for (let j = 0; j < h; j++) {
pX = i * space + x;
pY = 0 + y + j;
if (pow(x + w / 2 - pX, 2)
+ pow(pY - (y + h / 2), 2) <= pow(h - space * 6 * 2 - 10, 2)) {
point(pX, pY);
}
}
}
break;
case 'Rectangle':
w1 = w - (h - space * 6) / 2;
rect(x + w1 / 2, y + space * 3, (h - space * 6) / 2, h - space * 6)
break;
case 'Trapezoid':
w1 = w - (h - space * 6);
quad(x + w1 / 2, h - space * 3 + y, x + w1 / 2 + (h - space * 6) / 4, y + space * 3, x + w1 / 4 + h - space * 6, y + space * 3, x + w1 / 2 + h - space * 6, h - space * 3 + y)
break;
case 'Parallelogram':
w1 = w - (h - space * 6);
quad(x + w1 / 4, h - space * 3 + y, x + w1 / 2, y + space * 3, x + w1 / 2 + h - space * 6, y + space * 3, x + w1 / 4 + h - space * 6, h - space * 3 + y)
break;
break;
}
}
}
};
let node = document.createElement('div');
window.document.getElementById('p5-container').appendChild(node);
new p5(sketch, node);
body {
background-color:#efefef;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js"></script>
<div id="p5-container"></div>
No messages, everything is working, I just want to know if I have to do so much arduous job...
If you don't need actual line coordinates (for plotting for example), I'd just make most out of createGraphics() to easily render shapes and lines into (taking advantage of the fact that get() returns a p5.Image) and p5.Image's mask() function.
Here's a basic example:
function setup() {
createCanvas(600, 300);
let w = 300;
let h = 150;
let spacing = 12;
let strokeWidth = 1;
const BLUE = color('#005398');
const YELLOW = color('#f9db44');
const RED = color('#dc1215');
bg = getLinesRect(w, h, RED, BLUE, spacing, strokeWidth, true);
fg = getLinesRect(w, h, RED, YELLOW, spacing, strokeWidth, false);
mask = getCircleMask(w, h, w * 0.5, h * 0.5, 100, 0);
image(bg, 0, 0);
image(fg, w, 0);
// render opaque mask (for visualisation only), mask() requires alpha channel
image(getCircleMask(w, h, w * 0.5, h * 0.5, 100, 255),0, h);
// apply mask
fg.mask(mask);
// render bg + masked fg
image(bg, w, h);
image(fg, w, h);
// text labels
noStroke();
fill(255);
text("bg layer", 9, 12);
text("fg layer", w + 9, 12);
text("mask", 9, h + 12);
text("bg + masked fg", w + 9, h + 12);
}
function getLinesRect(w, h, bg, fg, spacing, strokeWidth, isHorizontal){
let rect = createGraphics(w, h);
rect.background(bg);
rect.stroke(fg);
rect.strokeWeight(strokeWidth);
if(isHorizontal){
for(let y = 0 ; y < h; y += spacing){
rect.line(0, y + strokeWidth, w, y + strokeWidth);
}
}else{
for(let x = 0 ; x < w; x += spacing){
rect.line(x + strokeWidth, 0, x + strokeWidth, h);
}
}
// convert from p5.Graphics to p5.Image
return rect.get();
}
function getCircleMask(w, h, cx, cy, cs, opacity){
let mask = createGraphics(w, h);
// make background transparent (alpha is used for masking)
mask.background(0, opacity);
mask.noStroke();
mask.fill(255);
mask.circle(cx, cy, cs);
// convert p5.Graphics to p5.Image
return mask.get();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>
You can apply the same logic for the rest of the shapes:
function setup() {
createCanvas(1620, 590);
let compWidth = 500;
let compHeight = 250;
let compSpacing= 30;
let lineWeight = 1.5;
let lineSpacing = 12;
const BLUE = color('#005398');
const YELLOW = color('#f9db44');
const RED = color('#dc1215');
// yellow square
circleMask = getCircleMask(compWidth, compHeight, compWidth * 0.5, compHeight * 0.5, 210);
redCircle = getComposition(compWidth, compHeight, RED,
BLUE,
YELLOW,
lineSpacing, lineWeight, circleMask);
// red box
boxMask = getRectMask(compWidth, compHeight, (compWidth - 100) * 0.5, 20, 100, 210);
redBox = getComposition(compWidth, compHeight, RED,
YELLOW,
BLUE,
lineSpacing, lineWeight, boxMask);
// yellow square
squareMask = getRectMask(compWidth, compHeight, 144, 20, 210, 210);
yellowSquare = getComposition(compWidth, compHeight, YELLOW,
RED,
BLUE,
lineSpacing, lineWeight, squareMask);
// yellow trapeze
trapezeMask = getQuadMask(compWidth, compHeight, 200, 25, 200 + 115, 25,
150 + 220, 220, 150, 220);
yellowTrapeze = getComposition(compWidth, compHeight, YELLOW,
BLUE,
RED,
lineSpacing, lineWeight, trapezeMask);
// blue triangle
triangleMask = getTriangleMask(compWidth, compHeight, compWidth * 0.5, 25,
150 + 220, 220, 150, 220);
blueTriangle = getComposition(compWidth, compHeight, BLUE,
YELLOW,
RED,
lineSpacing, lineWeight, triangleMask);
// blue parallelogram
parallelogramMask = getQuadMask(compWidth, compHeight, 200, 25, 200 + 145, 25,
150 + 145, 220, 150, 220);
blueParallelogram = getComposition(compWidth, compHeight, BLUE,
RED,
YELLOW,
lineSpacing, lineWeight, parallelogramMask);
// render compositions
image(redCircle, compSpacing, compSpacing);
image(redBox, compSpacing, compSpacing + (compHeight + compSpacing));
image(yellowSquare, compSpacing + (compWidth + compSpacing), compSpacing);
image(yellowTrapeze, compSpacing + (compWidth + compSpacing), compSpacing + (compHeight + compSpacing));
image(blueTriangle, compSpacing + (compWidth + compSpacing) * 2, compSpacing);
image(blueParallelogram, compSpacing + (compWidth + compSpacing) * 2, compSpacing + (compHeight + compSpacing));
}
function getComposition(w, h, bgFill, bgStroke, fgStroke, spacing, strokeWidth, mask){
let comp = createGraphics(w, h);
bg = getLinesRect(w, h, bgFill, bgStroke, spacing, strokeWidth, true);
fg = getLinesRect(w, h, bgFill, fgStroke, spacing, strokeWidth, false);
// apply mask
fg.mask(mask);
// render to final output
comp.image(bg, 0, 0);
comp.image(fg, 0, 0);
return comp;
}
function getRectMask(w, h, rx, ry, rw, rh){
let mask = createGraphics(w, h);
// make background transparent (alpha is used for masking)
mask.background(0,0);
mask.noStroke();
mask.fill(255);
mask.rect(rx, ry, rw, rh);
// convert p5.Graphics to p5.Image
return mask.get();
}
function getCircleMask(w, h, cx, cy, cs){
let mask = createGraphics(w, h);
// make background transparent (alpha is used for masking)
mask.background(0,0);
mask.noStroke();
mask.fill(255);
mask.circle(cx, cy, cs);
// convert p5.Graphics to p5.Image
return mask.get();
}
function getQuadMask(w, h, x1, y1, x2, y2, x3, y3, x4, y4){
let mask = createGraphics(w, h);
// make background transparent (alpha is used for masking)
mask.background(0,0);
mask.noStroke();
mask.fill(255);
mask.quad(x1, y1, x2, y2, x3, y3, x4, y4);
// convert p5.Graphics to p5.Image
return mask.get();
}
function getTriangleMask(w, h, x1, y1, x2, y2, x3, y3){
let mask = createGraphics(w, h);
// make background transparent (alpha is used for masking)
mask.background(0,0);
mask.noStroke();
mask.fill(255);
mask.triangle(x1, y1, x2, y2, x3, y3);
// convert p5.Graphics to p5.Image
return mask.get();
}
function getLinesRect(w, h, bg, fg, spacing, strokeWidth, isHorizontal){
let rect = createGraphics(w, h);
rect.background(bg);
rect.stroke(fg);
rect.strokeWeight(strokeWidth);
if(isHorizontal){
for(let y = 0 ; y < h; y += spacing){
rect.line(0, y + strokeWidth, w, y + strokeWidth);
}
}else{
for(let x = 0 ; x < w; x += spacing){
rect.line(x + strokeWidth, 0, x + strokeWidth, h);
}
}
// convert from p5.Graphics to p5.Image
return rect.get();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>
Probably both rectangles and the triangle could've been drawn using getQuadMask() making good use of coordinates.
Note that I've just eye balled the shapes a bit so they're not going to be perfect, but it should be easy to tweak. Bare in mind the placement of the mask will have an effect of on how the vertical lines will align.
There are probably other ways to get the same visual effect.
For example, using texture() and textureWrap(REPEAT) with beginShape()/endShape(), using pixels for each line and checking intersections before changing direction and colours, etc.
In terms of generating lines for plotting I would start with horizontal lines, doing line to convex polygon intersection to determine where to stop the horizontal lines and start vertical lines. #AgniusVasiliauskas's answer(+1) is good for that approach.
Freya Holmér has a pretty nice visual explanation for the test.
You need linear algebra stuff, basically noticing how vertical line starting/ending Y coordinate changes in relation to line's X coordinate. And of course a lot of experimenting until you get something usable. Something like :
var w = 600
h = 600
sp = 15
var slides = [fcircle, fsquare, ftriangle, ftrapezoid, fparallelogram];
var active = 0;
var ms;
function blines(){
stroke(0);
for (var i=0; i < h; i+=sp) {
line(0,i,w,i);
}
}
function vertlines(calcline) {
for (var x=w/2-w/4+sp; x < w/2+w/4; x+=sp) {
var pnts = calcline(x);
line(pnts[0],pnts[1],pnts[2],pnts[3]);
}
}
function fcircle() {
// cut background
noStroke();
circle(w/2, h/2, w/2);
stroke('red');
// draw figure lines
let calc = function (x){
var sx = x-w/2;
var sy = h/2;
var ey = h/2;
sy += 137*sin(2.5+x/135);
ey -= 137*sin(2.5+x/135);
return [x,sy,x,ey];
}
vertlines(calc);
}
function fsquare() {
// cut background
noStroke();
quad(w/2-w/4, h/2-h/4, w/2+w/4, h/2-h/4,
w/2+w/4, h/2+h/4, w/2-w/4, h/2+h/4);
stroke('red');
// draw figure lines
let calc = function (x){
return [x,h/2-h/4,x,h/2+h/4];
}
vertlines(calc);
}
function ftriangle() {
// cut background
noStroke();
quad(w/2, h/2-h/4, w/2+w/4, h/2+h/4,
w/2-w/4, h/2+h/4, w/2, h/2-h/4);
stroke('red');
// draw figure lines
let calc = function (x){
var inpx = x > w/2 ? w-x : x;
var ys = h/2+h/4;
ys += -(0.3*inpx*log(inpx)-220);
return [x,ys,x,h/2+h/4];
}
vertlines(calc);
}
function ftrapezoid() {
// cut background
noStroke();
quad(w/2-w/10, h/2-h/4, w/2+w/10, h/2-h/4,
w/2+w/4, h/2+h/4, w/2-w/4, h/2+h/4);
stroke('red');
// draw figure lines
let calc = function (x){
var inpx = x > w/2 ? w-x : x;
var ys = h/2+h/4;
ys += -(0.55*inpx*log(inpx)-420);
if (x >= w/2-w/10 && x <= w/2+w/10) {
ys=h/2-h/4;
}
return [x,ys,x,h/2+h/4];
}
vertlines(calc);
}
function fparallelogram() {
// cut background
noStroke();
quad(w/2-w/10, h/2-h/4, w/2+w/7, h/2-h/4,
w/2, h/2+h/4, w/2-w/4, h/2+h/4);
stroke('red');
// draw figure lines
let calc = function (x){
// guard condition
if (x > w/2+w/7)
return [0,0,0,0];
var inpx = x > w/2 ? w-x : x;
var ys = h/2+h/4;
ys += -(0.55*inpx*log(inpx)-420);
var ye=h/2+h/4
if (x >= w/2-w/10) {
ys=h/2-h/4;
}
if (x > w/2) {
ye = h/2+h/4;
ye += 0.50*inpx*log(inpx)-870;
}
return [x,ys,x,ye];
}
vertlines(calc);
}
function setup() {
ms = millis();
createCanvas(w, h);
}
function draw() {
if (millis() - ms > 2000) {
ms = millis();
active++;
if (active > slides.length-1)
active = 0;
}
background('#D6EAF8');
fill('#D6EAF8');
blines();
slides[active]();
}
Slideshow DEMO
I have a way to do some of the shapes, but I am not sure about others. One way you could do it is if you know where every point on the outline of the shape is, you could just use a for loop and connect every other point from the top and bottom using the line or rect function. This would be relatively easy with shapes like squares and parallelograms, but I am not sure what functions could be used to get this for the points of a circle or trapezoid.
See more here: https://www.openprocessing.org/sketch/745383
I'm looking to allow users to slice an existing canvas into two canvases in whatever direction they would like.
I know how to allow the user to draw a line and I also know how to copy the image data of one canvas onto two new ones, but how can I copy only the relevant color data on either side of the user-drawn line to its respective canvas?
For example, in the following demo I'd like the canvas to be "cut" where the white line is:
const canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d");
const red = "rgb(104, 0, 0)",
lb = "rgb(126, 139, 185)",
db = "rgb(20, 64, 87)";
var width,
height,
centerX,
centerY,
smallerDimen;
var canvasData,
inCoords;
function sizeCanvas() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
centerX = width / 2;
centerY = height / 2;
smallerDimen = Math.min(width, height);
}
function drawNormalState() {
// Color the bg
ctx.fillStyle = db;
ctx.fillRect(0, 0, width, height);
// Color the circle
ctx.arc(centerX, centerY, smallerDimen / 4, 0, Math.PI * 2, true);
ctx.fillStyle = red;
ctx.fill();
ctx.lineWidth = 3;
ctx.strokeStyle = lb;
ctx.stroke();
// Color the triangle
ctx.beginPath();
ctx.moveTo(centerX + smallerDimen / 17, centerY - smallerDimen / 10);
ctx.lineTo(centerX + smallerDimen / 17, centerY + smallerDimen / 10);
ctx.lineTo(centerX - smallerDimen / 9, centerY);
ctx.fillStyle = lb;
ctx.fill();
ctx.closePath();
screenshot();
ctx.beginPath();
ctx.strokeStyle = "rgb(255, 255, 255)";
ctx.moveTo(width - 20, 0);
ctx.lineTo(20, height);
ctx.stroke();
ctx.closePath();
}
function screenshot() {
canvasData = ctx.getImageData(0, 0, width, height).data;
}
function init() {
sizeCanvas();
drawNormalState();
}
init();
body {
margin: 0;
}
<canvas></canvas>
TL;DR the demo.
The best way I've found to do this is to 1) calculate "end points" for the line at the edge of (or outside) the canvas' bounds, 2) create two* polygons using the end points of the line generated in step 1 and the canvas' four corners, and 3) divide up the original canvas' image data into two new canvases based on the polygons we create.
* We actually create one, but the "second" is the remaining part of the original canvas.
1) Calculate the end points
You can use a very cheap algorithm to calculate some end points given a start coordinate, x and y difference (i.e. slope), and the bounds for the canvas. I used the following:
function getEndPoints(startX, startY, xDiff, yDiff, maxX, maxY) {
let currX = startX,
currY = startY;
while(currX > 0 && currY > 0 && currX < maxX && currY < maxY) {
currX += xDiff;
currY += yDiff;
}
let points = {
firstPoint: [currX, currY]
};
currX = startX;
currY = startY;
while(currX > 0 && currY > 0 && currX < maxX && currY < maxY) {
currX -= xDiff;
currY -= yDiff;
}
points.secondPoint = [currX, currY];
return points;
}
where
let xDiff = firstPoint.x - secondPoint.x,
yDiff = firstPoint.y - secondPoint.y;
2) Create two polygons
To create the polygons, I make use of Paul Bourke's Javascript line intersection:
function intersect(point1, point2, point3, point4) {
let x1 = point1[0],
y1 = point1[1],
x2 = point2[0],
y2 = point2[1],
x3 = point3[0],
y3 = point3[1],
x4 = point4[0],
y4 = point4[1];
// Check if none of the lines are of length 0
if((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
return false;
}
let denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
// Lines are parallel
if(denominator === 0) {
return false;;
}
let ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
// is the intersection along the segments
if(ua < 0 || ua > 1 || ub < 0 || ub > 1) {
return false;
}
// Return a object with the x and y coordinates of the intersection
let x = x1 + ua * (x2 - x1);
let y = y1 + ua * (y2 - y1);
return [x, y];
}
Along with some of my own logic:
let origin = [0, 0],
xBound = [width, 0],
xyBound = [width, height],
yBound = [0, height];
let polygon = [origin];
// Work clockwise from 0,0, adding points to our polygon as appropriate
// Check intersect with top bound
let topIntersect = intersect(origin, xBound, points.firstPoint, points.secondPoint);
if(topIntersect) {
polygon.push(topIntersect);
}
if(!topIntersect) {
polygon.push(xBound);
}
// Check intersect with right
let rightIntersect = intersect(xBound, xyBound, points.firstPoint, points.secondPoint);
if(rightIntersect) {
polygon.push(rightIntersect);
}
if((!topIntersect && !rightIntersect)
|| (topIntersect && rightIntersect)) {
polygon.push(xyBound);
}
// Check intersect with bottom
let bottomIntersect = intersect(xyBound, yBound, points.firstPoint, points.secondPoint);
if(bottomIntersect) {
polygon.push(bottomIntersect);
}
if((topIntersect && bottomIntersect)
|| (topIntersect && rightIntersect)) {
polygon.push(yBound);
}
// Check intersect with left
let leftIntersect = intersect(yBound, origin, points.firstPoint, points.secondPoint);
if(leftIntersect) {
polygon.push(leftIntersect);
}
3) Divide up the original canvas' image data
Now that we have our polygon, all that's left is putting this data into new canvases. The easiest way to do this is to use canvas' ctx.drawImage and ctx.globalCompositeOperation.
// Use or create 2 new canvases with the split original canvas
let newCanvas1 = document.querySelector("#newCanvas1");
if(newCanvas1 == null) {
newCanvas1 = document.createElement("canvas");
newCanvas1.id = "newCanvas1";
newCanvas1.width = width;
newCanvas1.height = height;
document.body.appendChild(newCanvas1);
}
let newCtx1 = newCanvas1.getContext("2d");
newCtx1.globalCompositeOperation = 'source-over';
newCtx1.drawImage(canvas, 0, 0);
newCtx1.globalCompositeOperation = 'destination-in';
newCtx1.beginPath();
newCtx1.moveTo(polygon[0][0], polygon[0][1]);
for(let item = 1; item < polygon.length; item++) {
newCtx1.lineTo(polygon[item][0], polygon[item][1]);
}
newCtx1.closePath();
newCtx1.fill();
let newCanvas2 = document.querySelector("#newCanvas2");
if(newCanvas2 == null) {
newCanvas2 = document.createElement("canvas");
newCanvas2.id = "newCanvas2";
newCanvas2.width = width;
newCanvas2.height = height;
document.body.appendChild(newCanvas2);
}
let newCtx2 = newCanvas2.getContext("2d");
newCtx2.globalCompositeOperation = 'source-over';
newCtx2.drawImage(canvas, 0, 0);
newCtx2.globalCompositeOperation = 'destination-out';
newCtx2.beginPath();
newCtx2.moveTo(polygon[0][0], polygon[0][1]);
for(let item = 1; item < polygon.length; item++) {
newCtx2.lineTo(polygon[item][0], polygon[item][1]);
}
newCtx2.closePath();
newCtx2.fill();
All of that put together gives us this demo!
I have written a javascript program that uses a genetic algorithm to recreate an image only using triangles. Here's the strategy:
generate a random pool of models, each model having an array of triangles (3 points and a color)
evaluate the fitness of each model. To do so, I compare the original image's pixel array with my model's. I use Cosine Similarity to compare arrays
keep the best models, and mate them to create new models
randomly mutate some of the models
evaluate the new pool and continue
It works quite well after some iterations as you can see here:
The problem I have, is that it is very slow, most of the time is spent getting model's pixels (converting list of triangles (color + points) to a pixel array).
Here's how I do so now:
My pixel-array is a 1D array, I need to be able to convert x,y coordinates to index:
static getIndex(x, y, width) {
return 4 * (width * y + x);
}
Then I am able to draw a point:
static plot(x, y, color, img) {
let idx = this.getIndex(x, y, img.width);
let added = [color.r, color.g, color.b, map(color.a, 0, 255, 0, 1)];
let base = [img.pixels[idx], img.pixels[idx + 1], img.pixels[idx + 2], map(img.pixels[idx + 3], 0, 255, 0, 1)];
let a01 = 1 - (1 - added[3]) * (1 - base[3]);
img.pixels[idx + 0] = Math.round((added[0] * added[3] / a01) + (base[0] * base[3] * (1 - added[3]) / a01)); // red
img.pixels[idx + 1] = Math.round((added[1] * added[3] / a01) + (base[1] * base[3] * (1 - added[3]) / a01)); // green
img.pixels[idx + 2] = Math.round((added[2] * added[3] / a01) + (base[2] * base[3] * (1 - added[3]) / a01)); // blue
img.pixels[idx + 3] = Math.round(map(a01, 0, 1, 0, 255));
}
Then a line:
static line(x0, y0, x1, y1, img, color) {
x0 = Math.round(x0);
y0 = Math.round(y0);
x1 = Math.round(x1);
y1 = Math.round(y1);
let dx = Math.abs(x1 - x0);
let dy = Math.abs(y1 - y0);
let sx = x0 < x1 ? 1 : -1;
let sy = y0 < y1 ? 1 : -1;
let err = dx - dy;
do {
this.plot(x0, y0, color, img);
let e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
} while (x0 != x1 || y0 != y1);
}
And finally, a triangle:
static drawTriangle(triangle, img) {
for (let i = 0; i < triangle.points.length; i++) {
let point = triangle.points[i];
let p1 =
i === triangle.points.length - 1
? triangle.points[0]
: triangle.points[i + 1];
this.line(point.x, point.y, p1.x, p1.y, img, triangle.color);
}
this.fillTriangle(triangle, img);
}
static fillTriangle(triangle, img) {
let vertices = Array.from(triangle.points);
vertices.sort((a, b) => a.y > b.y);
if (vertices[1].y == vertices[2].y) {
this.fillBottomFlatTriangle(vertices[0], vertices[1], vertices[2], img, triangle.color);
} else if (vertices[0].y == vertices[1].y) {
this.fillTopFlatTriangle(vertices[0], vertices[1], vertices[2], img, triangle.color);
} else {
let v4 = {
x: vertices[0].x + float(vertices[1].y - vertices[0].y) / float(vertices[2].y - vertices[0].y) * (vertices[2].x - vertices[0].x),
y: vertices[1].y
};
this.fillBottomFlatTriangle(vertices[0], vertices[1], v4, img, triangle.color);
this.fillTopFlatTriangle(vertices[1], v4, vertices[2], img, triangle.color);
}
}
static fillBottomFlatTriangle(v1, v2, v3, img, color) {
let invslope1 = (v2.x - v1.x) / (v2.y - v1.y);
let invslope2 = (v3.x - v1.x) / (v3.y - v1.y);
let curx1 = v1.x;
let curx2 = v1.x;
for (let scanlineY = v1.y; scanlineY <= v2.y; scanlineY++) {
this.line(curx1, scanlineY, curx2, scanlineY, img, color);
curx1 += invslope1;
curx2 += invslope2;
}
}
static fillTopFlatTriangle(v1, v2, v3, img, color) {
let invslope1 = (v3.x - v1.x) / (v3.y - v1.y);
let invslope2 = (v3.x - v2.x) / (v3.y - v2.y);
let curx1 = v3.x;
let curx2 = v3.x;
for (let scanlineY = v3.y; scanlineY > v1.y; scanlineY--) {
this.line(curx1, scanlineY, curx2, scanlineY, img, color);
curx1 -= invslope1;
curx2 -= invslope2;
}
}
You can see full code in action here
So, I would like to know:
is it possible to optimize this code ?
if yes, what would be the best way to do so ? Maybe there is a library doing all of the drawing stuff way better than I did ? Or by using workers ?
Thanks !
I have tested your suggestions, here's the results:
Use RMS instead of Cosine Similarity: I am not sur if the measure of similarity is better, but it is definitively not worse. It seems to run a little bit faster too.
Use UInt8Array: It surely have an impact, but does not runs a lot faster. Not slower though.
Draw to invisible canvas: Definitively faster and easier! I can remove all of my drawing functions and replace it with a few lines of code, and it runs a lot faster !
Here's the code to draw to an invisible canvas:
var canvas = document.createElement('canvas');
canvas.id = "CursorLayer";
canvas.width = this.width;
canvas.height = this.height;
canvas.display = "none";
var body = document.getElementsByTagName("body")[0];
body.appendChild(canvas);
var ctx = canvas.getContext("2d");
ctx.fillStyle = "rgba(0, 0, 0, 1)";
ctx.fillRect(0, 0, this.width, this.height);
for (let i = 0; i < this.items.length; i++) {
let item = this.items[i];
ctx.fillStyle = "rgba(" +item.color.r + ','+item.color.g+','+item.color.b+','+map(item.color.a, 0, 255, 0, 1)+")";
ctx.beginPath();
ctx.moveTo(item.points[0].x, item.points[0].y);
ctx.lineTo(item.points[1].x, item.points[1].y);
ctx.lineTo(item.points[2].x, item.points[2].y);
ctx.fill();
}
let pixels = ctx.getImageData(0, 0, this.width, this.height).data;
//delete canvas
body.removeChild(canvas);
return pixels;
Before those changements, my code were running at about 1.68 iterations per second.
Now it runs at about 16.45 iterations per second !
See full code here.
Thanks again !
I need to draw a circle and i have only two points.Now i need to find center point and radius of the circle? You can form the circle in clock wise direction.
Thanks in advance
Here is a Brute Force approach to the problem.
EDIT
Added a max iterations limit to cut off calculations if the line between the two points is almost straight along x (meaning a radius would be nearing Infinity)
Also animations, because that makes everything better :)
var canvas = document.body.appendChild(document.createElement("canvas"));
var ctx = canvas.getContext("2d");
canvas.width = 1000;
canvas.height = 1000;
var points = [
{ x: parseInt(prompt("x1", "110")), y: parseInt(prompt("y1", "120")), r: 5 },
{ x: parseInt(prompt("x2", "110")), y: parseInt(prompt("y2", "60")), r: 5 },
];
function calculateRemainingPoint(points, x, precision, maxIteration) {
if (x === void 0) { x = 0; }
if (precision === void 0) { precision = 0.001; }
if (maxIteration === void 0) { maxIteration = 100000; }
var newPoint = {
x: x,
y: (points[0].y + points[1].y) / 2,
r: 50
};
var d0 = distance(points[0].x, points[0].y, x, newPoint.y);
var d1 = distance(points[1].x, points[1].y, x, newPoint.y);
var iteration = 0;
//Bruteforce approach
while (Math.abs(d0 - d1) > precision && iteration < maxIteration) {
var oldDiff = Math.abs(d0 - d1);
var oldY = newPoint.y;
iteration++;
newPoint.y += oldDiff / 10;
d0 = distance(points[0].x, points[0].y, x, newPoint.y);
d1 = distance(points[1].x, points[1].y, x, newPoint.y);
var diff_1 = Math.abs(d0 - d1);
if (diff_1 > oldDiff) {
newPoint.y = oldY - oldDiff / 10;
d0 = distance(points[0].x, points[0].y, x, newPoint.y);
d1 = distance(points[1].x, points[1].y, x, newPoint.y);
}
}
var diff = (points[0].x + points[1].x) / points[0].x;
newPoint.r = d0;
return newPoint;
}
points.push(calculateRemainingPoint(points));
function distance(x1, y1, x2, y2) {
var a = x1 - x2;
var b = y1 - y2;
return Math.sqrt(a * a + b * b);
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.moveTo(-canvas.width, canvas.height / 2);
ctx.lineTo(canvas.width, canvas.height / 2);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.moveTo(canvas.width / 2, -canvas.height);
ctx.lineTo(canvas.width / 2, canvas.height);
ctx.stroke();
ctx.closePath();
for (var pointIndex = 0; pointIndex < points.length; pointIndex++) {
var point = points[pointIndex];
ctx.beginPath();
ctx.arc(point.x + canvas.width / 2, canvas.height / 2 - point.y, point.r, 0, Math.PI * 2);
ctx.arc(point.x + canvas.width / 2, canvas.height / 2 - point.y, 2, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();
}
}
setInterval(function () {
points = points.slice(0, 2);
points[Math.floor(Math.random() * points.length) % points.length][Math.random() > 0.5 ? 'x' : 'y'] = Math.random() * canvas.width - canvas.width / 2;
setTimeout(function () {
points.push(calculateRemainingPoint(points));
requestAnimationFrame(draw);
}, 1000 / 60);
}, 1000);
draw();
No that is impossible.
Create two circles with the same radius at centerpoints A + B. At the intersection of these two circles create an circle with the same radius....
Then make the same with an other radius....