motion 13.1.1 → 13.2.0

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.
@@ -0,0 +1,244 @@
1
+ import { createEffect, frame, rgba, color, clamp, hslaToRgba } from 'framer-motion/dom';
2
+
3
+ const axes = "xyzw";
4
+ const radiansPerDegree = Math.PI / 180;
5
+ const transforms = {
6
+ x: ["position", 0],
7
+ y: ["position", 1],
8
+ z: ["position", 2],
9
+ rotateX: ["rotation", 0, true],
10
+ rotateY: ["rotation", 1, true],
11
+ rotateZ: ["rotation", 2, true],
12
+ scaleX: ["scale", 0],
13
+ scaleY: ["scale", 1],
14
+ scaleZ: ["scale", 2],
15
+ };
16
+ /**
17
+ * Last vector written to each path. vgpu subjects like Effect and
18
+ * SharedUniforms have no getters, and SceneNode stores rotation as a
19
+ * quaternion, so this is how single components find their siblings.
20
+ */
21
+ const shadows = new WeakMap();
22
+ /**
23
+ * Values changed this frame, flushed into one set() per subject.
24
+ */
25
+ const pending = new Map();
26
+ function flush() {
27
+ pending.forEach(applyValues);
28
+ pending.clear();
29
+ }
30
+ /**
31
+ * Binds motion values to vgpu shared uniforms, Effect/Draw/Compute bindings
32
+ * ("params.time"), scene nodes (x, rotateY, scale), cameras, lights,
33
+ * materials, orbit controls and target clear colors.
34
+ *
35
+ * Register with `animate.addEffect(vgpuEffect)` so `animate()` can target
36
+ * these subjects directly, or call it yourself to wire up existing motion
37
+ * values:
38
+ *
39
+ * ```ts
40
+ * vgpuEffect(wave, { "params.time": time })
41
+ * vgpuEffect(cube, { x, rotateY })
42
+ * ```
43
+ *
44
+ * Changed values are batched into a single set() per subject per frame in
45
+ * `frame.preRender`, ahead of render loops scheduled with `frame.render`.
46
+ */
47
+ const vgpuEffect = createEffect((subject, state, key, value) => state.set(key, value, () => {
48
+ const bag = pending.get(subject) ?? {};
49
+ bag[key] = state.latest[key];
50
+ pending.set(subject, bag);
51
+ frame.preRender(flush, false, true);
52
+ }, undefined, false), {
53
+ test: isVGPUSubject,
54
+ read: readInitial,
55
+ step: frame.preRender,
56
+ });
57
+ /**
58
+ * Claims vgpu shader units (Effect, Draw, Compute), shared uniforms, scene
59
+ * nodes, materials, orbit controls and targets. Everything else, including
60
+ * Three.js vectors and plain objects, is left alone.
61
+ */
62
+ function isVGPUSubject(subject) {
63
+ if (!subject || typeof subject !== "object")
64
+ return false;
65
+ const s = subject;
66
+ return typeof s.set === "function"
67
+ ? typeof s.kind === "string" ||
68
+ "gpu" in s ||
69
+ "reflection" in s ||
70
+ typeof s.yaw === "number"
71
+ : "clearColor" in s && "gpu" in s;
72
+ }
73
+ function isNode(subject) {
74
+ return "quaternion" in subject;
75
+ }
76
+ function isVector(value) {
77
+ return Array.isArray(value) || ArrayBuffer.isView(value);
78
+ }
79
+ function resolveKey(subject, key) {
80
+ const transform = isNode(subject) && transforms[key];
81
+ if (transform) {
82
+ return {
83
+ path: [transform[0]],
84
+ index: transform[1],
85
+ degrees: transform[2],
86
+ };
87
+ }
88
+ const path = key.split(".");
89
+ const last = path[path.length - 1];
90
+ const index = axes.indexOf(last.slice(-1).toLowerCase());
91
+ if (last.length > 1 && index !== -1) {
92
+ const base = [...path.slice(0, -1), last.slice(0, -1)];
93
+ if (getVector(subject, base))
94
+ return { path: base, index };
95
+ }
96
+ return { path };
97
+ }
98
+ function readPath(subject, path) {
99
+ return path.reduce((value, key) => value?.[key], subject);
100
+ }
101
+ /**
102
+ * Reads a subject property, falling back to ShaderMaterial's `values` record.
103
+ */
104
+ function readValue(subject, path) {
105
+ return (readPath(subject, path) ??
106
+ (subject.values && readPath(subject.values, path)));
107
+ }
108
+ function getVector(subject, path) {
109
+ const value = readValue(subject, path);
110
+ if (isVector(value))
111
+ return Array.from(value);
112
+ const shadow = shadows.get(subject)?.[path.join(".")];
113
+ if (shadow)
114
+ return shadow;
115
+ return path[0] === "rotation" && isNode(subject)
116
+ ? eulerFromQuaternion(subject.quaternion)
117
+ : undefined;
118
+ }
119
+ function setShadow(subject, path, vector) {
120
+ const shadow = shadows.get(subject) ?? {};
121
+ shadow[path.join(".")] = vector;
122
+ shadows.set(subject, shadow);
123
+ }
124
+ function setPath(target, path, value) {
125
+ var _a;
126
+ const last = path.length - 1;
127
+ let current = target;
128
+ for (let i = 0; i < last; i++) {
129
+ current = current[_a = path[i]] ?? (current[_a] = {});
130
+ }
131
+ current[path[last]] = value;
132
+ }
133
+ function isColorTarget(target) {
134
+ const sample = Array.isArray(target)
135
+ ? target.find((value) => typeof value === "string")
136
+ : target;
137
+ return typeof sample === "string" && color.test(sample);
138
+ }
139
+ function readInitial(subject, key, target) {
140
+ const { path, index, degrees } = resolveKey(subject, key);
141
+ if (index !== undefined) {
142
+ const value = getVector(subject, path)?.[index];
143
+ return degrees && value !== undefined ? value / radiansPerDegree : value;
144
+ }
145
+ const value = readValue(subject, path);
146
+ if (typeof value === "number")
147
+ return value;
148
+ if (!isVector(value))
149
+ return undefined;
150
+ if (isColorTarget(target))
151
+ return toColorString(value);
152
+ const sample = Array.isArray(target)
153
+ ? target.find((keyframe) => keyframe !== null)
154
+ : target;
155
+ // A numeric target on a vector reads the first component (uniform scale)
156
+ return typeof sample === "string" ? Array.from(value).join(" ") : value[0];
157
+ }
158
+ function applyValues(values, subject) {
159
+ const bag = {};
160
+ const vectors = new Map();
161
+ for (const key in values) {
162
+ const target = resolveKey(subject, key);
163
+ const { path, index, degrees } = target;
164
+ const value = values[key];
165
+ if (index === undefined) {
166
+ const parsed = parseValue(value, getVector(subject, path));
167
+ Array.isArray(parsed) && setShadow(subject, path, parsed);
168
+ setPath(bag, path, parsed);
169
+ continue;
170
+ }
171
+ const id = path.join(".");
172
+ const entry = vectors.get(id) ?? {
173
+ ...target,
174
+ vector: getVector(subject, path) ?? [],
175
+ };
176
+ vectors.set(id, entry);
177
+ entry.vector[index] = degrees
178
+ ? value * radiansPerDegree
179
+ : value;
180
+ }
181
+ vectors.forEach(({ path, vector }) => {
182
+ setShadow(subject, path, vector);
183
+ setPath(bag, path, vector);
184
+ });
185
+ typeof subject.set === "function"
186
+ ? subject.set(bag)
187
+ : Object.assign(subject, bag);
188
+ }
189
+ /**
190
+ * Converts animated strings into vgpu values: CSS colors become linear RGB
191
+ * (matching vgpu/scene's srgb()), "1 0 0" becomes [1, 0, 0].
192
+ */
193
+ function parseValue(value, current) {
194
+ if (typeof value !== "string")
195
+ return value;
196
+ if (color.test(value)) {
197
+ const parsed = color.parse(value);
198
+ const { red, green, blue, alpha = 1, } = "hue" in parsed ? hslaToRgba(parsed) : parsed;
199
+ const linear = [red, green, blue].map((channel) => toLinear(channel / 255));
200
+ return current?.length === 4 ? [...linear, alpha] : linear;
201
+ }
202
+ const parts = value
203
+ .split(",")
204
+ .join(" ")
205
+ .split(" ")
206
+ .filter(Boolean)
207
+ .map(Number);
208
+ return parts.length > 1 && parts.every((part) => !isNaN(part))
209
+ ? parts
210
+ : value;
211
+ }
212
+ function toLinear(channel) {
213
+ return channel <= 0.04045
214
+ ? channel / 12.92
215
+ : ((channel + 0.055) / 1.055) ** 2.4;
216
+ }
217
+ function toSRGB(channel) {
218
+ return channel <= 0.0031308
219
+ ? channel * 12.92
220
+ : 1.055 * channel ** (1 / 2.4) - 0.055;
221
+ }
222
+ function toColorString(linear) {
223
+ const [red, green, blue, alpha = 1] = Array.from(linear).map((channel, i) => i < 3 ? toSRGB(channel) * 255 : channel);
224
+ return rgba.transform({ red, green, blue, alpha });
225
+ }
226
+ /**
227
+ * Inverse of vgpu's quatFromEuler (intrinsic XYZ).
228
+ */
229
+ function eulerFromQuaternion(quaternion) {
230
+ const [x, y, z, w] = Array.from(quaternion);
231
+ const m13 = 2 * (x * z + w * y);
232
+ const singular = Math.abs(m13) > 0.9999999;
233
+ return [
234
+ singular
235
+ ? Math.atan2(2 * (y * z + w * x), 1 - 2 * (x * x + z * z))
236
+ : Math.atan2(-2 * (y * z - w * x), 1 - 2 * (x * x + y * y)),
237
+ Math.asin(clamp(-1, 1, m13)),
238
+ singular
239
+ ? 0
240
+ : Math.atan2(-2 * (x * y - w * z), 1 - 2 * (y * y + z * z)),
241
+ ];
242
+ }
243
+
244
+ export { vgpuEffect };
@@ -1039,15 +1039,16 @@
1039
1039
  * to prevent infinite loops
1040
1040
  */
1041
1041
  const maxGeneratorDuration = 20000;
1042
- function calcGeneratorDuration(generator) {
1042
+ function calcGeneratorDuration(generator, timeStep = 50, maxDuration = maxGeneratorDuration, keyframes) {
1043
1043
  let duration = 0;
1044
- const timeStep = 50;
1045
1044
  let state = generator.next(duration);
1046
- while (!state.done && duration < maxGeneratorDuration) {
1045
+ keyframes?.push(state.value);
1046
+ while (!state.done && duration < maxDuration) {
1047
1047
  duration += timeStep;
1048
1048
  state = generator.next(duration);
1049
+ keyframes?.push(state.value);
1049
1050
  }
1050
- return duration >= maxGeneratorDuration ? Infinity : duration;
1051
+ return duration >= maxDuration ? Infinity : duration;
1051
1052
  }
1052
1053
 
1053
1054
  /**
@@ -1131,9 +1132,13 @@
1131
1132
  const exponentialDecay = undampedFreq * dampingRatio;
1132
1133
  const delta = exponentialDecay * duration;
1133
1134
  const d = delta * velocity + velocity;
1134
- const e = Math.pow(dampingRatio, 2) * Math.pow(undampedFreq, 2) * duration;
1135
+ const e = dampingRatio *
1136
+ dampingRatio *
1137
+ undampedFreq *
1138
+ undampedFreq *
1139
+ duration;
1135
1140
  const f = Math.exp(-delta);
1136
- const g = calcAngularFreq(Math.pow(undampedFreq, 2), dampingRatio);
1141
+ const g = calcAngularFreq(undampedFreq * undampedFreq, dampingRatio);
1137
1142
  const factor = -envelope(undampedFreq) + safeMin > 0 ? -1 : 1;
1138
1143
  return (factor * ((d - e) * f)) / g;
1139
1144
  };
@@ -1164,7 +1169,7 @@
1164
1169
  };
1165
1170
  }
1166
1171
  else {
1167
- const stiffness = Math.pow(undampedFreq, 2) * mass;
1172
+ const stiffness = undampedFreq * undampedFreq * mass;
1168
1173
  return {
1169
1174
  stiffness,
1170
1175
  damping: dampingRatio * 2 * Math.sqrt(mass * stiffness),
@@ -1244,6 +1249,7 @@
1244
1249
  const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass));
1245
1250
  const initialDelta = target - origin;
1246
1251
  const undampedAngularFreq = millisecondsToSeconds(Math.sqrt(stiffness / mass));
1252
+ const decay = dampingRatio * undampedAngularFreq;
1247
1253
  /**
1248
1254
  * If we're working on a granular scale, use smaller defaults for determining
1249
1255
  * when the spring is finished.
@@ -1260,35 +1266,38 @@
1260
1266
  : springDefaults.restDelta.default);
1261
1267
  let resolveSpring;
1262
1268
  let resolveVelocity;
1263
- // Underdamped coefficients, hoisted for use in the inlined next() hot path
1264
- let angularFreq;
1265
- let A;
1266
- let sinCoeff;
1267
- let cosCoeff;
1268
1269
  if (dampingRatio < 1) {
1269
- angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio);
1270
- A =
1271
- (initialVelocity +
1272
- dampingRatio * undampedAngularFreq * initialDelta) /
1273
- angularFreq;
1270
+ const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio);
1271
+ const A = (initialVelocity + decay * initialDelta) / angularFreq;
1272
+ // Coefficients for the analytical derivative (px/ms)
1273
+ const sinCoeff = decay * A + initialDelta * angularFreq;
1274
+ const cosCoeff = decay * initialDelta - A * angularFreq;
1275
+ /**
1276
+ * The underdamped hot path needs both position and velocity every
1277
+ * frame and they share the same exp/sin/cos terms, so sample both
1278
+ * at once, memoized by t, to only calculate them once per frame.
1279
+ */
1280
+ let sampledT = -1;
1281
+ let position = 0;
1282
+ let velocityAtT = 0;
1283
+ const sample = (t) => {
1284
+ if (t !== sampledT) {
1285
+ sampledT = t;
1286
+ const envelope = Math.exp(-decay * t);
1287
+ const sin = Math.sin(angularFreq * t);
1288
+ const cos = Math.cos(angularFreq * t);
1289
+ position = target - envelope * (A * sin + initialDelta * cos);
1290
+ velocityAtT = envelope * (sinCoeff * sin + cosCoeff * cos);
1291
+ }
1292
+ };
1274
1293
  // Underdamped spring
1275
1294
  resolveSpring = (t) => {
1276
- const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
1277
- return (target -
1278
- envelope *
1279
- (A * Math.sin(angularFreq * t) +
1280
- initialDelta * Math.cos(angularFreq * t)));
1295
+ sample(t);
1296
+ return position;
1281
1297
  };
1282
- // Analytical derivative of underdamped spring (px/ms)
1283
- sinCoeff =
1284
- dampingRatio * undampedAngularFreq * A + initialDelta * angularFreq;
1285
- cosCoeff =
1286
- dampingRatio * undampedAngularFreq * initialDelta - A * angularFreq;
1287
1298
  resolveVelocity = (t) => {
1288
- const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
1289
- return envelope *
1290
- (sinCoeff * Math.sin(angularFreq * t) +
1291
- cosCoeff * Math.cos(angularFreq * t));
1299
+ sample(t);
1300
+ return velocityAtT;
1292
1301
  };
1293
1302
  }
1294
1303
  else if (dampingRatio === 1) {
@@ -1306,13 +1315,12 @@
1306
1315
  // Overdamped spring
1307
1316
  const dampedAngularFreq = undampedAngularFreq * Math.sqrt(dampingRatio * dampingRatio - 1);
1308
1317
  resolveSpring = (t) => {
1309
- const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
1318
+ const envelope = Math.exp(-decay * t);
1310
1319
  // When performing sinh or cosh values can hit Infinity so we cap them here
1311
1320
  const freqForT = Math.min(dampedAngularFreq * t, 300);
1312
1321
  return (target -
1313
1322
  (envelope *
1314
- ((initialVelocity +
1315
- dampingRatio * undampedAngularFreq * initialDelta) *
1323
+ ((initialVelocity + decay * initialDelta) *
1316
1324
  Math.sinh(freqForT) +
1317
1325
  dampedAngularFreq *
1318
1326
  initialDelta *
@@ -1320,43 +1328,21 @@
1320
1328
  dampedAngularFreq);
1321
1329
  };
1322
1330
  // Analytical derivative of overdamped spring (px/ms)
1323
- const P = (initialVelocity +
1324
- dampingRatio * undampedAngularFreq * initialDelta) /
1325
- dampedAngularFreq;
1326
- const sinhCoeff = dampingRatio * undampedAngularFreq * P - initialDelta * dampedAngularFreq;
1327
- const coshCoeff = dampingRatio * undampedAngularFreq * initialDelta - P * dampedAngularFreq;
1331
+ const P = (initialVelocity + decay * initialDelta) / dampedAngularFreq;
1332
+ const sinhCoeff = decay * P - initialDelta * dampedAngularFreq;
1333
+ const coshCoeff = decay * initialDelta - P * dampedAngularFreq;
1328
1334
  resolveVelocity = (t) => {
1329
- const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
1335
+ const envelope = Math.exp(-decay * t);
1330
1336
  const freqForT = Math.min(dampedAngularFreq * t, 300);
1331
- return envelope *
1337
+ return (envelope *
1332
1338
  (sinhCoeff * Math.sinh(freqForT) +
1333
- coshCoeff * Math.cosh(freqForT));
1339
+ coshCoeff * Math.cosh(freqForT)));
1334
1340
  };
1335
1341
  }
1336
1342
  const generator = {
1337
1343
  calculatedDuration: isResolvedFromDuration ? duration || null : null,
1338
1344
  velocity: (t) => secondsToMilliseconds(resolveVelocity(t)),
1339
1345
  next: (t) => {
1340
- /**
1341
- * For underdamped physics springs we need both position and
1342
- * velocity each tick. Compute shared trig values once to avoid
1343
- * duplicate Math.exp/sin/cos calls on the hot path.
1344
- */
1345
- if (!isResolvedFromDuration && dampingRatio < 1) {
1346
- const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t);
1347
- const sin = Math.sin(angularFreq * t);
1348
- const cos = Math.cos(angularFreq * t);
1349
- const current = target -
1350
- envelope *
1351
- (A * sin + initialDelta * cos);
1352
- const currentVelocity = secondsToMilliseconds(envelope *
1353
- (sinCoeff * sin + cosCoeff * cos));
1354
- state.done =
1355
- Math.abs(currentVelocity) <= restSpeed &&
1356
- Math.abs(target - current) <= restDelta;
1357
- state.value = state.done ? target : current;
1358
- return state;
1359
- }
1360
1346
  const current = resolveSpring(t);
1361
1347
  if (!isResolvedFromDuration) {
1362
1348
  const currentVelocity = secondsToMilliseconds(resolveVelocity(t));
@@ -1387,19 +1373,13 @@
1387
1373
  return options;
1388
1374
  };
1389
1375
 
1390
- const velocitySampleDuration = 5; // ms
1391
- function getGeneratorVelocity(resolveValue, t, current) {
1392
- const prevT = Math.max(t - velocitySampleDuration, 0);
1393
- return velocityPerSecond(current - resolveValue(prevT), t - prevT);
1394
- }
1395
-
1396
1376
  function inertia({ keyframes, velocity = 0.0, power = 0.8, timeConstant = 325, bounceDamping = 10, bounceStiffness = 500, modifyTarget, min, max, restDelta = 0.5, restSpeed, }) {
1397
1377
  const origin = keyframes[0];
1398
1378
  const state = {
1399
1379
  done: false,
1400
1380
  value: origin,
1401
1381
  };
1402
- const isOutOfBounds = (v) => (min !== undefined && v < min) || (max !== undefined && v > max);
1382
+ const isOutOfBounds = (v) => v < min || v > max;
1403
1383
  const nearestBoundary = (v) => {
1404
1384
  if (min === undefined)
1405
1385
  return max;
@@ -1417,12 +1397,10 @@
1417
1397
  if (target !== ideal)
1418
1398
  amplitude = target - origin;
1419
1399
  const calcDelta = (t) => -amplitude * Math.exp(-t / timeConstant);
1420
- const calcLatest = (t) => target + calcDelta(t);
1421
1400
  const applyFriction = (t) => {
1422
1401
  const delta = calcDelta(t);
1423
- const latest = calcLatest(t);
1424
1402
  state.done = Math.abs(delta) <= restDelta;
1425
- state.value = state.done ? target : latest;
1403
+ state.value = state.done ? target : target + delta;
1426
1404
  };
1427
1405
  /**
1428
1406
  * Ideally this would resolve for t in a stateless way, we could
@@ -1438,7 +1416,12 @@
1438
1416
  timeReachedBoundary = t;
1439
1417
  spring$1 = spring({
1440
1418
  keyframes: [state.value, nearestBoundary(state.value)],
1441
- velocity: getGeneratorVelocity(calcLatest, t, state.value), // TODO: This should be passing * 1000
1419
+ /**
1420
+ * The friction curve is target + calcDelta(t), so its exact
1421
+ * derivative is -calcDelta(t) / timeConstant in units/ms,
1422
+ * converted here to the units/second expected by spring.
1423
+ */
1424
+ velocity: (-calcDelta(t) / timeConstant) * 1000,
1442
1425
  damping: bounceDamping,
1443
1426
  stiffness: bounceStiffness,
1444
1427
  restDelta,
@@ -1607,6 +1590,12 @@
1607
1590
  };
1608
1591
  }
1609
1592
 
1593
+ const velocitySampleDuration = 5; // ms
1594
+ function getGeneratorVelocity(resolveValue, t, current) {
1595
+ const prevT = Math.max(t - velocitySampleDuration, 0);
1596
+ return velocityPerSecond(current - resolveValue(prevT), t - prevT);
1597
+ }
1598
+
1610
1599
  const isNotNull = (value) => value !== null;
1611
1600
  function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyframe, speed = 1) {
1612
1601
  const resolvedKeyframes = keyframes.filter(isNotNull);
@@ -4612,6 +4601,47 @@
4612
4601
  return true;
4613
4602
  });
4614
4603
 
4604
+ /**
4605
+ * Effects registered via `animate.addEffect()`, most recent first.
4606
+ */
4607
+ const effects = [];
4608
+ function addEffect(effect) {
4609
+ exports.invariant(typeof effect.test === "function" && typeof effect.read === "function", "Effects passed to animate.addEffect() need test() and read().", "effect-missing-test");
4610
+ removeEffect(effect);
4611
+ effects.unshift(effect);
4612
+ }
4613
+ function removeEffect(effect) {
4614
+ removeItem(effects, effect);
4615
+ }
4616
+ function findEffect(subject) {
4617
+ return effects.find((effect) => effect.test(subject));
4618
+ }
4619
+ /**
4620
+ * Animate the keys of `subject` via `effect`. Motion values are created on
4621
+ * first animation, seeded from `effect.read()` or the first keyframe, and
4622
+ * then bound to the subject via the effect for the rest of its life.
4623
+ */
4624
+ function animateEffectSubject(effect, subject, keyframes, transition = {}) {
4625
+ const animations = [];
4626
+ for (const key in keyframes) {
4627
+ const target = keyframes[key];
4628
+ let value = effect.get(subject, key);
4629
+ if (!value) {
4630
+ const initial = effect.read(subject, key, target) ?? firstKeyframe(target);
4631
+ exports.invariant(initial !== undefined, `"${key}" can't be read from the animated subject. Provide [from, to] keyframes.`, "effect-unreadable-value");
4632
+ value = motionValue(initial);
4633
+ effect(subject, { [key]: value });
4634
+ }
4635
+ value.start(animateMotionValue(key, value, target, getValueTransition$1(transition, key)));
4636
+ value.animation && animations.push(value.animation);
4637
+ }
4638
+ return animations;
4639
+ }
4640
+ function firstKeyframe(target) {
4641
+ const first = Array.isArray(target) ? target[0] : undefined;
4642
+ return first === null ? undefined : first;
4643
+ }
4644
+
4615
4645
  function resolveElements(elementOrSelector, scope, selectorCache) {
4616
4646
  if (elementOrSelector == null) {
4617
4647
  return [];
@@ -4656,7 +4686,13 @@
4656
4686
  };
4657
4687
 
4658
4688
  class MotionValueState {
4659
- constructor() {
4689
+ /**
4690
+ * @param step - The frameloop step renders are scheduled in. Defaults
4691
+ * to `frame.render`. Effects that feed a render loop running in
4692
+ * `frame.render` (GPU scenes) should write in `frame.preRender`.
4693
+ */
4694
+ constructor(step = frame.render) {
4695
+ this.step = step;
4660
4696
  this.latest = {};
4661
4697
  this.values = new Map();
4662
4698
  }
@@ -4673,7 +4709,7 @@
4673
4709
  else {
4674
4710
  this.latest[name] = v;
4675
4711
  }
4676
- render && frame.render(render);
4712
+ render && this.step(render);
4677
4713
  };
4678
4714
  onChange();
4679
4715
  const cancelOnChange = value.on("change", onChange);
@@ -4692,10 +4728,10 @@
4692
4728
  }
4693
4729
  }
4694
4730
 
4695
- function createEffect(addValue) {
4731
+ function createEffect(addValue, { step, ...options } = {}) {
4696
4732
  const stateCache = new WeakMap();
4697
- return (subject, values) => {
4698
- const state = stateCache.get(subject) ?? new MotionValueState();
4733
+ const effect = (subject, values) => {
4734
+ const state = stateCache.get(subject) ?? new MotionValueState(step);
4699
4735
  stateCache.set(subject, state);
4700
4736
  const subscriptions = [];
4701
4737
  for (const key in values) {
@@ -4708,6 +4744,9 @@
4708
4744
  cancel();
4709
4745
  };
4710
4746
  };
4747
+ return Object.assign(effect, options, {
4748
+ get: (subject, key) => stateCache.get(subject)?.get(key),
4749
+ });
4711
4750
  }
4712
4751
 
4713
4752
  function canSetAsProperty(element, name) {
@@ -11533,13 +11572,6 @@
11533
11572
  exports.invariant(Boolean(numSubjects), "No valid elements provided.", "no-valid-elements");
11534
11573
  for (let i = 0; i < numSubjects; i++) {
11535
11574
  const thisSubject = subjects[i];
11536
- const createVisualElement = thisSubject instanceof Element
11537
- ? createDOMVisualElement
11538
- : createObjectVisualElement;
11539
- if (!visualElementStore.has(thisSubject)) {
11540
- createVisualElement(thisSubject);
11541
- }
11542
- const visualElement = visualElementStore.get(thisSubject);
11543
11575
  const transition = { ...options };
11544
11576
  /**
11545
11577
  * Resolve stagger function if provided.
@@ -11548,6 +11580,23 @@
11548
11580
  typeof transition.delay === "function") {
11549
11581
  transition.delay = transition.delay(i, numSubjects);
11550
11582
  }
11583
+ const isElement = thisSubject instanceof Element;
11584
+ /**
11585
+ * Registered effects (animate.addEffect) claim non-DOM subjects
11586
+ * before we fall back to treating them as plain objects.
11587
+ */
11588
+ const effect = isElement ? undefined : findEffect(thisSubject);
11589
+ if (effect) {
11590
+ animations.push(...animateEffectSubject(effect, thisSubject, keyframes, transition));
11591
+ continue;
11592
+ }
11593
+ const createVisualElement = isElement
11594
+ ? createDOMVisualElement
11595
+ : createObjectVisualElement;
11596
+ if (!visualElementStore.has(thisSubject)) {
11597
+ createVisualElement(thisSubject);
11598
+ }
11599
+ const visualElement = visualElementStore.get(thisSubject);
11551
11600
  animations.push(...animateTarget(visualElement, { ...keyframes, transition }, {}));
11552
11601
  }
11553
11602
  }
@@ -11633,7 +11682,16 @@
11633
11682
  }
11634
11683
  return scopedAnimate;
11635
11684
  }
11636
- const animate = createScopedAnimate();
11685
+ const animate = Object.assign(createScopedAnimate(), {
11686
+ /**
11687
+ * Register an effect so `animate()` can animate the subjects it
11688
+ * claims, for instance `animate.addEffect(threeEffect)`. The most
11689
+ * recently added effect is tested first. DOM elements are always
11690
+ * animated directly.
11691
+ */
11692
+ addEffect,
11693
+ removeEffect,
11694
+ });
11637
11695
 
11638
11696
  function animateElements(elementOrSelector, keyframes, options, scope) {
11639
11697
  // Gracefully handle null/undefined elements (e.g., from querySelector returning null)
@@ -12408,6 +12466,7 @@
12408
12466
  exports.LayoutAnimationBuilder = LayoutAnimationBuilder;
12409
12467
  exports.MotionGlobalConfig = MotionGlobalConfig;
12410
12468
  exports.MotionValue = MotionValue;
12469
+ exports.MotionValueState = MotionValueState;
12411
12470
  exports.NativeAnimation = NativeAnimation;
12412
12471
  exports.NativeAnimationExtended = NativeAnimationExtended;
12413
12472
  exports.NativeAnimationWrapper = NativeAnimationWrapper;
@@ -12420,6 +12479,7 @@
12420
12479
  exports.acceleratedValues = acceleratedValues;
12421
12480
  exports.addAttrValue = addAttrValue;
12422
12481
  exports.addDomEvent = addDomEvent;
12482
+ exports.addEffect = addEffect;
12423
12483
  exports.addScaleCorrector = addScaleCorrector;
12424
12484
  exports.addStyleValue = addStyleValue;
12425
12485
  exports.addUniqueItem = addUniqueItem;
@@ -12427,6 +12487,7 @@
12427
12487
  exports.alpha = alpha;
12428
12488
  exports.analyseComplexValue = analyseComplexValue;
12429
12489
  exports.animate = animate;
12490
+ exports.animateEffectSubject = animateEffectSubject;
12430
12491
  exports.animateMini = animateMini;
12431
12492
  exports.animateMotionValue = animateMotionValue;
12432
12493
  exports.animateSingleValue = animateSingleValue;
@@ -12499,6 +12560,7 @@
12499
12560
  exports.createAxisDelta = createAxisDelta;
12500
12561
  exports.createBox = createBox;
12501
12562
  exports.createDelta = createDelta;
12563
+ exports.createEffect = createEffect;
12502
12564
  exports.createGeneratorEasing = createGeneratorEasing;
12503
12565
  exports.createProjectionNode = createProjectionNode;
12504
12566
  exports.createRenderBatcher = createRenderBatcher;
@@ -12523,6 +12585,7 @@
12523
12585
  exports.fillOffset = fillOffset;
12524
12586
  exports.fillWildcards = fillWildcards;
12525
12587
  exports.findDimensionValueType = findDimensionValueType;
12588
+ exports.findEffect = findEffect;
12526
12589
  exports.findValueType = findValueType;
12527
12590
  exports.flushKeyframeResolvers = flushKeyframeResolvers;
12528
12591
  exports.followValue = followValue;
@@ -12641,6 +12704,7 @@
12641
12704
  exports.removeAxisDelta = removeAxisDelta;
12642
12705
  exports.removeAxisTransforms = removeAxisTransforms;
12643
12706
  exports.removeBoxTransforms = removeBoxTransforms;
12707
+ exports.removeEffect = removeEffect;
12644
12708
  exports.removeItem = removeItem;
12645
12709
  exports.removePointDelta = removePointDelta;
12646
12710
  exports.renderHTML = renderHTML;