xxxxxxxxxx
332
// Daniel Shiffman
// http://codingtra.in
// http://patreon.com/codingtrain
// QuadTree
// 1: https://www.youtube.com/watch?v=OJxEcs0w_kE
// 2: https://www.youtube.com/watch?v=QQx_NmCIuCY
// For more:
// https://github.com/CodingTrain/QuadTree
// this is an ugly adaptation of the above that
// recreates the whole quadtree structure and updates the nodes
// the quadtree limits the points from leaving the boundaries
// to make this better we should implement a replace or update function
DEBUGGING = 0;
var qtree;
var boundary;
const AMT = 300;
var mode = 0;
const MODES = ["off", "repulsion", "attraction", "tornado (unimpl)"];
function setup() {
createButton("cycle mode").mouseReleased(function (e) {
mode = (mode + 1) % MODES.length;
print(`set mode to ${MODES[mode]}`);
document.getElementById("modespan").textContent = `mode: ${MODES[mode]}`;
});
document.getElementById("modespan").textContent = `mode: ${MODES[mode]}`;
createCanvas(800, 600);
debugCanvas = createGraphics(width, height);
print(width, height);
background(255);
noiseSeed(200);
boundary = new Rectangle(width / 2, height / 2, width / 2, height / 2);
qtree = new QuadTree(boundary, 5);
for (let i = 0; i < AMT; i++) {
let x = randomGaussian(width / 2, width / 8);
let y = randomGaussian(height / 2, height / 8);
let p = new Point(x, y);
qtree.insert(p);
}
}
var tests = 0;
var testmax = 10;
var t = 0;
var dt = 0.005;
function keyPressed() {
if (
mouseX > 0 &&
mouseX < width &&
mouseY > 0 &&
mouseY < height &&
keyCode == 72
) {
// user pressed H, draw a heart
const HS = 5; //heartsize
tt = -PI;
dt = 0.1;
do {
x = mouseX + 16*HS * sin(tt) * sin(tt) * sin(tt);
y =
mouseY -
(13*HS * cos(tt) - 5*HS * cos(2 * tt) - 2*HS * cos(3 * tt) - cos(4 * tt));
qtree.insert(new Point(x, y));
tt += dt;
} while (tt < PI);
}
}
function globalEffect(stuff, qtree) {
for (const [di, p] of stuff.entries()) {
// update with a ltitle bit of randomness already
var px = p.x + noise(t / 10) * random(-1, 1);
var py = p.y + noise(t / 1000) * 5;
const v1 = createVector(px, py);
switch (MODES[mode]) {
// TODO seperate cases to their own loops to avoid N mode checks
case "attraction":
// attraction force
print("wrong function!");
break;
case "repulsion":
// repulsion force
print("wrong function!");
break;
default:
case "tornado":
// this is why the TODO is up there, since all is recalculated for no reason tbh
// to form a tornado around the centre {x, y forall x == width/2 && 0 < y < height}
// we decide whether the particle is to the right or left of the tornado.
// https://www.britannica.com/science/tornado/Physical-characteristics-of-tornadoes
// it looks like its way more complex (who would've thought)
// lets try and model both the turning around the edge, up motion close to the inside of the edge
// and downwards motion in the centre ?
// so lets start deciding in what way the tornado needs to turn.
// if a particle is to the right of the center, it should turn right and down
// if a particle it to the left of the center, it should turn left and up a bit.
const dcore = dist(v1.x, v1.y + 1, width / 2, v1.y);
const dground = height - v1.y + 1;
// we need some way to determine whether a particle is behind or in front of the tornado
// it would be easier to do this with 3d maybe
// but we can cheat and partition the tornado piecewise alternating
// inwardsdown | outwardsup
// outwardsup | inwardsup
// if we take the height of the particle
// left or right add pi
// then sin() the height
const yrate = 7;
const xrate = 4;
var ypart =
v1.x > width / 2 ? sin(v1.y / yrate + PI) : sin(v1.y / yrate);
var xthingy =
v1.x > width / 2 ? sin(v1.y / xrate + PI) : sin(v1.y / xrate);
direction = createVector(xthingy, ypart).setMag(2);
// YEAH WHATEVER THIS ISNT WORKING WE JUST NEED A 3D COMPONENT OR A BRAIN OR A HISTORY
if (di > stuff.length / 2 - 30 && di < stuff.length / 2 + 30) {
debugCanvas.push();
debugCanvas.translate(v1.x, v1.y);
debugCanvas.rotate(direction.heading());
debugCanvas.stroke(255, 0, 0, 255);
debugCanvas.line(0, 0, 100, 0);
debugCanvas.pop();
}
break;
}
v1.add(direction);
px = v1.x;
py = v1.y;
qtree.insert(new Point(px, py));
}
}
function areaEffect(stuff, qtree) {
for (const [di, p] of stuff.entries()) {
var px = p.x + noise(t / 10) * random(-1, 1);
var py = p.y + noise(t / 1000) * 5;
d = dist(mouseX, mouseY, px, py);
const EFFECT_RANGE = 100;
if (d < EFFECT_RANGE) {
v1 = createVector(px, py);
v2 = createVector(mouseX, mouseY);
switch (MODES[mode]) {
// TODO seperate cases to their own loops to avoid N mode checks
case "off":
direction = createVector(0, 0);
break;
case "attraction":
// attraction force
direction = p5.Vector.sub(v2, v1).setMag(EFFECT_RANGE / d);
break;
case "repulsion":
// repulsion force
direction = p5.Vector.sub(v1, v2).setMag(EFFECT_RANGE / 2 / d);
break;
default:
case "tornado":
print("wrong function!");
break;
}
v1.add(direction);
px = v1.x;
py = v1.y;
}
qtree.insert(new Point(px, py));
}
}
addlim = 1; // must be > 1
addindex = 0;
addsperadd = 5;
function draw() {
background(0);
// adds points to the canvas under mouse
if (
mouseX > 0 &&
mouseX < width &&
mouseY > 0 &&
mouseY < height &&
mouseIsPressed
) {
if (addindex == 0) {
for (addi = 0; addi < addsperadd; addi++) {
qtree.insert(new Point(mouseX, mouseY));
}
}
addindex = (addindex + 1) % addlim;
}
qtree.show();
stroke(0, 0, 255, 100);
rectMode(CENTER);
var range;
range = new Rectangle(mouseX, mouseY, 72, 72);
// This check has been introduced due to a bug discussed in https://github.com/CodingTrain/website/pull/556
if (mouseX < width && mouseY < height) {
rect(range.x, range.y, range.w * 2, range.h * 2);
stroke(0, 155, 255, 255);
let points = qtree.query(range);
if(!points) points=[];
for (let p of points) {
strokeWeight(5);
point(p.x, p.y);
}
}
range = new Rectangle(width / 2, height / 2, width / 2, height / 2);
var stuff = qtree.query(range);
strokeWeight(0.2);
stroke("red");
// rendering stuff
for (var p of stuff) {
// TODO make slider for this boi since its quite fun
var size = noise(p.x / 1000, p.y / 1000) * 70;
var someRange = new Rectangle(p.x, p.y, size, size);
var neighbours = qtree.query(someRange);
if (tests < testmax && DEBUGGING) {
tests++;
print(neighbours.length);
}
var pmax = noise(p.x / 1000, p.y / 1000) * 6;
var c = 0;
if (neighbours.length<2){
push();
stroke(200,0,0,200)
strokeWeight(4.3)
point(p.x,p.y)
pop();
}
// draw lines between dots
for (var p2 of neighbours) {
if(p2.x == p.x && p.y == p2.y) continue
if (tests < testmax && c < 2 && DEBUGGING) {
print(p2);
print(p2.x, p.x);
tests++;
}
line(p2.x, p2.y, p.x, p.y);
if (c > pmax) break;
c++;
}
}
qtree = new QuadTree(boundary, 5); //erm this should already reset it
qtree.points = []; // remove all points
qtree.divided = false; //reset tree
// upating stuff
switch (MODES[mode]) {
case "off":
areaEffect(stuff, qtree);
break;
case "attraction":
areaEffect(stuff, qtree);
break;
case "repulsion":
areaEffect(stuff, qtree);
break;
default:
case "tornado":
globalEffect(stuff, qtree);
break;
}
for (i = 0; i < AMT - stuff.length; i++) {
let x = randomGaussian(0, width);
let y = randomGaussian(0, 40);
let p = new Point(x, y);
qtree.insert(p);
}
t += dt;
image(debugCanvas, 0, 0);
debugCanvas.clear();
}