Skip to content

Commit c86d1a5

Browse files
feat(web): catenary sag, exit billow, edge-curl on paper-rail
Three additive physics layers on bendPaper, all guarded so they cost nothing when inactive: - replace symmetric pillow sag with a catenary droop that maximises at the bottom-middle, matching a sheet hung from its top edge - trailing-side Z lag during stage exits, so the paper visibly flutters air resistance as it flies between poses - cursor-proximity edge curl on all four edges, so paper corners peek toward the cursor when approached
1 parent a47c474 commit c86d1a5

1 file changed

Lines changed: 54 additions & 10 deletions

File tree

web/components/canvas/paper-rail.tsx

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,14 +1117,35 @@ function bendPaper(
11171117
cursorU: number,
11181118
cursorV: number,
11191119
cursorPress: number,
1120+
exitDirX: number,
1121+
exitAmt: number,
11201122
) {
11211123
if (!Number.isFinite(curl)) curl = 0;
11221124
if (!Number.isFinite(wave)) wave = 0;
11231125
if (!Number.isFinite(flap)) flap = 0;
11241126
if (!Number.isFinite(slack)) slack = 0;
11251127
if (!Number.isFinite(time)) time = 0;
11261128
if (!Number.isFinite(cursorPress)) cursorPress = 0;
1129+
if (!Number.isFinite(exitAmt)) exitAmt = 0;
1130+
if (!Number.isFinite(exitDirX)) exitDirX = 1;
11271131
const cursorActive = Math.abs(cursorPress) > 0.001;
1132+
// Edge-curl-on-cursor-proximity: the closer the cursor gets to a
1133+
// paper edge, the more that edge lifts toward the camera (real paper
1134+
// corners visibly peek up when a finger approaches). Hoisted out of
1135+
// the vertex loop because cursor proximity is constant per call.
1136+
const edgeRange = 0.18;
1137+
const invEdgeR = 1 / edgeRange;
1138+
const nearT = Math.max(0, (cursorV - (1 - edgeRange)) * invEdgeR);
1139+
const nearB = Math.max(0, (edgeRange - cursorV) * invEdgeR);
1140+
const nearR = Math.max(0, (cursorU - (1 - edgeRange)) * invEdgeR);
1141+
const nearL = Math.max(0, (edgeRange - cursorU) * invEdgeR);
1142+
const edgeCurlActive =
1143+
nearT + nearB + nearR + nearL > 0.001;
1144+
// Air-drag billow during stage flight: the trailing side of the
1145+
// paper (opposite of exitDirX) lags in -Z, like a sheet trailing off
1146+
// when thrown sideways. Skip the work entirely while the paper is on
1147+
// stage.
1148+
const billowActive = exitAmt > 0.005;
11281149
for (let i = 0; i < pos.length; i += 3) {
11291150
const px = rest[i];
11301151
const py = rest[i + 1];
@@ -1143,25 +1164,44 @@ function bendPaper(
11431164
const wind3 = Math.sin(u * 16.0 + time * 2.4) * Math.cos(v * 12.0 - time * 1.7) * 0.005;
11441165
const edgeFalloff = 1 - 4 * (u - 0.5) * (u - 0.5) * (v - 0.5) * (v - 0.5);
11451166
const ripple = (wind1 + wind2 + wind3) * (0.5 + edgeFalloff * 0.5);
1146-
const sagMask = Math.sin(u * Math.PI) * Math.sin(v * Math.PI);
1147-
const sagY = -sagMask * slack * 0.1;
1148-
const sagZ = -sagMask * slack * 0.06;
1167+
// Catenary-style sag: paper hangs from its top edge and droops
1168+
// nonlinearly toward the bottom. The old sin(πv) profile peaked
1169+
// sag at v=0.5 and returned to zero at the bottom, which is the
1170+
// wrong sign for a hung sheet. (1-v)² puts maximum droop at the
1171+
// bottom-middle and tapers smoothly to zero at the top.
1172+
const hPillow = Math.sin(u * Math.PI);
1173+
const vDroop = (1 - v) * (1 - v);
1174+
const sagMask = vDroop * (0.55 + 0.45 * hPillow);
1175+
const sagY = -sagMask * slack * 0.12;
1176+
const sagZ = -sagMask * slack * 0.07;
11491177
const yWobble = Math.sin(u * 5.0 + time * 1.6) * 0.006 * (1 - sagMask * 0.5);
1150-
// Cursor bump: subtle volume-conserving deformation centred on the
1151-
// cursor's UV. Most of the displacement is the positive gaussian
1152-
// lobe; the (1 - 0.6·r²) factor adds only a faint counter-curve at
1153-
// the rim, since real paper barely dips around a press. Tight radius
1154-
// weights (50/32) keep the footprint at ~10% of the paper, with
1155-
// mild vertical anisotropy along the washi grain.
11561178
let cursorZ = 0;
11571179
if (cursorActive) {
11581180
const du = u - cursorU;
11591181
const dv = v - cursorV;
11601182
const r2 = du * du * 50 + dv * dv * 32;
11611183
cursorZ = (1 - 0.6 * r2) * Math.exp(-r2) * cursorPress;
11621184
}
1185+
let edgeCurlZ = 0;
1186+
if (edgeCurlActive) {
1187+
const maskT = Math.max(0, (v - (1 - edgeRange)) * invEdgeR);
1188+
const maskB = Math.max(0, (edgeRange - v) * invEdgeR);
1189+
const maskR = Math.max(0, (u - (1 - edgeRange)) * invEdgeR);
1190+
const maskL = Math.max(0, (edgeRange - u) * invEdgeR);
1191+
edgeCurlZ =
1192+
(maskT * maskT * nearT
1193+
+ maskB * maskB * nearB
1194+
+ maskR * maskR * nearR
1195+
+ maskL * maskL * nearL) * 0.08;
1196+
}
1197+
let billowZ = 0;
1198+
if (billowActive) {
1199+
const trailMask = Math.max(0, -(u - 0.5) * exitDirX);
1200+
billowZ = -trailMask * trailMask * exitAmt * 0.28;
1201+
}
11631202
const newY = py + ripple * 0.5 + sagY + yWobble;
1164-
const newZ = curlZ + waveZ + flapZ + sagZ + ripple + verletZ + cursorZ;
1203+
const newZ =
1204+
curlZ + waveZ + flapZ + sagZ + ripple + verletZ + cursorZ + edgeCurlZ + billowZ;
11651205
pos[i] = px;
11661206
pos[i + 1] = Number.isFinite(newY) ? newY : py;
11671207
pos[i + 2] = Number.isFinite(newZ) ? newZ : 0;
@@ -2023,6 +2063,8 @@ export function PaperRail() {
20232063
bumpU,
20242064
bumpV,
20252065
bumpAmp,
2066+
exitDirX,
2067+
exitAmt,
20262068
);
20272069
bendPaper(
20282070
backPosArr,
@@ -2036,6 +2078,8 @@ export function PaperRail() {
20362078
bumpU,
20372079
bumpV,
20382080
bumpAmp,
2081+
exitDirX,
2082+
exitAmt,
20392083
);
20402084
frontGeom.attributes.position.needsUpdate = true;
20412085
backGeom.attributes.position.needsUpdate = true;

0 commit comments

Comments
 (0)