Add halfpipe and reverseHalfpipe section generators
- Register `halfpipe` and `reverseHalfpipe` in SECTION_TYPES and GENERATORS - Implement both as single cubic Bezier curves with vertical tangents at entrance/exit: - halfpipe: near-vertical drop into a rounded bottom, then a shorter climb; exit ends below entrance (net decline) - reverseHalfpipe: vertical mirror of halfpipe — short rise arcing up and over, then a longer drop; exit also ends below entrance - Use monotonic x(t) = W * t^2 * (3 - 2t) to keep terrain valid as y(x) - Sample each profile with 24 points and snap endpoints exactly for seamless section joins - Scale vertical magnitudes by vScale; chosen defaults yield ~350 base-unit drop and ~210 net decline at default width
This commit is contained in:
parent
b8b6e74c8d
commit
fba3ed040d
|
|
@ -31,6 +31,8 @@ export const SECTION_TYPES = [
|
||||||
{ id: 'gap', label: 'Gap' },
|
{ id: 'gap', label: 'Gap' },
|
||||||
{ id: 'lip', label: 'Lip' },
|
{ id: 'lip', label: 'Lip' },
|
||||||
{ id: 'dropGap', label: 'Drop Gap' },
|
{ id: 'dropGap', label: 'Drop Gap' },
|
||||||
|
{ id: 'halfpipe', label: 'Halfpipe' },
|
||||||
|
{ id: 'reverseHalfpipe', label: 'Reverse Halfpipe' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const RISE_SMOOTH = 90;
|
const RISE_SMOOTH = 90;
|
||||||
|
|
@ -297,6 +299,130 @@ function dropGap(startX, startY, width, vScale) {
|
||||||
return { type: 'gap', takeoffPoints, landingPoints, endY: landingY };
|
return { type: 'gap', takeoffPoints, landingPoints, endY: landingY };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "Halfpipe" - a backwards J. One smooth profile, left to right:
|
||||||
|
// a near-vertical drop straight down (the tangent at the entrance is
|
||||||
|
// exactly vertical) that eases into a rounded, halfpipe-style bottom,
|
||||||
|
// then a near-vertical climb (again vertical at the very end) up to the
|
||||||
|
// exit.
|
||||||
|
//
|
||||||
|
// The exit is deliberately BELOW the entrance: the climb is only HALF of
|
||||||
|
// the drop, so the section is a net decline (a half-pipe's exit wall is
|
||||||
|
// lower than its entrance wall) and the profile reads as a backwards J -
|
||||||
|
// tall stem on the left, round hook at the bottom, short stub climbing
|
||||||
|
// back up on the right.
|
||||||
|
//
|
||||||
|
// Built as a single cubic Bezier with vertical tangents at both ends
|
||||||
|
// (control handles hanging straight down from the entrance and the exit).
|
||||||
|
// A circular arc can't do this job - a circle tangent to both walls
|
||||||
|
// vertically would force the two junctions to the same height (a
|
||||||
|
// half-circle), so the different wall heights need a cubic, which is also
|
||||||
|
// the shape a vert-ramp transition actually uses.
|
||||||
|
//
|
||||||
|
// P0 = entrance P1 = (P0.x, P0.y + drop) [entry handle]
|
||||||
|
// P3 = exit P2 = (P3.x, P3.y + climb) [exit handle]
|
||||||
|
//
|
||||||
|
// Both handles hang straight down from their endpoint (entry handle from
|
||||||
|
// the entrance by `drop`, exit handle from the exit by `climb`), so both
|
||||||
|
// end tangents are vertical. Note P2 = (W, drop) - at the 2:1 ratio both
|
||||||
|
// handles happen to reach the same depth.
|
||||||
|
//
|
||||||
|
// x(t) = W * t^2 * (3 - 2t) (all x-control points in [0, W] =>
|
||||||
|
// strictly monotonic in x, so the terrain stays a valid y(x) curve)
|
||||||
|
// y(t) = 3*drop*t*(1-t)^2 + 3*drop*t^2*(1-t) + (drop - climb)*t^3
|
||||||
|
//
|
||||||
|
// drop/climb (both vScale-scaled) are the end-handle lengths: they set how
|
||||||
|
// long the near-vertical ends are and how deep the rounded bottom sits
|
||||||
|
// (the bottom lands ~0.83 * drop below the entrance at the 2:1 ratio,
|
||||||
|
// ~13% further right of center). At the default 700-wide section the
|
||||||
|
// profile falls ~350 base units ("drops for a ways") and the exit ends
|
||||||
|
// ~210 below the entrance ("significantly lower"). The very first/last
|
||||||
|
// segment is steep but finite; the sample count is chosen so that even at
|
||||||
|
// the editor's minimum width the end segments stay wider than
|
||||||
|
// Terrain._buildSegment's |dx| < 1 skip threshold.
|
||||||
|
const HALFPIPE_DROP = 420;
|
||||||
|
const HALFPIPE_CLIMB_RATIO = 0.5;
|
||||||
|
const HALFPIPE_SAMPLES = 24;
|
||||||
|
|
||||||
|
function halfpipe(startX, startY, width, vScale) {
|
||||||
|
const drop = HALFPIPE_DROP * vScale;
|
||||||
|
const climb = HALFPIPE_CLIMB_RATIO * drop;
|
||||||
|
const net = drop - climb; // entrance -> exit (positive: exit is lower)
|
||||||
|
|
||||||
|
const endY = startY + net;
|
||||||
|
const points = [];
|
||||||
|
for (let i = 0; i <= HALFPIPE_SAMPLES; i++) {
|
||||||
|
const t = i / HALFPIPE_SAMPLES;
|
||||||
|
const x = startX + width * t * t * (3 - 2 * t);
|
||||||
|
const y = startY
|
||||||
|
+ 3 * drop * t * (1 - t) * (1 - t)
|
||||||
|
+ 3 * drop * t * t * (1 - t)
|
||||||
|
+ net * t * t * t;
|
||||||
|
points.push({ x: Math.round(x), y: Math.round(y) });
|
||||||
|
}
|
||||||
|
// Snap the ends exactly so the section joins its neighbors seamlessly.
|
||||||
|
points[0] = { x: startX, y: Math.round(startY) };
|
||||||
|
points[points.length - 1] = { x: startX + width, y: Math.round(endY) };
|
||||||
|
|
||||||
|
return { type: 'simple', points, endY };
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Reverse Halfpipe" - the halfpipe flipped upside down (an upside-down J).
|
||||||
|
// One smooth profile, left to right: a near-vertical climb (vertical tangent
|
||||||
|
// at the entrance) that arcs up and over like a rainbow, then a
|
||||||
|
// near-vertical drop (vertical tangent at the exit) that ends BELOW the
|
||||||
|
// entrance.
|
||||||
|
//
|
||||||
|
// It's the exact vertical mirror of halfpipe about the line
|
||||||
|
// y = startY + 176 (the midpoint of its own end-to-end span): same x(t),
|
||||||
|
// same Bezier family, signs flipped, and the two ends swapped. Where
|
||||||
|
// halfpipe's entry is the long leg (drop) and exit the short one (climb),
|
||||||
|
// here the entry is the short leg (rise, ~half the drop) and the exit the
|
||||||
|
// long one (drop), so the section still nets lower on the right - the
|
||||||
|
// mirror of halfpipe's net-decline shape.
|
||||||
|
//
|
||||||
|
// Control handles stick straight UP from both endpoints (mirror of
|
||||||
|
// halfpipe's hanging handles):
|
||||||
|
//
|
||||||
|
// P0 = entrance P1 = (P0.x, P0.y - rise) [entry handle]
|
||||||
|
// P3 = exit P2 = (P3.x, P3.y - drop) [exit handle]
|
||||||
|
//
|
||||||
|
// x(t) = W * t^2 * (3 - 2t) (same as halfpipe, monotonic in x)
|
||||||
|
// y(t) = -3*rise*t*(1-t)^2 - 3*rise*t^2*(1-t) + (drop - rise)*t^3
|
||||||
|
//
|
||||||
|
// rise/drop (both vScale-scaled) are the end-handle lengths. At the
|
||||||
|
// default 700-wide section the arch crests ~176 base units above the
|
||||||
|
// entrance ("arcs up and over"), and the exit ends ~210 below it ("ends
|
||||||
|
// lower than the entrance") - the mirror image of halfpipe's 348-deep,
|
||||||
|
// 210-net-decline profile. End-segment widths stay above
|
||||||
|
// Terrain._buildSegment's |dx| < 1 skip threshold even at the editor's
|
||||||
|
// minimum width.
|
||||||
|
const REVERSE_HALFPIPE_RISE = 210;
|
||||||
|
const REVERSE_HALFPIPE_DROP = 420; // = 2 * RISE, the longer leg
|
||||||
|
const REVERSE_HALFPIPE_SAMPLES = 24;
|
||||||
|
|
||||||
|
function reverseHalfpipe(startX, startY, width, vScale) {
|
||||||
|
const rise = REVERSE_HALFPIPE_RISE * vScale;
|
||||||
|
const drop = REVERSE_HALFPIPE_DROP * vScale;
|
||||||
|
const net = drop - rise; // positive: exit is lower than entrance
|
||||||
|
|
||||||
|
const endY = startY + net;
|
||||||
|
const points = [];
|
||||||
|
for (let i = 0; i <= REVERSE_HALFPIPE_SAMPLES; i++) {
|
||||||
|
const t = i / REVERSE_HALFPIPE_SAMPLES;
|
||||||
|
const x = startX + width * t * t * (3 - 2 * t);
|
||||||
|
const y = startY
|
||||||
|
- 3 * rise * t * (1 - t) * (1 - t)
|
||||||
|
- 3 * rise * t * t * (1 - t)
|
||||||
|
+ net * t * t * t;
|
||||||
|
points.push({ x: Math.round(x), y: Math.round(y) });
|
||||||
|
}
|
||||||
|
// Snap the ends exactly so the section joins its neighbors seamlessly.
|
||||||
|
points[0] = { x: startX, y: Math.round(startY) };
|
||||||
|
points[points.length - 1] = { x: startX + width, y: Math.round(endY) };
|
||||||
|
|
||||||
|
return { type: 'simple', points, endY };
|
||||||
|
}
|
||||||
|
|
||||||
const GENERATORS = {
|
const GENERATORS = {
|
||||||
flat,
|
flat,
|
||||||
smoothIncline,
|
smoothIncline,
|
||||||
|
|
@ -313,6 +439,8 @@ const GENERATORS = {
|
||||||
gap,
|
gap,
|
||||||
lip,
|
lip,
|
||||||
dropGap,
|
dropGap,
|
||||||
|
halfpipe,
|
||||||
|
reverseHalfpipe,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function generateSection(typeId, startX, startY, width = SECTION_WIDTH, vScale = 1) {
|
export function generateSection(typeId, startX, startY, width = SECTION_WIDTH, vScale = 1) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue