Ported from The Art Of Code's great tutorial: youtu.be/nd7auhb9yn8
Export it to GIF for the animated version (best use a low precision).
Log in to post a comment.
// LL 2021
const turtle = new Turtle();
//Canvas.setpenopacity(-1);
const density = 71; // min=1, max=301, step=2
const precision = 1.0; // min=0.1, max=3, step=0.1
const passes = 2; /// min=1, max=3, step=1 (X, XY, XYZ)
const cam_dist = 1.7; /// min=0.1, max=10.5, step=0.01
const cam_angle = 1.4; /// min=-3.14159, max=3.15159, step=0.01
const iTime = 0.2; // min=0 max=1 step=0.01
var iTime_t = iTime;
const MAX_STEPS = 100;
const MAX_DIST = 100;
const SURF_DIST = .001;
const range = [ -1.6, 1.6 ];
function map(p3)
{
var d = MAX_DIST;
d = Math.min(d, sdCradle(p3));
return d;
}
// Newton's Cradle - https://www.youtube.com/watch?v=nd7Auhb9YN8
function sdCradle(p3)
{
const base = sdBox3(p3, [1,.1,.5]) - .1;
const bar = length2( [sdBox2([p3[0],p3[1]], [.8,1.4])-.15, Math.abs(p3[2])-.4] )-.04;
const a = Math.sin(iTime_t * Math.PI * 2);
const a1 = Math.min(0., a);
const a5 = Math.max(0., a);
const b1 = sdBall(sub3(p3,[ .6,.5,0]), a1);
const b2 = sdBall(sub3(p3,[ .3,.5,0]), (a+a1)*.05);
const b3 = sdBall(sub3(p3,[ 0,.5,0]), a*.05);
const b4 = sdBall(sub3(p3,[-.3,.5,0]), (a+a5)*.05);
const b5 = sdBall(sub3(p3,[-.6,.5,0]), a5);
const balls = Math.min(b1, Math.min(b2, Math.min(b3, Math.min(b4, b5))));
var d = base;
d = Math.min(d, bar);
d = Math.min(d, balls);
d = Math.max(d, -p3[1]);
return d;
}
// Cradle ball
function sdBall(p3, a)
{
p3[1] -= 1.01;
const cp2 = mul_mat2([p3[0], p3[1]], rotation_mat22(a));
p3[0] = cp2[0];
p3[1] = cp2[1];
p3[1] += 1.01;
var d = length3(p3) - .15;
const ring = length2([length2([p3[0], p3[2]-.15])-.03, p3[2]]) - .01;
p3[2] = Math.abs(p3[2]);
const line = sdLineSeg(p3, [0,.15,0], [0, 1.01, .4]) - .005;
d = Math.min(d, ring);
d = Math.min(d, line);
return d;
}
// Segment
function sdLineSeg(p3, a3, b3) {
const ap3 = sub3(p3, a3);
const ab3 = sub3(b3, a3);
const t = clamp(dot3(ap3, ab3)/dot3(ab3, ab3), 0., 1.);
const c3 = add3(a3, mul3(ab3, t));
return length3(sub3(p3,c3));
}
// Box 2D
function sdBox2(p2, s2)
{
return sdBox3([p2[0], p2[1], 0], [s2[0], s2[1], 0]);
}
// Box 3D
function sdBox3(p3, s3)
{
p3 = sub3(abs3(p3), s3);
var r = Math.min(Math.max(p3[0], Math.max(p3[1], p3[2])), 0);
var q3 = [Math.max(p3[0], 0), Math.max(p3[1], 0), Math.max(p3[2], 0)];
q3 = add3(q3, [r,r,r]);
return length3(q3);
}
function RayMarch(ro3, rd3)
{
var dO = 0;
for (var i = 0; i < MAX_STEPS; i++)
{
var p3 = add3(ro3, mul3(rd3, dO));
var dS = map(p3);
dO += dS;
if (dO > MAX_DIST) return MAX_DIST;
if (Math.abs(dS)<SURF_DIST) break;
}
return dO;
}
function GetRayDir(uv2, p3, l3, z)
{
var f3 = normalize3(sub3(l3, p3));
var r3 = normalize3(cross3([0,1,0], f3));
var u3 = cross3(f3, r3);
var c3 = mul3(f3, z);
var i3 = add3(add3(c3, mul3(r3, uv2[0])), mul3(u3, uv2[1]));
var d3 = normalize3(i3);
return d3;
}
var pass = -1;
function cache_key(p2) { return p2[0] * 10000 + p2[1] * 10; }
var cache = {};
var cache_tests = 0;
var cache_misses = 0;
function floor2(x) { return x; }
//function floor2(x) { const precision = 1; return Math.round(x / precision) * precision; }
function floor_p2(p2)
{
return [ floor2(p2[0]), floor2(p2[1]) ];
}
// p: 2D point in -100 to 100 range
function zFunc(p2)
{
p2 = floor_p2(p2);
// Ray origin
var ro3 = [ cam_dist * Math.cos(cam_angle), 1, cam_dist * Math.sin(cam_angle) ];
// Look at
var l3 = [ 0, 0.75, 0 ];
// Convert to -1 to 1
var uv2 = [ p2[0] / 100, -p2[1] / 100 ];
// Ray direction
var rd3 = GetRayDir(uv2, ro3, l3, 1.);
var key_p2 = cache_key(p2);
var dist = MAX_DIST;
cache_tests++;
if (key_p2 in cache)
{
dist = cache[key_p2];
}
else
{
cache_misses++;
// Get distance to intersection
dist = RayMarch(ro3, rd3);
cache[key_p2] = dist;
}
if (dist < MAX_DIST)
{
// Get intersection point
var p3 = add3(ro3, mul3(rd3, dist));
dist = (p3[(pass+2)%3]-range[0]) / (range[1]-range[0]) * density;
}
return dist;
}
//function floor(x) { const precision = 0.25; return Math.round(x / precision) * precision; }
function floor(x) { return x; }
function line_key(line)
{
return Math.min(line[0][0], line[1][0]) * 1000000000 + Math.min(line[0][1], line[1][1]) * 1000000 + Math.max(line[0][0], line[1][0]) * 1000 + Math.max(line[0][1], line[1][1]);
}
var line_cache = {};
function is_unique(line)
{
const key = line_key(line);
if (key in line_cache) return false
line_cache[key] = 1;
return true;
}
function floor_line(line)
{
return [ [ floor(line[0][0]), floor(line[0][1]) ], [ floor(line[1][0]), floor(line[1][1]) ] ];
}
var unique_line_count = 0;
var total_line_count = 0;
var start_time = performance.now();
function walk(i, t) {
if (i==0) {
cache = {};
line_cache = {};
iTime_t = iTime + t;
}
pass = Math.floor(i / (density+1));
if (pass >= passes)
{
const elapsed = ((performance.now() - start_time) / 1000).toFixed(1);
const percent1 = (unique_line_count * 100 / total_line_count).toFixed(1);
const percent2 = (cache_misses * 100 / cache_tests).toFixed(1);
console.log(`Time: ${elapsed} s | Unique lines: ${unique_line_count} of ${total_line_count} (${percent1}%) | Cache misses: ${cache_misses} of ${cache_tests} (${percent2}%)`)
return false;
}
const lines = ContourLines(i%density, 1/precision, zFunc);
//const lines = ContourLines(i%density, 1/precision, zFunc);
var last_end = [0, 0];
lines.forEach(line => {
total_line_count++;
// TODO: lines are in the wrong order. Find continous paths.
var line2 = floor_line(line);
if (is_unique(line2))
{
if (last_end[0] == line[1][0] && last_end[1] == line[1][1])
{
turtle.goto(line2[1]);
turtle.goto(line2[0]);
last_end = line[0];
}
else if (last_end[0] == line[0][0] && last_end[1] == line[0][1])
{
turtle.goto(line2[0]);
turtle.goto(line2[1]);
last_end = line[1];
}
else
{
turtle.jump(line2[1]);
turtle.goto(line2[0]);
last_end = line[0];
}
unique_line_count++;
}
});
return true;
}
function length2(v2) { return Math.sqrt(v2[0]*v2[0] + v2[1]*v2[1]); }
function length3(v3) { return Math.sqrt(v3[0]*v3[0] + v3[1]*v3[1] + v3[2]*v3[2]); }
function normalize3(v3) { var l = length3(v3); if (l<0.00001) l=1; return [v3[0]/l, v3[1]/l, v3[2]/l]; }
function mul2(a2, f) { return [a2[0]*f, a2[1]*f]; }
function mul3(a3, f) { return [a3[0]*f, a3[1]*f, a3[2]*f]; }
function add2(a2, b2) { return [a2[0]+b2[0], a2[1]+b2[1]]; }
function add3(a3, b3) { return [a3[0]+b3[0], a3[1]+b3[1], a3[2]+b3[2]]; }
function sub2(a2, b2) { return [a2[0]-b2[0], a2[1]-b2[1]]; }
function sub3(a3, b3) { return [a3[0]-b3[0], a3[1]-b3[1], a3[2]-b3[2]]; }
function cross3(a3, b3) { return [ a3[1] * b3[2] - a3[2] * b3[1], a3[2] * b3[0] - a3[0] * b3[2], a3[0] * b3[1] - a3[1] * b3[0] ]; }
function fract3(v3) { return [ v3[0]-Math.floor(v3[0]), v3[1]-Math.floor(v3[1]), v3[2]-Math.floor(v3[2]) ]; }
function floor3(v3) { return [ Math.floor(v3[0]), Math.floor(v3[1]), Math.floor(v3[2]) ]; }
function abs2(v2) { return [ Math.abs(v2[0]), Math.abs(v2[1]) ]; }
function abs3(v3) { return [ Math.abs(v3[0]), Math.abs(v3[1]), Math.abs(v3[2]) ]; }
function dot3(a3, b3) { return a3[0] * b3[0] + a3[1] * b3[1] + a3[2] * b3[2]; }
function mul_mat2(v2, m22) { return [ v2[0] * m22[0] + v2[1] * m22[1], v2[0] * m22[2] + v2[1] * m22[3] ]; }
function clamp(x, min, max) { return Math.min(max, Math.max(min, x)); }
function smoothstep(edge0, edge1, x) { x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); return x * x * (3 - 2 * x); }
function tri3(x3) { return abs3(sub3(fract3(x3), .5)); }
function mix(x, y, a) { return x * (1-a) + y * a; }
function mix3(a3, b3, f) { return [ mix(a3[0], b3[0], f), mix(a3[1], b3[1], f), mix(a3[2], b3[2], f) ]; }
function smin(a, b , s) { var h = clamp( 0.5 + 0.5*(b-a)/s, 0. , 1.); return mix(b, a, h) - h*(1.0-h)*s; }
function rotation_mat22(a)
{
var s = Math.sin(a);
var c = Math.cos(a);
return [c, -s, s, c];
}
// Metaball Contour Lines. Created by Reinder Nijhoff 2020 - @reindernijhoff
// The MIT License
// https://turtletoy.net/turtle/104c4775c5
function ContourLines(z, step, zFunc) {
const intersectSegmentZ = (z, v1, v2) => {
if (v1[2] === v2[2]) return false;
const t = (z - v1[2]) / (v2[2] - v1[2]);
if (t <= 0 || t > 1) return false;
return [v1[0]+(v2[0]-v1[0])*t, v1[1]+(v2[1]-v1[1])*t];
}
const intersectTriangleZ = (z, p1, p2, p3) => {
const p = [];
const v1 = intersectSegmentZ(z, p1, p2);
const v2 = intersectSegmentZ(z, p2, p3);
const v3 = intersectSegmentZ(z, p3, p1);
if (v1 && v2) p.push([v1, v2]);
if (v1 && v3) p.push([v1, v3]);
if (v2 && v3) p.push([v2, v3]);
return p;
}
const result = [];
for (let x = -100; x <= 100; x += step) {
for (let y = -100; y <= 100; y += step) {
const corners = [[x, y], [x+step, y], [x+step, y+step], [x, y+step]];
corners.forEach( c => c[2] = zFunc(c) );
const c3 = [x+step/2, y+step/2, zFunc([x+step/2, y+step/2])];
for (let i=0; i<4; i++) {
result.push(...intersectTriangleZ(z, corners[i], corners[(i+1) & 3], c3));
}
}
}
return result;
}