@unrulysystems/native-motion 0.1.0-alpha.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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/android/build.gradle +24 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/GestureExclusionModule.kt +55 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/NativeMotionPackage.kt +27 -0
- package/package.json +57 -0
- package/react-native.config.js +14 -0
- package/src/native/driver/instantWindow.ts +31 -0
- package/src/native/driver/strictModeReplay.ts +20 -0
- package/src/native/driver/uiLayoutEngine.ts +845 -0
- package/src/native/driver/uiLayoutGraph.ts +338 -0
- package/src/native/driver/uiValueChannel.ts +4640 -0
- package/src/native/driver/workletDriver.ts +3860 -0
- package/src/native/motion/AnimatePresence.tsx +1605 -0
- package/src/native/motion/LayoutGroup.tsx +163 -0
- package/src/native/motion/MotionConfig.tsx +400 -0
- package/src/native/motion/MotionRoot.tsx +250 -0
- package/src/native/motion/MotionView.tsx +6761 -0
- package/src/native/motion/addScaleCorrector.ts +97 -0
- package/src/native/motion/colorEndpointUiFeed.ts +32 -0
- package/src/native/motion/colorProjection.ts +39 -0
- package/src/native/motion/composeTransform.ts +271 -0
- package/src/native/motion/constraintMeasure.ts +157 -0
- package/src/native/motion/deferredPendingSnapshots.ts +25 -0
- package/src/native/motion/discreteProjection.ts +36 -0
- package/src/native/motion/dragAncestorPanContext.ts +28 -0
- package/src/native/motion/dragControls.ts +152 -0
- package/src/native/motion/dragGestureWiring.ts +1168 -0
- package/src/native/motion/dragHandoffBinding.ts +179 -0
- package/src/native/motion/dragHubStream.ts +756 -0
- package/src/native/motion/dragPropagationLock.ts +57 -0
- package/src/native/motion/driverValueChannel.ts +3288 -0
- package/src/native/motion/externalDragDriver.ts +214 -0
- package/src/native/motion/frameData.ts +31 -0
- package/src/native/motion/gestureBinding.ts +107 -0
- package/src/native/motion/gestureStateGate.ts +563 -0
- package/src/native/motion/gestureStateResolver.ts +286 -0
- package/src/native/motion/identityValueChannelControllerAdapter.ts +2043 -0
- package/src/native/motion/identityValueChannelControllerReconciliation.ts +176 -0
- package/src/native/motion/identityValueChannelLaneMarker.ts +14 -0
- package/src/native/motion/imperativeAnimate.ts +816 -0
- package/src/native/motion/keyframeTiming.ts +7 -0
- package/src/native/motion/layoutIdBinding.ts +261 -0
- package/src/native/motion/layoutIdFlightConfig.ts +51 -0
- package/src/native/motion/layoutProjection.ts +89 -0
- package/src/native/motion/layoutScroll.ts +204 -0
- package/src/native/motion/layoutTransition.ts +1302 -0
- package/src/native/motion/lengthLayoutContext.tsx +100 -0
- package/src/native/motion/lengthLayoutHost.ts +44 -0
- package/src/native/motion/mappedKeys.ts +184 -0
- package/src/native/motion/motionViewController.ts +3180 -0
- package/src/native/motion/nativeHostMarker.ts +22 -0
- package/src/native/motion/panSessionWiring.ts +127 -0
- package/src/native/motion/pathTransition.ts +101 -0
- package/src/native/motion/popLayout.ts +176 -0
- package/src/native/motion/presenceBinding.ts +382 -0
- package/src/native/motion/scaleCorrectorRegistry.ts +123 -0
- package/src/native/motion/scrollValues.ts +260 -0
- package/src/native/motion/serializablePayload.ts +151 -0
- package/src/native/motion/severity.ts +16 -0
- package/src/native/motion/shippedSurface.ts +843 -0
- package/src/native/motion/staticLengthGate.ts +86 -0
- package/src/native/motion/styleBaseGate.ts +72 -0
- package/src/native/motion/styleValueBinding.ts +657 -0
- package/src/native/motion/systemGestureExclusion.ts +61 -0
- package/src/native/motion/tapGestureWiring.ts +215 -0
- package/src/native/motion/transformOrder.ts +38 -0
- package/src/native/motion/transformStringBinding.ts +200 -0
- package/src/native/motion/transformTemplateGate.ts +45 -0
- package/src/native/motion/transitionGate.ts +435 -0
- package/src/native/motion/useAnimate.ts +52 -0
- package/src/native/motion/useCycle.ts +44 -0
- package/src/native/motion/useInstantTransition.ts +68 -0
- package/src/native/motion/useReducedMotion.ts +58 -0
- package/src/native/motion/useScroll.ts +162 -0
- package/src/native/motion/useViewportScroll.ts +29 -0
- package/src/native/motion/valueChannel.ts +7121 -0
- package/src/native/motion/valueHooks.ts +901 -0
- package/src/native/motion/variantChildRegistry.ts +67 -0
- package/src/native/motion/variantContext.tsx +172 -0
- package/src/native/motion/variantProps.ts +598 -0
- package/src/native.ts +159 -0
- package/src/verification/harnessMetrics.ts +78 -0
- package/src/verification/probe/LayoutIdentityWorkletProbe.tsx +251 -0
- package/src/verification/probe/WorkletParityProbe.tsx +162 -0
- package/src/verification/probe/layoutIdentityProbeEngine.ts +429 -0
- package/src/verification/screens/ArcPathChecksScreen.tsx +273 -0
- package/src/verification/screens/BooleanAnimateChecksScreen.tsx +358 -0
- package/src/verification/screens/ChoreographyGalleryScreen.tsx +1010 -0
- package/src/verification/screens/ColorBindingChecksScreen.tsx +568 -0
- package/src/verification/screens/CompletionChecksScreen.tsx +294 -0
- package/src/verification/screens/ConformanceScreen.tsx +573 -0
- package/src/verification/screens/ContentionProbeScreen.tsx +224 -0
- package/src/verification/screens/DriverSmokeScreen.tsx +99 -0
- package/src/verification/screens/DurationOnlyTweenChecksScreen.tsx +294 -0
- package/src/verification/screens/DynamicDragConfigChecksScreen.tsx +237 -0
- package/src/verification/screens/FrameDataChecksScreen.tsx +252 -0
- package/src/verification/screens/GestureChecksScreen.tsx +685 -0
- package/src/verification/screens/InstantTransitionChecksScreen.tsx +839 -0
- package/src/verification/screens/LayoutAnimationStartChecksScreen.tsx +648 -0
- package/src/verification/screens/LayoutChecksScreen.tsx +822 -0
- package/src/verification/screens/LayoutCommitSpikeScreen.tsx +142 -0
- package/src/verification/screens/MotionViewChecksScreen.tsx +930 -0
- package/src/verification/screens/PresenceChecksScreen.tsx +614 -0
- package/src/verification/screens/ReducedMotionChecksScreen.tsx +614 -0
- package/src/verification/screens/RestThresholdChecksScreen.tsx +634 -0
- package/src/verification/screens/ScaleCorrectorChecksScreen.tsx +425 -0
- package/src/verification/screens/SharedLayoutContinuityChecksScreen.tsx +338 -0
- package/src/verification/screens/SharedLayoutCrossfadeChecksScreen.tsx +425 -0
- package/src/verification/screens/TransitionDefaultSelectionChecksScreen.tsx +514 -0
- package/src/verification/screens/ViewportScrollAliasChecksScreen.tsx +303 -0
- package/src/verification/screens/conformanceBanner.ts +21 -0
- package/src/verification/screens/proofConsoleTap.ts +18 -0
- package/src/verification.ts +47 -0
- package/src/web.ts +98 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
// transition-default-selection device checks (findings F1/F5/F6) — how an under-specified flat
|
|
2
|
+
// property-lane transition bag resolves to a generator, proven on the committed-sample channel
|
|
3
|
+
// (onUpdate), not host paint. iOS has no tap injection, so the screen self-drives.
|
|
4
|
+
//
|
|
5
|
+
// The three arms, all against `motion@12.42.2`:
|
|
6
|
+
// F1 — a TYPED `{ type: 'tween', duration }` with no authored ease runs the pin's easeOut base
|
|
7
|
+
// `[0, 0, 0.58, 1]` (`animateMotionValue`'s options base, motion-value.ts:45-49), NOT the
|
|
8
|
+
// generator default `[0.25, 0.1, 0.35, 1]`. The sibling DurationOnlyTweenChecksScreen
|
|
9
|
+
// covers the UNTYPED arm; this is the typed one.
|
|
10
|
+
// F5 — `{ duration: 0 }` is INSTANT: the final value commits with no constructed trajectory
|
|
11
|
+
// (`makeAnimationInstant`, motion-value.ts:89-98). An authored `delay` is still honored.
|
|
12
|
+
// F6 — a flat ease-only bag is a 300ms keyframes tween carrying the authored ease; a flat
|
|
13
|
+
// orchestration-only bag merges the PER-KEY default, never the generic REQ-SPRING-003
|
|
14
|
+
// spring 100/10/1.
|
|
15
|
+
//
|
|
16
|
+
// THE F6 LEGS MUST MOUNT, NEVER RETARGET. F6's flat-arm merge lives in
|
|
17
|
+
// `motionViewController.perPropTransition`, which the controller consults only on START-shaped
|
|
18
|
+
// seams (mount entrance, array/color starts, paired/deferred flushes). The scalar UPDATE retarget
|
|
19
|
+
// lane does not consult it TODAY — and that gap is not a design law but finding F6-a, a DEFECT
|
|
20
|
+
// this unit's packet DECLARES DEFERRED. The distinction is load-bearing for whoever reads this
|
|
21
|
+
// next: the merge is unreachable at the retarget seam only until F6-a closes, at which point the
|
|
22
|
+
// lane does consult it and an F6 retarget row becomes valid (and owed). Until then, driving an F6
|
|
23
|
+
// leg as a retarget would exercise deferred behavior and fail legitimately, so each F6 leg is
|
|
24
|
+
// mounted on demand and its MOUNT is its start. Measured at the seam, a flat `{ delay: 0.2 }`
|
|
25
|
+
// resolves `{type:'spring', stiffness:500, damping:25, restSpeed:10, delay:0.2}` on the transform
|
|
26
|
+
// key `x`, and `{type:'keyframes', ease:[0.25,0.1,0.35,1], duration:0.3, delay:0.2}` on the
|
|
27
|
+
// non-transform key `opacity`.
|
|
28
|
+
//
|
|
29
|
+
// The per-key TRANSFORM spring is deliberately NOT a row: it settles in ~100ms, so under a 40ms dt
|
|
30
|
+
// clamp the whole flight is 2-3 frames and its 12.0% overshoot peak can be missed entirely — a
|
|
31
|
+
// band over it would flake on a loaded host instead of measuring the engine. The non-transform leg
|
|
32
|
+
// carries the same F6 per-key law over a 300ms flight that stays well sampled at every clamp, and
|
|
33
|
+
// discriminates just as sharply: the generic spring overshoots 16.3% (measured stable across
|
|
34
|
+
// dt 16.67/25/40) while a keyframes tween never exceeds its target at all.
|
|
35
|
+
//
|
|
36
|
+
// LOAD INDEPENDENCE is load-bearing here. The driver's dt CEILING (40ms; owner REQ-CLOCK-003,
|
|
37
|
+
// consumer REQ-DRIVER-012) stretches a
|
|
38
|
+
// flight under host load — the charter's floor cites a 1s flight reaching ~1.9s — so a mid-flight
|
|
39
|
+
// band keyed to a WALL-CLOCK offset reads a different PROGRESS on a busy host than on an idle one,
|
|
40
|
+
// and flakes. Every curve-shape band below is sampled at a fraction of the flight's OWN observed
|
|
41
|
+
// span (`atSpanFraction`), and every band was measured across a dt 16.67/25/40 sweep against the
|
|
42
|
+
// reference driver before being written down.
|
|
43
|
+
|
|
44
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
45
|
+
import { StyleSheet, Text, View } from 'react-native'
|
|
46
|
+
import { MotionRoot } from '../../native/motion/MotionRoot'
|
|
47
|
+
import { MotionView } from '../../native/motion/MotionView'
|
|
48
|
+
import { ambientNativeSeverity } from '../../native/motion/severity'
|
|
49
|
+
import { armProofConsoleTap } from './proofConsoleTap'
|
|
50
|
+
|
|
51
|
+
const ROW_F1_EASEOUT = 'F1: typed { type: "tween", duration } with no ease rides the easeOut base'
|
|
52
|
+
const ROW_F1_AUTHORED = 'F1 control: an authored ease spelling suppresses the easeOut base'
|
|
53
|
+
const ROW_F5_INSTANT = 'F5: { duration: 0 } commits the target with NO trajectory'
|
|
54
|
+
const ROW_F5_DELAY = 'F5: { duration: 0, delay } holds, then commits instantly'
|
|
55
|
+
const ROW_F6_FLAT_EASE = 'F6: a flat ease-only bag is a 300ms keyframes tween, not a spring'
|
|
56
|
+
const ROW_F6_FLAT_DELAY = 'F6: a flat delay-only bag merges the per-key default at MOUNT'
|
|
57
|
+
const ROW_CLEAN = 'clean run: zero unexpected reports'
|
|
58
|
+
const ROWS = [
|
|
59
|
+
ROW_F1_EASEOUT,
|
|
60
|
+
ROW_F1_AUTHORED,
|
|
61
|
+
ROW_F5_INSTANT,
|
|
62
|
+
ROW_F5_DELAY,
|
|
63
|
+
ROW_F6_FLAT_EASE,
|
|
64
|
+
ROW_F6_FLAT_DELAY,
|
|
65
|
+
ROW_CLEAN,
|
|
66
|
+
] as const
|
|
67
|
+
|
|
68
|
+
// Stage boundaries. Each leg is given far more than its nominal length so a dt-clamp stretch under
|
|
69
|
+
// host load lands inside its own window rather than being cut mid-flight (a truncated leg reads as
|
|
70
|
+
// a FAIL, which would be the harness lying about the engine).
|
|
71
|
+
const T_F1_START = 80
|
|
72
|
+
const T_F1_END = 2600
|
|
73
|
+
const T_F5_MOUNT = 2650
|
|
74
|
+
const T_F5_END = 4000
|
|
75
|
+
const T_F5_DELAY_MOUNT = 4050
|
|
76
|
+
const T_F5_DELAY_END = 5800
|
|
77
|
+
const T_F6_EASE_MOUNT = 5850
|
|
78
|
+
const T_F6_EASE_END = 7200
|
|
79
|
+
const T_F6_DELAY_MOUNT = 7250
|
|
80
|
+
const T_F6_DELAY_END = 8800
|
|
81
|
+
const T_CLEAN = 9000
|
|
82
|
+
|
|
83
|
+
const X_TARGET = 100
|
|
84
|
+
const OPACITY_TARGET = 1
|
|
85
|
+
|
|
86
|
+
interface RowResult {
|
|
87
|
+
readonly pass: boolean
|
|
88
|
+
readonly detail: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface Sample {
|
|
92
|
+
readonly value: number
|
|
93
|
+
readonly at: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The sample nearest `fraction` of the way through the flight's OWN observed span.
|
|
98
|
+
*
|
|
99
|
+
* t0 is the first sample off the origin — never the mount/command-edge sample, because on device
|
|
100
|
+
* the first onUpdate can carry the pre-flight value one crossing before the tween clock starts.
|
|
101
|
+
* tEnd is the first sample that has effectively arrived. Normalizing by the observed span is what
|
|
102
|
+
* makes the curve-shape bands immune to a dt-clamp stretch: a flight that takes 1.9s instead of
|
|
103
|
+
* 1.0s under load is still sampled at the same PROGRESS, because the stretch scales the flight.
|
|
104
|
+
*/
|
|
105
|
+
function atSpanFraction(
|
|
106
|
+
samples: readonly Sample[],
|
|
107
|
+
target: number,
|
|
108
|
+
fraction: number,
|
|
109
|
+
): Sample | null {
|
|
110
|
+
const first = samples.find((s) => s.value > target * 0.005)
|
|
111
|
+
if (first === undefined) return null
|
|
112
|
+
const arrived = samples.find((s) => s.value >= target * 0.995)
|
|
113
|
+
if (arrived === undefined) return null
|
|
114
|
+
const at = first.at + (arrived.at - first.at) * fraction
|
|
115
|
+
let best: Sample | null = null
|
|
116
|
+
for (const sample of samples) {
|
|
117
|
+
if (sample.at < first.at || sample.at > arrived.at) continue
|
|
118
|
+
if (best === null || Math.abs(sample.at - at) < Math.abs(best.at - at)) best = sample
|
|
119
|
+
}
|
|
120
|
+
return best
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Samples strictly between the origin and the target — the evidence that a trajectory was built. */
|
|
124
|
+
function interiorSamples(samples: readonly Sample[], target: number): readonly Sample[] {
|
|
125
|
+
return samples.filter((s) => s.value > target * 0.02 && s.value < target * 0.98)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function peakOf(samples: readonly Sample[]): number {
|
|
129
|
+
return samples.reduce((max, s) => Math.max(max, s.value), 0)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function TransitionDefaultSelectionChecksScreen() {
|
|
133
|
+
const severity = ambientNativeSeverity()
|
|
134
|
+
const [targetF1, setTargetF1] = useState<'a' | 'b'>('a')
|
|
135
|
+
const [targetF1Control, setTargetF1Control] = useState<'a' | 'b'>('a')
|
|
136
|
+
// The F5/F6 legs mount on demand: their MOUNT is the start seam F6's per-key merge requires
|
|
137
|
+
// (see the F6-a note in the header). A boolean per leg, never a retarget.
|
|
138
|
+
const [mountF5, setMountF5] = useState(false)
|
|
139
|
+
const [mountF5Delay, setMountF5Delay] = useState(false)
|
|
140
|
+
const [mountF6Ease, setMountF6Ease] = useState(false)
|
|
141
|
+
const [mountF6Delay, setMountF6Delay] = useState(false)
|
|
142
|
+
const [stage, setStage] = useState('boot')
|
|
143
|
+
const [rows, setRows] = useState<Partial<Record<(typeof ROWS)[number], RowResult>>>({})
|
|
144
|
+
|
|
145
|
+
const samplesF1 = useRef<Sample[]>([])
|
|
146
|
+
const samplesF1Control = useRef<Sample[]>([])
|
|
147
|
+
const samplesF5 = useRef<Sample[]>([])
|
|
148
|
+
const samplesF5Delay = useRef<Sample[]>([])
|
|
149
|
+
const samplesF6Ease = useRef<Sample[]>([])
|
|
150
|
+
const samplesF6Delay = useRef<Sample[]>([])
|
|
151
|
+
// The mount edge each on-demand leg is measured against — an instant commit and an honored
|
|
152
|
+
// delay are both only meaningful relative to when the leg actually mounted.
|
|
153
|
+
const mountedAtF5Delay = useRef(0)
|
|
154
|
+
const mountedAtF6Delay = useRef(0)
|
|
155
|
+
|
|
156
|
+
const reportsRef = useRef<string[]>([])
|
|
157
|
+
const restoreTapRef = useRef<null | (() => void)>(null)
|
|
158
|
+
if (restoreTapRef.current === null) {
|
|
159
|
+
restoreTapRef.current = armProofConsoleTap(reportsRef.current, (message) =>
|
|
160
|
+
/motion|transition|spring|tween/i.test(message),
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const finalize = (id: (typeof ROWS)[number], pass: boolean, detail: string): void => {
|
|
165
|
+
setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const pushX =
|
|
169
|
+
(sink: { current: Sample[] }) =>
|
|
170
|
+
(latest: Readonly<Record<string, string | number>>): void => {
|
|
171
|
+
if (typeof latest.x === 'number') sink.current.push({ value: latest.x, at: Date.now() })
|
|
172
|
+
}
|
|
173
|
+
const pushOpacity =
|
|
174
|
+
(sink: { current: Sample[] }) =>
|
|
175
|
+
(latest: Readonly<Record<string, string | number>>): void => {
|
|
176
|
+
if (typeof latest.opacity === 'number') {
|
|
177
|
+
sink.current.push({ value: latest.opacity, at: Date.now() })
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const onF1 = useCallback(pushX(samplesF1), [])
|
|
181
|
+
const onF1Control = useCallback(pushX(samplesF1Control), [])
|
|
182
|
+
const onF5 = useCallback(pushX(samplesF5), [])
|
|
183
|
+
const onF5Delay = useCallback(pushX(samplesF5Delay), [])
|
|
184
|
+
const onF6Ease = useCallback(pushX(samplesF6Ease), [])
|
|
185
|
+
const onF6Delay = useCallback(pushOpacity(samplesF6Delay), [])
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
const timers: ReturnType<typeof setTimeout>[] = []
|
|
189
|
+
const at = (ms: number, run: () => void): void => {
|
|
190
|
+
timers.push(setTimeout(run, ms))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
at(T_F1_START, () => {
|
|
194
|
+
setTargetF1('b')
|
|
195
|
+
setTargetF1Control('b')
|
|
196
|
+
setStage('F1 typed-tween legs started (no ease / authored linear)')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
at(T_F1_END, () => {
|
|
200
|
+
// Sampled at ONE FIFTH of the span, not the half. The easeOut and retired-default curves
|
|
201
|
+
// CROSS near f=0.35, so at the half they are only ~4.7 apart and a band admitting easeOut
|
|
202
|
+
// would also admit the regression. Measured across dt 16.67/25/40: easeOut 30.85..36.42,
|
|
203
|
+
// the retired [0.25,0.1,0.35,1] default 25.33..25.45, linear 21.67..24.00. The band admits
|
|
204
|
+
// easeOut at every clamp and excludes both alternatives.
|
|
205
|
+
const mid = atSpanFraction(samplesF1.current, X_TARGET, 0.2)
|
|
206
|
+
const pass = mid !== null && mid.value >= 28 && mid.value <= 40
|
|
207
|
+
finalize(
|
|
208
|
+
ROW_F1_EASEOUT,
|
|
209
|
+
pass,
|
|
210
|
+
mid === null
|
|
211
|
+
? `no arrival within the window (${samplesF1.current.length} samples)`
|
|
212
|
+
: `fifth-span sample reads ${mid.value.toFixed(1)} — easeOut 30.9..36.4, ` +
|
|
213
|
+
`the retired default 25.4, linear 21.7..24.0`,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
// The control reads cleanest at the HALF, where linear (50.0) is far from easeOut (66.7);
|
|
217
|
+
// its question is only whether an authored ease suppresses the base, so it does not need
|
|
218
|
+
// to separate linear from the retired default.
|
|
219
|
+
const control = atSpanFraction(samplesF1Control.current, X_TARGET, 0.5)
|
|
220
|
+
const controlPass = control !== null && control.value >= 44 && control.value <= 56
|
|
221
|
+
finalize(
|
|
222
|
+
ROW_F1_AUTHORED,
|
|
223
|
+
controlPass,
|
|
224
|
+
control === null
|
|
225
|
+
? `no arrival within the window (${samplesF1Control.current.length} samples)`
|
|
226
|
+
: `half-span sample reads ${control.value.toFixed(1)} — authored linear 50.0 ` +
|
|
227
|
+
`(the easeOut base is suppressed, never merged; easeOut would read 66.7)`,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
setStage(pass && controlPass ? 'F1 pass; F5 instant leg mounts' : 'F1 FAIL; F5 leg mounts')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
at(T_F5_MOUNT, () => {
|
|
234
|
+
setMountF5(true)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
at(T_F5_END, () => {
|
|
238
|
+
const samples = samplesF5.current
|
|
239
|
+
const interior = interiorSamples(samples, X_TARGET)
|
|
240
|
+
const arrived = samples.some((s) => s.value >= X_TARGET * 0.995)
|
|
241
|
+
// An instant builds NO trajectory: the final value commits and nothing lands between the
|
|
242
|
+
// origin and the target. Interior samples would mean a constructed animation.
|
|
243
|
+
const pass = arrived && interior.length === 0
|
|
244
|
+
finalize(
|
|
245
|
+
ROW_F5_INSTANT,
|
|
246
|
+
pass,
|
|
247
|
+
`${interior.length} interior samples (an instant builds none), arrived=${String(arrived)}, ` +
|
|
248
|
+
`${samples.length} samples total`,
|
|
249
|
+
)
|
|
250
|
+
setStage(pass ? 'F5 instant pass; delayed-instant mounts' : 'F5 FAIL; delay leg mounts')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
at(T_F5_DELAY_MOUNT, () => {
|
|
254
|
+
mountedAtF5Delay.current = Date.now()
|
|
255
|
+
setMountF5Delay(true)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
at(T_F5_DELAY_END, () => {
|
|
259
|
+
const samples = samplesF5Delay.current
|
|
260
|
+
const interior = interiorSamples(samples, X_TARGET)
|
|
261
|
+
const arrival = samples.find((s) => s.value >= X_TARGET * 0.995)
|
|
262
|
+
const heldMs = arrival === undefined ? 0 : arrival.at - mountedAtF5Delay.current
|
|
263
|
+
// The authored 500ms delay is honored, then the commit is still instant. Measured 500..525ms
|
|
264
|
+
// across the dt sweep; the floor is generous below because a stretch only ever makes the
|
|
265
|
+
// hold LONGER, and the mount edge adds host latency on top.
|
|
266
|
+
const pass = arrival !== undefined && interior.length === 0 && heldMs >= 350
|
|
267
|
+
finalize(
|
|
268
|
+
ROW_F5_DELAY,
|
|
269
|
+
pass,
|
|
270
|
+
arrival === undefined
|
|
271
|
+
? `never committed (${samples.length} samples)`
|
|
272
|
+
: `held ${heldMs}ms from mount before committing (authored delay 500ms), ` +
|
|
273
|
+
`${interior.length} interior samples`,
|
|
274
|
+
)
|
|
275
|
+
setStage(pass ? 'F5 delay pass; F6 flat-ease mounts' : 'F5 delay FAIL; F6 ease mounts')
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
at(T_F6_EASE_MOUNT, () => {
|
|
279
|
+
setMountF6Ease(true)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
at(T_F6_EASE_END, () => {
|
|
283
|
+
// A flat `{ ease: 'linear' }` bag is DEFINED-but-typeless: the keyframes generator at its
|
|
284
|
+
// 300ms default, carrying the authored ease. Measured peak exactly 1.0000x target and span
|
|
285
|
+
// 275..283ms across the dt sweep. The generic spring would overshoot; linear never does.
|
|
286
|
+
const samples = samplesF6Ease.current
|
|
287
|
+
const mid = atSpanFraction(samples, X_TARGET, 0.5)
|
|
288
|
+
const peak = peakOf(samples)
|
|
289
|
+
const pass = mid !== null && mid.value >= 42 && mid.value <= 58 && peak <= X_TARGET * 1.005
|
|
290
|
+
finalize(
|
|
291
|
+
ROW_F6_FLAT_EASE,
|
|
292
|
+
pass,
|
|
293
|
+
mid === null
|
|
294
|
+
? `no arrival within the window (${samples.length} samples)`
|
|
295
|
+
: `half-span sample reads ${mid.value.toFixed(1)} (linear 50), peak ` +
|
|
296
|
+
`${peak.toFixed(2)} — a spring would overshoot the target`,
|
|
297
|
+
)
|
|
298
|
+
setStage(pass ? 'F6 ease pass; F6 flat-delay mounts' : 'F6 ease FAIL; delay leg mounts')
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
at(T_F6_DELAY_MOUNT, () => {
|
|
302
|
+
mountedAtF6Delay.current = Date.now()
|
|
303
|
+
setMountF6Delay(true)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
at(T_F6_DELAY_END, () => {
|
|
307
|
+
// A flat `{ delay: 0.2 }` bag on the NON-TRANSFORM key `opacity` merges the per-key default
|
|
308
|
+
// `{type:'keyframes', ease:[0.25,0.1,0.35,1], duration:0.3, delay:0.2}` at the mount seam.
|
|
309
|
+
// The discriminator is binary and clock-free: a keyframes tween never exceeds its target,
|
|
310
|
+
// while the generic REQ-SPRING-003 spring the pre-F6 backstop resolved overshoots 16.3%
|
|
311
|
+
// (measured stable at dt 16.67/25/40). The delay must also survive the merge.
|
|
312
|
+
const samples = samplesF6Delay.current
|
|
313
|
+
const peak = peakOf(samples)
|
|
314
|
+
const arrival = samples.find((s) => s.value > OPACITY_TARGET * 0.02)
|
|
315
|
+
const delayedMs = arrival === undefined ? 0 : arrival.at - mountedAtF6Delay.current
|
|
316
|
+
const arrived = samples.some((s) => s.value >= OPACITY_TARGET * 0.995)
|
|
317
|
+
const pass = arrived && peak <= OPACITY_TARGET * 1.005 && delayedMs >= 120
|
|
318
|
+
finalize(
|
|
319
|
+
ROW_F6_FLAT_DELAY,
|
|
320
|
+
pass,
|
|
321
|
+
`peak ${peak.toFixed(4)} — the per-key 0.3s tween never exceeds ${OPACITY_TARGET}, ` +
|
|
322
|
+
`the generic spring peaks 1.163; flight began ${delayedMs}ms after mount ` +
|
|
323
|
+
`(authored delay 200ms), arrived=${String(arrived)}`,
|
|
324
|
+
)
|
|
325
|
+
setStage(pass ? 'F6 delay pass' : 'F6 delay FAIL')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
at(T_CLEAN, () => {
|
|
329
|
+
const pass = reportsRef.current.length === 0
|
|
330
|
+
finalize(
|
|
331
|
+
ROW_CLEAN,
|
|
332
|
+
pass,
|
|
333
|
+
pass
|
|
334
|
+
? '0 unexpected reports'
|
|
335
|
+
: `${reportsRef.current.length} unexpected: ${reportsRef.current
|
|
336
|
+
.map((message) => message.slice(0, 80))
|
|
337
|
+
.join(' | ')}`,
|
|
338
|
+
)
|
|
339
|
+
setStage('done')
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
return () => {
|
|
343
|
+
for (const timer of timers) clearTimeout(timer)
|
|
344
|
+
restoreTapRef.current?.()
|
|
345
|
+
restoreTapRef.current = null
|
|
346
|
+
}
|
|
347
|
+
}, [])
|
|
348
|
+
|
|
349
|
+
const done = ROWS.every((id) => rows[id] !== undefined)
|
|
350
|
+
const failing = ROWS.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
|
|
351
|
+
const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
|
|
352
|
+
const passed = Object.keys(rows).length - failing.length
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<MotionRoot>
|
|
356
|
+
<View style={styles.scroll}>
|
|
357
|
+
<View style={styles.container}>
|
|
358
|
+
<Text style={styles.title}>transition-default-selection checks</Text>
|
|
359
|
+
<Text style={styles.subtitle}>
|
|
360
|
+
F1 easeOut base · F5 duration-zero instant · F6 flat sparse-bag ({severity} severity)
|
|
361
|
+
</Text>
|
|
362
|
+
<View
|
|
363
|
+
style={[
|
|
364
|
+
styles.banner,
|
|
365
|
+
verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
|
|
366
|
+
]}
|
|
367
|
+
>
|
|
368
|
+
<Text style={styles.bannerText}>{verdict}</Text>
|
|
369
|
+
</View>
|
|
370
|
+
<Text style={styles.tally}>
|
|
371
|
+
{done
|
|
372
|
+
? `${passed} PASS / ${failing.length} FAIL of ${ROWS.length} rows`
|
|
373
|
+
: `${Object.keys(rows).length}/${ROWS.length} rows complete — RUNNING`}
|
|
374
|
+
</Text>
|
|
375
|
+
<Text style={styles.tallyDetail}>
|
|
376
|
+
{done
|
|
377
|
+
? failing.length === 0
|
|
378
|
+
? 'no failing rows'
|
|
379
|
+
: `FAILING: ${failing.join(' | ')}`
|
|
380
|
+
: `stage: ${stage}`}
|
|
381
|
+
</Text>
|
|
382
|
+
|
|
383
|
+
<View style={styles.track}>
|
|
384
|
+
<MotionView
|
|
385
|
+
animate={targetF1 === 'a' ? { x: 0 } : { x: X_TARGET }}
|
|
386
|
+
transition={{ type: 'tween', duration: 1 }}
|
|
387
|
+
onUpdate={onF1}
|
|
388
|
+
style={styles.box}
|
|
389
|
+
accessibilityLabel="tds-f1-easeout-box"
|
|
390
|
+
/>
|
|
391
|
+
<MotionView
|
|
392
|
+
animate={targetF1Control === 'a' ? { x: 0 } : { x: X_TARGET }}
|
|
393
|
+
transition={{ type: 'tween', duration: 1, ease: 'linear' }}
|
|
394
|
+
onUpdate={onF1Control}
|
|
395
|
+
style={[styles.box, styles.boxControl]}
|
|
396
|
+
accessibilityLabel="tds-f1-authored-box"
|
|
397
|
+
/>
|
|
398
|
+
{mountF5 ? (
|
|
399
|
+
<MotionView
|
|
400
|
+
initial={{ x: 0 }}
|
|
401
|
+
animate={{ x: X_TARGET }}
|
|
402
|
+
transition={{ duration: 0 }}
|
|
403
|
+
onUpdate={onF5}
|
|
404
|
+
style={[styles.box, styles.boxInstant]}
|
|
405
|
+
accessibilityLabel="tds-f5-instant-box"
|
|
406
|
+
/>
|
|
407
|
+
) : (
|
|
408
|
+
<View style={[styles.box, styles.boxIdle]} />
|
|
409
|
+
)}
|
|
410
|
+
{mountF5Delay ? (
|
|
411
|
+
<MotionView
|
|
412
|
+
initial={{ x: 0 }}
|
|
413
|
+
animate={{ x: X_TARGET }}
|
|
414
|
+
transition={{ duration: 0, delay: 0.5 }}
|
|
415
|
+
onUpdate={onF5Delay}
|
|
416
|
+
style={[styles.box, styles.boxInstant]}
|
|
417
|
+
accessibilityLabel="tds-f5-delay-box"
|
|
418
|
+
/>
|
|
419
|
+
) : (
|
|
420
|
+
<View style={[styles.box, styles.boxIdle]} />
|
|
421
|
+
)}
|
|
422
|
+
{mountF6Ease ? (
|
|
423
|
+
<MotionView
|
|
424
|
+
initial={{ x: 0 }}
|
|
425
|
+
animate={{ x: X_TARGET }}
|
|
426
|
+
transition={{ ease: 'linear' }}
|
|
427
|
+
onUpdate={onF6Ease}
|
|
428
|
+
style={[styles.box, styles.boxFlat]}
|
|
429
|
+
accessibilityLabel="tds-f6-flatease-box"
|
|
430
|
+
/>
|
|
431
|
+
) : (
|
|
432
|
+
<View style={[styles.box, styles.boxIdle]} />
|
|
433
|
+
)}
|
|
434
|
+
{mountF6Delay ? (
|
|
435
|
+
<MotionView
|
|
436
|
+
initial={{ opacity: 0 }}
|
|
437
|
+
animate={{ opacity: OPACITY_TARGET }}
|
|
438
|
+
transition={{ delay: 0.2 }}
|
|
439
|
+
onUpdate={onF6Delay}
|
|
440
|
+
style={[styles.box, styles.boxFlat]}
|
|
441
|
+
accessibilityLabel="tds-f6-flatdelay-box"
|
|
442
|
+
/>
|
|
443
|
+
) : (
|
|
444
|
+
<View style={[styles.box, styles.boxIdle]} />
|
|
445
|
+
)}
|
|
446
|
+
</View>
|
|
447
|
+
|
|
448
|
+
{ROWS.map((id) => {
|
|
449
|
+
const row = rows[id]
|
|
450
|
+
return (
|
|
451
|
+
<View key={id} style={styles.row}>
|
|
452
|
+
<Text
|
|
453
|
+
style={[
|
|
454
|
+
styles.verdict,
|
|
455
|
+
row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
|
|
456
|
+
]}
|
|
457
|
+
>
|
|
458
|
+
{row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
|
|
459
|
+
</Text>
|
|
460
|
+
<View style={styles.rowBody}>
|
|
461
|
+
<Text style={styles.rowId}>{id}</Text>
|
|
462
|
+
<Text style={styles.rowDetail} numberOfLines={3}>
|
|
463
|
+
{row === undefined ? 'pending' : row.detail}
|
|
464
|
+
</Text>
|
|
465
|
+
</View>
|
|
466
|
+
</View>
|
|
467
|
+
)
|
|
468
|
+
})}
|
|
469
|
+
</View>
|
|
470
|
+
</View>
|
|
471
|
+
</MotionRoot>
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const styles = StyleSheet.create({
|
|
476
|
+
scroll: { flex: 1, backgroundColor: '#0b0b12' },
|
|
477
|
+
container: { paddingVertical: 52, paddingHorizontal: 8 },
|
|
478
|
+
title: { color: '#fff', fontSize: 20, fontWeight: '700', textAlign: 'center' },
|
|
479
|
+
subtitle: { color: '#8a8aa0', fontSize: 11, marginTop: 6, textAlign: 'center' },
|
|
480
|
+
banner: {
|
|
481
|
+
alignSelf: 'center',
|
|
482
|
+
paddingHorizontal: 36,
|
|
483
|
+
paddingVertical: 8,
|
|
484
|
+
borderRadius: 10,
|
|
485
|
+
marginTop: 14,
|
|
486
|
+
marginBottom: 8,
|
|
487
|
+
},
|
|
488
|
+
pass: { backgroundColor: '#1d7a3e' },
|
|
489
|
+
fail: { backgroundColor: '#a32633' },
|
|
490
|
+
running: { backgroundColor: '#7a641d' },
|
|
491
|
+
bannerText: { color: '#fff', fontSize: 20, fontWeight: '800', letterSpacing: 2 },
|
|
492
|
+
tally: { color: '#e6e6f0', fontSize: 13, fontWeight: '700', textAlign: 'center' },
|
|
493
|
+
tallyDetail: { color: '#f87171', fontSize: 10, marginTop: 3, textAlign: 'center' },
|
|
494
|
+
track: { marginTop: 8, marginBottom: 4 },
|
|
495
|
+
box: {
|
|
496
|
+
width: 24,
|
|
497
|
+
height: 24,
|
|
498
|
+
marginTop: 5,
|
|
499
|
+
backgroundColor: '#ef4444',
|
|
500
|
+
borderRadius: 6,
|
|
501
|
+
},
|
|
502
|
+
boxControl: { backgroundColor: '#38bdf8' },
|
|
503
|
+
boxInstant: { backgroundColor: '#a78bfa' },
|
|
504
|
+
boxFlat: { backgroundColor: '#f97316' },
|
|
505
|
+
boxIdle: { backgroundColor: '#23233a' },
|
|
506
|
+
row: { flexDirection: 'row', marginTop: 8 },
|
|
507
|
+
verdict: { width: 46, fontSize: 11, fontWeight: '800', paddingTop: 1 },
|
|
508
|
+
ok: { color: '#4ade80' },
|
|
509
|
+
bad: { color: '#f87171' },
|
|
510
|
+
pending: { color: '#8a8aa0' },
|
|
511
|
+
rowBody: { flex: 1 },
|
|
512
|
+
rowId: { color: '#e6e6f0', fontSize: 12, fontWeight: '600' },
|
|
513
|
+
rowDetail: { color: '#6f6f85', fontSize: 10, marginTop: 2 },
|
|
514
|
+
})
|