UNPKG

one

Version:

One is a React framework for web, iOS, and Android apps.

158 lines (141 loc) • 7.15 kB
import type { OneNativeStyle } from './generated/controlTypes' // a PhaseAnimator or KeyframeAnimator child's swiftStyle carries every evaluation // of the animator's content closure under this key. the visible fields are the // first evaluation's, so callbacks and any reader that does not know about // animators see a plain style. const animatorKey = Symbol.for('one.native.animator') export type AnimatorStyle = | { id: number; kind: 'phase'; owner: string; samples: readonly OneNativeStyle[] } | { id: number kind: 'keyframe' owner: string samples: readonly OneNativeStyle[] // the keyframe value each sample was evaluated at. values: readonly (readonly number[])[] } export function animatedSwiftStyle(animator: AnimatorStyle): OneNativeStyle { return { ...animator.samples[0], [animatorKey]: animator } as OneNativeStyle } export function animatorStyleOf(style: OneNativeStyle): AnimatorStyle | undefined { return (style as Record<symbol, AnimatorStyle | undefined>)[animatorKey] } type NativeStyle = Readonly<Record<string, unknown>> & { sdkModifiers?: string } const colorFields = new Set(['foregroundStyle', 'tint', 'background', 'borderColor', 'glassEffectTint']) function same(a: unknown, b: unknown) { return a === b || JSON.stringify(a) === JSON.stringify(b) } function sdkPairs(style: NativeStyle): [string, string][] { return style.sdkModifiers ? (JSON.parse(style.sdkModifiers) as [string, string][]) : [] } function withPayload(base: NativeStyle, payload: unknown): NativeStyle { return { ...base, sdkModifiers: JSON.stringify([...sdkPairs(base), ['oneNativeAnimator', JSON.stringify(payload)]]), } } function phaseNative(animator: Extract<AnimatorStyle, { kind: 'phase' }>, natives: NativeStyle[]) { const keys = [...new Set(natives.flatMap((native) => Object.keys(native)))] const varying = keys.filter((key) => natives.some((native) => !same(native[key], natives[0][key]))) for (const key of varying) { if (colorFields.has(key) && natives.some((native) => native[key] !== undefined && typeof native[key] !== 'number')) throw new Error(`${animator.owner} content can vary ${key} by phase only with plain colors, not platform colors`) } const phases = natives.map((native) => Object.fromEntries(varying.filter((key) => native[key] !== undefined).map((key) => [key, native[key]])) ) return withPayload(natives[0], { id: animator.id, kind: 'phase', varying, phases }) } // numeric leaves of one sdk modifier value: a numeric string, or the numbers and // numeric strings inside its JSON. the shape keeps everything else literal. const numericText = /^-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i function leavesOf(value: string): { shape: string; leaves: number[]; texts: string[] } { const leaves: number[] = [] const texts: string[] = [] const leaf = (number: number, text: string, quoted: boolean) => { leaves.push(number) texts.push(text) return quoted ? `__oneKS${leaves.length - 1}__` : `__oneKN${leaves.length - 1}__` } if (numericText.test(value)) return { shape: leaf(Number(value), value, true), leaves, texts } let parsed: unknown try { parsed = /^[[{]/.test(value) ? JSON.parse(value) : undefined } catch { parsed = undefined } if (parsed === undefined) return { shape: value, leaves, texts } const walk = (item: unknown): unknown => { if (typeof item === 'number') return leaf(item, JSON.stringify(item), false) if (typeof item === 'string' && numericText.test(item)) return leaf(Number(item), item, true) if (Array.isArray(item)) return item.map(walk) if (item && typeof item === 'object') return Object.fromEntries(Object.entries(item).map(([key, entry]) => [key, walk(entry)])) return item } return { shape: JSON.stringify(walk(parsed)), leaves, texts } } function keyframeNative(animator: Extract<AnimatorStyle, { kind: 'keyframe' }>, natives: NativeStyle[]) { const { owner, values } = animator const fieldCount = values[0].length // samples: the initial value, one unit step per field, then a check point. const tokens: { base: number; coefficients: number[] }[] = [] const bind = (samples: number[], where: string) => { if (samples.every((sample) => sample === samples[0])) return undefined const coefficients = Array.from({ length: fieldCount }, (_, field) => samples[field + 1] - samples[0]) const check = values[fieldCount + 1] let predicted = samples[0] for (let field = 0; field < fieldCount; field++) predicted += coefficients[field] * (check[field] - values[0][field]) const actual = samples[fieldCount + 1] if (!(Math.abs(predicted - actual) <= 1e-6 * Math.max(1, Math.abs(actual)))) throw new Error(`${owner} content must map its value linearly to swiftStyle numbers; ${where} does not`) let base = samples[0] for (let field = 0; field < fieldCount; field++) base -= coefficients[field] * values[0][field] tokens.push({ base, coefficients }) return tokens.length - 1 } const keys = [...new Set(natives.flatMap((native) => Object.keys(native)))].filter((key) => key !== 'sdkModifiers') const fields: Record<string, number> = {} for (const key of keys) { const samples = natives.map((native) => native[key]) if (samples.every((sample) => same(sample, samples[0]))) continue if (colorFields.has(key) || !samples.every((sample) => typeof sample === 'number')) throw new Error(`${owner} content can follow its value only through swiftStyle numbers; ${key} changed`) const token = bind(samples as number[], key) if (token !== undefined) fields[key] = token } const pairs = natives.map(sdkPairs) const names = pairs[0].map(([name]) => name) if (pairs.some((list) => !same(list.map(([name]) => name), names))) throw new Error(`${owner} content must apply the same swiftStyle modifiers at every value`) const template = names.map((name, index) => { const parts = pairs.map((list) => leavesOf(list[index][1])) if (parts.some((part) => part.shape !== parts[0].shape)) throw new Error(`${owner} content can follow its value only through swiftStyle numbers; ${name} changed`) let value = parts[0].shape parts[0].leaves.forEach((_, leaf) => { const token = bind(parts.map((part) => part.leaves[leaf]), name) const text = token === undefined ? parts[0].texts[leaf] : `@@${token}@@` value = value .replace(`"__oneKN${leaf}__"`, text) .replace(`__oneKS${leaf}__`, text) }) return [name, value] }) return withPayload(natives[0], { id: animator.id, kind: 'keyframe', fields, sdkModifiers: template.length ? JSON.stringify(template) : undefined, tokens, }) } export function animatorStyleNative<Native>( animator: AnimatorStyle, convert: (style: OneNativeStyle | undefined) => Native | undefined ): Native { const natives = animator.samples.map((sample) => (convert(sample) ?? {}) as NativeStyle) const native = animator.kind === 'phase' ? phaseNative(animator, natives) : keyframeNative(animator, natives) return native as Native }