Пользовательский результат
Физически корректная пружина описывается и вычисляется без скрытого изменения намерения. Ограничения frame-loop, compositor-компилятора и finite easing применяются только соответствующим исполнителем, а не маскируются под общую «валидность spring».
Подтверждённая проблема
Сейчас validateSpringParams() смешивает два разных закона:
- физическая валидность — конечные
mass > 0, stiffness > 0, damping >= 0;
- представимость конкретным frame-loop — аналитическое время оседания должно уложиться в
MAX_FRAMES * FIXED_DT_S ≈ 33.3 s.
Из-за этого чистый аналитический spring(params, t) отвергает медленные или незатухающие, но математически корректные системы, хотя их значение в произвольный момент вычисляется замкнутой формой без scheduler.
fromBounce() и fromVisualDuration() дополнительно скрыто меняют параметры, чтобы пройти тот же executor-budget:
- точное преобразование
duration=100, bounce=0, mass=1 даёт ω₀=2π/100, ζ=1, k≈0.00394784176, c≈0.12566370614; текущий путь ускоряет систему до другого ω₀;
bounce=1 математически означает ζ=0, то есть damping=0; текущий путь превращает запрос в сильно ускоренную слабо затухающую систему, чтобы она искусственно «успела» осесть;
- такое преобразование не является ни SwiftUI/Motion mapping, ни физической валидацией и не обратимо.
Математический закон
Канонические координаты формы движения:
ω₀ = sqrt(k / m)
ζ = c / (2 sqrt(km))
v₀ = initial velocity
Для duration + bounce:
ω₀ = 2π / duration
ζ = 1 - bounce
k = m ω₀²
c = 2 m ζ ω₀
Это точное преобразование. Масштабирование (m,k,c) -> (λm,λk,λc) не меняет ω₀, ζ и траекторию. Следовательно, mass в этой параметризации не является независимой perceptual-ручкой «тяжести».
Требуемая архитектура
1. Физическая граница
Ввести одну узкую проверку физического домена, например:
validateSpringPhysics(params)
Она проверяет только:
- конечный
mass > 0;
- конечный
stiffness > 0;
- конечный
damping >= 0.
Публичный чистый spring(params, t) использует только эту границу и должен вычислять любую физически валидную систему, включая:
- очень медленную;
damping = 0;
- систему, которая не имеет конечного settle horizon.
2. Executor-specific representability
Отдельные границы принадлежат исполнителям:
drive / MotionValue — может ли автономный frame-loop гарантированно завершиться в своём бюджете;
- compositor/compiler — помещается ли кривая в
BASE_GRID_MAX и допустимый синхронный compile-cost;
- finite easing — существует ли конечный нормализованный горизонт;
- прямой аналитический sampler — не имеет frame-budget.
Не создавать второй общий «валидатор на всё». Каждый executor использует собственный предикат/ошибку рядом со своей политикой.
3. Точные constructors
fromBounce() и fromVisualDuration() обязаны:
- возвращать точное математическое преобразование запрошенных координат;
- не изменять
duration, visualDuration, bounce, ζ или ω₀ ради runtime-budget;
- сохранять точные scale-invariance и round-trip свойства;
- для
bounce=1 честно возвращать damping=0.
Если продукту действительно нужен coercion-helper, он должен быть отдельным явно названным API и возвращать metadata о том, что изменено. Он не входит в constructors по умолчанию и не должен появиться без отдельного доказанного consumer-case.
4. Ошибки и migration
- сохранить структурированные ошибки;
- разделить «нефизические параметры» и «этот executor не может гарантированно завершить систему»;
- обновить error catalog и migration note как pre-1.0 behavioral correction;
- не добавлять semantic presets
light/heavy/playful в engine.
RED / доказательства
- Characterization: обычные текущие параметры, которые не подвергаются coercion, остаются bit-exact.
- Exact mapping: property-тесты
duration/bounce/mass -> ω₀/ζ -> k/c на широком конечном домене.
- Scale invariance: для конечного
λ>0 траектории (m,k,c) и (λm,λk,λc) совпадают в ULP-допуске.
- Edge:
bounce=1 возвращает damping=0; чистый spring() вычисляет конечные value/velocity, а finite/live executor fail-fast с executor-specific причиной.
- Slow spring: точный
spring({m:1,k:1,c:1}, t) доступен; автономный runner отдельно решает, допускает ли его lifecycle-budget.
- No hidden mutation: constructor output сравнивается с независимой формулой, а не с production helper.
- Fuzz/mutation: три режима +
ζ=0, scale extremes, near-critical и very-slow cases; мутанты, возвращающие старый clamp/coercion, обязаны погибать.
- Browser: runtime-paths, которых касается разделение policy, проходят Chromium/Firefox/WebKit; чистая математика остаётся headless.
Производительность и размер
- per-frame путь не получает новых проверок;
- параметры валидируются один раз на executor-boundary;
nano остаётся byte-identical и <=1024 B gzip;
- существующие root/animate/compositor thresholds не повышаются;
- удаление общего settle-check из чистого sampler не должно ухудшить import cost.
Не входит
Связано: #93, #105, #106.
Пользовательский результат
Физически корректная пружина описывается и вычисляется без скрытого изменения намерения. Ограничения frame-loop, compositor-компилятора и finite easing применяются только соответствующим исполнителем, а не маскируются под общую «валидность spring».
Подтверждённая проблема
Сейчас
validateSpringParams()смешивает два разных закона:mass > 0,stiffness > 0,damping >= 0;MAX_FRAMES * FIXED_DT_S ≈ 33.3 s.Из-за этого чистый аналитический
spring(params, t)отвергает медленные или незатухающие, но математически корректные системы, хотя их значение в произвольный момент вычисляется замкнутой формой без scheduler.fromBounce()иfromVisualDuration()дополнительно скрыто меняют параметры, чтобы пройти тот же executor-budget:duration=100, bounce=0, mass=1даётω₀=2π/100,ζ=1,k≈0.00394784176,c≈0.12566370614; текущий путь ускоряет систему до другогоω₀;bounce=1математически означаетζ=0, то естьdamping=0; текущий путь превращает запрос в сильно ускоренную слабо затухающую систему, чтобы она искусственно «успела» осесть;Математический закон
Канонические координаты формы движения:
Для
duration + bounce:Это точное преобразование. Масштабирование
(m,k,c) -> (λm,λk,λc)не меняетω₀,ζи траекторию. Следовательно,massв этой параметризации не является независимой perceptual-ручкой «тяжести».Требуемая архитектура
1. Физическая граница
Ввести одну узкую проверку физического домена, например:
Она проверяет только:
mass > 0;stiffness > 0;damping >= 0.Публичный чистый
spring(params, t)использует только эту границу и должен вычислять любую физически валидную систему, включая:damping = 0;2. Executor-specific representability
Отдельные границы принадлежат исполнителям:
drive/MotionValue— может ли автономный frame-loop гарантированно завершиться в своём бюджете;BASE_GRID_MAXи допустимый синхронный compile-cost;Не создавать второй общий «валидатор на всё». Каждый executor использует собственный предикат/ошибку рядом со своей политикой.
3. Точные constructors
fromBounce()иfromVisualDuration()обязаны:duration,visualDuration,bounce,ζилиω₀ради runtime-budget;bounce=1честно возвращатьdamping=0.Если продукту действительно нужен coercion-helper, он должен быть отдельным явно названным API и возвращать metadata о том, что изменено. Он не входит в constructors по умолчанию и не должен появиться без отдельного доказанного consumer-case.
4. Ошибки и migration
light/heavy/playfulв engine.RED / доказательства
duration/bounce/mass -> ω₀/ζ -> k/cна широком конечном домене.λ>0траектории(m,k,c)и(λm,λk,λc)совпадают в ULP-допуске.bounce=1возвращаетdamping=0; чистыйspring()вычисляет конечныеvalue/velocity, а finite/live executor fail-fast с executor-specific причиной.spring({m:1,k:1,c:1}, t)доступен; автономный runner отдельно решает, допускает ли его lifecycle-budget.ζ=0, scale extremes, near-critical и very-slow cases; мутанты, возвращающие старый clamp/coercion, обязаны погибать.Производительность и размер
nanoостаётся byte-identical и<=1024 B gzip;Не входит
Связано: #93, #105, #106.