Smooth washboard section: half-sine arches instead of spiky zig-zag

This commit is contained in:
Brian Fertig 2026-08-23 12:56:34 -06:00
parent 3be7b857ed
commit 864e76098a
1 changed files with 29 additions and 24 deletions

View File

@ -38,14 +38,17 @@ const RISE_SHARP = 160;
const AMP_HILLS = 45; const AMP_HILLS = 45;
const DEPTH_DITCH = 130; const DEPTH_DITCH = 130;
// Washboard: a repeating corrugated-bump wave (like washboard/"bumpy road" // Washboard: a repeating run of smooth arches (half-sine humps, like a
// track in Trials-style games) over a flat or sloped baseline. The bump // corrugated/washboard road in Trials-style games) over a flat or sloped
// count is derived from the section's width (target WASH_PERIOD base units // baseline. The arch count is derived from the section's width (target
// per bump cycle) so the bumps keep a roughly constant width when the user // WASH_PERIOD base units per arch) so the arches keep a roughly constant
// stretches/contracts a section instead of every bump stretching with it, // width when the user stretches/contracts a section instead of every arch
// and the floor keeps the pattern visible even at the editor's minimum // stretching with it, and the floor keeps the pattern visible even at the
// section width. Amplitude, and for the up/down variants the overall rise // editor's minimum section width. A half-sine arch's steepest tangent is
// too, scale with vScale like every other section's rise/amplitude/depth. // 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_AMPLITUDE = 26;
const WASH_PERIOD = 100; const WASH_PERIOD = 100;
const WASH_RISE = 120; const WASH_RISE = 120;
@ -54,11 +57,13 @@ function washBumps(width) {
return Math.max(4, Math.round(width / WASH_PERIOD)); return Math.max(4, Math.round(width / WASH_PERIOD));
} }
// 4 samples per bump cycle: enough that each crest and trough lands exactly // Samples per arch. A half-sine arch only reads as a smooth arch once it's
// on a sample (a washboard tooth made of a couple of straight segments // sampled densely - at 4 samples per arch the points (foot, crest, foot,
// reads as a bumpy road rather than a sawtooth spike), while the default // midline) joined into a zig-zag diamond that looked like sharp spikes
// 700-wide section stays a modest 29 points. // instead of rounded arches, so this is 12: every arch is a run of short
const WASH_SAMPLES_PER_BUMP = 4; // 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_TAKEOFF_FRACTION = 0.45;
const JUMP_GAP_FRACTION = 0.25; 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 // 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 // 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. // WASH_SAMPLES_PER_BUMP); every other generator keeps the POINT_STEP grid.
function sampleBlock(startX, startY, width, shape, step = POINT_STEP) { function sampleBlock(startX, startY, width, shape, step = POINT_STEP) {
const points = []; const points = [];
@ -161,36 +166,36 @@ function ditch(startX, startY, width, vScale) {
return { type: 'simple', points, endY: startY }; return { type: 'simple', points, endY: startY };
} }
// Washboard (flat): N full sine periods, each one a bump cycle. Sin starts // Washboard (flat): N full half-sine arches (humps). Sin starts and ends
// and ends at zero displacement, so both ends sit exactly on the neighbor // at zero displacement, so both ends sit exactly on the neighbor sections'
// sections' baseline (no seam step) while the interior is the uniform // baseline (no seam step) while the interior is a uniform run of smooth
// corrugated profile. // humps.
function washboard(startX, startY, width, vScale) { function washboard(startX, startY, width, vScale) {
const amp = WASH_AMPLITUDE * vScale; const amp = WASH_AMPLITUDE * vScale;
const bumps = washBumps(width); 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 }; 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 // 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 // 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) { function washboardUp(startX, startY, width, vScale) {
const amp = WASH_AMPLITUDE * vScale; const amp = WASH_AMPLITUDE * vScale;
const rise = WASH_RISE * vScale; const rise = WASH_RISE * vScale;
const bumps = washBumps(width); 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 }; return { type: 'simple', points, endY: startY - rise };
} }
// Washboard Down: mirror of washboardUp - a smoothstep decline baseline // 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) { function washboardDown(startX, startY, width, vScale) {
const amp = WASH_AMPLITUDE * vScale; const amp = WASH_AMPLITUDE * vScale;
const rise = WASH_RISE * vScale; const rise = WASH_RISE * vScale;
const bumps = washBumps(width); 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 }; return { type: 'simple', points, endY: startY + rise };
} }