@@ -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