Draw a path for a fireworks trail.
Or maybe palms? Or dandelions?
Log in to post a comment.
// Forked from "Path input type" by reinder
// https://turtletoy.net/turtle/46adb0ad70
Canvas.setpenopacity(-0.35);
const numTrails = 500; //min=5,max=1000,step=1
const maxTrailLength = 70; //min=1,max=100,step=0.1
const force = 10; //min=0,max=20,step=0.1
const gravity = 1.2; //min=-1,max=4,step=0.1
const inertia = 0.02; //min=0,max=0.2,step=0.001
const pathInput = `M-60,90 C-60,59 -38,3 -28,-28`; // type=path
const path = Path(pathInput);
const turtle = new Turtle();
turtle.jump(path.p(0));
function walk(i) {
const steps = path.length() | 0;
const pos = path.p( i/steps );
turtle.goto(pos);
if (i == steps - 1) {
circle(turtle, force**1.2);
spawnBurst([pos[0], pos[1]], [pos[2], pos[3]]);
}
return i < steps;
}
let toggle = 0;
function spawnBurst(point, derivative) {
const t = new Turtle();
for (let b = 0; b < numTrails; b++) {
t.jump(point);
const l = maxTrailLength * (1.0 - Math.random() * 0.5);
const a = Math.random() * 360;
let p = point;
let d = l;
let v = scale([Math.cos(a), Math.sin(a)], force);
v = add(v, scale(derivative, inertia));
while (d > 0) {
const vl = length(v);
d -= vl;
if (d < 0) v = scale(v, (-d/vl));
p = add(p, v);
t.goto(p);
v[1] += gravity;
}
circle(t, 0.2);
}
}
////////////////////////////////////////////////////////////////
// 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);
}
function circle(turtle, radius) {
const pos = turtle.pos();
turtle.penup();
turtle.sety(turtle.ycor() - radius);
turtle.pendown();
turtle.circle(radius);
turtle.jump(pos);
}
// vec2 functions
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])*(a[0]-b[0])+(a[1]-b[1])*(a[1]-b[1]); }
function dist(a,b) { return Math.sqrt(dist_sqr(a,b)); }
function length(a) { return Math.sqrt(dot(a,a)); }
function segment_intersect(a,b,d,c) {
const e=(c[1]-d[1])*(b[0]-a[0])-(c[0]-d[0])*(b[1]-a[1]);
if(0==e)return false;
c=((c[0]-d[0])*(a[1]-d[1])-(c[1]-d[1])*(a[0]-d[0]))/e;
d=((b[0]-a[0])*(a[1]-d[1])-(b[1]-a[1])*(a[0]-d[0]))/e;
return 0<=c&&1>=c&&0<=d&&1>=d?[a[0]+c*(b[0]-a[0]),a[1]+c*(b[1]-a[1])]:false;
}