summaryrefslogtreecommitdiff
path: root/apps/web/src/animation/binding-values.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/animation/binding-values.ts')
-rw-r--r--apps/web/src/animation/binding-values.ts344
1 files changed, 0 insertions, 344 deletions
diff --git a/apps/web/src/animation/binding-values.ts b/apps/web/src/animation/binding-values.ts
deleted file mode 100644
index a46aa9a6..00000000
--- a/apps/web/src/animation/binding-values.ts
+++ /dev/null
@@ -1,344 +0,0 @@
-import { converter, formatHex, formatHex8, parse } from "culori";
-import type {
- AnimationBindingComponent,
- AnimationBindingOfKind,
- AnimationBindingInstance,
- AnimationBindingKind,
- ColorAnimationBinding,
- DiscreteAnimationBinding,
- NumberAnimationBinding,
- AnimationPath,
- AnimationValue,
- DiscreteValue,
- Vector2AnimationBinding,
- VectorValue,
-} from "@/animation/types";
-import { clamp } from "@/utils/math";
-
-interface LinearRgba {
- r: number;
- g: number;
- b: number;
- a: number;
-}
-
-export type AnimationComponentValue = number | DiscreteValue;
-
-const toRgb = converter("rgb");
-
-function srgbToLinear({ value }: { value: number }): number {
- return value <= 0.04045
- ? value / 12.92
- : Math.pow((value + 0.055) / 1.055, 2.4);
-}
-
-function linearToSrgb({ value }: { value: number }): number {
- const clamped = clamp({ value, min: 0, max: 1 });
- return clamped <= 0.0031308
- ? clamped * 12.92
- : 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;
-}
-
-function isRecord(value: unknown): value is Record<string, unknown> {
- return typeof value === "object" && value !== null;
-}
-
-export function isVectorValue(value: unknown): value is VectorValue {
- return isRecord(value) && typeof value.x === "number" && typeof value.y === "number";
-}
-
-export type EasingMode = "independent" | "shared";
-
-/**
- * Declares how easing curves apply to a binding's components.
- * "shared" means all components always use the same curve (e.g. color — you never
- * want to ease R independently from G/B/A). "independent" means each component
- * can have its own curve.
- */
-export function getEasingModeForKind(kind: AnimationBindingKind): EasingMode {
- return kind === "color" ? "shared" : "independent";
-}
-
-export function getBindingComponentKeys({
- kind,
-}: {
- kind: AnimationBindingKind;
-}): string[] {
- if (kind === "vector2") {
- return ["x", "y"];
- }
-
- if (kind === "color") {
- return ["r", "g", "b", "a"];
- }
-
- return ["value"];
-}
-
-export function buildBindingChannelId({
- path,
- componentKey,
-}: {
- path: AnimationPath;
- componentKey: string;
-}): string {
- return `${path}:${componentKey}`;
-}
-
-function createBindingComponent<TKey extends string>({
- path,
- key,
-}: {
- path: AnimationPath;
- key: TKey;
-}): AnimationBindingComponent<TKey> {
- return {
- key,
- channelId: buildBindingChannelId({ path, componentKey: key }),
- };
-}
-
-function cloneBindingComponents<TKey extends string>({
- components,
-}: {
- components: AnimationBindingComponent<TKey>[];
-}): AnimationBindingComponent<TKey>[] {
- return components.map((component) => ({ ...component }));
-}
-
-const animationBindingFactories = {
- color: ({ path }: { path: AnimationPath }): ColorAnimationBinding => ({
- path,
- kind: "color",
- colorSpace: "srgb-linear",
- components: [
- createBindingComponent({ path, key: "r" }),
- createBindingComponent({ path, key: "g" }),
- createBindingComponent({ path, key: "b" }),
- createBindingComponent({ path, key: "a" }),
- ],
- }),
- vector2: ({ path }: { path: AnimationPath }): Vector2AnimationBinding => ({
- path,
- kind: "vector2",
- components: [
- createBindingComponent({ path, key: "x" }),
- createBindingComponent({ path, key: "y" }),
- ],
- }),
- number: ({ path }: { path: AnimationPath }): NumberAnimationBinding => ({
- path,
- kind: "number",
- components: [createBindingComponent({ path, key: "value" })],
- }),
- discrete: ({ path }: { path: AnimationPath }): DiscreteAnimationBinding => ({
- path,
- kind: "discrete",
- components: [createBindingComponent({ path, key: "value" })],
- }),
-} satisfies {
- [K in AnimationBindingKind]: ({
- path,
- }: {
- path: AnimationPath;
- }) => AnimationBindingOfKind<K>;
-};
-
-export function createAnimationBinding<TKind extends AnimationBindingKind>({
- path,
- kind,
-}: {
- path: AnimationPath;
- kind: TKind;
-}): AnimationBindingOfKind<TKind>;
-export function createAnimationBinding({
- path,
- kind,
-}: {
- path: AnimationPath;
- kind: AnimationBindingKind;
-}): AnimationBindingInstance {
- return animationBindingFactories[kind]({ path });
-}
-
-const animationBindingCloners = {
- color: ({ binding }: { binding: ColorAnimationBinding }): ColorAnimationBinding => ({
- ...binding,
- components: cloneBindingComponents({
- components: binding.components,
- }),
- }),
- vector2: ({
- binding,
- }: {
- binding: Vector2AnimationBinding;
- }): Vector2AnimationBinding => ({
- ...binding,
- components: cloneBindingComponents({
- components: binding.components,
- }),
- }),
- number: ({
- binding,
- }: {
- binding: NumberAnimationBinding;
- }): NumberAnimationBinding => ({
- ...binding,
- components: cloneBindingComponents({
- components: binding.components,
- }),
- }),
- discrete: ({
- binding,
- }: {
- binding: DiscreteAnimationBinding;
- }): DiscreteAnimationBinding => ({
- ...binding,
- components: cloneBindingComponents({
- components: binding.components,
- }),
- }),
-} satisfies {
- [K in AnimationBindingKind]: ({
- binding,
- }: {
- binding: AnimationBindingOfKind<K>;
- }) => AnimationBindingOfKind<K>;
-};
-
-export function cloneAnimationBinding<TKind extends AnimationBindingKind>({
- binding,
-}: {
- binding: AnimationBindingOfKind<TKind>;
-}): AnimationBindingOfKind<TKind>;
-export function cloneAnimationBinding({
- binding,
-}: {
- binding: AnimationBindingInstance;
-}): AnimationBindingInstance {
- switch (binding.kind) {
- case "color":
- return animationBindingCloners.color({ binding });
- case "vector2":
- return animationBindingCloners.vector2({ binding });
- case "number":
- return animationBindingCloners.number({ binding });
- case "discrete":
- return animationBindingCloners.discrete({ binding });
- }
-}
-
-export function parseColorToLinearRgba({
- color,
-}: {
- color: string;
-}): LinearRgba | null {
- const parsed = parse(color);
- const rgb = parsed ? toRgb(parsed) : null;
- if (!rgb) {
- return null;
- }
-
- return {
- r: srgbToLinear({ value: rgb.r ?? 0 }),
- g: srgbToLinear({ value: rgb.g ?? 0 }),
- b: srgbToLinear({ value: rgb.b ?? 0 }),
- a: clamp({ value: rgb.alpha ?? 1, min: 0, max: 1 }),
- };
-}
-
-export function formatLinearRgba({
- color,
-}: {
- color: LinearRgba;
-}): string {
- const rgb = {
- mode: "rgb",
- r: linearToSrgb({ value: color.r }),
- g: linearToSrgb({ value: color.g }),
- b: linearToSrgb({ value: color.b }),
- alpha: clamp({ value: color.a, min: 0, max: 1 }),
- } as const;
- return rgb.alpha < 1 ? formatHex8(rgb) : formatHex(rgb);
-}
-
-export function decomposeAnimationValue({
- kind,
- value,
-}: {
- kind: AnimationBindingKind;
- value: AnimationValue;
-}): Record<string, AnimationComponentValue> | null {
- if (kind === "number") {
- return typeof value === "number" ? { value } : null;
- }
-
- if (kind === "vector2") {
- return isVectorValue(value) ? { x: value.x, y: value.y } : null;
- }
-
- if (kind === "color") {
- if (typeof value !== "string") {
- return null;
- }
- const parsed = parseColorToLinearRgba({ color: value });
- if (!parsed) {
- return null;
- }
- return {
- r: parsed.r,
- g: parsed.g,
- b: parsed.b,
- a: parsed.a,
- };
- }
-
- return typeof value === "string" || typeof value === "boolean"
- ? { value }
- : null;
-}
-
-export function composeAnimationValue({
- binding,
- componentValues,
-}: {
- binding: AnimationBindingInstance;
- componentValues: Record<string, AnimationComponentValue | undefined>;
-}): AnimationValue | null {
- if (binding.kind === "number") {
- const value = componentValues.value;
- return typeof value === "number" ? value : null;
- }
-
- if (binding.kind === "vector2") {
- const x = componentValues.x;
- const y = componentValues.y;
- return typeof x === "number" && typeof y === "number" ? { x, y } : null;
- }
-
- if (binding.kind === "color") {
- const r = componentValues.r;
- const g = componentValues.g;
- const b = componentValues.b;
- const a = componentValues.a;
- if (
- typeof r !== "number" ||
- typeof g !== "number" ||
- typeof b !== "number" ||
- typeof a !== "number"
- ) {
- return null;
- }
- return formatLinearRgba({
- color: {
- r,
- g,
- b,
- a,
- },
- });
- }
-
- const value = componentValues.value;
- return typeof value === "string" || typeof value === "boolean" ? value : null;
-}