export type WidgetLayoutSize = { width: number; height: number; }; export type WidgetLayoutCubicBezier = readonly [ x1: number, y1: number, x2: number, y2: number, ]; export type WidgetLayoutInstantSizeTransition = { kind: "instant"; durationMs: 0; }; export type WidgetLayoutTimedSizeTransition = { kind: "timed"; durationMs: number; easing: { kind: "cubic-bezier"; points: WidgetLayoutCubicBezier; }; }; export type WidgetLayoutSizeTransition = WidgetLayoutInstantSizeTransition; export type WidgetLayoutSizeUpdate = { size: WidgetLayoutSize; transition: WidgetLayoutSizeTransition; }; export type WidgetLayoutTimedSizeUpdate = { size: WidgetLayoutSize; transition: WidgetLayoutTimedSizeTransition; }; export const INSTANT_WIDGET_LAYOUT_SIZE_TRANSITION: WidgetLayoutInstantSizeTransition = { kind: "instant", durationMs: 0, }; const isFiniteNumber = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value); export function isWidgetLayoutCubicBezier( points: unknown, ): points is WidgetLayoutCubicBezier { if (!Array.isArray(points) || points.length !== 4) { return false; } const [x1, y1, x2, y2] = points; return ( isFiniteNumber(x1) && x1 >= 0 && x1 <= 1 && isFiniteNumber(y1) && isFiniteNumber(x2) && x2 >= 0 && x2 <= 1 && isFiniteNumber(y2) ); } export function toInstantWidgetLayoutSizeUpdate( size: WidgetLayoutSize, ): WidgetLayoutSizeUpdate { return { size: { ...size }, transition: INSTANT_WIDGET_LAYOUT_SIZE_TRANSITION, }; } export function toTimedWidgetLayoutSizeTransition(input: { durationMs: number; easing: WidgetLayoutCubicBezier; }): WidgetLayoutTimedSizeTransition { if (!Number.isFinite(input.durationMs) || input.durationMs <= 0) { throw new RangeError( "Timed widget layout transitions require durationMs > 0", ); } if (!isWidgetLayoutCubicBezier(input.easing)) { throw new RangeError( "Timed widget layout transitions require cubic-bezier x control points in [0, 1] and finite y control points", ); } return { kind: "timed", durationMs: input.durationMs, easing: { kind: "cubic-bezier", points: [...input.easing], }, }; } export function toTimedWidgetLayoutSizeUpdate(input: { size: WidgetLayoutSize; durationMs: number; easing: WidgetLayoutCubicBezier; }): WidgetLayoutTimedSizeUpdate { return { size: { ...input.size }, transition: toTimedWidgetLayoutSizeTransition({ durationMs: input.durationMs, easing: input.easing, }), }; }