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 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 };
}