How do I oscillate the height of individual shapes whilst keeping the total height of their sum constant? - processing

I am trying to create an effect where the total height of a group of shapes is constant (say 300), whilst each shape within that group has a dynamic, oscillating, height. In one instance, maybe the middle shape is 'taller' whilst the outer shapes are shorter.
This desired effect is similar to if you held a slinky, with each end in one hand fixed at 30cm apart, and then shook it around: the total height remains the same (30cm) but the 'sections' inside the slinky are having their individual heights bounce up and down.
My attempts so far use the sin function to get an oscillating number as an angle value increases. This works for the sections, but I can't figure out how to maintain the constant overall height. See the code snippet below; red (and the tip of the bottom black triangle) should always be touching the bottom of the container.
// Prepare variables for angles, separated by 1
let a1 = 0;
let a2 = 1;
let a3 = 2;
let a4 = 3;
let a5 = 4;
// Prepare shape width
let shapeW = 150;
function setup() {
createCanvas(300, 300);
rect(10, 10, 10, 10);
}
function draw() {
background(240);
noStroke();
// Use the sin ratio to 'oscillate' a height value between 0 and 60
let x1 = map(sin(a1), -1, 1, 0, height / 5);
let x2 = map(sin(a2), -1, 1, 0, height / 5);
let x3 = map(sin(a3), -1, 1, 0, height / 5);
let x4 = map(sin(a4), -1, 1, 0, height / 5);
let x5 = map(sin(a5), -1, 1, 0, height / 5);
// Store these in an array so I can loop through
let listOfValues = [x1, x2, x3, x4, x5];
// Loop through and draw shapes
push();
translate((width / 2) - shapeW / 2, 0)
for (let i = 0; i < listOfValues.length; i++) {
fill(255, 0, 0);
rect(0, 0, shapeW, listOfValues[i]);
fill(0)
triangle(0, 0, shapeW / 2, listOfValues[i], shapeW, 0)
translate(0, listOfValues[i]);
}
pop();
// Increment each angle by the same amount
let incAmount = 0.1;
a1 += incAmount;
a2 += incAmount;
a3 += incAmount;
a4 += incAmount;
a5 += incAmount;
}
html,
body {
margin: 0;
padding: 0;
}
canvas {
display: block;
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.0/lib/p5.js"></script>

With help, I've found the solution is to use binomial coefficients. That is achieved here via a binomial() function. The only caveat is that the number of 'sections' (represented as n) must be an even number.
let angle = 0;
let N;
let containerW = 300;
let shapeW = 150;
let n = 6;
let speed = 0.0075;
function setup() {
createCanvas(containerW, containerW);
N = n * binomial(n, n / 2);
}
function draw() {
background(240);
noStroke();
let listOfVals = [];
for (let i = 0; i < n; i++) {
listOfVals.push(x(i + 1));
}
push();
translate(width / 2 - shapeW / 2, 0);
for (let i = 0; i < listOfVals.length; i++) {
fill(255, 0, 0);
rect(0, 0, shapeW, listOfVals[i] * height);
fill(0);
triangle(0, 0, shapeW / 2, listOfVals[i] * height, shapeW, 0);
translate(0, listOfVals[i] * height);
}
pop();
// Increment angle
angle += speed;
}
function x(k) {
return (2 ** n * sin(angle + (k * PI) / n) ** n) / N;
}
function binomial(n, k) {
if (typeof n !== "number" || typeof k !== "number") return false;
var coeff = 1;
for (var x = n - k + 1; x <= n; x++) coeff *= x;
for (x = 1; x <= k; x++) coeff /= x;
return coeff;
}
html,
body {
margin: 0;
padding: 0;
}
canvas {
display: block;
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.0/lib/p5.js"></script>

Nice self answer (+1).
This is more of an idea for a slightly different approach, hopefully with a few simplifications:
// Prepare shape width
let shapeW = 150;
// Prepare shape height
let shapeH;
// total number of shapes
let numShapes = 5;
// Increment each angle offset by the same amount
let incAmount = 0.05;
function setup() {
createCanvas(300, 300);
rect(10, 10, 10, 10);
// assign shape height after sketch height has been set
shapeH = height / 5;
}
function draw() {
background(240);
noStroke();
// Loop through and draw shapes
push();
// horizontally center shapes
translate((width - shapeW) / 2, 0);
// for each shape
for (let i = 0; i < numShapes; i++) {
// map the current height to the increment asdasdakrk
let currentH = map(sin(i + (frameCount * incAmount)), -1, 1, 0, shapeH);
fill(255, 0, 0);
rect(0, 0, shapeW, currentH);
fill(0)
triangle(0, 0, shapeW / 2, currentH, shapeW, 0)
translate(0, currentH);
}
pop();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
The above is using the same logic, mostly removing the need for the a1, a2, a3, a4, a5 values as they coincide with the i counter for each shape.
A visual way I think about it having to connect the tip of one triangle with the base of the next triangle (or the current triangle's base being the same as the the previous triangle tip's y position):
// Prepare shape width
let shapeW = 150;
// Prepare shape height
let shapeH;
// total number of shapes
let numShapes = 5;
// Increment each angle offset by the same amount
let incAmount = 0.05;
// sine driven scales
let minYScale = 0.5;
let maxYScale = 2.0;
function setup() {
createCanvas(300, 300);
rect(10, 10, 10, 10);
// assign shape height after sketch height has been set
shapeH = height / 5;
}
function draw() {
background(240);
noStroke();
// Loop through and draw shapes
push();
// horizontally center shapes
translate((width - shapeW) / 2, 0);
// draw red background
fill(255, 0, 0);
rect(0, 0, shapeW, height);
// remember where the previous array base was
let lastY = 0;
// for each shape
for (let i = 0; i < numShapes; i++) {
// map the current y scale to the increment
let currentYScale = map(sin(i + (frameCount * incAmount)), -1, 1, minYScale, maxYScale);
// compute the current scale based on the sine scalar
let currentH = currentYScale * shapeH;
fill(0);
triangle(0, lastY,
shapeW / 2, lastY + currentH,
shapeW, lastY);
// update absolute y position of the arrow base
lastY += currentH;
// optional: for debugging only, visualise lastY
if(mouseIsPressed) rect(-shapeW, lastY, width + shapeW, 3);
}
pop();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

Related

p5.js: make an animated spiral with line instead of ellipse?

I'm trying to animate a spiral using a line, but can only seem to get it to work using ellipses.
Does anyone know how to replace the ellipse() with line()?
here is the code:
var angle = 0.0;
var offset = 60;
var scalar = 10;
var speed = 0.05;
function setup() {
createCanvas(600, 120);
fill(0);
}
function draw() {
var x = offset + cos(angle) * scalar;
var y = offset + sin(angle) * scalar;
ellipse( x, y, 2, 2);
angle += speed;
scalar += speed;
}
Assuming you would like to draw the entire spiral instantaneously using line segments, the you simply need a for loop that calculates the x and y coordinates for the current and next point in the spiral for some increment of change, and then draw lines between each pair of points. There are certainly numerous ways to write such a for loop, depending on what the constrains are (do you want a specific number of rings in your spiral? a specific number of degrees of rotation?), but importantly the bigger your increment of change the less smooth your spiral will look. Here is an example that uses the mouse position to determine the number of rings and the size of the change increments:
function setup() {
createCanvas(windowWidth, windowHeight);
stroke(0);
strokeWeight(4);
textAlign(LEFT, TOP);
}
function draw() {
background(255);
// let the horizontal mouse position indicate the
// size of the steps
let speed = map(mouseX, 0, width, 0.01, 1, true);
// let the vertical mouse position indicate the
// total amount of rotation
let maxRotation = map(mouseY, 0, height, TWO_PI, TWO_PI * 50, true);
push();
noStroke();
fill('red');
text(`Rings: ${(maxRotation / TWO_PI).toFixed(1)}, Speed: ${speed.toFixed(2)}`, 10, 10);
pop();
translate(width / 2, height / 2);
let scalar = 10;
if (speed <= 0) {
console.error('cannot have <= 0 speed');
return;
}
for (let angle = 0; angle < maxRotation; angle += speed, scalar += speed) {
const x = cos(angle) * scalar;
const y = sin(angle) * scalar;
const x2 = cos(angle + speed) * (scalar + speed);
const y2 = sin(angle + speed) * (scalar + speed);
line(x, y, x2, y2);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>

How to draw regular geometrical shapes with a series of horizontal or vertical lines? [duplicate]

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

Separate shapes as lines split the canvas + Select points to draw shapes from arrays

I want to detect separate shapes as random-generated lines split the canvas. I saved line intersection points in separate arrays for x and y positions (same order), but don't know how to connect
points that complete multiple pieces of shapes .
Is there any way to detect nearby points to close a minimal possible shape whether it be a triangle, rectangle, or polygon (e.g., by using beginShape and endShape)?
If 1) is too complicated, is there any method to select 3 or more random points from an array?
Here's a sample image that has 4 lines splitting the canvas with their intersection points marked in red. I also saved the top and bottom points (marked in black) of each random-generated line, plus the four corners of the canvas in the same arrays for x and y positions separately (px, py).
Multiple lines split the canvas.
How to get shapes split by lines in Processing?
I was able to get all the intersection points, but having a problem with connecting them into separate shapes. Here's the Processing code that I am working on:
//Run in Processing.
//Press r to refresh.
//Top and bottom points are added to px and py when refreshed (filled in black).
//Intersection points are added to px and py when detected (filled in red).
int l = 4; //set number of lines
float[] r1 = new float[l];
float[] r2 = new float[l];
float[] px = {}; //array to save x positions of all possible points
float[] py = {}; //array to save y positions of all possible points
boolean added = false;
void setup(){
size(800, 800);
background(255);
refresh();
}
void draw(){
background(255);
stroke(0, 150, 255, 150);
strokeWeight(1);
for(int i=0; i < r1.length; i++){
for(int j=0; j < r1.length; j++){
if(i>j){
boolean hit = lineLine(r1[i], 0, r2[i], height, r1[j], 0, r2[j], height);
if (hit) stroke(255, 150, 0, 150);
else stroke(0, 150, 255, 150);
}
line(r1[i], 0, r2[i], height);
}
}
added = true;
print(px.length);
}
//source: http://jeffreythompson.org/collision-detection/line-line.php
boolean lineLine(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) {
// calculate the distance to intersection point
float uA = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
float uB = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
// if uA and uB are between 0-1, lines are colliding
if (uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1) {
// optionally, draw a circle where the lines meet
float intersectionX = x1 + (uA * (x2-x1));
float intersectionY = y1 + (uA * (y2-y1));
fill(255,0,0);
noStroke();
ellipse(intersectionX,intersectionY, 20,20);
if(added==false){
px = append(px, intersectionX);
py = append(py, intersectionY);
}
return true;
}
return false;
}
void refresh(){
added = false;
px = new float[0];
py = new float[0];
r1 = new float[l];
r2 = new float[l];
px = append(px, 0);
py = append(py, 0);
px = append(px, 0);
py = append(py, height);
px = append(px, width);
py = append(py, 0);
px = append(px, width);
py = append(py, height);
for(int i=0; i< r1.length; i++){
r1[i] = random(800);
}
for(int i=0; i< r2.length; i++){
r2[i] = random(800);
}
for(int i=0; i < r1.length; i++){
stroke(0);
line(r1[i], 0, r2[i], height);
px = append(px, r1[i]);
py = append(py, 0);
px = append(px, r2[i]);
py = append(py, height);
}
}
void keyReleased() {
if (key == 'r') refresh();
}
If you want to draw a shape made of the intersection points only you're on the right track with beginShape()/endShape().
Currently it looks like you're placing all the points in px, py: the intersection points and also the points defining the lines used to compute the intersections in the first place.
You might want to separate the two, for example a couply of arrays for points defining lines only and another pair of x,y arrays for the intersection points only. You'd only need to iterated through the intersected coordinates to place vertex(x, y) calls inbetween beginShape()/endShape(). Here's a modified version of you code to illustrate the idea:
//Run in Processing.
//Press r to refresh.
//Top and bottom points are added to px and py when refreshed (filled in black).
//Intersection points are added to px and py when detected (filled in red).
int l = 4; //set number of lines
float[] r1 = new float[l];
float[] r2 = new float[l];
float[] px = {}; //array to save x positions of all possible points
float[] py = {}; //array to save y positions of all possible points
float[] ipx = {}; // array to save x for intersections only
float[] ipy = {}; // array to save y for intersections only
boolean added = false;
void setup(){
size(800, 800);
background(255);
refresh();
}
void draw(){
background(255);
stroke(0, 150, 255, 150);
strokeWeight(1);
for(int i=0; i < r1.length; i++){
for(int j=0; j < r1.length; j++){
if(i>j){
boolean hit = lineLine(r1[i], 0, r2[i], height, r1[j], 0, r2[j], height);
if (hit) stroke(255, 150, 0, 150);
else stroke(0, 150, 255, 150);
}
line(r1[i], 0, r2[i], height);
}
}
added = true;
// draw intersections
beginShape();
for(int i = 0 ; i < ipx.length; i++){
vertex(ipx[i], ipy[i]);
}
endShape();
//print(px.length);
//println(px.length, py.length);
}
//source: http://jeffreythompson.org/collision-detection/line-line.php
boolean lineLine(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) {
// calculate the distance to intersection point
float uA = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
float uB = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
// if uA and uB are between 0-1, lines are colliding
if (uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1) {
// optionally, draw a circle where the lines meet
float intersectionX = x1 + (uA * (x2-x1));
float intersectionY = y1 + (uA * (y2-y1));
fill(255,0,0);
noStroke();
ellipse(intersectionX,intersectionY, 20,20);
if(added==false){
px = append(px, intersectionX);
py = append(py, intersectionY);
// store intersections
ipx = append(ipx, intersectionX);
ipy = append(ipy, intersectionY);
}
return true;
}
return false;
}
void refresh(){
added = false;
px = new float[0];
py = new float[0];
ipx = new float[0];
ipy = new float[0];
r1 = new float[l];
r2 = new float[l];
px = append(px, 0);
py = append(py, 0);
px = append(px, 0);
py = append(py, height);
px = append(px, width);
py = append(py, 0);
px = append(px, width);
py = append(py, height);
for(int i=0; i< r1.length; i++){
r1[i] = random(800);
}
for(int i=0; i< r2.length; i++){
r2[i] = random(800);
}
for(int i=0; i < r1.length; i++){
stroke(0);
line(r1[i], 0, r2[i], height);
px = append(px, r1[i]);
py = append(py, 0);
px = append(px, r2[i]);
py = append(py, height);
}
}
void keyReleased() {
if (key == 'r') refresh();
}
Bare in mind this simlpy draws the points in the order in which the intersections were computed. On a good day you'll get something like this:
It doesn't exclude the possiblity of polygons with the wrong vertex order (winding):
and you might be get convave polygons too.
If you only need the outer 'shell' of these intersection points you might need something like a convex hull algorithm
One option to at least visually split shapes might to use beginShape(TRIANGLES); with endShape(CLOSE); which should iterate through points and draw a triangle for every coordinate triplate, however given random points and number of interesections you might end up with a missing triangle or two (e.g. 6 points = 2 triangles, 7 points = 2 triangles and 1 point with no missing pairs)
The only other note I have is around syntax: arrays are ok to get started with but you might want to look into ArrayList and PVector. This would allow you to use a single dynamic array of PVector instances which have x, y properties.
Update
Overall the code can be simplified. If we take out the line intersection related code we can get away with something like:
int l = 4; //set number of random lines
float[] r1 = new float[l]; // random x top
float[] r2 = new float[l]; // random x bottom
void setup() {
size(800, 800);
strokeWeight(3);
stroke(0, 150, 255, 150);
refresh();
}
void draw() {
background(255);
// random lines
for (int i=0; i < r1.length; i++) {
line(r1[i], 0, r2[i], height);
}
// borders
line(0, 0, width, 0);
line(width, 0, width - 1, height - 1);
line(0, height - 1, width - 1, height - 1);
line(0, 0, 0, height - 1);
}
void refresh() {
r1 = new float[l];
r2 = new float[l];
for (int i=0; i< r1.length; i++) {
r1[i] = random(800);
r2[i] = random(800);
}
}
void keyReleased() {
if (key == 'r') refresh();
}
If we were to use a basic Line class and make use of PVector and ArrayList we could rewrite the above as:
int numRandomLines = 4;
ArrayList<PVector> points = new ArrayList<PVector>();
void setup() {
size(800, 800);
stroke(0, 150, 255, 150);
strokeWeight(3);
refresh();
}
void refresh(){
// remove previous points
points.clear();
//add borders
points.add(new PVector(0, 0)); points.add(new PVector(width, 0));
points.add(new PVector(width, 0));points.add(new PVector(width - 1, height - 1));
points.add(new PVector(0, height - 1));points.add(new PVector(width - 1, height - 1));
points.add(new PVector(0, 0)); points.add(new PVector(0, height - 1));
// add random lines
for (int i=0; i< numRandomLines; i++) {
points.add(new PVector(random(800), 0)); points.add(new PVector(random(800), height));
}
}
void draw(){
background(255);
beginShape(LINES);
for(PVector point : points) vertex(point.x, point.y);
endShape();
}
void keyReleased() {
if (key == 'r') refresh();
}
and grouping a pair of points (PVector) into a Line class:
int numRandomLines = 4;
ArrayList<Line> lines = new ArrayList<Line>();
void setup() {
size(800, 800);
stroke(0, 150, 255, 150);
strokeWeight(3);
refresh();
}
void refresh(){
// remove previous points
lines.clear();
//add borders
lines.add(new Line(new PVector(0, 0), new PVector(width, 0)));
lines.add(new Line(new PVector(width, 0), new PVector(width - 1, height - 1)));
lines.add(new Line(new PVector(0, height - 1), new PVector(width - 1, height - 1)));
lines.add(new Line(new PVector(0, 0), new PVector(0, height - 1)));
// add random lines
for (int i=0; i< numRandomLines; i++) {
lines.add(new Line(new PVector(random(800), 0), new PVector(random(800), height)));
}
}
void draw(){
background(255);
for(Line line : lines) line.draw();
}
void keyReleased() {
if (key == 'r') refresh();
}
class Line{
PVector start;
PVector end;
Line(PVector start, PVector end){
this.start = start;
this.end = end;
}
void draw(){
line(start.x, start.y, end.x, end.y);
}
}
At this stage to get the individual shapes as your diagram describes, we could cheat and use a computer vision library like OpenCV. This is if course overkill (as we'd get() a PImage copy of the drawing, convert that to an OpenCV image) then simply use findContours() to get each shape/contour.
Going back to the original approach, the line to line intersection function could be integrated into the Line class:
int numRandomLines = 4;
ArrayList<Line> lines = new ArrayList<Line>();
ArrayList<PVector> intersections = new ArrayList<PVector>();
void setup() {
size(800, 800);
strokeWeight(3);
refresh();
}
void refresh(){
// remove previous points
lines.clear();
intersections.clear();
//add borders
lines.add(new Line(new PVector(0, 0), new PVector(width, 0)));
lines.add(new Line(new PVector(width, 0), new PVector(width - 1, height - 1)));
lines.add(new Line(new PVector(0, height - 1), new PVector(width - 1, height - 1)));
lines.add(new Line(new PVector(0, 0), new PVector(0, height - 1)));
// add random lines
for (int i=0; i< numRandomLines; i++) {
lines.add(new Line(new PVector(random(800), 0), new PVector(random(800), height)));
}
// compute intersections
int numLines = lines.size();
// when looping only check if lineA intersects lineB but not also if lineB intersects lineA (redundant)
for (int i = 0; i < numLines - 1; i++){
Line lineA = lines.get(i);
for (int j = i + 1; j < numLines; j++){
Line lineB = lines.get(j);
// check intersection
PVector intersection = lineA.intersect(lineB);
// if there is one, append the intersection point to the list
if(intersection != null){
intersections.add(intersection);
}
}
}
}
void draw(){
background(255);
stroke(0, 150, 255, 150);
// draw lines
for(Line line : lines) line.draw();
stroke(255, 0, 0, 150);
// draw intersections
for(PVector intersection : intersections) ellipse(intersection.x, intersection.y, 9, 9);
}
void keyReleased() {
if (key == 'r') refresh();
}
class Line{
PVector start;
PVector end;
Line(PVector start, PVector end){
this.start = start;
this.end = end;
}
void draw(){
line(start.x, start.y, end.x, end.y);
}
//source: http://jeffreythompson.org/collision-detection/line-line.php
//boolean lineLine(float this.start.x, float this.start.y, float this.end.x, float this.end.y,
//float other.start.x, float other.start.y, float other.end.x, float other.end.y) {
PVector intersect(Line other) {
// calculate the distance to intersection point
float uA = ((other.end.x-other.start.x)*(this.start.y-other.start.y) - (other.end.y-other.start.y)*(this.start.x-other.start.x)) / ((other.end.y-other.start.y)*(this.end.x-this.start.x) - (other.end.x-other.start.x)*(this.end.y-this.start.y));
float uB = ((this.end.x-this.start.x)*(this.start.y-other.start.y) - (this.end.y-this.start.y)*(this.start.x-other.start.x)) / ((other.end.y-other.start.y)*(this.end.x-this.start.x) - (other.end.x-other.start.x)*(this.end.y-this.start.y));
// if uA and uB are between 0-1, lines are colliding
if (uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1) {
// optionally, draw a circle where the lines meet
float intersectionX = this.start.x + (uA * (this.end.x-this.start.x));
float intersectionY = this.start.y + (uA * (this.end.y-this.start.y));
return new PVector(intersectionX, intersectionY);
}
return null;
}
}
The next step would be a more complex algorithm to sort the points based on x, y position (e.g. top to bottom , left to right), iterate though points comparing the first to the rest by distance and angle and trying to work out if consecutive points with minimal distance and angle changes connect.
Having a quick look online I can see such algorithms for example:
Polygon Detection from a Set of Lines
Bentley Ottman algorithm (one of the algorithms mentioned in the paper above) is actually implemented in CGAL. (While there are CGAL Java bindings building these and either interfacing or making a wrapper for Processing isn't trivial).
I can see your code isn't javascript but since you didn't specify a language I assume you just want a method and can convert to your language.
The way I handled this was to assign each line a line number. If I can identify 2 adjacent points on one line then I will know if the third point exist by checking if there is a point at the crossing of the lines they are not sharing.
Example:
There's 3 lines (line 1, 2, 3)
I have an intersection point between lines 3 & 1 now I walk down line 3 for an adjacent point. I find one and its intersection is 3 & 2. Well the only way I could have a triangle is by lines 1 & 2 crossing somewhere. So we can programmatically check that.
Keep in mind that I never actually use and angles for this. I do calculate them in the functions but decided not to use them as I went with the method explained above. I have colored the triangles using an alpha value of 0.1 so you can see where there is overlap.
This is only check triangles
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
canvas.width = 400;
canvas.height = 400;
let lines = []; //holds each line
let points = []; //all intersection point are pushed here [{x: num, y: num}, {x: num, y: num},...]
let sortedPts = []; //all points sorted bu first number are pushed here in 2d array.
let lineNum = 15;
class Lines {
constructor(num) {
this.x = Math.round(Math.random() * canvas.width);
this.x2 = Math.round(Math.random() * canvas.width);
this.pt1 = {
x: this.x,
y: 0
};
this.pt2 = {
x: this.x2,
y: canvas.height
};
this.num = num;
this.rads = Math.atan2(this.pt2.y - this.pt1.y, this.pt2.x - this.pt1.x);
this.angle = this.rads * (180 / Math.PI);
}
draw() {
ctx.beginPath();
ctx.moveTo(this.pt1.x, this.pt1.y);
ctx.lineTo(this.pt2.x, this.pt2.y);
ctx.stroke();
}
}
//creates the lines. I also use this function to prepare the 2d array by pushing an empty array for each line into sortedPts.
function createLines() {
for (let i = 0; i < lineNum; i++) {
lines.push(new Lines(i + 1));
sortedPts.push([])
}
}
createLines();
//Visually draws lines on screen
function drawLines() {
for (let i = 0; i < lines.length; i++) {
lines[i].draw();
}
}
drawLines();
//intersecting formula
function lineSegmentsIntersect(line1, line2) {
let a_dx = line1.pt2.x - line1.pt1.x;
let a_dy = line1.pt2.y - line1.pt1.y;
let b_dx = line2.pt2.x - line2.pt1.x;
let b_dy = line2.pt2.y - line2.pt1.y;
let s =
(-a_dy * (line1.pt1.x - line2.pt1.x) + a_dx * (line1.pt1.y - line2.pt1.y)) /
(-b_dx * a_dy + a_dx * b_dy);
let t =
(+b_dx * (line1.pt1.y - line2.pt1.y) - b_dy * (line1.pt1.x - line2.pt1.x)) /
(-b_dx * a_dy + a_dx * b_dy);
if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
//this is where we create our array but we also add the line number of where each point intersects. I also add the angle but have not used it throughout the rest of this...yet.
points.push({
x: Math.round(line1.pt1.x + t * (line1.pt2.x - line1.pt1.x)),
y: Math.round(line1.pt1.y + t * (line1.pt2.y - line1.pt1.y)),
num: {
first: line1.num,
second: line2.num
},
angle: {
a1: line1.angle,
a2: line2.angle
}
});
}
}
//just checks each line against the others by passing to lineSegmentsIntersect() function
function callIntersect() {
for (let i = 0; i < lines.length; i++) {
for (let j = i + 1; j < lines.length; j++) {
lineSegmentsIntersect(lines[i], lines[j]);
}
}
}
callIntersect();
function drawPoints() {
//just draws the black points for reference
for (let i = 0; i < points.length; i++) {
ctx.beginPath();
ctx.arc(points[i].x, points[i].y, 2, 0, Math.PI * 2);
ctx.fill();
}
}
drawPoints();
function createSortedArray() {
//Now we take the points array and sort the points by the first number to make using i and j below possible
points.sort((a, b) => a.num.first - b.num.first)
//We push each group of points into an array inside sortedPts creating the 2d array
for (let i = 0; i < lineNum; i++) {
for (let j = 0; j < points.length; j++) {
if (points[j].num.first == (i + 1)) {
sortedPts[i].push(points[j]);
}
}
}
//now sort the 2d arrays by y value. This allows or next check to go in order from point to point per line.
sortedPts.forEach(arr => arr.sort((a, b) => a.y - b.y));
fillTriangles();
}
createSortedArray();
/*
The last step iterates through each point in the original points array
and check to see if either the first or second number matches the second
number of a point in our sortedPts array AND do the first or second number
match the next points in the sortedPtsd array. If so then we must have a
triangle.
Quick breakdown. If we have 3 lines (line 1, 2, 3) and I have a points on lines
2 & 3. I also have another point on lines 2 & 1. Then in order to have a triangle
the last point must be on lines 1 & 3.
That's all this is doing.
*/
function fillTriangles() {
//iterate through each array inside sortedPts array
for (let i = 0; i < sortedPts.length; i++) {
//iterate through all points inside each array of points inside the sortedPts array
for (let j = 0; j < sortedPts[i].length - 1; j++) {
//iterate over the original points and compare
for (let k = 0; k < points.length; k++) {
if (
(points[k].num.first == sortedPts[i][j].num.second ||
points[k].num.second == sortedPts[i][j].num.second) &&
(points[k].num.first == sortedPts[i][j + 1].num.second ||
points[k].num.second == sortedPts[i][j + 1].num.second)
) {
ctx.fillStyle = "rgba(200, 100, 0, 0.1)";
ctx.beginPath();
ctx.moveTo(sortedPts[i][j].x, sortedPts[i][j].y);
ctx.lineTo(sortedPts[i][j + 1].x, sortedPts[i][j + 1].y);
ctx.lineTo(points[k].x, points[k].y);
ctx.closePath();
ctx.fill();
}
}
}
}
}
<canvas id="canvas"></canvas>
I also think there's a good way to do this with the angles of the crossing lines and am working on something to do it that way. I am hoping I can get it to determine the type of shape based on the number of sides but I don't see that being a quick project.
Your goal is not clear to me. You can connect any arbitrary set of points in any arbitrary order and call it a shape. What are your criteria?
If you want to find the shortest path that connects all the points of a given subset, I suggest looking for travelling salesman problem.

How could I move randomly this triangle? p5.js

Since few days I try to animate my triangle, I want to move it randomly on my canvas. All of my tests were a failure so if you have some tips I am open to it!
I wish my triangle move on x and y axis randomly like a free electron and in the future I would like to have other triangles that move randomly and when they touch each other they bounce but it's another step!
My code:
let x = 50;
let y = 200;
let y1 = 100;
let y2 = 200
let x1 = 100;
let x2= 150;
let speed = 5;
let startColor;
let endColor;
let amt = 0;
function setup() {
startColor = color("hsl(172, 100%, 50%)");
endColor = color("hsl(335, 100%, 50%)");
createCanvas(windowWidth, 800);
frameRate(45);
}
function draw() {
colorMode(RGB);
background(252, 238, 10);
shape(); // Appel de la function shape
bounce();// appel de la fonction bounce
}
function bounce() {
x = x + speed;
x1 = x1 + speed;
x2 = x2 + speed;
y = y + speed
y1 = y1 + speed
y2 = y2 + speed
if (x2 > windowWidth || x < 0) {
speed = speed * -1;
}
}
function shape() {
if (amt >= 1) {
amt = 0;
let tmpColor = startColor;
startColor = endColor;
endColor = tmpColor;
}
amt += 0.01;
let colorTransition = lerpColor(startColor, endColor, amt);
noStroke();
fill(colorTransition);
triangle(x, y, x1, y1, x2, y2);
}
First you have a code working to make your triangle always move in the same direction. What you could do to make it random is to change the speed you are using:
For now each call to bounce moves the triangle by speed pixels. So if in draw() before calling shape() you add the following, triangle will begin to randomly move by a small amount:
speed = map(random(), 0, 1, -5, 5);
There are tons of different ways to do it, here we have making use of processing's random() to generate a number between 0 and 1 and map() to get a value between -5 and 5.
Now the issue is that you have only one type of speed and you apply to both axis x and y. What you want is probably to have speedX and speedY with two different values applied to both component of your position.
Once you try to do that you'll realize that having two variables for speedX and speedY is not very convenient and that you'd rather have one variable for your position with two component x and y and same for your speed. This way you'll be able to do position = position + speed. This requires that you refactor your code to use a more object oriented paradigm. To learn how to do that, one of the best resources online is the "Nature of Code" playlist by The coding train youtube channel.
I work every day and i followed all your advices and now this is what i have product, thanks for all !!
let triangle1;
let triangle2;
let triangle3;
let triangle4;
let triangle5;
let speedX;
let speedY;
let startColor;
let endColor;
let amt = 0;
function setup() {
startColor = color("hsl(172, 100%, 50%)");
endColor = color("hsl(335, 100%, 50%)");
createCanvas(windowWidth, 800);
//creer notre triangle
triangle1 = new Triangles(200, 100, 0, 4);
triangle2 = new Triangles(100, 50, 2, 0);
triangle3 = new Triangles(50, 200, -1, 4);
triangle4 = new Triangles(250, 400, 4, 4);
triangle5 = new Triangles(150, 500, 0, 2);
}
function draw() {
colorMode(RGB);
background(252, 238, 10);
triangle1.show();
triangle1.move();
triangle2.show();
triangle2.move();
triangle3.show();
triangle3.move();
triangle4.show();
triangle4.move();
triangle5.show();
triangle5.move();
}
class Triangles {
//configuration de l'objet
constructor(triX, triY, speedX, speedY){
this.x = triX;
this.y = triY;
this.speedX = speedX;
this.speedY = speedY;
}
show(){
if (amt >= 1) {
amt = 0;
let tmpColor = startColor;
startColor = endColor;
endColor = tmpColor;
}
amt += 0.01;
let colorTransition = lerpColor(startColor, endColor, amt);
noStroke();
fill(colorTransition);
noStroke();
triangle(this.x, this.y, this.x + 25, this.y + 40, this.x -25, this.y + 40);
}
move(){
this.x += this.speedX;
this.y += this.speedY;
if(this.x > width || this.x < 0){
this.speedX *= -1;
}
if(this.y > height || this.y < 0){
this.speedY = this.speedY * -1;
}
}
}

Developing an Algorithm to Transform Four Cartesian Coordinates Into Square Coordinates

I am working on a project where four randomly placed robots each have unique Cartesian coordinates. I need to find a way to transform these coordinates into the coordinates of a square with side length defined by the user of the program.
For example, let's say I have four coordinates (5,13), (8,17), (13,2), and (6,24) that represent the coordinates of four robots. I need to find a square's coordinates such that the four robots are closest to these coordinates.
Thanks in advance.
As far as I understand your question you are looking for the centroid of the four points, the point which has equal — and thus minimal — distance to all points. It is calculated as the average for each coordinate:
The square's edge length is irrelevant to the position, though.
Update
If you additionally want to minimize the square corners' distance to a robot position, you can do the following:
Calculate the centroid c like described above and place the square there.
Imagine a circle with center at c and diameter of the square's edge length.
For each robot position calculate the point on the circle with shortest distance to the robot and use that as a corner of the square.
It looks as if the original poster is not coming back to share his solution here, so I'll post what I was working on.
Finding the center point of the four robots and then drawing the square around this point is indeed a good way to start, but it doesn't necessarily give the optimal result. For the example given in the question, the center point is (8,14) and the total distance is 22.688 (assuming a square size of 10).
When you draw the vector from a corner of the square to the closest robot, this vector shows you in which direction the square should move to reduce the distance from that corner to its closest robot. If you calculate the sum of the direction of these four vectors (by changing the vectors to size 1 before adding them up) then moving the square in the resulting direction will reduce the total distance.
I dreaded venturing into differential equation territory here, so I devised a simple algorithm which repeatedly calculates the direction to move in, and moves the square in ever decreasing steps, until a certain precision is reached.
For the example in the question, the optimal location it finds is (10,18), and the total distance is 21.814, which is an improvement of 0.874 over the center position (assuming a square size of 10).
Press "run code snippet" to see the algorithm in action with randomly generated positions. The scattered green dots are the center points that are considered while searching the optimal location for the square.
function positionSquare(points, size) {
var center = {x: 0, y:0};
for (var i in points) {
center.x += points[i].x / points.length;
center.y += points[i].y / points.length;
}
paintSquare(canvas, square(center), 1, "#D0D0D0");
order(points);
textOutput("<P>center position: " + center.x.toFixed(3) + "," + center.y.toFixed(3) + "<BR>total distance: " + distance(center, points).toFixed(3) + "</P>");
for (var step = 1; step > 0.0001; step /= 2)
{
var point = center;
var shortest, dist = distance(center, points);
do
{
center = point;
shortest = dist;
var dir = direction();
paintDot(canvas, center.x, center.y, 1, "green");
point.x = center.x + Math.cos(dir) * step;
point.y = center.y + Math.sin(dir) * step;
dist = distance(point, points);
}
while (dist < shortest)
}
textOutput("<P>optimal position: " + center.x.toFixed(3) + "," + center.y.toFixed(3) + "<BR>total distance: " + distance(point, points).toFixed(3) + "</P>");
return square(center);
function order(points) {
var clone = [], best = 0;
for (var i = 0; i < 2; i++) {
clone[i] = points.slice();
for (var j in clone[i]) clone[i][j].n = j;
if (i) {
clone[i].sort(function(a, b) {return b.y - a.y});
if (clone[i][0].x > clone[i][1].x) swap(clone[i], 0, 1);
if (clone[i][2].x < clone[i][3].x) swap(clone[i], 2, 3);
} else {
clone[i].sort(function(a, b) {return a.x - b.x});
swap(clone[i], 1, 3);
if (clone[i][0].y < clone[i][3].y) swap(clone[i], 0, 3);
if (clone[i][1].y < clone[i][2].y) swap(clone[i], 1, 2);
}
}
if (distance(center, clone[0]) > distance(center, clone[1])) best = 1;
for (var i in points) points[i] = {x: clone[best][i].x, y: clone[best][i].y};
function swap(a, i, j) {
var temp = a[i]; a[i] = a[j]; a[j] = temp;
}
}
function direction() {
var d, dx = 0, dy = 0, corners = square(center);
for (var i in points) {
d = Math.atan2(points[i].y - corners[i].y, points[i].x - corners[i].x);
dx += Math.cos(d);
dy += Math.sin(d);
}
return Math.atan2(dy, dx);
}
function distance(center, points) {
var d = 0, corners = square(center);
for (var i in points) {
var dx = points[i].x - corners[i].x;
var dy = points[i].y - corners[i].y;
d += Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
}
return d;
}
function square(center) {
return [{x: center.x - size / 2, y: center.y + size / 2},
{x: center.x + size / 2, y: center.y + size / 2},
{x: center.x + size / 2, y: center.y - size / 2},
{x: center.x - size / 2, y: center.y - size / 2}];
}
}
// PREPARE CANVAS
var canvas = document.getElementById("canvas");
canvas.width = 200; canvas.height = 200;
canvas = canvas.getContext("2d");
// GENERATE TEST DATA AND RUN FUNCTION
var points = [{x:5, y:13}, {x:8, y:17}, {x:13, y:2}, {x:6, y:24}];
for (var i = 0; i < 4; i++) {
points[i].x = 1 + 23 * Math.random(); points[i].y = 1 + 23 * Math.random();
}
for (var i in points) textOutput("point: " + points[i].x.toFixed(3) + "," + points[i].y.toFixed(3) + "<BR>");
var size = 10;
var square = positionSquare(points, size);
// SHOW RESULT ON CANVAS
for (var i in points) {
paintDot(canvas, points[i].x, points[i].y, 5, "red");
paintLine(canvas, points[i].x, points[i].y, square[i].x, square[i].y, 1, "blue");
}
paintSquare(canvas, square, 1, "green");
function paintDot(canvas, x, y, size, color) {
canvas.beginPath();
canvas.arc(8 * x, 200 - 8 * y, size, 0, 6.2831853);
canvas.closePath();
canvas.fillStyle = color;
canvas.fill();
}
function paintLine(canvas, x1, y1, x2, y2, width, color) {
canvas.beginPath();
canvas.moveTo(8 * x1, 200 - 8 * y1);
canvas.lineTo(8 * x2, 200 - 8 * y2);
canvas.strokeStyle = color;
canvas.stroke();
}
function paintSquare(canvas, square, width, color) {
canvas.rect(8 * square[0].x , 200 - 8 * square[0].y, 8 * size, 8 * size);
canvas.strokeStyle = color;
canvas.stroke();
}
// TEXT OUTPUT
function textOutput(t) {
var output = document.getElementById("output");
output.innerHTML += t;
}
<BODY STYLE="margin: 0; border: 0; padding: 0;">
<CANVAS ID="canvas" STYLE="width: 200px; height: 200px; float: left; background-color: #F8F8F8;"></CANVAS>
<DIV ID="output" STYLE="width: 400px; height: 200px; float: left; margin-left: 10px;"></DIV>
</BODY>
Further improvements: I haven't yet taken into account what happens when a corner and a robot are in the same spot, but the overall position isn't optimal. Since the direction from the corner to the robot is undefined, it should probably be taken out of the equation temporarily.

Resources