Light rays 🔦
Show me the path, Ill pass the light.
Had this turtle in my draft for very long time, it's similar to this turtle by @reinder Path Path Tracer.
Log in to post a comment.
Canvas.setpenopacity(-0.1);
const turtle = new Turtle();
const fxrand = _=> Math.random();
const scl=(a, b) => [a[0] * b, a[1] * b];
const add=(a, b) => [a[0] + b[0], a[1] + b[1]];
const pick = a => a[fxrand()*a.length|0];
const lerp = (a,b,t) => a+(b-a)*t;
const range = (a,b) => lerp(a,b,fxrand());
let rays = [];
const dieChance = 0.5; //min=0.00, max=1, step=0.01
const totalRays = 9000; //min=100, max=15000, step=100
const size = 190;
const pathInput = `M41,-35C41,-48 17,-54 5,-50C-3,-47 -12,-37 -10,-28C-7,-11 14,-1 30,7C37,10 40,20 42,27C44,34 48,43 45,50C34,73 -8,74 -31,63C-41,58 -42,42 -47,32C-49,28 -44,23 -44,18`; // type=path, Click here to redraw the path
const path = Path(pathInput);
const steps = path.length() | 0;
const maxSize = size/2;
const polygons = [Array.from({length:path.length()}, (_,i) => path.p( i/steps )).map(p => ({x:p[0],y:p[1]})).flat()];
const border = 0; // min=0, max=1, step=1 (No bounce, Bounce)
if (border) polygons.push([
{x:-maxSize,y:-maxSize},
{x:maxSize,y:-maxSize},
{x:maxSize,y:maxSize},
{x:-maxSize,y:maxSize},
{x:-maxSize,y:-maxSize},
]);
const isInRect = (x,y, x1,y1,w,h) => x > x1 && y > y1 && x < x1+w && y < y1+h;
const isOutsideScreen = (x,y) => !isInRect(x,y,-size/2,-size/2,size,size);
const removeRay = (ray) => rays = rays.filter(b => b !== ray);
const variation = 0.15;//min=0.00, max=0.25, step=0.001
const speed = 1; //min=0.1, max=3, step=0.1
// init
for (let i = 0; i < totalRays; i++) {
let t = i/totalRays;
let s = size/2 * range(0,0.65);
let x = range(-s,s);
let y = range(-s,s);
rays.push({
x: x,
y: y,
dx: Math.sin(Math.PI*2*t) * speed,
dy: Math.cos(Math.PI*2*t) * speed,
// distanceMoved: 0,
});
}
function walk(i) {
let ray = rays[i % rays.length];
if (ray) {
let oldPos = {x: ray.x, y: ray.y};
ray.x += ray.dx;
ray.y += ray.dy;
let newPos = {x: ray.x, y: ray.y};
for(let polygon of polygons) {
for(let i = 0; i < polygon.length-1; i++) {
let start = polygon[i];
let end = polygon[(i + 1) % polygon.length];
let collision = lineToLine(oldPos, newPos, start, end);
if (collision) {
if (fxrand() > dieChance) {
removeRay(ray);
}
// chance to die
let normal = {x: -(end.y - start.y), y: end.x - start.x};
let length = Math.sqrt(normal.x * normal.x + normal.y * normal.y);
normal.x /= length;
normal.y /= length;
// vary impact angle a bit
normal.x += range(-variation,variation);
normal.y += range(-variation,variation);
let dot = ray.dx * normal.x + ray.dy * normal.y;
ray.dx -= 2 * dot * normal.x;
ray.dy -= 2 * dot * normal.y;
ray.x = oldPos.x + ray.dx * collision;
ray.y = oldPos.y + ray.dy * collision;
}
}
}
ray.oldPos = oldPos;
if (isOutsideScreen(ray.x, ray.y)) removeRay(ray);
if (ray.oldPos) {
turtle.jump(ray.oldPos.x,ray.oldPos.y);
turtle.goto(ray.x,ray.y);
}
}
return rays.length > 1;
}
function lineToLine(p1,p2, p3,p4) {
let det = (p2.x - p1.x) * (p4.y - p3.y) - (p2.y - p1.y) * (p4.x - p3.x);
if (det === 0) return false; // Lines are parallel
let lambda = ((p4.y - p3.y) * (p4.x - p1.x) + (p3.x - p4.x) * (p4.y - p1.y)) / det;
let gamma = ((p1.y - p2.y) * (p4.x - p1.x) + (p2.x - p1.x) * (p4.y - p1.y)) / det;
return (0 < lambda && lambda < 1) && (0 < gamma && gamma < 1);
}
////////////////////////////////////////////////////////////////
// Path utility code. Created by Reinder Nijhoff 2023
// Parses a single SVG path (only M, C and L statements are
// supported). The p-method will return
// [...position, ...derivative] for a normalized point t.
//
// https://turtletoy.net/turtle/46adb0ad70
////////////////////////////////////////////////////////////////
function Path(svg) {
class MoveTo {
constructor(p) { this.p0 = p; }
p(t, s) { return [...this.p0, 1, 0]; }
length() { return 0; }
}
class LineTo {
constructor(p0, p1) { this.p0 = p0, this.p1 = p1; }
p(t, s = 1) {
const nt = 1 - t, p0 = this.p0, p1 = this.p1;
return [
nt*p0[0] + t*p1[0],
nt*p0[1] + t*p1[1],
(p1[0] - p0[0]) * s,
(p1[1] - p0[1]) * s,
];
}
length() {
const p0 = this.p0, p1 = this.p1;
return Math.hypot(p0[0]-p1[0], p0[1]-p1[1]);
}
}
class BezierTo {
constructor(p0, c0, c1, p1) { this.p0 = p0, this.c0 = c0, this.c1 = c1, this.p1 = p1; }
p(t, s = 1) {
const nt = 1 - t, p0 = this.p0, c0 = this.c0, c1 = this.c1, p1 = this.p1;
return [
nt*nt*nt*p0[0] + 3*t*nt*nt*c0[0] + 3*t*t*nt*c1[0] + t*t*t*p1[0],
nt*nt*nt*p0[1] + 3*t*nt*nt*c0[1] + 3*t*t*nt*c1[1] + t*t*t*p1[1],
(3*nt*nt*(c0[0]-p0[0]) + 6*t*nt*(c1[0]-c0[0]) + 3*t*t*(p1[0]-c1[0])) * s,
(3*nt*nt*(c0[1]-p0[1]) + 6*t*nt*(c1[1]-c0[1]) + 3*t*t*(p1[1]-c1[1])) * s,
];
}
length() {
return this._length || (
this._length = Array.from({length:25}, (x, i) => this.p(i/25)).reduce(
(a,c,i,v) => i > 0 ? a + Math.hypot(c[0]-v[i-1][0], c[1]-v[i-1][1]) : a, 0));
}
}
class Path {
constructor(svg) {
this.segments = [];
this.parsePath(svg);
}
parsePath(svg) {
const t = svg.match(/([0-9.-]+|[MLC])/g);
for (let s, i=0; i<t.length;) {
switch (t[i++]) {
case 'M': this.add(new MoveTo(s=[t[i++],t[i++]]));
break;
case 'L': this.add(new LineTo(s, s=[t[i++],t[i++]]));
break;
case 'C': this.add(new BezierTo(s, [t[i++],t[i++]], [t[i++],t[i++]], s=[t[i++],t[i++]]));
break;
default: i++;
}
}
}
add(segment) {
this.segments.push(segment);
this._length = 0;
}
length() {
return this._length || (this._length = this.segments.reduce((a,c) => a + c.length(), 0));
}
p(t) {
t = Math.max(Math.min(t, 1), 0) * this.length();
for (let l=0, i=0, sl=0; i<this.segments.length; i++, l+=sl) {
sl = this.segments[i].length();
if (t > l && t <= l + sl) {
return this.segments[i].p((t-l)/sl, sl/this.length());
}
}
return this.segments[Math.min(1, this.segments.length-1)].p(0);
}
}
return new Path(svg);
}