spine twist
spine
Log in to post a comment.
const spineLength = 175; // min=100,max=220,step=1
const spineSamples = 140; // min=60,max=220,step=1
const curveAmp1 = 18; // min=0,max=60,step=1
const curveFreq1 = 1.4; // min=0.2,max=4,step=0.1
const curveAmp2 = 10; // min=0,max=40,step=1
const curveFreq2 = 3.2; // min=0.2,max=8,step=0.1
const spineDrift = 0; // min=-30,max=30,step=1
const cageCount = 34; // min=8,max=80,step=1
const cageLayers = 12; // min=3,max=30,step=1
const cageWidth = 34; // min=8,max=70,step=1
const cageHeight = 13; // min=4,max=30,step=1
const cageWave = 6; // min=0,max=20,step=1
const twistTurns = 2.6; // min=0,max=8,step=0.1
const twistAmount = 0.9; // min=0,max=2.5,step=0.05
const taper = 0.42; // min=0,max=0.9,step=0.02
const strandCount = 16; // min=4,max=40,step=1
const strandSamples = 140; // min=40,max=240,step=1
const fieldLines = 90; // min=10,max=180,step=1
const fieldSteps = 95; // min=20,max=200,step=1
const fieldStepSize = 2.3; // min=0.5,max=5,step=0.1
const fieldSpread = 78; // min=20,max=130,step=1
const fieldPull = 0.95; // min=0,max=3,step=0.05
const fieldSwirl = 1.05; // min=0,max=3,step=0.05
const fieldDriftX = 0.0; // min=-1,max=1,step=0.05
const fieldDriftY = 0.15; // min=-1,max=1,step=0.05
const closestSamples = 100; // min=20,max=180,step=1
const border = 115; // min=90,max=140,step=1
Canvas.setpenopacity(0.65);
const turtle = new Turtle();
function clamp(v, a, b) {
return Math.max(a, Math.min(b, v));
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function vec(x, y) {
return { x, y };
}
function add(a, b) {
return { x: a.x + b.x, y: a.y + b.y };
}
function sub(a, b) {
return { x: a.x - b.x, y: a.y - b.y };
}
function mul(a, s) {
return { x: a.x * s, y: a.y * s };
}
function dot(a, b) {
return a.x * b.x + a.y * b.y;
}
function len(a) {
return Math.sqrt(a.x * a.x + a.y * a.y);
}
function norm(a) {
const l = len(a);
if (l < 1e-9) return { x: 0, y: 0 };
return { x: a.x / l, y: a.y / l };
}
function perp(a) {
return { x: -a.y, y: a.x };
}
function rotateBasis(n, t, ang) {
const c = Math.cos(ang);
const s = Math.sin(ang);
const bx = add(mul(n, c), mul(t, s)); // local x axis
const by = add(mul(t, c), mul(n, -s)); // local y axis
return { bx, by };
}
function polyline(points) {
if (!points || points.length < 2) return;
turtle.penup();
turtle.goto(points[0].x, points[0].y);
turtle.pendown();
for (let i = 1; i < points.length; i++) {
turtle.goto(points[i].x, points[i].y);
}
turtle.penup();
}
function inBounds(p) {
return (
p.x > -border && p.x < border &&
p.y > -border && p.y < border
);
}
function spinePoint(t) {
const y = lerp(-spineLength / 2, spineLength / 2, t);
let x =
curveAmp1 * Math.sin(Math.PI * 2 * curveFreq1 * t) +
curveAmp2 * Math.sin(Math.PI * 2 * curveFreq2 * t + 0.9);
x += spineDrift * (t - 0.5);
return vec(x, y);
}
function spineTangent(t) {
const e = 0.002;
const t0 = clamp(t - e, 0, 1);
const t1 = clamp(t + e, 0, 1);
const p0 = spinePoint(t0);
const p1 = spinePoint(t1);
return norm(sub(p1, p0));
}
function closestOnSpine(p) {
let bestT = 0;
let bestP = spinePoint(0);
let bestD2 = 1e18;
for (let i = 0; i <= closestSamples; i++) {
const t = i / closestSamples;
const s = spinePoint(t);
const dx = p.x - s.x;
const dy = p.y - s.y;
const d2 = dx * dx + dy * dy;
if (d2 < bestD2) {
bestD2 = d2;
bestT = t;
bestP = s;
}
}
const tan = spineTangent(bestT);
const nor = perp(tan);
const rel = sub(p, bestP);
const offset = dot(rel, nor);
return {
t: bestT,
p: bestP,
tan: tan,
nor: nor,
d2: bestD2,
offset: offset
};
}
function localSection(u, width, height, wave, phase) {
// u in [-1, 1]
let x = Math.sin(Math.PI * u) * width;
// pinch around center to feel more vertebra-like
const pinch = 1 - 0.38 * Math.exp(-7 * u * u);
x *= pinch;
let y = u * height;
y += Math.sin(Math.PI * 2 * u + phase) * wave;
return vec(x, y);
}
function drawCageRibs() {
for (let i = 0; i < cageCount; i++) {
const t = i / (cageCount - 1);
const c = spinePoint(t);
const tan = spineTangent(t);
const nor = perp(tan);
const midBoost = 0.62 + 0.38 * Math.sin(Math.PI * t);
const endTaper = 1 - taper * Math.abs(t - 0.5) * 2;
const w0 = cageWidth * midBoost * endTaper;
const h0 = cageHeight * (0.8 + 0.2 * Math.sin(Math.PI * t));
const phase = Math.PI * 2 * twistTurns * t;
const ang = twistAmount * Math.sin(phase);
const basis = rotateBasis(nor, tan, ang);
for (let layer = 0; layer < cageLayers; layer++) {
const v = (cageLayers <= 1) ? 0 : layer / (cageLayers - 1);
const centered = lerp(-1, 1, v);
const width = w0 * (1 - 0.52 * v);
const height = h0 * (1 + 0.18 * centered);
const wave = cageWave * (1 - 0.65 * v);
const pts = [];
const steps = 60;
for (let s = 0; s <= steps; s++) {
const u = lerp(-1, 1, s / steps);
const local = localSection(u, width, height, wave, phase + centered * 0.8);
// slight layer shift along tangent to get stacked vertebra feel
const shifted = add(local, vec(0, centered * 2.0));
const world = add(
c,
add(
mul(basis.bx, shifted.x),
mul(basis.by, shifted.y)
)
);
pts.push(world);
}
polyline(pts);
}
}
}
function drawSpineStrands() {
for (let k = 0; k < strandCount; k++) {
const band = (strandCount <= 1) ? 0 : k / (strandCount - 1);
const uBase = lerp(-0.92, 0.92, band);
const pts = [];
for (let i = 0; i <= strandSamples; i++) {
const t = i / strandSamples;
const c = spinePoint(t);
const tan = spineTangent(t);
const nor = perp(tan);
const midBoost = 0.62 + 0.38 * Math.sin(Math.PI * t);
const endTaper = 1 - taper * Math.abs(t - 0.5) * 2;
const width = cageWidth * midBoost * endTaper * (0.72 + 0.28 * Math.cos(uBase * Math.PI));
const height = cageHeight * (0.9 + 0.1 * Math.sin(Math.PI * t));
const phase = Math.PI * 2 * twistTurns * t;
const ang = twistAmount * Math.sin(phase);
const basis = rotateBasis(nor, tan, ang);
const u = uBase;
const local = localSection(u, width, height, cageWave * 0.55, phase + uBase * 1.4);
const world = add(
c,
add(
mul(basis.bx, local.x),
mul(basis.by, local.y)
)
);
pts.push(world);
}
polyline(pts);
}
}
function fieldDirection(p, sign) {
const hit = closestOnSpine(p);
const d = Math.sqrt(hit.d2) + 1e-6;
// stronger near the spine
const near = Math.exp(-d / fieldSpread);
// pull toward spine centerline
const pull = (-hit.offset / (fieldSpread * 0.5)) * fieldPull * near;
// swirl around it
const swirlPhase = Math.PI * 2 * twistTurns * hit.t + d * 0.08;
const swirl = Math.sign(hit.offset || 1) * fieldSwirl * (0.35 + 0.65 * near) * Math.sin(swirlPhase);
let dir = vec(0, 0);
dir = add(dir, mul(hit.tan, sign * 1.0));
dir = add(dir, mul(hit.nor, pull + swirl));
dir = add(dir, vec(fieldDriftX, fieldDriftY));
return norm(dir);
}
function traceField(seed) {
const forward = [seed];
let p = vec(seed.x, seed.y);
for (let i = 0; i < fieldSteps; i++) {
const d = fieldDirection(p, 1);
p = add(p, mul(d, fieldStepSize));
if (!inBounds(p)) break;
forward.push(p);
}
const backward = [];
p = vec(seed.x, seed.y);
for (let i = 0; i < fieldSteps; i++) {
const d = fieldDirection(p, -1);
p = add(p, mul(d, fieldStepSize));
if (!inBounds(p)) break;
backward.push(p);
}
backward.reverse();
return backward.concat(forward);
}
function drawField() {
const half = Math.floor(fieldLines / 2);
for (let i = 0; i < fieldLines; i++) {
const side = (i % 2 === 0) ? -1 : 1;
const j = Math.floor(i / 2);
const t = (half <= 1) ? 0.5 : j / (half - 1);
const y = lerp(-spineLength / 2 - 18, spineLength / 2 + 18, t);
// seed lines further away, but not uniformly
const spread = lerp(fieldSpread * 0.45, fieldSpread, (Math.sin(t * 11.7) * 0.5 + 0.5));
const x = side * spread;
const seed = vec(x, y);
const pts = traceField(seed);
polyline(pts);
}
}
function drawCenterSpine() {
const pts = [];
for (let i = 0; i <= spineSamples; i++) {
pts.push(spinePoint(i / spineSamples));
}
polyline(pts);
}
drawField();
drawCageRibs();
drawSpineStrands();
drawCenterSpine();