@brandocms/jupiter 5.0.0-beta.18 → 5.0.0-beta.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/modules/Looper/index.js +183 -4
package/package.json
CHANGED
|
@@ -57,6 +57,12 @@ const DEFAULT_OPTIONS = {
|
|
|
57
57
|
snapDuration: 0.5, // Duration of snap animation in seconds (0.3-1.0, lower = faster/snappier)
|
|
58
58
|
snapBounce: 0.15, // Spring bounce amount (0-1, 0 = no bounce, higher = more bouncy)
|
|
59
59
|
|
|
60
|
+
// Edge spring (loop: false) — a throw past either end keeps its speed, runs
|
|
61
|
+
// past the edge and springs back, instead of gliding in slowly
|
|
62
|
+
edgeStiffness: 260, // Pull back towards the edge (higher = shorter overshoot)
|
|
63
|
+
edgeDamping: 29, // Damping of the pull (29 at 260 is just under critical: one overshoot, no wobble)
|
|
64
|
+
edgeElasticity: 0.55, // Dragging past an end follows the finger with this much give (0 = hard stop at the edge)
|
|
65
|
+
|
|
60
66
|
speed: {
|
|
61
67
|
sm: 0.1, // Speed for mobile (multiplier)
|
|
62
68
|
lg: 0.35, // Speed for desktop (multiplier)
|
|
@@ -910,7 +916,9 @@ function horizontalLoop(app, items, config) {
|
|
|
910
916
|
indexSetByNav = false
|
|
911
917
|
startX = e.clientX
|
|
912
918
|
startY = e.clientY
|
|
913
|
-
|
|
919
|
+
// Grabbed past an end (mid-bounce): continue from the drag distance that
|
|
920
|
+
// shows the row there, or the rubber band would jump
|
|
921
|
+
startPosition = shouldLoop ? position.get() : unstretch(position.get())
|
|
914
922
|
velocityTracker = [{ x: e.clientX, time: e.timeStamp }]
|
|
915
923
|
hasDragged = false // Reset - will be set true if movement exceeds threshold
|
|
916
924
|
axisDecided = false // Reset - will be decided on first significant movement
|
|
@@ -1030,9 +1038,8 @@ function horizontalLoop(app, items, config) {
|
|
|
1030
1038
|
// For looping, position grows freely - items wrap as groups
|
|
1031
1039
|
position.set(newPosition)
|
|
1032
1040
|
} else {
|
|
1033
|
-
// For non-looping,
|
|
1034
|
-
|
|
1035
|
-
position.set(clampedPos)
|
|
1041
|
+
// For non-looping, past either end the row gives with resistance
|
|
1042
|
+
position.set(stretch(newPosition))
|
|
1036
1043
|
}
|
|
1037
1044
|
}
|
|
1038
1045
|
|
|
@@ -1093,6 +1100,18 @@ function horizontalLoop(app, items, config) {
|
|
|
1093
1100
|
if (overallDelta > 0 && velocity < 0) velocity = 0
|
|
1094
1101
|
if (overallDelta < 0 && velocity > 0) velocity = 0
|
|
1095
1102
|
|
|
1103
|
+
// Released past an end: always spring back to it, whatever the velocity
|
|
1104
|
+
if (!shouldLoop && outOfBounds(position.get())) {
|
|
1105
|
+
inertiaAnimation = throwPastEdge(-velocity * config.throwVelocityMultiplier)
|
|
1106
|
+
inertiaAnimation
|
|
1107
|
+
.then(() => {
|
|
1108
|
+
inertiaAnimation = null
|
|
1109
|
+
updateIndexDisplay()
|
|
1110
|
+
})
|
|
1111
|
+
.catch(() => { inertiaAnimation = null })
|
|
1112
|
+
return
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1096
1115
|
// If snap is enabled, always use it (GSAP-style: snap modifies inertia target)
|
|
1097
1116
|
// Otherwise use old logic: inertia if velocity, or resume crawl
|
|
1098
1117
|
if (config.snap) {
|
|
@@ -1124,6 +1143,15 @@ function horizontalLoop(app, items, config) {
|
|
|
1124
1143
|
const velocityMultiplier = reducedMotion ? config.throwVelocityMultiplier * 0.3 : config.throwVelocityMultiplier
|
|
1125
1144
|
const motionVelocity = -velocity * velocityMultiplier
|
|
1126
1145
|
|
|
1146
|
+
// A throw that would land past either end springs off the edge instead
|
|
1147
|
+
if (!reducedMotion && throwsPastEdge(currentPos, motionVelocity)) {
|
|
1148
|
+
inertiaAnimation = throwPastEdge(motionVelocity)
|
|
1149
|
+
inertiaAnimation
|
|
1150
|
+
.then(() => { inertiaAnimation = null })
|
|
1151
|
+
.catch(() => { inertiaAnimation = null })
|
|
1152
|
+
return
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1127
1155
|
// Estimate target for Motion.js (it recomputes internally for type: 'inertia',
|
|
1128
1156
|
// but we pass a reasonable target to avoid a zero-distance animation)
|
|
1129
1157
|
const power = config.throwPower
|
|
@@ -1162,6 +1190,142 @@ function horizontalLoop(app, items, config) {
|
|
|
1162
1190
|
})
|
|
1163
1191
|
}
|
|
1164
1192
|
|
|
1193
|
+
function outOfBounds(pos) {
|
|
1194
|
+
return pos > maxScrollPosition || pos < 0
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Rubber band for dragging past an end of a non-looping row — the curve
|
|
1199
|
+
* iOS uses, (1 - 1 / (x·c/d + 1)) · d: the row follows the pointer at
|
|
1200
|
+
* `edgeElasticity` of its speed at the edge, slower the further out, and
|
|
1201
|
+
* never further than its own width. `stretch` maps drag distance to the
|
|
1202
|
+
* shown position, `unstretch` inverts it, `stretchRate` is the slope.
|
|
1203
|
+
*/
|
|
1204
|
+
function stretch(raw) {
|
|
1205
|
+
const c = config.edgeElasticity
|
|
1206
|
+
if (!c) return Math.max(0, Math.min(maxScrollPosition, raw))
|
|
1207
|
+
const d = containerWidth || container.offsetWidth
|
|
1208
|
+
const band = over => (1 - 1 / ((over * c) / d + 1)) * d
|
|
1209
|
+
if (raw > maxScrollPosition) return maxScrollPosition + band(raw - maxScrollPosition)
|
|
1210
|
+
if (raw < 0) return -band(-raw)
|
|
1211
|
+
return raw
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function unstretch(shown) {
|
|
1215
|
+
const c = config.edgeElasticity
|
|
1216
|
+
if (!c || !outOfBounds(shown)) return shown
|
|
1217
|
+
const d = containerWidth || container.offsetWidth
|
|
1218
|
+
const over = shown > maxScrollPosition ? shown - maxScrollPosition : -shown
|
|
1219
|
+
const raw = (d / c) * (1 / (1 - Math.min(over, d - 1) / d) - 1)
|
|
1220
|
+
return shown > maxScrollPosition ? maxScrollPosition + raw : -raw
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function stretchRate(shown) {
|
|
1224
|
+
const c = config.edgeElasticity
|
|
1225
|
+
if (!c || !outOfBounds(shown)) return 1
|
|
1226
|
+
const d = containerWidth || container.offsetWidth
|
|
1227
|
+
const over = shown > maxScrollPosition ? shown - maxScrollPosition : -shown
|
|
1228
|
+
// dy/dx = c / (x·c/d + 1)², written in terms of the shown overshoot
|
|
1229
|
+
return c * (1 - over / d) ** 2
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Whether a throw would come to rest past either end of a non-looping row.
|
|
1234
|
+
* Uses Motion's own inertia arithmetic: it rests at origin + power * velocity.
|
|
1235
|
+
*/
|
|
1236
|
+
function throwsPastEdge(currentPos, motionVelocity) {
|
|
1237
|
+
if (shouldLoop) return false
|
|
1238
|
+
if (outOfBounds(currentPos)) return true
|
|
1239
|
+
const ideal = currentPos + config.throwPower * motionVelocity
|
|
1240
|
+
return ideal > maxScrollPosition || ideal < 0
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* Throw into an end of a non-looping row: keep the speed, reach the edge,
|
|
1245
|
+
* run past it and spring back.
|
|
1246
|
+
*
|
|
1247
|
+
* The plain inertia animations clamp their target to the row (snap via
|
|
1248
|
+
* `modifyTarget`), and Motion then rescales the whole throw to that shorter
|
|
1249
|
+
* distance — so a hard throw near the end crawled the last stretch on the
|
|
1250
|
+
* exponential tail, two seconds of it. Motion's own `min`/`max` bounce
|
|
1251
|
+
* cannot help: the clamped target never reaches the boundary, and its
|
|
1252
|
+
* spring is handed a velocity in px/ms rather than px/s (see the TODO in
|
|
1253
|
+
* motion-dom's inertia generator), so it barely overshoots when it does.
|
|
1254
|
+
*
|
|
1255
|
+
* Motion's inertia is closed-form — x(t) = target - A·e^(-t/τ), with
|
|
1256
|
+
* A = power · velocity and target = origin + A — so the moment it crosses
|
|
1257
|
+
* the edge, and its speed there, are known up front. Phase one is a tween
|
|
1258
|
+
* to the edge whose easing *is* that curve; phase two a spring to the edge
|
|
1259
|
+
* starting at that speed, which carries it past and back.
|
|
1260
|
+
*
|
|
1261
|
+
* Not a listener on the value: starting the spring from inside the decay's
|
|
1262
|
+
* own `change` notification left the value NaN (the decay was mid-frame),
|
|
1263
|
+
* and the row froze with every later drag computing from NaN.
|
|
1264
|
+
*
|
|
1265
|
+
* Returns Motion-like controls — `stop()` and a thenable — so
|
|
1266
|
+
* `onPointerDown` interrupts it like any other animation.
|
|
1267
|
+
*/
|
|
1268
|
+
function throwPastEdge(motionVelocity) {
|
|
1269
|
+
const origin = position.get()
|
|
1270
|
+
// Already past an end (released mid-stretch): back to that end, starting
|
|
1271
|
+
// at the speed the row is actually moving on screen — the pointer's,
|
|
1272
|
+
// slowed by the rubber band.
|
|
1273
|
+
const outside = outOfBounds(origin)
|
|
1274
|
+
const edge = outside
|
|
1275
|
+
? (origin > maxScrollPosition ? maxScrollPosition : 0)
|
|
1276
|
+
: (motionVelocity > 0 ? maxScrollPosition : 0)
|
|
1277
|
+
findNearestSnapPoint(edge) // lands on the last (or first) item: keep the index in step
|
|
1278
|
+
|
|
1279
|
+
const tau = config.throwResistance // ms
|
|
1280
|
+
const amplitude = config.throwPower * motionVelocity
|
|
1281
|
+
const beyond = origin + amplitude - edge // how far past the edge the decay would rest
|
|
1282
|
+
// Time (ms) the decay takes to reach the edge, and its speed there (px/s)
|
|
1283
|
+
const tEdge = outside ? 0 : Math.max(0, -tau * Math.log(beyond / amplitude))
|
|
1284
|
+
const edgeVelocity = outside
|
|
1285
|
+
? motionVelocity * stretchRate(origin)
|
|
1286
|
+
: (beyond / tau) * 1000
|
|
1287
|
+
|
|
1288
|
+
let current = null
|
|
1289
|
+
let stopped = false
|
|
1290
|
+
let resolve
|
|
1291
|
+
const done = new Promise(r => { resolve = r })
|
|
1292
|
+
|
|
1293
|
+
const bounce = () => {
|
|
1294
|
+
if (stopped) return
|
|
1295
|
+
current = animate(position, edge, {
|
|
1296
|
+
type: 'spring',
|
|
1297
|
+
velocity: edgeVelocity,
|
|
1298
|
+
stiffness: config.edgeStiffness,
|
|
1299
|
+
damping: config.edgeDamping,
|
|
1300
|
+
restSpeed: 10,
|
|
1301
|
+
restDelta: 0.5,
|
|
1302
|
+
})
|
|
1303
|
+
current.then(resolve, resolve)
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if (tEdge < 1 || edge === origin) {
|
|
1307
|
+
bounce()
|
|
1308
|
+
} else {
|
|
1309
|
+
const span = edge - origin
|
|
1310
|
+
current = animate(position, edge, {
|
|
1311
|
+
duration: tEdge / 1000,
|
|
1312
|
+
// The decay's own curve, normalised to 0..1 over the way to the edge
|
|
1313
|
+
ease: p => (amplitude * (1 - Math.exp((-p * tEdge) / tau))) / span,
|
|
1314
|
+
})
|
|
1315
|
+
current.then(bounce, () => {})
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
return {
|
|
1319
|
+
stop() {
|
|
1320
|
+
stopped = true
|
|
1321
|
+
current?.stop()
|
|
1322
|
+
resolve()
|
|
1323
|
+
},
|
|
1324
|
+
then: (onResolve, onReject) => done.then(onResolve, onReject),
|
|
1325
|
+
catch: onReject => done.catch(onReject),
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1165
1329
|
/**
|
|
1166
1330
|
* Find the nearest snap point to a given position
|
|
1167
1331
|
* @param {number} targetPos - Position to find nearest snap point for
|
|
@@ -1248,6 +1412,18 @@ function horizontalLoop(app, items, config) {
|
|
|
1248
1412
|
const motionVelocity =
|
|
1249
1413
|
-velocity * config.throwVelocityMultiplier * config.snapVelocityMultiplier
|
|
1250
1414
|
|
|
1415
|
+
// A throw that would land past either end springs off the edge instead
|
|
1416
|
+
if (throwsPastEdge(currentPos, motionVelocity)) {
|
|
1417
|
+
snapAnimation = throwPastEdge(motionVelocity)
|
|
1418
|
+
snapAnimation
|
|
1419
|
+
.then(() => {
|
|
1420
|
+
snapAnimation = null
|
|
1421
|
+
updateIndexDisplay()
|
|
1422
|
+
})
|
|
1423
|
+
.catch(() => { snapAnimation = null })
|
|
1424
|
+
return
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1251
1427
|
// Calculate ideal inertia target (Motion will recalculate, but we need a non-zero animation)
|
|
1252
1428
|
// This ensures Motion starts the inertia physics
|
|
1253
1429
|
const idealTarget =
|
|
@@ -1909,6 +2085,9 @@ export default class Looper {
|
|
|
1909
2085
|
snapVelocityMultiplier: this.opts.snapVelocityMultiplier,
|
|
1910
2086
|
snapDuration: this.opts.snapDuration,
|
|
1911
2087
|
snapBounce: this.opts.snapBounce,
|
|
2088
|
+
edgeStiffness: this.opts.edgeStiffness,
|
|
2089
|
+
edgeDamping: this.opts.edgeDamping,
|
|
2090
|
+
edgeElasticity: this.opts.edgeElasticity,
|
|
1912
2091
|
minimumMovement: this.opts.minimumMovement,
|
|
1913
2092
|
touchMinimumMovement: this.opts.touchMinimumMovement,
|
|
1914
2093
|
},
|