Light rays bouncing around.
#path #pathtracer
Log in to post a comment.
// Path Path Tracer. Created by Reinder Nijhoff 2025 - @reindernijhoff // // https://turtletoy.net/turtle/01524ccc80 // const pathInput1 = `M-24,-55C50,-86 -34,87 82,-59`; // type=path, Click here to redraw the path const pathInput2 = `M-82,64C-38,40 47,12 -26,84`; // type=path, Click here to redraw the path const pathInput3 = `M-90,-90 L90,-90 L90,90 L-90,90 L-90,-90`; // type=path, Click here to redraw the path const absorption = 0.25; // min=0, max=1, step=0.0001 const smoothness = 1; // min=0, max=1, step=0.0001 const lightSpread = 0; // min=0, max=30, step=0.0001 const opacity = -0.05; // min=-0.1, max=0.1, step=0.00001 const rays = 3500; // min=1, max=10000, step=1 const maxRecursion = 8; // min=0, max=30, step=1 Canvas.setpenopacity(opacity); const path1 = Path(pathInput1); const path2 = Path(pathInput2); const path3 = Path(pathInput3); const turtle = new Turtle(); function intersect(s, e) { const paths = [path1, path2, path3]; return paths.reduce((closest, currentPath) => { const p = currentPath.intersect(s, e); if (p && (!closest || dist_sqr(s, p) < dist_sqr(s, closest))) { return p; } return closest; }, false); } function trace(s, e, r=0) { const rd = normalize(sub(e, s)); let p = intersect(s, e); turtle.jump(s); turtle.goto(p || e); if (p && r < maxRecursion && Math.random() > absorption) { let n = normalize([-p[3], p[2]]); const d = dot(n, rd); if (d > 0) { n = scale(n, -1); } if (0.01 < Math.abs(d)) { const r0 = Math.sqrt(Math.random()); const diff = normalize(add(scale(n, r0), scale([-n[1], n[0]], Math.sign(Math.random()-.5) * Math.sqrt(Math.abs(1-r0*r0))))); const refl = normalize(reflect(rd, n)); const d = lerp(diff, refl, smoothness); trace(add(p, scale(d, 0.1)), scale(d, 200), r+1); } } } function walk(i) { const a = i * (3 - Math.sqrt(5))*Math.PI; const la = Math.random()*2*Math.PI, lr = Math.random()**2 * lightSpread; trace([Math.cos(la)*lr, Math.sin(la)*lr], [Math.cos(a)*200, Math.sin(a)*200]); return i < rays-1; } // // 2D Vector math // function cross(a, b) { return a[0]*b[1]-a[1]*b[0]; } function equal(a,b) { return .001>dist_sqr(a,b); } function scale(a,b) { return [a[0]*b,a[1]*b]; } function add(a,b) { return [a[0]+b[0],a[1]+b[1]]; } function sub(a,b) { return [a[0]-b[0],a[1]-b[1]]; } function dot(a,b) { return a[0]*b[0]+a[1]*b[1]; } function dist_sqr(a,b) { return (a[0]-b[0])**2+(a[1]-b[1])**2; } function dist(a,b) { return Math.sqrt(dist_sqr(a,b)); } function length(a) { return Math.sqrt(dot(a,a)); } function normalize(a) { return scale(a, 1/length(a)); } function transform(a, mat) { return [a[0] * mat[0] + a[1] * mat[2], a[0] * mat[1] + a[1] * mat[3]]; } function reflect(rd, n) { return sub(rd, scale(n, 2 * dot(rd, n))); } function lerp(a, b, t) { return add(a, scale(sub(b, a), t)); } //port of http://paulbourke.net/geometry/pointlineplane/Helpers.cs function segment_intersect(l1p1, l1p2, l2p1, l2p2) { const d = (l2p2[1] - l2p1[1]) * (l1p2[0] - l1p1[0]) - (l2p2[0] - l2p1[0]) * (l1p2[1] - l1p1[1]); if (d === 0) return false; const n_a = (l2p2[0] - l2p1[0]) * (l1p1[1] - l2p1[1]) - (l2p2[1] - l2p1[1]) * (l1p1[0] - l2p1[0]); const n_b = (l1p2[0] - l1p1[0]) * (l1p1[1] - l2p1[1]) - (l1p2[1] - l1p1[1]) * (l1p1[0] - l2p1[0]); const ua = n_a / d; const ub = n_b / d; if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) { return [l1p1[0] + ua * (l1p2[0] - l1p1[0]), l1p1[1] + ua * (l1p2[1] - l1p1[1]), ub]; } return false; } //////////////////////////////////////////////////////////////// // Modified path utility code. Created by Reinder Nijhoff 2025 // // Added a distance method // // 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; } intersect(p0, p1) { return false; } } 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]); } intersect(p0, p1) { const p = segment_intersect(p0, p1, this.p0, this.p1), hw = this.p(.5); return p ? [p[0], p[1], hw[2], hw[3], dist_sqr(p0, p)] : false; } } 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)); } intersect(p0, p1) { // todo, analytical solution let b0 = this.p(0); const steps = 100; return Array.from({length:steps}, (_,i)=>i+1).reduce((a,c) => { const b1 = this.p(c/steps); const p = segment_intersect(p0, p1, b0, b1); b0 = [...b1]; if (p && (!a || dist_sqr(p0, p) < dist_sqr(p0, a))) { const normal = this.p(c/steps + p[2]/steps) return [p[0], p[1], normal[2], normal[3], dist_sqr(p0, p)]; } else { return a; } }, false); } } 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); } intersect(p0, p1) { return this.segments.reduce((a,c) => { const p = c && c.intersect(p0, p1); if (p && (!a || dist_sqr(p0, p) < dist_sqr(p0, a))) { return [...p, dist_sqr(p0, p)]; } else { return a; } }, false); } } return new Path(svg); }