From 864e76098af8e2dbef781ffbf1b3f4c5f1feca3c Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Sun, 23 Aug 2026 12:56:34 -0600 Subject: [PATCH] Smooth washboard section: half-sine arches instead of spiky zig-zag --- src/editor/sections.js | 53 +++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/editor/sections.js b/src/editor/sections.js index 81a3e95..09b09fe 100644 --- a/src/editor/sections.js +++ b/src/editor/sections.js @@ -38,14 +38,17 @@ const RISE_SHARP = 160; const AMP_HILLS = 45; const DEPTH_DITCH = 130; -// Washboard: a repeating corrugated-bump wave (like washboard/"bumpy road" -// track in Trials-style games) over a flat or sloped baseline. The bump -// count is derived from the section's width (target WASH_PERIOD base units -// per bump cycle) so the bumps keep a roughly constant width when the user -// stretches/contracts a section instead of every bump stretching with it, -// and the floor keeps the pattern visible even at the editor's minimum -// section width. Amplitude, and for the up/down variants the overall rise -// too, scale with vScale like every other section's rise/amplitude/depth. +// Washboard: a repeating run of smooth arches (half-sine humps, like a +// corrugated/washboard road in Trials-style games) over a flat or sloped +// baseline. The arch count is derived from the section's width (target +// WASH_PERIOD base units per arch) so the arches keep a roughly constant +// width when the user stretches/contracts a section instead of every arch +// stretching with it, and the floor keeps the pattern visible even at the +// editor's minimum section width. A half-sine arch's steepest tangent is +// only PI*amplitude/period, so at these numbers each hump reads as a +// gentle rounded arch (~40 degrees at its steepest) rather than a spiky +// wave. Amplitude, and for the up/down variants the overall rise too, +// scale with vScale like every other section's rise/amplitude/depth. const WASH_AMPLITUDE = 26; const WASH_PERIOD = 100; const WASH_RISE = 120; @@ -54,11 +57,13 @@ function washBumps(width) { return Math.max(4, Math.round(width / WASH_PERIOD)); } -// 4 samples per bump cycle: enough that each crest and trough lands exactly -// on a sample (a washboard tooth made of a couple of straight segments -// reads as a bumpy road rather than a sawtooth spike), while the default -// 700-wide section stays a modest 29 points. -const WASH_SAMPLES_PER_BUMP = 4; +// Samples per arch. A half-sine arch only reads as a smooth arch once it's +// sampled densely - at 4 samples per arch the points (foot, crest, foot, +// midline) joined into a zig-zag diamond that looked like sharp spikes +// instead of rounded arches, so this is 12: every arch is a run of short +// segments that reads as one smooth hump, while the default 700-wide +// section stays a modest ~85 points. +const WASH_SAMPLES_PER_BUMP = 12; const JUMP_TAKEOFF_FRACTION = 0.45; const JUMP_GAP_FRACTION = 0.25; @@ -103,7 +108,7 @@ function smoothstep(t) { // Samples x from startX to startX+width every `step` (inclusive of both // ends), calling shape(t) for the y-offset at each sample, t in [0,1]. The -// washboard passes a custom step (4 samples per bump, see +// washboard passes a custom step (samples per arch, see // WASH_SAMPLES_PER_BUMP); every other generator keeps the POINT_STEP grid. function sampleBlock(startX, startY, width, shape, step = POINT_STEP) { const points = []; @@ -161,36 +166,36 @@ function ditch(startX, startY, width, vScale) { return { type: 'simple', points, endY: startY }; } -// Washboard (flat): N full sine periods, each one a bump cycle. Sin starts -// and ends at zero displacement, so both ends sit exactly on the neighbor -// sections' baseline (no seam step) while the interior is the uniform -// corrugated profile. +// Washboard (flat): N full half-sine arches (humps). Sin starts and ends +// at zero displacement, so both ends sit exactly on the neighbor sections' +// baseline (no seam step) while the interior is a uniform run of smooth +// humps. function washboard(startX, startY, width, vScale) { const amp = WASH_AMPLITUDE * vScale; const bumps = washBumps(width); - const points = sampleBlock(startX, startY, width, (t) => amp * Math.sin(2 * Math.PI * bumps * t), width / (WASH_SAMPLES_PER_BUMP * bumps)); + const points = sampleBlock(startX, startY, width, (t) => -amp * Math.sin(Math.PI * bumps * t), width / (WASH_SAMPLES_PER_BUMP * bumps)); return { type: 'simple', points, endY: startY }; } -// Washboard Up: the same washboard wave riding on an overall incline. The +// Washboard Up: the same smooth arches riding on an overall incline. The // baseline uses smoothstep (zero slope at both ends) like smoothIncline, so // the section's ends match a flat neighbor's baseline and the climb eases -// in/out instead of ramping at a constant angle under the bumps. +// in/out instead of ramping at a constant angle under the arches. function washboardUp(startX, startY, width, vScale) { const amp = WASH_AMPLITUDE * vScale; const rise = WASH_RISE * vScale; const bumps = washBumps(width); - const points = sampleBlock(startX, startY, width, (t) => -rise * smoothstep(t) + amp * Math.sin(2 * Math.PI * bumps * t), width / (WASH_SAMPLES_PER_BUMP * bumps)); + const points = sampleBlock(startX, startY, width, (t) => -rise * smoothstep(t) - amp * Math.sin(Math.PI * bumps * t), width / (WASH_SAMPLES_PER_BUMP * bumps)); return { type: 'simple', points, endY: startY - rise }; } // Washboard Down: mirror of washboardUp - a smoothstep decline baseline -// with the washboard wave on top. +// with the smooth arches on top. function washboardDown(startX, startY, width, vScale) { const amp = WASH_AMPLITUDE * vScale; const rise = WASH_RISE * vScale; const bumps = washBumps(width); - const points = sampleBlock(startX, startY, width, (t) => rise * smoothstep(t) + amp * Math.sin(2 * Math.PI * bumps * t), width / (WASH_SAMPLES_PER_BUMP * bumps)); + const points = sampleBlock(startX, startY, width, (t) => rise * smoothstep(t) - amp * Math.sin(Math.PI * bumps * t), width / (WASH_SAMPLES_PER_BUMP * bumps)); return { type: 'simple', points, endY: startY + rise }; }