xxxxxxxxxx
715
// shoutout mohamad aljanabi https://youtu.be/Cv7Sbuuo2X8
// ok so now the hard part: turning the lines into uninterrupted polygons
// and then from that it makes polygon arrays for the actual tiles, could be similar to the voronoi construction method, along with an array of coordinates/rotation
// so now i have all segments, and i know each segment should border a polygon on two side, with the exception of segments on or outside the frame border, so i can loop over my segments until this condition is satisfied for each.
// then the function that draws all of them
// variables:
// basically my ratios. maybe i can tweak em
const gp1 = 25;
const gp2 = 50;
let gp3 = 10; //100/gp3 = the divisions of the square (used to be 10)
let startingPoint = [0, 100];
let animationTime = 750; //in millis
let animating = false, animationDirection = -1, animationStart = 0;
let drawOrder = [], drawing = 0;
let strokeOn = false;
let conPts = [];
let lPts = [], lPts2 = [];
let segs = [], segs2 = [];
let intersections = [];
let polygons = [];
let polygonRepetitions = [];
let tileSize = 100;
let prevDir;
let errorBreaker = 0;
let slider;
function setup() {
// createCanvas(800, 400);
createCanvas(windowWidth, windowHeight);
angleMode(DEGREES);
slider = createSlider(max(height,width)/40, max(height,width), min(height,width)/3, 0);
slider.style("width", width + "px");
createButton("regenerate")
.mousePressed(regen)
.position(10, 10);
createButton("animate (bit jarring still)")
.mousePressed(animate)
.position(10, 35);
createButton("save png")
.mousePressed(savePNG)
.position(95, 10);
createButton("save pattern data")
.mousePressed(saveCSV)
.position(169, 10);
createButton("toggle stroke")
.mousePressed( ()=> strokeOn = !strokeOn)
.position(168, 35);
regen();
}
function regen() {
noiseSeed(random(0,100));
constructionPoints();
generatePath();
print('points on line: ' + lPts.length);
findIntersections();
print('segments: ' + segs.length);
calculatePolygons();
print('polygons: ' +polygons.length);
}
function constructionPoints() {
// generate construction points
conPts = [];
for (let i = gp1; i < gp1 * 16; i += 2 * gp1) {
let startx = floor(i/100) * gp1;
let starty = (i % (gp1 * 8)) % (gp1 * 5);
conPts.push(
[startx + gp3, starty],
[startx - gp3, starty],
[startx, starty + gp3],
[startx, starty - gp3]
);
}
print(conPts.length)
// move points around a bit
for (let i = 0; i < conPts.length * 2; i++) {
if (conPts[floor(i / 2)][i % 2] < 0) {
conPts[floor(i / 2)][i % 2] = conPts[floor(i / 2)][i % 2] + 100;
} else if (conPts[floor(i / 2)][i % 2] == 0) {
conPts.push([0.01, 0.01]);
conPts[conPts.length - 1][i % 2] = conPts[floor(i / 2)][i % 2] + 100;
conPts[conPts.length - 1][1 - (i % 2)] =
conPts[floor(i / 2)][1 - (i % 2)];
}
}
conPts.push([0, 0], [0, 100], [100, 0], [100, 100]); //add the corners
print(conPts.length)
}
function generatePath() {
let hitDia = false;
lPts = [startingPoint];
for (let i = 0; i < 100; i++) {
// down, right, up, left, sw, nw, ne, se
let dirs = [[], [], [], [], [], [], [], []];
// check availabile jumps
for (let j = 0; j < conPts.length; j++) {
// check orth directions
for (let k = 0; k < 2; k++) {
if (conPts[j][k] == lPts[i][k] && conPts[j][1 - k] != lPts[i][1 - k]) {
//x, y
dirs[k + 1 + Math.sign(lPts[i][1 - k] - conPts[j][1 - k])].push(j);
}
}
// check diagonal directions
for (let k = -1; k < 2; k += 2) {
if (lPts[i][0] - conPts[j][0] == k * (lPts[i][1] - conPts[j][1]) &&
conPts[j][0] != lPts[i][0] ) {
dirs[5 + (1 + k) / 2 + Math.sign(conPts[j][0] - lPts[i][0])].push(j);
}
}
}
// choose direction
let dir = findDir(dirs, hitDia);
let newpoint = conPts[dirs[dir][floor(random(0, dirs[dir].length))]];
errorBreaker = 0;
while(abs(lPts[i][0] - newpoint[0]) == 100 ||
abs(lPts[i][1] - newpoint[1]) == 100){
if(dirs[dir].length == 1){
dir = findDir(dirs, hitDia);
}
newpoint = conPts[dirs[dir][floor(random(0, dirs[dir].length))]];
errorBreaker++;
if (errorBreaker > 50) {
print("error 2!");
print(lPts[i], dirs);
break;
}
}
prevDir = dir;
lPts.push([newpoint[0], newpoint[1]]);
if (lPts[i + 1][0] > lPts[i + 1][1]) {
hitDia = true;
}
if (hitDia && lPts[i + 1][0] == 0) {
break;
}
}
lPts2 = [];
for (let i = 0; i < lPts.length; i++) {
lPts2.push([lPts[i][1], lPts[i][0]]);
}
}
function findDir(dirs, hitDia){
let dir = floor(random(0, 8));
errorBreaker = 0;
while (
(dir >= 3 && dir <= 5 && hitDia == false) ||
dirs[dir].length == 0 ||
dir % 4 == (prevDir + 2) % 4 ||
dir == prevDir
) {
dir = floor(random(0, 8));
errorBreaker++;
if (errorBreaker > 50) {
print("error 1!");
break;
}
}
return dir;
}
function findIntersections(){
// find intersections between lines and append
// cut em up into shortest segments
segs = [];
//turn them all into segments
for(let i=0; i<lPts.length-1; i++){
segs.push([lPts[i].slice(),lPts[i+1].slice()])
segs.push([lPts2[i].slice(),lPts2[i+1].slice()])
}
//find all the intersections
let currentLength = segs.length;
for(let i=0; i<currentLength; i++){
//check intersections
for(let j=0;j<currentLength;j++){
// check self intersections
let intersection = intersect (segs[i], segs[j])
if(intersection &&
!(segs[i][0][0] == intersection[0] && segs[i][0][1] == intersection[1]) &&
!(segs[i][1][0] == intersection[0] && segs[i][1][1] == intersection[1])){
let coords = [segs[i][0],intersection]
segs.push(coords);
}
}
}
//cut up overlapping ones, this function is so fucking bad holy shit also it creates wayyy too many
currentLength = segs.length;
for(let i=0;i<currentLength;i++){
for(let j=i+1;j<currentLength;j++){
if(overlap(segs[i],segs[j]) &&
!(segs[i][0][0] == segs[j][0][0] && segs[i][0][1] == segs[j][0][1] &&
segs[i][1][0] == segs[j][1][0] && segs[i][1][1] == segs[j][1][1]) &&
!(segs[i][0][0] == segs[j][1][0] && segs[i][0][1] == segs[j][1][1] &&
segs[i][1][0] == segs[j][0][0] && segs[i][1][1] == segs[j][0][1]) ){
//check if vertical
let dir = 0;
if( segs[i][0][0] == segs[i][1][0]){
dir = 1;
}
let orderedSegmentPoints = [];
let orderingArray = [ [segs[i][0][0],segs[i][0][1]],
[segs[i][1][0],segs[i][1][1]],
[segs[j][0][0],segs[j][0][1]],
[segs[j][1][0],segs[j][1][1]]];
let orderingArray2 = [segs[i][0][dir],
segs[i][1][dir],
segs[j][0][dir],
segs[j][1][dir]];
for(let k=0;k<4;k++){
for(let l=0;l<orderingArray.length;l++){
if(orderingArray2[l] == min(orderingArray2)){
orderedSegmentPoints.push(orderingArray[l].slice())
orderingArray.splice(l,1);
orderingArray2.splice(l,1);
break;
}
}
}
for(let k=0;k<3;k++){
if(orderedSegmentPoints[k][dir] != orderedSegmentPoints[k+1][dir]){
segs.push([orderedSegmentPoints[k].slice(), orderedSegmentPoints[k+1].slice()])
}
}
}
}
}
//filter duplicates/overlap
for(let i=0;i<segs.length;i++){
for(let j=0;j<segs.length;j++){
if(i!=j && overlap(segs[i],segs[j])){
// delete the longest one!!!!
let segLi = (segs[i][0][0]-segs[i][1][0])**2 + (segs[i][0][1]-segs[i][1][1])**2 ;
let segLj = (segs[j][0][0]-segs[j][1][0])**2 + (segs[j][0][1]-segs[j][1][1])**2 ;
if(segLi > segLj){
segs.splice(i,1)
i--;
break;
}
if(segLi <= segLj){
segs.splice(j,1)
if(j<i){
i--;
}
j--;
}
}
}
}
}
function intersect(c1,c2){
// how to find a single intersection?
// -> between two lines? between a line and every other line? between my dick and your ass, cockboy?
// bunch of stolen code
let cmP = [c2[0][0] - c1[0][0], c2[0][1] - c1[0][1]];
let r = [c1[1][0] - c1[0][0], c1[1][1] - c1[0][1]];
let s = [c2[1][0] - c2[0][0], c2[1][1] - c2[0][1]];
let cmPxr = cmP[0] * r[1] - cmP[1] * r[0];
let cmPxs = cmP[0] * s[1] - cmP[1] * s[0];
let rxs = r[0] * s[1] - r[1] * s[0];
// lines are collinear or parallel
if (cmPxr == 0 || rxs == 0) {
return false;
}
let rxsr = 1 / rxs;
let t = cmPxs * rxsr;
let u = cmPxr * rxsr;
if((t < 0) || (t > 1) || (u < 0) || (u > 1)){
return false
}
let inxs = [ round(c1[0][0] + t * r[0]) , round(c1[0][1] + t * r[1]) ]
return inxs
}
function overlap(c1, c2){
// bunch of stolen code, same as intersect
let cmP = [c2[0][0] - c1[0][0], c2[0][1] - c1[0][1]];
let r = [c1[1][0] - c1[0][0], c1[1][1] - c1[0][1]];
let s = [c2[1][0] - c2[0][0], c2[1][1] - c2[0][1]];
let cmPxr = cmP[0] * r[1] - cmP[1] * r[0];
let rxs = r[0] * s[1] - r[1] * s[0];
// check parallel & aligned
if (cmPxr != 0 || rxs != 0) {
return false;
}
//check if vertical
let dir = 0;
if(c1[0][0] == c1[1][0]){
dir = 1;
}
// find overlap
if(max(c1[0][dir],c1[1][dir]) > min(c2[0][dir],c2[1][dir]) &&
max(c2[0][dir],c2[1][dir]) > min(c1[0][dir],c1[1][dir]) ){
return true;
}
return false;
}
function calculatePolygons(){
// i think this is the algorithm for finding a polygon:
// 1. start at a segment, write down its first coord and go in the direction of its second coord.
// 2. for the second coord, find all the other segments that carry it and find the one that is most clockwise, then go to that ones other coord
// 3. repeat until we're back at the starting coord
// we know we're done when each line segment is walked TWICE and only ONCE PER POLYGON. with the exception of segments on or outside the border which are walked ONCE IN TOTAL.
let segmentStatus = [];
for(let i=0; i<segs.length;i++){
//some tricky evaluations to see in which direction the border ones need to go to be clockwise
if( ((segs[i][0][0] == 0 && segs[i][1][0] == 0) &&
segs[i][0][0] + segs[i][0][1] < segs[i][1][0] + segs[i][1][1]) ||
((segs[i][0][0] == 100 && segs[i][1][0] == 100) &&
segs[i][0][0] + segs[i][0][1] > segs[i][1][0] + segs[i][1][1]) ||
((segs[i][0][1] == 0 && segs[i][1][1] == 0) &&
segs[i][0][0] + segs[i][0][1] > segs[i][1][0] + segs[i][1][1]) ||
((segs[i][0][1] == 100 && segs[i][1][1] == 100) &&
segs[i][0][0] + segs[i][0][1] < segs[i][1][0] + segs[i][1][1])){
segmentStatus.push([false, true]);
}
else if((segs[i][0][0] == 0 && segs[i][1][0] == 0) ||
(segs[i][0][0] == 100 && segs[i][1][0] == 100) ||
(segs[i][0][1] == 0 && segs[i][1][1] == 0) ||
(segs[i][0][1] == 100 && segs[i][1][1] == 100) ){
segmentStatus.push([true, false]);
}
//all others need both directions
else{
segmentStatus.push([false,false]);
}
}
let altSegs = [[[],[],[]],[[],[],[]],[[],[],[]]];
let m = [-1,1,-1], p = [0,0,200]
for(let i=0;i<segs.length;i++){
for(let xi=0;xi<3;xi++){
for(let yi=0;yi<3;yi++){
altSegs[xi][yi].push([ [segs[i][0][0] * m[xi] + p[xi], segs[i][0][1] * m[yi] + p[yi]],
[segs[i][1][0] * m[xi] + p[xi], segs[i][1][1] * m[yi] + p[yi]] ])
}
}
}
polygons = [];
polygonRepetitions = [];
while(true){
//find an unsatisfied segment, if there are none then break
let cSeg, direction;
for(let i=0;i<segmentStatus.length;i++){
if(!segmentStatus[i][0]){
cSeg = i;
direction = 0;
break;
}
if(!segmentStatus[i][1]){
cSeg = i;
direction = 1;
break;
}
}
if(cSeg == undefined){
break;
}
// HERE GOES POLYGON WALKER CODE
// exciting stuff
let newPolygon = [];
//should it repeat? ->> [left, right, top, bottom] (if left&to)
polygonRepetitions.push([true,true,true,true]);
//add the points on the first segment
newPolygon.push(segs[cSeg][0+direction].slice(), segs[cSeg][1-direction].slice());
segmentStatus[cSeg][direction] = true;
while( !arrayEquals(newPolygon[0], newPolygon[newPolygon.length-1]) ){
let cPoint = newPolygon[newPolygon.length-1].slice();
let thisVect = [newPolygon[newPolygon.length - 2][0] - cPoint[0],
newPolygon[newPolygon.length - 2][1] - cPoint[1]];
let thisAngle = findAngle(thisVect);
let pickedIndex;
let nextAngle = 360;
let searchSegs = [];
for(let xi=0;xi<3;xi++){
for(let yi=0;yi<3;yi++){
if(cPoint[0] >= -100 + 100*xi && cPoint[0] <= -100 + 100*(xi+1) &&
cPoint[1] >= -100 + 100*yi && cPoint[1] <= -100 + 100*(yi+1)){
searchSegs = concat(searchSegs, altSegs[xi][yi])
}
}
}
//first find all segments starting at the latest point
for(let i=0;i<searchSegs.length*2;i++){
if( arrayEquals(searchSegs[floor(i/2)][i%2], newPolygon[newPolygon.length-1]) &&
!arrayEquals(searchSegs[floor(i/2)][1 - i%2], newPolygon[newPolygon.length-2])){
let nextPoint = searchSegs[floor(i/2)][1 - i%2];
let nextVect = [nextPoint[0] - cPoint[0], nextPoint[1] - cPoint[1]]
let angleD = ( thisAngle + 360 - findAngle(nextVect) ) %360;
if(angleD < nextAngle){
nextAngle = angleD;
pickedIndex = i;
}
}
}
let pickedPoint = searchSegs[floor(pickedIndex/2)][1 - pickedIndex%2].slice()
newPolygon.push(pickedPoint.slice());
//mark this direction as done
if(cPoint[0] >= 0 && cPoint[1] >= 0 && pickedPoint[0] >= 0 && pickedPoint[1] >= 0 &&
cPoint[0] <= 100 && cPoint[1] <= 100 && pickedPoint[0] <= 100 && pickedPoint[1] <= 100){
segmentStatus[floor((pickedIndex%(segs.length*2))/2)][(pickedIndex%(segs.length*2))%2] = true;
}
// repeat killa? [tl,tr,bl,br]
if( pickedPoint[0] < 0 || pickedPoint[0] > 100 ){
polygonRepetitions[polygonRepetitions.length-1][1] = false;
polygonRepetitions[polygonRepetitions.length-1][3] = false;
}
if( pickedPoint[1] < 0 || pickedPoint[1] > 100 ){
polygonRepetitions[polygonRepetitions.length-1][2] = false;
polygonRepetitions[polygonRepetitions.length-1][3] = false;
}
if(arrayEquals(newPolygon[0], newPolygon[newPolygon.length-1])){
polygons.push(newPolygon);
}
}
newPolygon.pop();
}
}
function arrayEquals(ar1,ar2){
if(ar1 == undefined || ar2 == undefined){
return true
}
for(let i=0;i<ar1.length;i++){
if(ar1[i] != ar2[i]){
return false
}
}
return true
}
function findAngle(vect){
return -1 * acos( vect[0] / (sqrt(vect[0]*vect[0] + vect[1]*vect[1])) ) * Math.sign(vect[1]+0.01);
}
// DRAW FUNCTIONS
function draw() {
background(240,240,210);
if(animating){
//we wanna make this an ease ease function
let progress = (millis() - animationStart) / animationTime;
if(progress<0.5){
// progress = 2*progress*progress
progress = 8*progress**4
}
else if(progress < 1){
// progress = -2 * (progress-1)**2 + 1
progress = 1 - 8 * (progress-1)**4
}
if(animationDirection==1){
drawing = progress * drawOrder.length;
} else{
drawing = (1 - progress) * drawOrder.length;
}
if(drawing<0){
regen();
generateOrder();
animationStart = millis();
animationDirection = 1;
}
if(drawing>drawOrder.length){
animating = false;
}
}
tileSize = slider.value();
if(strokeOn){
strokeWeight(2);
stroke(240,240,210);
}
else{
noStroke();
strokeWeight(0.1);
}
let thisTile = 0;
for(let x= -1* ceil(width/(2*tileSize)); x<ceil(width/(2*tileSize)) + 1; x++){
for(let y= -1* ceil(height/(2*tileSize)); y<ceil(height/(2*tileSize)) + 1; y++ ){
fourPatterns(width/2 + x*tileSize, height/2 + y*tileSize, thisTile);
thisTile++;
}
}
}
function fourPatterns(x,y, tileNum){
//mirror the original twice
for(let i=0;i<4;i++){
let xori = i%2;
let yori = floor(i/2);
for(let j=0;j<polygons.length;j++){
if(polygonRepetitions[j][i] == false){
continue;
}
if(animating){
if(drawOrder[tileNum*polygons.length*4 + i*polygons.length + j] > drawing){
continue;
}
}
let col = {
r: noise(polygons[j].length),
g: noise(polygons[j].length+20),
b: noise(polygons[j].length+40)
}
// let lightness = 135 + col.r*40 + col.g*40 + col.b*40
// fill(col.r*lightness, col.g*lightness, col.b*lightness);
fill(col.r*255, col.g*255, col.b*255, 135+ col.r*40 + col.g*40 + col.b*40);
// if(!strokeOn){
// fill(col.r*255, col.g*255, col.b*255, 255);
// stroke(col.r*255, col.g*255, col.b*255, 255)
// }
beginShape()
for(let k=0;k<polygons[j].length;k++){
vertex((1 - 2*xori) * polygons[j][k][0] * (tileSize/200) + xori * tileSize + x,
(1 - 2*yori) * polygons[j][k][1] * (tileSize/200) + yori * tileSize + y)
}
endShape(CLOSE)
}
}
}
// BONUS FEATURES
function animate(){
animating = true;
animationDirection = -1;
generateOrder();
animationStart = millis();
}
function generateOrder(){
//makes a flat array with every polygon
drawOrder = [];
for(let i=0; i<polygons.length * 4 *
(2 * ceil(width/(2*tileSize)) + 1) *
(2 * ceil(height/(2*tileSize)) + 1);i++){
drawOrder.push(i);
}
shuffle(drawOrder, true);
}
function savePNG(){
saveCanvas()
}
function saveCSV(){
let data = new p5.Table();
data.addColumn('poly');
data.addColumn('x');
data.addColumn('y');
for(let i=0;i<polygons.length;i++){
for(let j=0;j<polygons[i].length;j++){
let newRow = data.addRow();
newRow.setNum('poly', i);
newRow.setNum('x', polygons[i][j][0]);
newRow.setNum('y', polygons[i][j][1]);
}
}
saveTable(data, 'pattern.csv');
}