From 7e7f4155df490c9e72fbc444c893d10919f59025 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Tue, 13 Jun 2023 17:17:51 -0300 Subject: [PATCH 01/20] Core motion runner --- packages/core/src/utils/index.ts | 1 + packages/core/src/utils/isObject.ts | 3 + packages/motion/CHANGELOG.md | 19 ++ packages/motion/package.json | 36 ++ .../src/context/create-motion-context.ts | 106 ++++++ packages/motion/src/context/index.ts | 2 + .../motion/src/context/motion-runner.test.ts | 89 +++++ packages/motion/src/context/motion-runner.ts | 96 ++++++ packages/motion/src/index.ts | 1 + packages/motion/src/movie/make-movie.test.ts | 309 ++++++++++++++++++ packages/motion/src/movie/make-movie.ts | 188 +++++++++++ packages/motion/src/movie/make-scene.ts | 18 + packages/motion/src/test-utils/frameMaker.ts | 30 ++ packages/motion/src/test-utils/index.ts | 1 + packages/motion/src/types.ts | 0 packages/motion/src/utils/generateMeta.ts | 35 ++ packages/motion/src/utils/index.ts | 1 + packages/motion/tsconfig.json | 13 + packages/motion/vitest.config.ts | 9 + pnpm-lock.yaml | 12 + 20 files changed, 969 insertions(+) create mode 100644 packages/core/src/utils/isObject.ts create mode 100644 packages/motion/CHANGELOG.md create mode 100644 packages/motion/package.json create mode 100644 packages/motion/src/context/create-motion-context.ts create mode 100644 packages/motion/src/context/index.ts create mode 100644 packages/motion/src/context/motion-runner.test.ts create mode 100644 packages/motion/src/context/motion-runner.ts create mode 100644 packages/motion/src/index.ts create mode 100644 packages/motion/src/movie/make-movie.test.ts create mode 100644 packages/motion/src/movie/make-movie.ts create mode 100644 packages/motion/src/movie/make-scene.ts create mode 100644 packages/motion/src/test-utils/frameMaker.ts create mode 100644 packages/motion/src/test-utils/index.ts create mode 100644 packages/motion/src/types.ts create mode 100644 packages/motion/src/utils/generateMeta.ts create mode 100644 packages/motion/src/utils/index.ts create mode 100644 packages/motion/tsconfig.json create mode 100644 packages/motion/vitest.config.ts diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 00a39d2..62046d0 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,2 +1,3 @@ export * from "./remap"; export * from "./lerp"; +export * from "./isObject"; diff --git a/packages/core/src/utils/isObject.ts b/packages/core/src/utils/isObject.ts new file mode 100644 index 0000000..562ae36 --- /dev/null +++ b/packages/core/src/utils/isObject.ts @@ -0,0 +1,3 @@ +export function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} diff --git a/packages/motion/CHANGELOG.md b/packages/motion/CHANGELOG.md new file mode 100644 index 0000000..512ea6d --- /dev/null +++ b/packages/motion/CHANGELOG.md @@ -0,0 +1,19 @@ +# @coord/core + +## 0.3.0 + +### Minor Changes + +- 7b7d5cb: Adds publish workflow + +## 0.2.0 + +### Minor Changes + +- Improves documentation + +## 0.1.0 + +### Minor Changes + +- 88533a2: Integrate Vitest testing framework diff --git a/packages/motion/package.json b/packages/motion/package.json new file mode 100644 index 0000000..23c771a --- /dev/null +++ b/packages/motion/package.json @@ -0,0 +1,36 @@ +{ + "name": "@coord/motion", + "version": "0.3.0", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.js", + "license": "MIT", + "scripts": { + "lint": "tsc", + "build": "tsup", + "dev": "tsup --watch", + "test": "vitest run && vitest typecheck", + "test:watch": "vitest", + "clean": "rm -rf ./dist ./.turbo ./node_modules" + }, + "tsup": { + "dts": true, + "format": [ + "cjs", + "esm" + ], + "minify": true, + "entryPoints": [ + "src/index.ts" + ] + }, + "devDependencies": { + "@coord/core": "workspace:*", + "tsconfig": "workspace:*", + "tsup": "^6.7.0", + "typescript": "^5.1.3" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/motion/src/context/create-motion-context.ts b/packages/motion/src/context/create-motion-context.ts new file mode 100644 index 0000000..f816104 --- /dev/null +++ b/packages/motion/src/context/create-motion-context.ts @@ -0,0 +1,106 @@ +import { isObject } from "@coord/core"; + +export const DEFAULT_MOTION_CONTEXT_SETTINGS = { + fps: 60, + physicsFps: 120, +}; + +export type MotionContextSettings = typeof DEFAULT_MOTION_CONTEXT_SETTINGS; + +export type MotionContextSceneMeta = { + title: string; + description?: string; + frame: number; + duration: number; +}; + +export type MotionContextMeta = { + title: string; + description?: string; + scenes: MotionContextSceneMeta[]; +}; + +export type MotionState = { + [key: string]: unknown; +}; + +export type MotionStateContextProps = { + $frame: number; + $transitionIn: number; +}; + +export class MotionContext { + _childContexts: Map> = new Map(); + _state: TState & MotionStateContextProps; + frames: (TState & MotionStateContextProps)[]; + meta: MotionContextMeta; + settings: MotionContextSettings; + + constructor( + initialState: TState, + contextSettings: Partial = {}, + meta: Partial = {} + ) { + this._state = { ...initialState, $frame: 0, $transitionIn: 1 }; + this.frames = []; + this.settings = { + ...DEFAULT_MOTION_CONTEXT_SETTINGS, + ...contextSettings, + }; + this.meta = { + title: "Untitled", + scenes: [], + ...meta, + }; + } + + state(state?: { + [K in keyof TState]?: TState[K]; + }): TState & Readonly { + if (state) { + Object.assign(this._state, state); + } + return this._state; + } + + collectChildStates() { + for (const [key, childContext] of this._childContexts.entries()) { + Object.assign(this._state, { [key]: childContext._state }); + childContext.pushFrame(); + } + } + + createChildContext(key: TKey & string) { + const childContext = new MotionContext( + this._state[key] as MotionState, + this.settings + ); + this._childContexts.set(key, childContext); + return childContext; + } + + removeChildContext(context: MotionContext) { + for (const [key, childContext] of this._childContexts.entries()) { + if (childContext === context) { + this._childContexts.delete(key); + return; + } + } + } + pushFrame() { + this.collectChildStates(); + this.frames.push(this._state); + + this._state = { + ...this._state, + $frame: this.frames.length, + }; + } +} + +export function createMotionContext( + initialState: TState, + contextSettings: Partial = {} +) { + return new MotionContext(initialState, contextSettings); +} diff --git a/packages/motion/src/context/index.ts b/packages/motion/src/context/index.ts new file mode 100644 index 0000000..805935a --- /dev/null +++ b/packages/motion/src/context/index.ts @@ -0,0 +1,2 @@ +export * from "./create-motion-context"; +export * from "./motion-runner"; diff --git a/packages/motion/src/context/motion-runner.test.ts b/packages/motion/src/context/motion-runner.test.ts new file mode 100644 index 0000000..ba2dc11 --- /dev/null +++ b/packages/motion/src/context/motion-runner.test.ts @@ -0,0 +1,89 @@ +import { test, describe, expect } from "vitest"; +import { createMotion, requestContext, runMotion } from "./motion-runner"; +import { MotionContext, createMotionContext } from "./create-motion-context"; +import { frameMaker } from "@/test-utils"; + +describe("createMotion", async () => { + test("should create motion context and execute correctly", async () => { + const [context, runner] = createMotion( + { + value: "", + }, + function* motionBuilder(context) { + yield; + context.state({ + value: "Hello", + }); + yield; + context.state({ + value: "World", + }); + yield; + } + ); + + runner.next(); + expect(context?.frames.length).toBe(1); + expect(context?.state().value).toBe(""); + runner.next(); + expect(context?.frames.length).toBe(2); + expect(context?.state().value).toBe("Hello"); + runner.next(); + expect(context?.frames.length).toBe(3); + expect(context?.state().value).toBe("World"); + + expect(runner.next().done).toBe(true); + }); +}); + +describe("runMotion", async () => { + test("should create motion context and execute correctly", async () => { + const context = runMotion( + { + value: "", + }, + function* motionBuilder(context) { + yield; + context.state({ + value: "Hello", + }); + yield; + context.state({ + value: "World", + }); + yield; + } + ); + + const makeSceneFrame = frameMaker( + { + value: "", + }, + 0 + ); + + expect(context.frames).toEqual([ + makeSceneFrame(), + makeSceneFrame({ + value: "Hello", + }), + makeSceneFrame({ + value: "World", + }), + ]); + }); +}); + +describe("requestContext", async () => { + test("should provide context when requested", async () => { + runMotion( + { + value: "", + }, + function* motionBuilder() { + const requestedContext = yield* requestContext(); + expect(requestedContext).instanceOf(MotionContext); + } + ); + }); +}); diff --git a/packages/motion/src/context/motion-runner.ts b/packages/motion/src/context/motion-runner.ts new file mode 100644 index 0000000..974d4bb --- /dev/null +++ b/packages/motion/src/context/motion-runner.ts @@ -0,0 +1,96 @@ +import { isObject } from "@coord/core"; +import { + MotionContext, + MotionContextSettings, + MotionState, + createMotionContext, +} from "./create-motion-context"; + +export type YieldedType any> = + ReturnType extends Generator ? U : never; + +export type MotionBuilderRequest = + | YieldedType> + | YieldedType; + +export const isMotionBuilderRequest = ( + value: unknown +): value is MotionBuilderRequest => { + return isObject(value) && "type" in value; +}; + +export type MotionBuilder = ( + context: MotionContext +) => Generator | undefined, void, unknown>; + +export function* motionRunner( + context: MotionContext, + builder: MotionBuilder +) { + const bulderIterator = builder(context); + + while (true) { + const currentIteration = bulderIterator.next(); + + if (currentIteration.value) { + if (currentIteration.value.type === "REQUEST_CONTEXT") { + currentIteration.value.context = context; + continue; + } + } + if (currentIteration.done) { + break; + } + + context.pushFrame(); + yield context; + } +} + +export function createMotion( + initialState: TState, + builder: MotionBuilder, + contextSettings: Partial = {} +) { + const context = createMotionContext(initialState, contextSettings); + const runner = motionRunner(context, builder); + return [context, runner] as const; +} + +export function runMotion( + initialState: TState, + builder: MotionBuilder, + contextSettings: Partial = {} +) { + const [context, runner] = createMotion( + initialState, + builder, + contextSettings + ); + while (!runner.next().done) { + continue; + } + + return context; +} + +export function* requestContext() { + const request: { + type: "REQUEST_CONTEXT"; + context?: MotionContext; + } = { + type: "REQUEST_CONTEXT" as const, + }; + yield request; + if (!request.context) { + throw new Error("Context is not provided"); + } + return request.context; +} + +export function* requestTransition(duration?: number) { + yield { + type: "REQUEST_TRANSITION" as const, + duration, + }; +} diff --git a/packages/motion/src/index.ts b/packages/motion/src/index.ts new file mode 100644 index 0000000..8e8cbab --- /dev/null +++ b/packages/motion/src/index.ts @@ -0,0 +1 @@ +console.log("motion"); diff --git a/packages/motion/src/movie/make-movie.test.ts b/packages/motion/src/movie/make-movie.test.ts new file mode 100644 index 0000000..c7006e9 --- /dev/null +++ b/packages/motion/src/movie/make-movie.test.ts @@ -0,0 +1,309 @@ +import { test, describe, expect } from "vitest"; +import { makeMovie } from "./make-movie"; +import { makeScene } from "./make-scene"; +import { runMotion } from "@/context"; +import { frameMaker } from "@/test-utils"; + +describe("makeMovie", async () => { + test("Single scene without transition", async () => { + const makeScene1Frame = frameMaker( + { + a: "Waiting", + }, + 0 + ); + const makeMovieFrame = frameMaker( + { + scene1: makeScene1Frame.initialState, + }, + 0 + ); + + const movie = makeMovie( + "Test", + { + scene1: makeScene("Scene 1", { a: "Waiting" }, function* (context) { + context.state({ + a: "Hello", + }); + yield; + + context.state({ + a: "world", + }); + yield; + + context.state({ + a: "!", + }); + yield; + }), + }, + { + transitionDuration: 0, + } + ); + + const executed = runMotion(movie.initialState, movie.builder); + + expect(executed.frames.length).toBe(3); + expect(executed.frames).toEqual([ + makeMovieFrame({ + scene1: makeScene1Frame({ + a: "Hello", + }), + }), + makeMovieFrame({ + scene1: makeScene1Frame({ + a: "world", + }), + }), + makeMovieFrame({ + scene1: makeScene1Frame({ + a: "!", + }), + }), + ]); + }); + + test("Sequence of scenes without transition", async () => { + const makeScene1Frame = frameMaker( + { + a: "Waiting", + }, + 0 + ); + const makeScene2Frame = frameMaker( + { + b: "Waiting", + }, + 0 + ); + const makeMovieFrame = frameMaker( + { + scene1: makeScene1Frame.initialState, + scene2: makeScene2Frame.initialState, + }, + 0 + ); + const movie = makeMovie( + "Test", + { + scene1: makeScene("Scene 1", { a: "Waiting" }, function* (context) { + context.state({ + a: "Hello", + }); + yield; + + context.state({ + a: "world", + }); + yield; + }), + scene2: makeScene("Scene 2", { b: "Waiting" }, function* (context) { + context.state({ + b: "Hello", + }); + yield; + context.state({ + b: "world", + }); + yield; + }), + }, + { + transitionDuration: 0, + } + ); + + const executed = runMotion(movie.initialState, movie.builder); + + expect(executed.frames.length).toBe(4); + + expect(executed.frames).toEqual([ + makeMovieFrame({ + scene1: makeScene1Frame({ + a: "Hello", + }), + }), + makeMovieFrame({ + scene1: makeScene1Frame({ + a: "world", + }), + }), + makeMovieFrame({ + scene2: makeScene2Frame({ + b: "Hello", + }), + }), + makeMovieFrame({ + scene2: makeScene2Frame({ + b: "world", + }), + }), + ]); + }); + + test("Sequence of scenes with transition", async () => { + const makeScene1Frame = frameMaker( + { + a: "Waiting", + }, + 0 + ); + const makeScene2Frame = frameMaker( + { + b: "Waiting", + }, + 3 + ); + const makeMovieFrame = frameMaker( + { + scene1: makeScene1Frame.initialState, + scene2: makeScene2Frame.initialState, + }, + 0 + ); + const movie = makeMovie( + "Test", + { + scene1: makeScene("Scene 1", { a: "Waiting" }, function* (context) { + context.state({ + a: "Hello World!", + }); + + yield; + }), + scene2: makeScene( + "Scene 2", + { + b: "Waiting", + }, + function* (context) { + context.state({ + b: "Hello", + }); + yield; + context.state({ + b: "Hello World", + }); + yield; + + context.state({ + b: "Hello World!", + }); + yield; + } + ), + }, + { + transitionDuration: 3, + } + ); + + const executed = runMotion(movie.initialState, movie.builder, { + fps: 1, + }); + + expect(executed.frames.length).toBe(4); + expect(executed.frames).toEqual([ + makeMovieFrame({ + scene1: makeScene1Frame({ + a: "Hello World!", + }), + }), + makeMovieFrame({ + scene2: makeScene2Frame({ + b: "Hello", + }), + }), + makeMovieFrame({ + scene2: makeScene2Frame({ + b: "Hello World", + }), + }), + makeMovieFrame({ + scene2: makeScene2Frame({ + b: "Hello World!", + }), + }), + ]); + }); + + test("Sequence of scenes with transition longer than its duration", async () => { + const makeScene1Frame = frameMaker( + { + a: "Waiting", + }, + 0 + ); + const makeScene2Frame = frameMaker( + { + b: "Waiting", + }, + 3 + ); + const makeMovieFrame = frameMaker( + { + scene1: makeScene1Frame.initialState, + scene2: makeScene2Frame.initialState, + }, + 0 + ); + const movie = makeMovie( + "Test", + { + scene1: makeScene("Scene 1", { a: "Waiting" }, function* (context) { + context.state({ + a: "Hello World!", + }); + + yield; + }), + scene2: makeScene( + "Scene 2", + { + b: "Waiting", + }, + function* (context) { + context.state({ + b: "Hello World!", + }); + yield; + } + ), + }, + { + transitionDuration: 3, + } + ); + + const executed = runMotion(movie.initialState, movie.builder, { + fps: 1, + }); + + expect(executed.frames.length).toBe(4); + expect(executed.frames).toEqual([ + makeMovieFrame({ + scene1: makeScene1Frame({ + a: "Hello World!", + }), + }), + makeMovieFrame({ + scene2: makeScene2Frame({ + b: "Hello World!", + }), + }), + makeMovieFrame({ + scene2: makeScene2Frame({ + b: "Hello World!", + }), + }), + makeMovieFrame({ + scene2: makeScene2Frame({ + b: "Hello World!", + }), + }), + ]); + }); +}); diff --git a/packages/motion/src/movie/make-movie.ts b/packages/motion/src/movie/make-movie.ts new file mode 100644 index 0000000..426ec8a --- /dev/null +++ b/packages/motion/src/movie/make-movie.ts @@ -0,0 +1,188 @@ +import { + MotionBuilder, + MotionContext, + MotionState, + isMotionBuilderRequest, + requestTransition, +} from "@/context"; +import { SceneMetaish, generateMeta } from "@/utils"; +import { Scene, makeScene } from "./make-scene"; + +export type MovieSettings = { + transitionDuration: number; +}; +export type SceneMap = Record> & { + [key: number]: never; +}; + +export type SceneMapInitialState = { + [K in keyof TSceneMap]: TSceneMap[K] extends Scene + ? TState & { $transitionIn: number } + : never; +}; + +export function mergeStates(scenes: TSceneMap) { + return Object.entries(scenes).reduce((acc, [key, scene]) => { + acc[key as keyof TSceneMap] = { + ...scene.initialState, + $transitionIn: 0, + $frame: 0, + }; + return acc; + }, {} as SceneMapInitialState); +} + +export function* runSceneBuilder( + parentContext: MotionContext, + builder: ReturnType>, + context: MotionContext, + transition: number +) { + let hasRequestedTransition = false; + + let frame = 0; + const fps = context.settings.fps; + const transitionInFrames = Math.round(fps * transition); + + while (true) { + const { value, done } = builder.next(); + + if (isMotionBuilderRequest(value)) { + if (value.type === "REQUEST_CONTEXT") { + value.context = context; + continue; + } + if (value.type === "REQUEST_TRANSITION") { + if (hasRequestedTransition) { + throw new Error("Cannot request transition twice"); + } + hasRequestedTransition = true; + yield value; + continue; + } + } + + if (done) { + if (!hasRequestedTransition) { + yield* requestTransition(); + } + break; + } + + context.state({ + $transitionIn: + transitionInFrames === 0 + ? 1 + : Math.min((frame + 1) / transitionInFrames, 1), + }); + frame++; + + yield value; + } + + // In case the scene was shorter the transition, we want to run it at least for the transition duration + while (frame < transitionInFrames) { + context.state({ + $transitionIn: + transitionInFrames === 0 + ? 1 + : Math.min((frame + 1) / transitionInFrames, 1), + }); + frame++; + + yield; + } + parentContext.removeChildContext(context); +} + +export function createMovieBuilder( + scenes: TSceneMap, + settings: MovieSettings +): MotionBuilder> { + const sceneStack = Object.keys(scenes).reverse() as (keyof TSceneMap & + string)[]; + + let sceneBuilders: (ReturnType | null)[] = []; + + return function* (context) { + const startNext = (transition = 0) => { + const scene = sceneStack.pop(); + if (!scene) { + return; + } + const childContext = context.createChildContext(scene); + const childScene = scenes[scene]; + + if (!childScene) { + throw new Error(`Scene ${String(scene)} not found`); + } + + sceneBuilders.push( + runSceneBuilder( + context, + childScene.builder(childContext), + childContext, + transition + ) + ); + }; + + // starts the first scene without transition + startNext(0); + + while (true) { + let i = 0; + while (true) { + const current = sceneBuilders[i]; + if (!current) { + break; + } + while (true) { + const { value, done } = current.next(); + + if (done) { + // Mark for removal + sceneBuilders[i] = null; + + break; + } + if (isMotionBuilderRequest(value)) { + if (value.type === "REQUEST_TRANSITION") { + const duration = value.duration ?? settings.transitionDuration; + + startNext(duration); + continue; + } + continue; + } + + break; + } + i++; + } + + sceneBuilders = sceneBuilders.filter((x) => x !== null); + + if (sceneBuilders.length === 0) { + break; + } + + yield; + } + }; +} + +export function makeMovie< + TMeta extends SceneMetaish, + const TSceneMap extends SceneMap +>(meta: TMeta, scenes: TSceneMap, movieSettings: Partial = {}) { + const settings = { + transitionDuration: 0.5, + ...movieSettings, + }; + const movieMeta = { ...generateMeta(meta), scenes: [] }; + const initialState = mergeStates(scenes); + const builder = createMovieBuilder(scenes, settings); + + return makeScene(movieMeta, initialState, builder); +} diff --git a/packages/motion/src/movie/make-scene.ts b/packages/motion/src/movie/make-scene.ts new file mode 100644 index 0000000..c3f26b6 --- /dev/null +++ b/packages/motion/src/movie/make-scene.ts @@ -0,0 +1,18 @@ +import { MotionBuilder, MotionState } from "@/context"; +import { SceneMetaish, generateMeta } from "@/utils"; + +export function makeScene< + TMeta extends SceneMetaish, + TState extends MotionState +>(meta: TMeta, initialState: TState, builder: MotionBuilder) { + return { + meta: generateMeta(meta), + builder, + initialState, + }; +} + +export type Scene< + TMeta extends SceneMetaish, + TState extends MotionState +> = ReturnType>; diff --git a/packages/motion/src/test-utils/frameMaker.ts b/packages/motion/src/test-utils/frameMaker.ts new file mode 100644 index 0000000..1285043 --- /dev/null +++ b/packages/motion/src/test-utils/frameMaker.ts @@ -0,0 +1,30 @@ +import { MotionState } from "@/context"; + +export function frameMaker( + initialState: TState, + expectedTransitionDurationInFrames: number +) { + let currentState = initialState; + let frame = -1; + const out = function (stateUpdate: Partial = {}) { + currentState = { + ...currentState, + ...stateUpdate, + }; + frame++; + return { + ...currentState, + $frame: frame, + $transitionIn: + expectedTransitionDurationInFrames === 0 + ? 1 + : Math.min((frame + 1) / expectedTransitionDurationInFrames, 1), + }; + }; + out.initialState = { + ...currentState, + $transitionIn: 0, + $frame: 0, + }; + return out; +} diff --git a/packages/motion/src/test-utils/index.ts b/packages/motion/src/test-utils/index.ts new file mode 100644 index 0000000..a4b6619 --- /dev/null +++ b/packages/motion/src/test-utils/index.ts @@ -0,0 +1 @@ +export * from "./frameMaker"; diff --git a/packages/motion/src/types.ts b/packages/motion/src/types.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/motion/src/utils/generateMeta.ts b/packages/motion/src/utils/generateMeta.ts new file mode 100644 index 0000000..e0c8c7e --- /dev/null +++ b/packages/motion/src/utils/generateMeta.ts @@ -0,0 +1,35 @@ +export type SceneMeta = { + title: T; + description?: string; +}; + +export type InferSceneName = T extends SceneMeta< + infer TName +> + ? TName + : T; + +export type SceneMetaish = SceneMeta | T; + +const getMetaName = (meta: T) => { + if (typeof meta === "string") { + return meta as InferSceneName; + } + return meta.title as InferSceneName; +}; + +const getMetaDescription = (meta: T) => { + if (typeof meta === "string") { + return undefined; + } + return meta.description; +}; + +export function generateMeta( + meta: TMeta +): SceneMeta> { + return { + title: getMetaName(meta), + description: getMetaDescription(meta), + }; +} diff --git a/packages/motion/src/utils/index.ts b/packages/motion/src/utils/index.ts new file mode 100644 index 0000000..0d30919 --- /dev/null +++ b/packages/motion/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./generateMeta"; diff --git a/packages/motion/tsconfig.json b/packages/motion/tsconfig.json new file mode 100644 index 0000000..c6a68cd --- /dev/null +++ b/packages/motion/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "tsconfig/base.json", + "compilerOptions": { + "target": "ESNext", + "noUncheckedIndexedAccess": true, + "noEmit": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/motion/vitest.config.ts b/packages/motion/vitest.config.ts new file mode 100644 index 0000000..05a201d --- /dev/null +++ b/packages/motion/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineProject } from "vitest/config"; + +export default defineProject({ + test: { + alias: { + "@/": "src/", + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d80c4d7..805c954 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,6 +126,18 @@ importers: typescript: 5.1.3 vitest: 0.31.4 + packages/motion: + specifiers: + '@coord/core': workspace:* + tsconfig: workspace:* + tsup: ^6.7.0 + typescript: ^5.1.3 + devDependencies: + '@coord/core': link:../core + tsconfig: link:../tsconfig + tsup: 6.7.0_typescript@5.1.3 + typescript: 5.1.3 + packages/tsconfig: specifiers: {} From 0b14ba5629675a9240d53f05626773141f47fc6d Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Tue, 13 Jun 2023 18:30:27 -0300 Subject: [PATCH 02/20] adds base flow controls --- packages/motion/src/flow/all.test.ts | 64 ++++++++++++++++++++++++ packages/motion/src/flow/all.ts | 37 ++++++++++++++ packages/motion/src/flow/chain.test.ts | 65 +++++++++++++++++++++++++ packages/motion/src/flow/chain.ts | 15 ++++++ packages/motion/src/flow/delay.test.ts | 49 +++++++++++++++++++ packages/motion/src/flow/delay.ts | 11 +++++ packages/motion/src/flow/index.ts | 4 ++ packages/motion/src/flow/wait.test.ts | 48 ++++++++++++++++++ packages/motion/src/flow/wait.ts | 10 ++++ packages/motion/src/utils/asIterable.ts | 15 ++++++ packages/motion/src/utils/index.ts | 1 + 11 files changed, 319 insertions(+) create mode 100644 packages/motion/src/flow/all.test.ts create mode 100644 packages/motion/src/flow/all.ts create mode 100644 packages/motion/src/flow/chain.test.ts create mode 100644 packages/motion/src/flow/chain.ts create mode 100644 packages/motion/src/flow/delay.test.ts create mode 100644 packages/motion/src/flow/delay.ts create mode 100644 packages/motion/src/flow/index.ts create mode 100644 packages/motion/src/flow/wait.test.ts create mode 100644 packages/motion/src/flow/wait.ts create mode 100644 packages/motion/src/utils/asIterable.ts diff --git a/packages/motion/src/flow/all.test.ts b/packages/motion/src/flow/all.test.ts new file mode 100644 index 0000000..0d56f08 --- /dev/null +++ b/packages/motion/src/flow/all.test.ts @@ -0,0 +1,64 @@ +import { test, describe, expect } from "vitest"; + +import { runMotion } from "@/context"; +import { frameMaker } from "@/test-utils"; + +import { all } from "./all"; +import { wait } from "./wait"; + +describe("all", async () => { + test("should run threads in parallel", async () => { + const makeSceneFrame = frameMaker( + { + a: "Waiting", + b: "Waiting", + c: "Waiting", + }, + 0 + ); + + const executed = runMotion( + { + a: "Waiting", + b: "Waiting", + c: "Waiting", + }, + function* (context) { + yield* all( + function* A() { + context.state({ + a: "A", + }); + yield; + }, + function* B() { + context.state({ + b: "B", + }); + yield; + }, + function* C() { + context.state({ + c: "C", + }); + yield; + }, + wait(1) + ); + }, + { + fps: 3, + } + ); + + expect(executed.frames).toEqual([ + makeSceneFrame({ + a: "A", + b: "B", + c: "C", + }), + makeSceneFrame(), + makeSceneFrame(), + ]); + }); +}); diff --git a/packages/motion/src/flow/all.ts b/packages/motion/src/flow/all.ts new file mode 100644 index 0000000..1fa51d3 --- /dev/null +++ b/packages/motion/src/flow/all.ts @@ -0,0 +1,37 @@ +import { + MotionBuilder, + MotionState, + isMotionBuilderRequest, + requestContext, +} from "@/context"; +import { MotionBuilderish, asIterable } from "@/utils"; + +export function* all( + ...threads: MotionBuilderish[] +) { + const context = yield* requestContext(); + + let threadIterables: (ReturnType> | null)[] = + threads.map((thread) => asIterable(thread, context)); + while (true) { + for (let i = 0; i < threadIterables.length; i++) { + const thread = threadIterables[i]; + if (!thread) continue; + while (true) { + const { done, value } = thread.next(); + if (isMotionBuilderRequest(value)) { + yield value; + continue; + } + if (done) { + threadIterables[i] = null; + } + break; + } + } + + threadIterables = threadIterables.filter((x) => x !== null); + if (!threadIterables.length) break; + yield; + } +} diff --git a/packages/motion/src/flow/chain.test.ts b/packages/motion/src/flow/chain.test.ts new file mode 100644 index 0000000..149d6df --- /dev/null +++ b/packages/motion/src/flow/chain.test.ts @@ -0,0 +1,65 @@ +import { test, describe, expect } from "vitest"; + +import { runMotion } from "@/context"; +import { frameMaker } from "@/test-utils"; +import { chain } from "./chain"; +import { wait } from "./wait"; + +describe("chain", async () => { + test("should run threads in sequence", async () => { + const makeSceneFrame = frameMaker( + { + a: "Waiting", + }, + 0 + ); + + const executed = runMotion( + { + a: "Waiting", + }, + function* (context) { + yield* chain( + function* () { + context.state({ + a: "First", + }); + yield; + }, + function* () { + context.state({ + a: "Second", + }); + yield; + }, + function* () { + context.state({ + a: "Third", + }); + yield; + }, + wait(1) + ); + }, + { + fps: 3, + } + ); + + expect(executed.frames.length).toBe(6); + expect(executed.frames).toEqual([ + makeSceneFrame({ + a: "First", + }), + makeSceneFrame({ + a: "Second", + }), + makeSceneFrame({ + a: "Third", + }), + makeSceneFrame(), + makeSceneFrame(), + makeSceneFrame(), + ]); + }); +}); diff --git a/packages/motion/src/flow/chain.ts b/packages/motion/src/flow/chain.ts new file mode 100644 index 0000000..f70a61a --- /dev/null +++ b/packages/motion/src/flow/chain.ts @@ -0,0 +1,15 @@ +import { MotionState, requestContext } from "@/context"; +import { MotionBuilderish, asIterable } from "@/utils"; + +export function* chain( + ...threads: MotionBuilderish[] +) { + const context = yield* requestContext(); + + for (let i = 0; i < threads.length; i++) { + const thread = threads[i]!; + + const threadIterable = asIterable(thread, context); + yield* threadIterable; + } +} diff --git a/packages/motion/src/flow/delay.test.ts b/packages/motion/src/flow/delay.test.ts new file mode 100644 index 0000000..9325e94 --- /dev/null +++ b/packages/motion/src/flow/delay.test.ts @@ -0,0 +1,49 @@ +import { test, describe, expect } from "vitest"; + +import { runMotion } from "@/context"; +import { frameMaker } from "@/test-utils"; +import { delay } from "./delay"; + +describe("delay", async () => { + test("waits the specified time before continuing", async () => { + const makeSceneFrame = frameMaker( + { + a: "Waiting", + }, + 0 + ); + + const executed = runMotion( + { + a: "Waiting", + }, + function* (context) { + yield* delay(1, function* () { + context.state({ + a: "One second later", + }); + yield; + }); + }, + { + fps: 3, + } + ); + + expect(executed.frames.length).toBe(4); + expect(executed.frames).toEqual([ + makeSceneFrame({ + a: "Waiting", + }), + makeSceneFrame({ + a: "Waiting", + }), + makeSceneFrame({ + a: "Waiting", + }), + makeSceneFrame({ + a: "One second later", + }), + ]); + }); +}); diff --git a/packages/motion/src/flow/delay.ts b/packages/motion/src/flow/delay.ts new file mode 100644 index 0000000..810f8c3 --- /dev/null +++ b/packages/motion/src/flow/delay.ts @@ -0,0 +1,11 @@ +import { MotionState } from "@/context"; +import { chain } from "./chain"; +import { wait } from "./wait"; +import { MotionBuilderish } from "@/utils"; + +export function* delay( + time: number, + ...threads: MotionBuilderish[] +) { + yield* chain(wait(time), ...threads); +} diff --git a/packages/motion/src/flow/index.ts b/packages/motion/src/flow/index.ts new file mode 100644 index 0000000..b79ea82 --- /dev/null +++ b/packages/motion/src/flow/index.ts @@ -0,0 +1,4 @@ +export * from "./all"; +export * from "./chain"; +export * from "./delay"; +export * from "./wait"; diff --git a/packages/motion/src/flow/wait.test.ts b/packages/motion/src/flow/wait.test.ts new file mode 100644 index 0000000..83a4f2a --- /dev/null +++ b/packages/motion/src/flow/wait.test.ts @@ -0,0 +1,48 @@ +import { test, describe, expect } from "vitest"; + +import { runMotion } from "@/context"; +import { frameMaker } from "@/test-utils"; +import { wait } from "./wait"; + +describe("wait", async () => { + test("waits the specified time before continuing", async () => { + const makeSceneFrame = frameMaker( + { + a: "Waiting", + }, + 0 + ); + + const executed = runMotion( + { + a: "Waiting", + }, + function* (context) { + yield* wait(1); + context.state({ + a: "One second later", + }); + yield; + }, + { + fps: 3, + } + ); + + expect(executed.frames.length).toBe(4); + expect(executed.frames).toEqual([ + makeSceneFrame({ + a: "Waiting", + }), + makeSceneFrame({ + a: "Waiting", + }), + makeSceneFrame({ + a: "Waiting", + }), + makeSceneFrame({ + a: "One second later", + }), + ]); + }); +}); diff --git a/packages/motion/src/flow/wait.ts b/packages/motion/src/flow/wait.ts new file mode 100644 index 0000000..40a4bba --- /dev/null +++ b/packages/motion/src/flow/wait.ts @@ -0,0 +1,10 @@ +import { MotionState, requestContext } from "@/context"; + +export function* wait(time: number) { + const context = yield* requestContext(); + const { fps } = context.settings; + + for (let i = 0; i < Math.round(fps * time); i++) { + yield; + } +} diff --git a/packages/motion/src/utils/asIterable.ts b/packages/motion/src/utils/asIterable.ts new file mode 100644 index 0000000..a5ca6f3 --- /dev/null +++ b/packages/motion/src/utils/asIterable.ts @@ -0,0 +1,15 @@ +import { MotionBuilder, MotionContext, MotionState } from "@/context"; + +export type MotionBuilderish = + | MotionBuilder + | ReturnType>; + +export function asIterable( + iterable: MotionBuilderish, + context: MotionContext +) { + if (Symbol.iterator in iterable) { + return iterable; + } + return iterable(context); +} diff --git a/packages/motion/src/utils/index.ts b/packages/motion/src/utils/index.ts index 0d30919..b0a9db6 100644 --- a/packages/motion/src/utils/index.ts +++ b/packages/motion/src/utils/index.ts @@ -1 +1,2 @@ export * from "./generateMeta"; +export * from "./asIterable"; From 9224168d22964620e33da0834c95b351d38b25b4 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Tue, 13 Jun 2023 19:53:29 -0300 Subject: [PATCH 03/20] Implements tweening --- packages/core/src/utils/easing-functions.ts | 150 +++++++++++++++++++ packages/core/src/utils/index.ts | 2 + packages/core/src/utils/inverseLerp.ts | 6 + packages/motion/src/movie/make-movie.test.ts | 38 +++++ packages/motion/src/tweening/index.ts | 1 + packages/motion/src/tweening/spring.test.ts | 28 ++++ packages/motion/src/tweening/spring.ts | 133 ++++++++++++++++ packages/motion/src/tweening/tween.test.ts | 41 +++++ packages/motion/src/tweening/tween.ts | 19 +++ 9 files changed, 418 insertions(+) create mode 100644 packages/core/src/utils/easing-functions.ts create mode 100644 packages/core/src/utils/inverseLerp.ts create mode 100644 packages/motion/src/tweening/index.ts create mode 100644 packages/motion/src/tweening/spring.test.ts create mode 100644 packages/motion/src/tweening/spring.ts create mode 100644 packages/motion/src/tweening/tween.test.ts create mode 100644 packages/motion/src/tweening/tween.ts diff --git a/packages/core/src/utils/easing-functions.ts b/packages/core/src/utils/easing-functions.ts new file mode 100644 index 0000000..691c876 --- /dev/null +++ b/packages/core/src/utils/easing-functions.ts @@ -0,0 +1,150 @@ +// All copied from +// https://github.com/ai/easings.net/blob/master/src/easings/easingsFunctions.ts + +const pow = Math.pow; +const sqrt = Math.sqrt; +const sin = Math.sin; +const cos = Math.cos; +const PI = Math.PI; +const c1 = 1.70158; +const c2 = c1 * 1.525; +const c3 = c1 + 1; +const c4 = (2 * PI) / 3; +const c5 = (2 * PI) / 4.5; + +const bounceOut: EasingFunction = function (x: number) { + const n1 = 7.5625; + const d1 = 2.75; + + if (x < 1 / d1) { + return n1 * x * x; + } else if (x < 2 / d1) { + return n1 * (x -= 1.5 / d1) * x + 0.75; + } else if (x < 2.5 / d1) { + return n1 * (x -= 2.25 / d1) * x + 0.9375; + } else { + return n1 * (x -= 2.625 / d1) * x + 0.984375; + } +}; + +export const easingsFunctions = { + linear: (x: number) => x, + easeInQuad: function (x: number) { + return x * x; + }, + easeOutQuad: function (x: number) { + return 1 - (1 - x) * (1 - x); + }, + easeInOutQuad: function (x: number) { + return x < 0.5 ? 2 * x * x : 1 - pow(-2 * x + 2, 2) / 2; + }, + easeInCubic: function (x: number) { + return x * x * x; + }, + easeOutCubic: function (x: number) { + return 1 - pow(1 - x, 3); + }, + easeInOutCubic: function (x: number) { + return x < 0.5 ? 4 * x * x * x : 1 - pow(-2 * x + 2, 3) / 2; + }, + easeInQuart: function (x: number) { + return x * x * x * x; + }, + easeOutQuart: function (x: number) { + return 1 - pow(1 - x, 4); + }, + easeInOutQuart: function (x: number) { + return x < 0.5 ? 8 * x * x * x * x : 1 - pow(-2 * x + 2, 4) / 2; + }, + easeInQuint: function (x: number) { + return x * x * x * x * x; + }, + easeOutQuint: function (x: number) { + return 1 - pow(1 - x, 5); + }, + easeInOutQuint: function (x: number) { + return x < 0.5 ? 16 * x * x * x * x * x : 1 - pow(-2 * x + 2, 5) / 2; + }, + easeInSine: function (x: number) { + return 1 - cos((x * PI) / 2); + }, + easeOutSine: function (x: number) { + return sin((x * PI) / 2); + }, + easeInOutSine: function (x: number) { + return -(cos(PI * x) - 1) / 2; + }, + easeInExpo: function (x: number) { + return x === 0 ? 0 : pow(2, 10 * x - 10); + }, + easeOutExpo: function (x: number) { + return x === 1 ? 1 : 1 - pow(2, -10 * x); + }, + easeInOutExpo: function (x: number) { + return x === 0 + ? 0 + : x === 1 + ? 1 + : x < 0.5 + ? pow(2, 20 * x - 10) / 2 + : (2 - pow(2, -20 * x + 10)) / 2; + }, + easeInCirc: function (x: number) { + return 1 - sqrt(1 - pow(x, 2)); + }, + easeOutCirc: function (x: number) { + return sqrt(1 - pow(x - 1, 2)); + }, + easeInOutCirc: function (x: number) { + return x < 0.5 + ? (1 - sqrt(1 - pow(2 * x, 2))) / 2 + : (sqrt(1 - pow(-2 * x + 2, 2)) + 1) / 2; + }, + easeInBack: function (x: number) { + return c3 * x * x * x - c1 * x * x; + }, + easeOutBack: function (x: number) { + return 1 + c3 * pow(x - 1, 3) + c1 * pow(x - 1, 2); + }, + easeInOutBack: function (x: number) { + return x < 0.5 + ? (pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2 + : (pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2; + }, + easeInElastic: function (x: number) { + return x === 0 + ? 0 + : x === 1 + ? 1 + : -pow(2, 10 * x - 10) * sin((x * 10 - 10.75) * c4); + }, + easeOutElastic: function (x: number) { + return x === 0 + ? 0 + : x === 1 + ? 1 + : pow(2, -10 * x) * sin((x * 10 - 0.75) * c4) + 1; + }, + easeInOutElastic: function (x: number) { + return x === 0 + ? 0 + : x === 1 + ? 1 + : x < 0.5 + ? -(pow(2, 20 * x - 10) * sin((20 * x - 11.125) * c5)) / 2 + : (pow(2, -20 * x + 10) * sin((20 * x - 11.125) * c5)) / 2 + 1; + }, + easeInBounce: function (x: number) { + return 1 - bounceOut(1 - x); + }, + easeOutBounce: bounceOut, + easeInOutBounce: function (x: number) { + return x < 0.5 + ? (1 - bounceOut(1 - 2 * x)) / 2 + : (1 + bounceOut(2 * x - 1)) / 2; + }, +}; + +export type EasingKeys = keyof typeof easingsFunctions; +export type EasingFunction = (progress: number) => number; +export type EasingOptions = EasingKeys | EasingFunction; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 62046d0..3cfa1a8 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,3 +1,5 @@ export * from "./remap"; export * from "./lerp"; +export * from "./inverseLerp"; export * from "./isObject"; +export * from "./easing-functions"; diff --git a/packages/core/src/utils/inverseLerp.ts b/packages/core/src/utils/inverseLerp.ts new file mode 100644 index 0000000..4cd00cf --- /dev/null +++ b/packages/core/src/utils/inverseLerp.ts @@ -0,0 +1,6 @@ +export const inverseLerp = (a: number, b: number, v: number) => { + if (a === b) { + return 0; + } + return (v - a) / (b - a); +}; diff --git a/packages/motion/src/movie/make-movie.test.ts b/packages/motion/src/movie/make-movie.test.ts index c7006e9..c03ef7e 100644 --- a/packages/motion/src/movie/make-movie.test.ts +++ b/packages/motion/src/movie/make-movie.test.ts @@ -306,4 +306,42 @@ describe("makeMovie", async () => { }), ]); }); + + test("should update $transitionIn correctly", () => { + const movie = makeMovie( + "Test", + { + scene1: makeScene("Scene 1", {}, function* (context) { + yield; + }), + scene2: makeScene("Scene 2", {}, function* (context) { + yield; + yield; + yield; + }), + }, + { + transitionDuration: 3, + } + ); + + const executed = runMotion(movie.initialState, movie.builder, { + fps: 1, + }); + + expect(executed.frames.length).toBe(4); + + // First frame + expect(executed.frames[0]?.scene1.$transitionIn).toEqual(1); + expect(executed.frames[0]?.scene2.$transitionIn).toEqual(0); + + // Second frame + expect(executed.frames[1]?.scene2.$transitionIn).toEqual(1 / 3); + + // Third frame + expect(executed.frames[2]?.scene2.$transitionIn).toEqual(2 / 3); + + // Fourth frame + expect(executed.frames[3]?.scene2.$transitionIn).toEqual(1); + }); }); diff --git a/packages/motion/src/tweening/index.ts b/packages/motion/src/tweening/index.ts new file mode 100644 index 0000000..7f92c92 --- /dev/null +++ b/packages/motion/src/tweening/index.ts @@ -0,0 +1 @@ +export * from "./tween"; diff --git a/packages/motion/src/tweening/spring.test.ts b/packages/motion/src/tweening/spring.test.ts new file mode 100644 index 0000000..75cb0e3 --- /dev/null +++ b/packages/motion/src/tweening/spring.test.ts @@ -0,0 +1,28 @@ +import { test, describe, expect } from "vitest"; + +import { runMotion } from "@/context"; +import { frameMaker } from "@/test-utils"; +import { spring } from "./spring"; + +// describe("spring", async () => { +// test("aaa", () => { +// const makeSceneFrame = frameMaker( +// { +// t: 0, +// }, +// 0 +// ); + +// const executed = runMotion( +// { +// t: 0, +// }, +// function* (context) { +// yield* spring(0, 100, (t) => context.state({ t })); +// }, +// { +// fps: 30, +// } +// ); +// }); +// }); diff --git a/packages/motion/src/tweening/spring.ts b/packages/motion/src/tweening/spring.ts new file mode 100644 index 0000000..e07ca28 --- /dev/null +++ b/packages/motion/src/tweening/spring.ts @@ -0,0 +1,133 @@ +import { MotionState, requestContext } from "@/context"; +import { Vec2, point } from "@coord/core"; +const isNumber = (n: unknown): n is number => typeof n === "number"; + +export function* spring( + from: T, + to: T, + fn: (t: T) => void, + spring: SpringParameters = Spring.Plop +) { + const { settings } = yield* requestContext(); + + const { settleTolerance = 0.001 } = spring; + + let position = isNumber(from) ? point(from, from) : from.clone(); + const target = isNumber(to) ? point(to, to) : to.clone(); + const velocity = isNumber(spring.initialVelocity) + ? point(spring.initialVelocity, spring.initialVelocity) + : spring.initialVelocity; + const update = (dt: number) => { + if (spring === null) { + return; + } + const positionDelta = position.sub(target); + + // Using hooks law: F=-kx; with k being the spring constant and x the offset + // to the settling position + + const force = point( + -spring.stiffness * positionDelta.x - spring.damping * velocity.x, + -spring.stiffness * positionDelta.y - spring.damping * velocity.y + ); + + // Update the velocity based on the given timestep + velocity.x += (force.x / spring.mass) * dt; + velocity.y += (force.y / spring.mass) * dt; + + position = position.add(velocity.scale(dt)); + }; + + const fixedStep = 1 / settings.physicsFps; + const updateStep = 1 / settings.fps; + const loopStep = Math.min(fixedStep, updateStep); + + let settled = false; + + let frameTime = 0; + let fixedTime = 0; + const toleranceSquared = Math.pow(settleTolerance, 2); + while (!settled) { + frameTime += loopStep; + fixedTime += loopStep; + + if (fixedTime >= fixedStep) { + fixedTime -= fixedStep; + update(fixedStep); + + if ( + Math.abs(position.squaredDistanceTo(target)) < toleranceSquared && + Math.abs(velocity.lengthSquared()) < toleranceSquared + ) { + settled = true; + position = target; + } + } + + if (frameTime >= updateStep) { + frameTime -= updateStep; + fn((Array.isArray(from) ? position : position.x) as T); + yield; + } + } +} + +export interface SpringParameters { + mass: number; + stiffness: number; + damping: number; + initialVelocity: number | Vec2; + settleTolerance?: number; +} + +export const Spring = { + Beat: { + mass: 0.13, + stiffness: 5.7, + damping: 1.2, + initialVelocity: 10.0, + settleTolerance: 0.001, + }, + Plop: { + mass: 0.2, + stiffness: 20.0, + damping: 0.68, + initialVelocity: 0.0, + settleTolerance: 0.001, + }, + Bounce: { + mass: 0.08, + stiffness: 4.75, + damping: 0.05, + initialVelocity: 0.0, + settleTolerance: 0.001, + }, + Swing: { + mass: 0.39, + stiffness: 19.85, + damping: 2.82, + initialVelocity: 0.0, + settleTolerance: 0.001, + }, + Jump: { + mass: 0.04, + stiffness: 10.0, + damping: 0.7, + initialVelocity: 8.0, + settleTolerance: 0.001, + }, + Strike: { + mass: 0.03, + stiffness: 20.0, + damping: 0.9, + initialVelocity: 4.8, + settleTolerance: 0.001, + }, + Smooth: { + mass: 0.16, + stiffness: 15.35, + damping: 1.88, + initialVelocity: 0.0, + settleTolerance: 0.001, + }, +}; diff --git a/packages/motion/src/tweening/tween.test.ts b/packages/motion/src/tweening/tween.test.ts new file mode 100644 index 0000000..209144b --- /dev/null +++ b/packages/motion/src/tweening/tween.test.ts @@ -0,0 +1,41 @@ +import { test, describe, expect } from "vitest"; + +import { runMotion } from "@/context"; +import { frameMaker } from "@/test-utils"; +import { tween } from "./tween"; + +describe("tween", async () => { + test("waits the specified time before continuing", async () => { + const makeSceneFrame = frameMaker( + { + t: 0, + }, + 0 + ); + + const executed = runMotion( + { + t: 0, + }, + function* (context) { + yield* tween(1, (t) => context.state({ t })); + }, + { + fps: 3, + } + ); + + expect(executed.frames.length).toBe(3); + expect(executed.frames).toEqual([ + makeSceneFrame({ + t: 1 / 3, + }), + makeSceneFrame({ + t: 2 / 3, + }), + makeSceneFrame({ + t: 1, + }), + ]); + }); +}); diff --git a/packages/motion/src/tweening/tween.ts b/packages/motion/src/tweening/tween.ts new file mode 100644 index 0000000..f491f71 --- /dev/null +++ b/packages/motion/src/tweening/tween.ts @@ -0,0 +1,19 @@ +import { MotionState, requestContext } from "@/context"; +import { EasingOptions, easingsFunctions } from "@coord/core"; + +export function* tween( + duration: number, + fn: (t: number) => void, + easing: EasingOptions = "linear" +) { + const easingFn = + typeof easing === "function" ? easing : easingsFunctions[easing]; + const context = yield* requestContext(); + const { fps } = context.settings; + const frames = Math.round(fps * duration); + + for (let i = 1; i <= frames; i++) { + fn(easingFn(i / frames)); + yield; + } +} From 20e34d565f7d048fb9c0b64a900a5046b1e6ebb3 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Wed, 14 Jun 2023 18:05:36 -0300 Subject: [PATCH 04/20] adds basic controls --- packages/core/package.json | 8 +- packages/core/src/index.ts | 1 + packages/core/src/types/deep-paths.test-d.ts | 322 +++++++++++------- packages/core/src/types/deep-paths.ts | 110 ++++-- packages/core/src/utils/getDeep.ts | 10 + packages/core/src/utils/index.ts | 2 + packages/core/src/utils/setDeep.test.ts | 48 +++ packages/core/src/utils/setDeep.ts | 104 ++++++ packages/motion/src/context/motion-runner.ts | 17 +- packages/motion/src/controls/index.ts | 0 .../src/controls/number-control.test.ts | 33 ++ .../motion/src/controls/number-control.ts | 88 +++++ .../motion/src/controls/point-control.test.ts | 27 ++ packages/motion/src/controls/point-control.ts | 96 ++++++ packages/motion/src/flow/all.ts | 5 +- packages/motion/src/tweening/index.ts | 1 + packages/motion/src/tweening/spring.test.ts | 28 -- pnpm-lock.yaml | 6 + 18 files changed, 717 insertions(+), 189 deletions(-) create mode 100644 packages/core/src/utils/getDeep.ts create mode 100644 packages/core/src/utils/setDeep.test.ts create mode 100644 packages/core/src/utils/setDeep.ts create mode 100644 packages/motion/src/controls/index.ts create mode 100644 packages/motion/src/controls/number-control.test.ts create mode 100644 packages/motion/src/controls/number-control.ts create mode 100644 packages/motion/src/controls/point-control.test.ts create mode 100644 packages/motion/src/controls/point-control.ts delete mode 100644 packages/motion/src/tweening/spring.test.ts diff --git a/packages/core/package.json b/packages/core/package.json index 4bdd75e..67ee992 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,14 +25,18 @@ ] }, "devDependencies": { + "@types/lodash-es": "^4.17.7", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "react": "^17.0.2", "tsconfig": "workspace:*", "tsup": "^6.7.0", - "typescript": "^5.1.3" + "typescript": "^5.1.3", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21" }, "publishConfig": { "access": "public" - } + }, + "dependencies": {} } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1c75624..3ee64f0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,2 +1,3 @@ export * from "./math"; export * from "./utils"; +export * from "./types"; diff --git a/packages/core/src/types/deep-paths.test-d.ts b/packages/core/src/types/deep-paths.test-d.ts index e4cd080..a0b2303 100644 --- a/packages/core/src/types/deep-paths.test-d.ts +++ b/packages/core/src/types/deep-paths.test-d.ts @@ -1,34 +1,43 @@ import { assertType, describe } from "vitest"; import { + AnythingButTupleOrRecord, InferMultiplePathValues, InferPath, InferPathValue, InferPathValueTree, } from "./deep-paths"; +import { Equal } from "./test-utils"; describe("types: deep paths", () => { describe("InferPath", () => { assertType< - InferPath<{ - foo: { - bar: { - baz: 1; + Equal< + InferPath<{ + foo: { + bar: { + baz: 1; + }; }; - }; - }> - >("" as "foo" | "foo.bar" | "foo.bar.baz"); + }>, + "foo" | "foo.bar" | "foo.bar.baz" + > + >(true); + assertType< - InferPath<{ - foo: { + Equal< + InferPath<{ + foo: { + bar: { + baz: 1; + }; + }; bar: { baz: 1; }; - }; - bar: { - baz: 1; - }; - }> - >("" as "foo" | "foo.bar" | "foo.bar.baz" | "bar" | "bar.baz"); + }>, + "foo" | "foo.bar" | "foo.bar.baz" | "bar" | "bar.baz" + > + >(true); assertType< InferPath<{ @@ -43,136 +52,183 @@ describe("types: deep paths", () => { >("" as "foo" | "foo.bar" | "foo.baz"); assertType< - InferPath<{ - foo: [ - { - bar: "hello"; - }, - { - baz: "world"; - } - ]; - }> - >("" as "foo" | "foo.0" | "foo.0.bar" | "foo.1" | "foo.1.baz"); - - assertType< - InferPath<{ - foo: ( - | { + Equal< + InferPath<{ + foo: [ + { bar: "hello"; - } - | { + }, + { baz: "world"; } - )[]; - }> - >( - "" as "foo" | `foo.${number}` | `foo.${number}.bar` | `foo.${number}.baz` - ); + ]; + }>, + "foo" | "foo.0" | "foo.0.bar" | "foo.1" | "foo.1.baz" + > + >(true); assertType< - InferPath<{ - foo?: - | { - bar: 2; - } - | "hello"; - }> - >("" as "foo" | "foo.bar"); + Equal< + InferPath<{ + foo: ( + | { + bar: "hello"; + } + | { + baz: "world"; + } + )[]; + }>, + "foo" | `foo.${number}` | `foo.${number}.bar` | `foo.${number}.baz` + > + >(true); + + assertType< + Equal< + InferPath<{ + foo?: + | { + bar: 2; + } + | "hello"; + }>, + "foo" | "foo.bar" + > + >(true); }); describe("InferPathValue", () => { assertType< - InferPathValue< - { - foo: { - bar: { - baz: 1; + Equal< + InferPathValue< + { + foo: { + bar: { + baz: 1; + }; }; - }; - }, - "foo.bar.baz" + }, + "foo.bar.baz" + >, + 1 > - >(1); + >(true); assertType< - InferPathValue< - { - foo: { + Equal< + InferPathValue< + { + foo: { + bar: { + baz: 1; + }; + }; bar: { - baz: 1; + baz: 2; }; - }; - bar: { - baz: 2; - }; - }, - "foo.bar.baz" | "bar.baz" + }, + "foo.bar.baz" | "bar.baz" + >, + 1 | 2 > - >({} as 1 | 2); + >(true); assertType< - InferPathValue< - { - foo?: { - bar: 1; - }; - }, - "foo.bar" + Equal< + InferPathValue< + { + foo?: { + bar: 1; + }; + }, + "foo.bar" + >, + 1 | undefined > - >({} as 1 | undefined); + >(true); assertType< - InferPathValue< - { - foo: - | { + Equal< + InferPathValue< + { + foo: + | { + bar: 1; + } + | { + baz: 2; + }; + }, + "foo.bar" + >, + 1 | undefined + > + >(true); + + assertType< + Equal< + InferPathValue< + { + foo: [ + { bar: 1; - } - | { + }, + { baz: 2; - }; - }, - "foo.bar" + } + ]; + }, + "foo.0.bar" | "foo.1.baz" + >, + 1 | 2 > - >({} as 1 | undefined); - + >(true); assertType< - InferPathValue< - { - foo: [ - { - bar: 1; - }, - { - baz: 2; - } - ]; - }, - "foo.0.bar" | "foo.1.baz" + Equal< + InferPathValue< + { + foo: number[]; + }, + "foo.0" + >, + number | undefined + > + >(true); + assertType< + Equal< + InferPathValue< + { + foo: { bar: number }[]; + }, + "foo.0.bar" + >, + number | undefined > - >({} as 1 | 2); + >(true); }); describe("InferMultiplePathValues", () => { assertType< - InferMultiplePathValues< - { - foo: { + Equal< + InferMultiplePathValues< + { + foo: { + bar: { + baz: 1; + }; + }; bar: { - baz: 1; + baz: 2; }; - }; - bar: { - baz: 2; - }; - baz: { - hello: { - world: 3; + baz: { + hello: { + world: 3; + }; }; - }; - }, - ["foo.bar.baz", "bar.baz", "baz.hello.world"] + }, + ["foo.bar.baz", "bar.baz", "baz.hello.world"] + >, + [1, 2, 3] > - >([1, 2, 3]); + >(true); }); describe("InferPathValueTree", () => { @@ -192,19 +248,35 @@ describe("types: deep paths", () => { }; }; - assertType>( - {} as { - foo: testCase1["foo"]; - "foo.bar": testCase1["foo"]["bar"]; - "foo.bar.baz": testCase1["foo"]["bar"]["baz"]; + type actual = InferPathValueTree; + type expected = { + foo: testCase1["foo"]; + "foo.bar": testCase1["foo"]["bar"]; + "foo.bar.baz": testCase1["foo"]["bar"]["baz"]; - bar: testCase1["bar"]; - "bar.baz": testCase1["bar"]["baz"]; + bar: testCase1["bar"]; + "bar.baz": testCase1["bar"]["baz"]; - baz: testCase1["baz"]; - "baz.hello": testCase1["baz"]["hello"]; - "baz.hello.world": testCase1["baz"]["hello"]["world"]; - } - ); + baz: testCase1["baz"]; + "baz.hello": testCase1["baz"]["hello"]; + "baz.hello.world": testCase1["baz"]["hello"]["world"]; + }; + assertType>(true); + }); + + describe("AnythingButTupleOrRecord", () => { + assertType>(true); + assertType>>(true); + assertType[]>>(true); + + assertType>(false); + assertType>>(false); + assertType< + AnythingButTupleOrRecord<{ + foo: { + bar: 2; + }; + }> + >(false); }); }); diff --git a/packages/core/src/types/deep-paths.ts b/packages/core/src/types/deep-paths.ts index 8bca03a..fe77e9d 100644 --- a/packages/core/src/types/deep-paths.ts +++ b/packages/core/src/types/deep-paths.ts @@ -6,41 +6,65 @@ type TupleKey = Exclude; type ArrayKey = number; -export type AnythingButTupleOrRecord = T extends Record - ? T extends Array - ? IsTuple extends true - ? false - : true - : false +export type AnythingButTupleOrRecord = [T] extends [never] + ? true + : IsTupleOrRecord extends true + ? false : true; -export type ExtractBranches = TKey extends keyof any +export type IsTupleOrRecord = [T] extends [never] + ? false + : T extends Record + ? T extends Array + ? IsTuple + : true + : false; + +export type ExtractBranches = TKey extends `${number}` + ? Extract> + : TKey extends keyof any ? Extract : never; -export type InferPathValueImpl< +type GetValue = TKey extends keyof TShape + ? TShape[TKey] + : TKey extends `${number}` + ? TShape extends Array + ? IsTuple extends false + ? TShape[number] + : never + : never + : never; + +type Maybe = T | undefined; +type MaybefyIf = TCondition extends true ? Maybe : T; + +export type ShouldMaybefy = IsMaybeAlready extends true + ? true + : IsTupleOrRecord extends true + ? false + : true; + +export type InferPathValueImplementation< TShape, TPath, - TUndefined = false -> = TPath extends keyof TShape - ? TShape[TPath] | (TUndefined extends true ? undefined : never) - : TPath extends `${infer THead}.${infer TTail}` | `${infer THead}` - ? THead extends keyof TShape - ? InferPathValueImpl< - TShape[THead], + IsMaybeBranch = false +> = SplitHead extends [infer THead, infer TTail] + ? TTail extends undefined + ? MaybefyIf< + GetValue, THead>, + ShouldMaybefy + > + : InferPathValueImplementation< + GetValue, THead>, TTail, - TUndefined extends true ? true : AnythingButTupleOrRecord + ShouldMaybefy > - : // The lines below basically normalizes the inputs and recursively call the type utility again - THead extends keyof ExtractBranches - ? InferPathValueImpl, TPath, true> - : never - : never; + : TPath; -export type InferPathValue< - TShape extends Record, - TPath extends InferPath -> = InferPathValueImpl; +export type InferPathValue = { + [K in TPaths]: InferPathValueImplementation; +}[TPaths]; type MakePaths = V extends Record ? `${K}` | `${K}.${InferPathImpl}` @@ -56,19 +80,47 @@ export type InferPathImpl = T extends ReadonlyArray [K in keyof T]-?: MakePaths; }[keyof T]; -export type InferPath> = - InferPathImpl; +export type InferPath = InferPathImpl; export type InferMultiplePathValues< TFieldValues extends Record, TPath extends InferPath[] > = { - [K in keyof TPath]: InferPathValueImpl< + [K in keyof TPath]: InferPathValueImplementation< TFieldValues, TPath[K] & InferPath >; }; export type InferPathValueTree = { - [K in InferPathImpl]-?: InferPathValueImpl; + [K in InferPath]: InferPathValue; }; + +type test = InferPathValueTree< + | { + z: 2; + } + | { + foo: { + bar: { + baz: 1; + }; + }; + bar: { + baz: 2; + }; + } +>; + +export type OnlyKeysOfType = Omit< + T, + { + [K in keyof T]: T[K] extends TType ? never : K; + }[keyof T] +>; + +type SplitHead = T extends `${infer U}.${infer V}` + ? [U, V] + : T extends string + ? [T, undefined] + : never; diff --git a/packages/core/src/utils/getDeep.ts b/packages/core/src/utils/getDeep.ts new file mode 100644 index 0000000..3d9f7dc --- /dev/null +++ b/packages/core/src/utils/getDeep.ts @@ -0,0 +1,10 @@ +import { InferPath, InferPathValue, InferPathValueTree } from "../types"; +import { get } from "lodash-es"; + +export function getDeep< + T, + TTree extends InferPathValueTree, + TKey extends keyof TTree +>(obj: T, path: TKey) { + return get(obj, path) as TTree[TKey]; +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 3cfa1a8..afd962a 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -3,3 +3,5 @@ export * from "./lerp"; export * from "./inverseLerp"; export * from "./isObject"; export * from "./easing-functions"; +export * from "./setDeep"; +export * from "./getDeep"; diff --git a/packages/core/src/utils/setDeep.test.ts b/packages/core/src/utils/setDeep.test.ts new file mode 100644 index 0000000..1b585d8 --- /dev/null +++ b/packages/core/src/utils/setDeep.test.ts @@ -0,0 +1,48 @@ +import { test, expect, describe } from "vitest"; +import { setDeep } from "./setDeep"; +describe("setDeep", () => { + test("sets a value at a deep path", () => { + const obj = { + foo: { + bar: { + baz: 1, + }, + }, + }; + const newObj = setDeep(obj, "foo.bar.baz", 2); + + expect(newObj).toEqual({ + foo: { + bar: { + baz: 2, + }, + }, + }); + }); + + test("keep the changed path immutable", () => { + const obj = { + foo: { + bar: { + baz: 1, + }, + shouldNotMutate: { + baz: 1, + }, + }, + shouldNotMutate: { + baz: 1, + }, + }; + const newObj = setDeep(obj, "foo.bar.baz", 2); + + // branches that should mutate + expect(newObj).not.toBe(obj); + expect(newObj.foo).not.toBe(obj.foo); + expect(newObj.foo.bar).not.toBe(obj.foo.bar); + + // branches that should NOT mutate + expect(newObj.foo.shouldNotMutate).toBe(obj.foo.shouldNotMutate); + expect(newObj.shouldNotMutate).toBe(obj.shouldNotMutate); + }); +}); diff --git a/packages/core/src/utils/setDeep.ts b/packages/core/src/utils/setDeep.ts new file mode 100644 index 0000000..a01e7de --- /dev/null +++ b/packages/core/src/utils/setDeep.ts @@ -0,0 +1,104 @@ +import { InferPath, InferPathValue, InferPathValueTree } from "../types"; +import compact from "lodash-es/compact"; +import { isObject } from "./isObject"; +// const copy = | Record>( +// obj: T +// ): T => { +// return Array.isArray(obj) ? ([...obj] as T) : ({ ...obj } as T); +// }; + +// const insertAt = (obj: TObj, path: string[], value: TValue) => {}; +// export function setDeep< +// T extends { +// [key: string]: unknown; +// }, +// TKey extends InferPath +// >(obj: T, path: TKey, value: InferPathValue) { +// if (path === "") { +// return value; +// } +// const parts = path.split("."); + +// let current: Record | unknown[] = copy(obj); + +// for (let i = 0; i < parts.length - 1; i++) { +// const part = parts[i]!; +// let value: unknown = isObject(current) +// ? current[part] +// : current[parseInt(part)]; + +// if (Array.isArray(value)) { +// const newV = copy(value); +// current[part] = newV; +// current = newV; + +// // current = value; +// } +// } + +// const lastKey = parts[parts.length - 1]!; +// current[lastKey] = value; +// } + +export const isKey = (value: string) => /^\w*$/.test(value); +export const stringToPath = (input: string): string[] => + compact(input.replace(/["|']|\]/g, "").split(/\.|\[/)); + +const copy = | object>(obj: T): T => { + return Array.isArray(obj) ? ([...obj] as T) : ({ ...obj } as T); +}; +// export const splitPathAtHead = (path: T) => { +// const [head, ...rest] = stringToPath(path); +// return [head, rest.join(".")] as SplitAtHead; +// }; + +export const setDeep = < + // TFieldValues extends { + // [key: string]: unknown; + // }, + // TFieldPath extends InferPath, + // TValue extends InferPathValue + + T extends { + [key: string]: unknown; + }, + TTree extends InferPathValueTree, + TKey extends keyof TTree & string +>( + obj: T, + path: TKey, + value: TTree[TKey] +) => { + let index = -1; + // if (path === "") { + // return value; + // } + + const tempPath = isKey(path) ? [path] : stringToPath(path); + const length = tempPath.length; + const lastIndex = length - 1; + const newObj = copy(obj); + let tempObj: { + [key: string]: unknown; + } = newObj; + while (++index < length) { + const key = tempPath[index] as keyof typeof tempObj; + + let newValue = value; + if (index !== lastIndex) { + const objValue = newObj[key]; + + if (isObject(objValue) || Array.isArray(objValue)) { + newValue = copy(objValue) as TTree[TKey]; + } else if (isNaN(+tempPath[index + 1]!)) { + newValue = {} as TTree[TKey]; + } else { + newValue = [] as TTree[TKey]; + } + } + + tempObj[key] = newValue; + tempObj = tempObj[key] as typeof tempObj; + } + return newObj; +}; diff --git a/packages/motion/src/context/motion-runner.ts b/packages/motion/src/context/motion-runner.ts index 974d4bb..37eaaa0 100644 --- a/packages/motion/src/context/motion-runner.ts +++ b/packages/motion/src/context/motion-runner.ts @@ -21,7 +21,13 @@ export const isMotionBuilderRequest = ( export type MotionBuilder = ( context: MotionContext -) => Generator | undefined, void, unknown>; +) => MotionBuilderGenerator; + +export type MotionBuilderGenerator = Generator< + MotionBuilderRequest | undefined, + unknown, + unknown +>; export function* motionRunner( context: MotionContext, @@ -32,7 +38,7 @@ export function* motionRunner( while (true) { const currentIteration = bulderIterator.next(); - if (currentIteration.value) { + if (isMotionBuilderRequest(currentIteration.value)) { if (currentIteration.value.type === "REQUEST_CONTEXT") { currentIteration.value.context = context; continue; @@ -57,9 +63,12 @@ export function createMotion( return [context, runner] as const; } -export function runMotion( +export function runMotion< + TState extends MotionState, + TBuilder extends MotionBuilder +>( initialState: TState, - builder: MotionBuilder, + builder: TBuilder, contextSettings: Partial = {} ) { const [context, runner] = createMotion( diff --git a/packages/motion/src/controls/index.ts b/packages/motion/src/controls/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/motion/src/controls/number-control.test.ts b/packages/motion/src/controls/number-control.test.ts new file mode 100644 index 0000000..818690e --- /dev/null +++ b/packages/motion/src/controls/number-control.test.ts @@ -0,0 +1,33 @@ +import { test, describe, expect } from "vitest"; + +import { runMotion } from "@/context"; +import { frameMaker } from "@/test-utils"; +import { controlNumber } from "./number-control"; +import { all, chain } from "@/flow"; + +describe("number control", async () => { + test("tweens to a value", async () => { + let executed = runMotion( + { + t: 0, + b: [0, 2], + }, + function* () { + const t = yield* controlNumber("t"); + yield* all( + t.tweenTo(10, 1), + controlNumber("b.0", (t) => t.tweenTo(10, 1)) + ); + } + ); + + expect(executed.frames.length).toBe(60); + + expect(executed.frames.at(-1)).toEqual({ + $frame: 59, + $transitionIn: 1, + t: 10, + b: [10, 2], + }); + }); +}); diff --git a/packages/motion/src/controls/number-control.ts b/packages/motion/src/controls/number-control.ts new file mode 100644 index 0000000..0436fc7 --- /dev/null +++ b/packages/motion/src/controls/number-control.ts @@ -0,0 +1,88 @@ +import { + MotionBuilder, + MotionContext, + MotionState, + requestContext, +} from "@/context"; +import { tween, spring, SpringParameters } from "@/tweening"; +import { + getDeep, + setDeep, + InferPathValueTree, + lerp, + EasingOptions, +} from "@coord/core"; + +type OnlyKeysOfType = Omit< + T, + { + [K in keyof T]: T[K] extends TType ? never : K; + }[keyof T] +>; + +export function* controlNumber< + TState extends MotionState, + TTree extends OnlyKeysOfType, number | undefined>, + TKey extends keyof TTree, + TControl extends NumberControl +>( + key: TKey & string, + fn?: (control: TControl) => ReturnType> +) { + const context = yield* requestContext(); + const control = new NumberControl(context, key) as TControl; + if (fn) { + yield* fn(control); + } + return control; +} + +const isFinite = (value: unknown): value is number => Number.isFinite(value); + +export class NumberControl { + chain: ReturnType>[] = []; + + constructor(public context: MotionContext, public key: string) {} + + get() { + return getDeep(this.context._state, this.key) as TType; + } + set(value: number) { + this.context._state = setDeep(this.context._state, this.key, value); + } + + tweenTo(value: number, duration: number, easing?: EasingOptions) { + const initialValue = this.get() ?? 0; + if (!isFinite(initialValue)) { + throw new Error( + `Cannot tween to a non-finite value, got ${initialValue}` + ); + } + + return tween( + duration, + (t) => { + this.set(lerp(initialValue, value, t)); + }, + easing + ); + } + + springTo(to: number, parameters?: SpringParameters) { + const initialValue = this.get() ?? 0; + if (!isFinite(initialValue)) { + throw new Error( + `Cannot spring to a non-finite value, got ${initialValue}` + ); + } + + return spring( + initialValue, + to, + (t) => { + this.set(t); + }, + parameters + ); + } +} diff --git a/packages/motion/src/controls/point-control.test.ts b/packages/motion/src/controls/point-control.test.ts new file mode 100644 index 0000000..fc14e31 --- /dev/null +++ b/packages/motion/src/controls/point-control.test.ts @@ -0,0 +1,27 @@ +import { test, describe, expect } from "vitest"; + +import { runMotion } from "@/context"; + +import { all } from "@/flow"; +import { point } from "@coord/core"; +import { controlPoint } from "./point-control"; + +describe("point control", async () => { + test("tweens to a value", async () => { + let executed = runMotion( + { + point: point(0, 0), + }, + function* () { + yield* all(controlPoint("point", (t) => t.tweenTo([10, 10], 1))); + } + ); + + expect(executed.frames.length).toBe(60); + expect(executed.frames.at(-1)).toEqual({ + $frame: 59, + $transitionIn: 1, + point: point(10, 10), + }); + }); +}); diff --git a/packages/motion/src/controls/point-control.ts b/packages/motion/src/controls/point-control.ts new file mode 100644 index 0000000..7ec879d --- /dev/null +++ b/packages/motion/src/controls/point-control.ts @@ -0,0 +1,96 @@ +import { + MotionBuilder, + MotionContext, + MotionState, + requestContext, +} from "@/context"; +import { tween, spring, SpringParameters } from "@/tweening"; +import { + getDeep, + setDeep, + InferPathValueTree, + EasingOptions, + Vec2ish, + Vec2, +} from "@coord/core"; + +type OnlyKeysOfType = Omit< + T, + { + [K in keyof T]: T[K] extends TType ? never : K; + }[keyof T] +>; + +export function* controlPoint< + TState extends MotionState, + TTree extends OnlyKeysOfType, Vec2 | undefined>, + TKey extends keyof TTree, + TControl extends PointControl +>( + key: TKey & string, + fn?: (control: TControl) => ReturnType> +) { + const context = yield* requestContext(); + const control = new PointControl(context, key) as TControl; + if (fn) { + yield* fn(control); + } + return control; +} + +export class PointControl { + chain: ReturnType>[] = []; + + constructor(public context: MotionContext, public key: string) {} + + get() { + const value = getDeep(this.context._state, this.key); + return value as TType; + } + set(value: Vec2ish) { + this.context._state = setDeep(this.context._state, this.key, value); + } + + tweenTo(value: Vec2ish, duration: number, easing?: EasingOptions) { + const initialValue = this.get(); + if ( + !(initialValue instanceof Vec2) || + (initialValue instanceof Vec2 && !initialValue.isFinite()) + ) { + throw new Error( + `Cannot tween to a non-finite value, got ${initialValue}` + ); + } + const target = Vec2.of(value); + + return tween( + duration, + (t) => { + this.set(initialValue.lerp(target, t)); + }, + easing + ); + } + + springTo(to: Vec2ish, parameters?: SpringParameters) { + const initialValue = this.get() ?? 0; + if ( + !(initialValue instanceof Vec2) || + (initialValue instanceof Vec2 && !initialValue.isFinite()) + ) { + throw new Error( + `Cannot tween to a non-finite value, got ${initialValue}` + ); + } + const target = Vec2.of(to); + + return spring( + initialValue, + target, + (t) => { + this.set(t); + }, + parameters + ); + } +} diff --git a/packages/motion/src/flow/all.ts b/packages/motion/src/flow/all.ts index 1fa51d3..4079531 100644 --- a/packages/motion/src/flow/all.ts +++ b/packages/motion/src/flow/all.ts @@ -1,5 +1,7 @@ import { MotionBuilder, + MotionBuilderGenerator, + MotionBuilderRequest, MotionState, isMotionBuilderRequest, requestContext, @@ -20,7 +22,7 @@ export function* all( while (true) { const { done, value } = thread.next(); if (isMotionBuilderRequest(value)) { - yield value; + yield value as MotionBuilderRequest; continue; } if (done) { @@ -34,4 +36,5 @@ export function* all( if (!threadIterables.length) break; yield; } + return; } diff --git a/packages/motion/src/tweening/index.ts b/packages/motion/src/tweening/index.ts index 7f92c92..6323b6c 100644 --- a/packages/motion/src/tweening/index.ts +++ b/packages/motion/src/tweening/index.ts @@ -1 +1,2 @@ export * from "./tween"; +export * from "./spring"; diff --git a/packages/motion/src/tweening/spring.test.ts b/packages/motion/src/tweening/spring.test.ts deleted file mode 100644 index 75cb0e3..0000000 --- a/packages/motion/src/tweening/spring.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { test, describe, expect } from "vitest"; - -import { runMotion } from "@/context"; -import { frameMaker } from "@/test-utils"; -import { spring } from "./spring"; - -// describe("spring", async () => { -// test("aaa", () => { -// const makeSceneFrame = frameMaker( -// { -// t: 0, -// }, -// 0 -// ); - -// const executed = runMotion( -// { -// t: 0, -// }, -// function* (context) { -// yield* spring(0, 100, (t) => context.state({ t })); -// }, -// { -// fps: 30, -// } -// ); -// }); -// }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 805c954..33d2281 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,15 +22,21 @@ importers: packages/core: specifiers: + '@types/lodash-es': ^4.17.7 '@types/react': ^18.2.0 '@types/react-dom': ^18.2.0 + lodash: ^4.17.21 + lodash-es: ^4.17.21 react: ^17.0.2 tsconfig: workspace:* tsup: ^6.7.0 typescript: ^5.1.3 devDependencies: + '@types/lodash-es': 4.17.7 '@types/react': 18.2.8 '@types/react-dom': 18.2.4 + lodash: 4.17.21 + lodash-es: 4.17.21 react: 17.0.2 tsconfig: link:../tsconfig tsup: 6.7.0_typescript@5.1.3 From 43910d4026f95159ca4e2437d0dcba471d4e7537 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Fri, 16 Jun 2023 19:05:44 -0300 Subject: [PATCH 05/20] implements player --- packages/core/package.json | 3 +- packages/core/src/math/transform.test.ts | 8 +- packages/core/src/math/transform.ts | 53 +++-- packages/core/src/math/vec2.ts | 4 + packages/core/src/types/deep-paths.ts | 28 ++- packages/core/src/utils/clamp.ts | 2 + packages/core/src/utils/formatDuration.ts | 10 + packages/core/src/utils/index.ts | 2 + packages/core/src/utils/setDeep.ts | 2 +- packages/docs/package.json | 6 +- packages/docs/src/app/motion/page.tsx | 119 ++++++++++++ packages/graph/package.json | 4 +- packages/graph/src/components/Marker.tsx | 14 +- packages/graph/src/components/Text.tsx | 6 +- packages/motion-react/CHANGELOG.md | 19 ++ packages/motion-react/package.json | 53 +++++ packages/motion-react/postcss.config.js | 3 + .../src/components/Player/MotionPlayer.tsx | 93 +++++++++ .../Player/MotionPlayerControls.tsx | 97 ++++++++++ .../Player/MotionPlayerProgressBar.tsx | 119 ++++++++++++ .../src/components/Player/index.tsx | 2 + packages/motion-react/src/components/index.ts | 1 + packages/motion-react/src/hooks/index.ts | 1 + .../src/hooks/useMotionController.ts | 67 +++++++ packages/motion-react/src/index.ts | 3 + packages/motion-react/src/styles.css | 3 + packages/motion-react/src/types.ts | 0 packages/motion-react/tailwind.config.js | 8 + packages/motion-react/tsconfig.json | 15 ++ packages/motion-react/vitest.config.ts | 9 + packages/motion/package.json | 16 +- .../src/context/create-motion-context.ts | 46 ++--- .../motion/src/context/motion-runner.test.ts | 17 +- packages/motion/src/context/motion-runner.ts | 50 +++-- packages/motion/src/controls/color-control.ts | 109 +++++++++++ packages/motion/src/controls/index.ts | 4 + .../src/controls/number-control.test.ts | 11 +- .../motion/src/controls/number-control.ts | 63 ++++-- .../motion/src/controls/point-control.test.ts | 7 +- packages/motion/src/controls/point-control.ts | 68 +++++-- .../motion/src/controls/transform-control.ts | 108 +++++++++++ packages/motion/src/flow/all.test.ts | 13 +- packages/motion/src/flow/all.ts | 1 - packages/motion/src/flow/chain.test.ts | 12 +- packages/motion/src/flow/delay.test.ts | 14 +- packages/motion/src/flow/wait.test.ts | 12 +- packages/motion/src/index.ts | 8 +- packages/motion/src/movie/index.ts | 2 + packages/motion/src/movie/make-movie.test.ts | 12 +- packages/motion/src/movie/make-movie.ts | 21 +- packages/motion/src/movie/make-scene.ts | 18 +- packages/motion/src/player/index.ts | 1 + .../motion/src/player/motion-controller.ts | 183 ++++++++++++++++++ packages/motion/src/tweening/spring.ts | 2 +- packages/motion/src/tweening/tween.test.ts | 16 +- packages/motion/src/tweening/tween.ts | 2 +- pnpm-lock.yaml | 125 +++++++++++- 57 files changed, 1488 insertions(+), 207 deletions(-) create mode 100644 packages/core/src/utils/clamp.ts create mode 100644 packages/core/src/utils/formatDuration.ts create mode 100644 packages/docs/src/app/motion/page.tsx create mode 100644 packages/motion-react/CHANGELOG.md create mode 100644 packages/motion-react/package.json create mode 100644 packages/motion-react/postcss.config.js create mode 100644 packages/motion-react/src/components/Player/MotionPlayer.tsx create mode 100644 packages/motion-react/src/components/Player/MotionPlayerControls.tsx create mode 100644 packages/motion-react/src/components/Player/MotionPlayerProgressBar.tsx create mode 100644 packages/motion-react/src/components/Player/index.tsx create mode 100644 packages/motion-react/src/components/index.ts create mode 100644 packages/motion-react/src/hooks/index.ts create mode 100644 packages/motion-react/src/hooks/useMotionController.ts create mode 100644 packages/motion-react/src/index.ts create mode 100644 packages/motion-react/src/styles.css create mode 100644 packages/motion-react/src/types.ts create mode 100644 packages/motion-react/tailwind.config.js create mode 100644 packages/motion-react/tsconfig.json create mode 100644 packages/motion-react/vitest.config.ts create mode 100644 packages/motion/src/controls/color-control.ts create mode 100644 packages/motion/src/controls/transform-control.ts create mode 100644 packages/motion/src/movie/index.ts create mode 100644 packages/motion/src/player/index.ts create mode 100644 packages/motion/src/player/motion-controller.ts diff --git a/packages/core/package.json b/packages/core/package.json index 67ee992..b64248f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -19,7 +19,8 @@ "cjs", "esm" ], - "minify": true, + "minify": false, + "sourcemap": true, "entryPoints": [ "src/index.ts" ] diff --git a/packages/core/src/math/transform.test.ts b/packages/core/src/math/transform.test.ts index 7a6aba4..652aef4 100644 --- a/packages/core/src/math/transform.test.ts +++ b/packages/core/src/math/transform.test.ts @@ -49,7 +49,7 @@ describe("Transform", () => { test("scale", () => { const t1 = transform().setScale({ x: 2, y: 3 }); const t2 = t1.scale(2); - const t3 = t1.scale(2, 3); + const t3 = t1.scale([2, 3]); expect(t1.getScale()).toEqual({ x: 2, y: 3 }); expect(t2.getScale()).toEqual({ x: 4, y: 6 }); expect(t3.getScale()).toEqual({ x: 4, y: 9 }); @@ -65,7 +65,7 @@ describe("Transform", () => { test("translate", () => { const t1 = transform().setPosition({ x: 3, y: 6 }); - const t2 = t1.translate(2, 3); + const t2 = t1.translate([2, 3]); expect(t1.getPosition()).toEqual({ x: 3, y: 6 }); expect(t2.getPosition()).toEqual({ x: 5, y: 9 }); }); @@ -95,7 +95,7 @@ describe("Transform", () => { }); test("invert", () => { - const t1 = transform().scale(2).translate(5, 5); + const t1 = transform().scale(2).translate([5, 5]); const t2 = t1.invert(); const p = point(5, 5); @@ -107,7 +107,7 @@ describe("Transform", () => { }); test("applyInverseTo", () => { - const t1 = transform().scale(2).translate(5, 5); + const t1 = transform().scale(2).translate([5, 5]); const p = point(5, 5); const q = t1.applyTo(p); diff --git a/packages/core/src/math/transform.ts b/packages/core/src/math/transform.ts index e342105..8b09483 100644 --- a/packages/core/src/math/transform.ts +++ b/packages/core/src/math/transform.ts @@ -1,4 +1,4 @@ -import { point, Vec2 } from "./vec2"; +import { point, Vec2, Vec2ish } from "./vec2"; export class Transform { _matrix: Mat3x3; @@ -15,6 +15,16 @@ export class Transform { static identity() { return new Transform(1, 0, 0, 1, 0, 0); } + static fromTransform(transform: Transform) { + return new Transform( + transform._matrix[0], + transform._matrix[1], + transform._matrix[3], + transform._matrix[4], + transform._matrix[2], + transform._matrix[5] + ); + } static fromMatrix(matrix: Mat3x3) { return new Transform( matrix[0], @@ -41,11 +51,11 @@ export class Transform { copy() { return Transform.fromMatrix(this._matrix); } - setPosition(position: { x: number; y: number }) { + setPosition(position: Vec2ish) { return this.copy().setPositionSelf(position); } - setPositionSelf(position: { x: number; y: number }) { - const { x, y } = position; + setPositionSelf(position: Vec2ish) { + const { x, y } = Vec2.of(position); const { _matrix } = this; _matrix[2] = x; _matrix[5] = y; @@ -63,14 +73,17 @@ export class Transform { _matrix[1] = sin; _matrix[3] = -sin; _matrix[4] = cos; + return this; } - setScale(size: { x: number; y: number }) { - return this.copy().setScaleSelf(size); + setScale(factor: Vec2ish | number) { + return this.copy().setScaleSelf(factor); } - setScaleSelf(size: { x: number; y: number }) { - const { x, y } = size; + setScaleSelf(factor: Vec2ish | number) { + const { x, y } = Vec2.of( + typeof factor === "number" ? [factor, factor] : factor + ); const { _matrix } = this; _matrix[0] = x; _matrix[4] = y; @@ -80,14 +93,15 @@ export class Transform { /* * Scale the transform by the given factor. */ - scale(factor: number): this; - scale(x: number, y: number): this; - scale(x: number, y: number = x) { - return this.copy().scaleSelf(x, y); - } - scaleSelf(factor: number): this; - scaleSelf(x: number, y: number): this; - scaleSelf(x: number, y: number = x) { + + scale(factor: Vec2ish | number) { + return this.copy().scaleSelf(factor); + } + + scaleSelf(factor: Vec2ish | number) { + const { x, y } = Vec2.of( + typeof factor === "number" ? [factor, factor] : factor + ); const { _matrix } = this; _matrix[0] *= x; _matrix[1] *= x; @@ -119,11 +133,12 @@ export class Transform { return this; } - translate(x: number, y: number) { - return this.copy().translateSelf(x, y); + translate(offset: Vec2ish) { + return this.copy().translateSelf(offset); } - translateSelf(x: number, y: number) { + translateSelf(offset: Vec2ish) { + const { x, y } = Vec2.of(offset); const { _matrix } = this; _matrix[2] += x; _matrix[5] += y; diff --git a/packages/core/src/math/vec2.ts b/packages/core/src/math/vec2.ts index c956df2..922cb2d 100644 --- a/packages/core/src/math/vec2.ts +++ b/packages/core/src/math/vec2.ts @@ -86,6 +86,10 @@ export class Vec2 { normal() { return point(-this.y, this.x); } + + toString() { + return `[${this.x}, ${this.y}]`; + } } export function point(x: number, y: number) { diff --git a/packages/core/src/types/deep-paths.ts b/packages/core/src/types/deep-paths.ts index fe77e9d..0bd54d2 100644 --- a/packages/core/src/types/deep-paths.ts +++ b/packages/core/src/types/deep-paths.ts @@ -1,3 +1,5 @@ +import { B, T } from "vitest/dist/types-dea83b3d"; + type IsTuple> = number extends T["length"] ? false : true; @@ -96,22 +98,6 @@ export type InferPathValueTree = { [K in InferPath]: InferPathValue; }; -type test = InferPathValueTree< - | { - z: 2; - } - | { - foo: { - bar: { - baz: 1; - }; - }; - bar: { - baz: 2; - }; - } ->; - export type OnlyKeysOfType = Omit< T, { @@ -124,3 +110,13 @@ type SplitHead = T extends `${infer U}.${infer V}` : T extends string ? [T, undefined] : never; + +export type ExtractPathsOfType = { + [K in InferPath]: InferPathValueImplementation< + TObject, + K, + false + > extends TType | undefined + ? K + : never; +}[InferPath]; diff --git a/packages/core/src/utils/clamp.ts b/packages/core/src/utils/clamp.ts new file mode 100644 index 0000000..4368d20 --- /dev/null +++ b/packages/core/src/utils/clamp.ts @@ -0,0 +1,2 @@ +export const clamp = (value: number, min: number, max: number) => + Math.min(Math.max(value, min), max); diff --git a/packages/core/src/utils/formatDuration.ts b/packages/core/src/utils/formatDuration.ts new file mode 100644 index 0000000..7fef17d --- /dev/null +++ b/packages/core/src/utils/formatDuration.ts @@ -0,0 +1,10 @@ +export function formatDuration(milliseconds: number, showHours = false) { + const seconds = Math.floor(milliseconds / 1000); + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secondsLeft = Math.floor(seconds % 60); + + return (showHours ? [hours, minutes, secondsLeft] : [minutes, secondsLeft]) + .map((n) => n.toString().padStart(2, "0")) + .join(":"); +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index afd962a..e3c62e6 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -5,3 +5,5 @@ export * from "./isObject"; export * from "./easing-functions"; export * from "./setDeep"; export * from "./getDeep"; +export * from "./clamp"; +export * from "./formatDuration"; diff --git a/packages/core/src/utils/setDeep.ts b/packages/core/src/utils/setDeep.ts index a01e7de..a8c2fe7 100644 --- a/packages/core/src/utils/setDeep.ts +++ b/packages/core/src/utils/setDeep.ts @@ -1,5 +1,5 @@ import { InferPath, InferPathValue, InferPathValueTree } from "../types"; -import compact from "lodash-es/compact"; +import { compact } from "lodash-es"; import { isObject } from "./isObject"; // const copy = | Record>( // obj: T diff --git a/packages/docs/package.json b/packages/docs/package.json index ee10b6d..dce52d5 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -24,10 +24,12 @@ "rehype-katex": "^6.0.3", "rehype-mdx-code-props": "^1.0.0", "remark-gfm": "^3.0.1", - "remark-math": "^5.1.1" + "remark-math": "^5.1.1", + "@coord/graph": "workspace:*", + "@coord/motion": "workspace:*", + "@coord/motion-react": "workspace:*" }, "devDependencies": { - "@coord/graph": "workspace:*", "@tailwindcss/typography": "^0.5.9", "@total-typescript/ts-reset": "^0.4.2", "@types/node": "20.2.5", diff --git a/packages/docs/src/app/motion/page.tsx b/packages/docs/src/app/motion/page.tsx new file mode 100644 index 0000000..14e30fe --- /dev/null +++ b/packages/docs/src/app/motion/page.tsx @@ -0,0 +1,119 @@ +"use client"; +import { Graph, Grid, Marker, point, transform } from "@coord/graph"; +import { + controlColor, + all, + controlTransform, + makeScene, + makeMovie, +} from "@coord/motion"; +import { + useMotionController, + MotionPlayerControls, + MotionPlayer, +} from "@coord/motion-react"; +import "@coord/motion-react/dist/index.css"; +const sceneA = makeScene( + "Scene A", + { + color: "blue", + shape: transform(), + }, + function* () { + const color = yield* controlColor("color"); + const shape = yield* controlTransform("shape"); + + yield* all( + color.tweenTo("red", 1), + shape.scale(2).positionTo([3, 3]).in(1) + ); + + yield* all( + color.tweenTo("yellow", 1), + shape + .positionTo([5, 5]) + + .scale(3) + + .in(1) + ); + + yield* all( + color.tweenTo("blue", 1), + shape + .positionTo([0, 0]) + + .scaleTo(1) + .in(1) + ); + } +); + +const sceneB = makeScene( + "Scene B: Scene with a really long name", + { + color: "blue", + shape: transform(), + }, + function* () { + const color = yield* controlColor("color"); + const shape = yield* controlTransform("shape"); + + yield* all( + color.tweenTo("red", 1), + shape.scale(2).positionTo([3, 3]).in(1) + ); + + yield* all( + color.tweenTo("yellow", 1), + shape + .positionTo([5, 5]) + + .scale(3) + + .in(1) + ); + + yield* all( + color.tweenTo("blue", 1), + shape + .positionTo([0, 0]) + + .scaleTo(1) + .in(1) + ); + } +); + +export default function Page() { + const controls = useMotionController( + makeMovie("Finding the intersection of two lines", { + sceneA: sceneA, + sceneB: sceneB, + }), + { + fps: 60, + } + ); + + const { sceneA: state } = controls.state; + + return ( +
+
+
{JSON.stringify(controls.meta, null, 2)}
+ + + + + + +
+
+ ); +} diff --git a/packages/graph/package.json b/packages/graph/package.json index 8aef7ea..d286846 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -35,7 +35,6 @@ "react-dom": ">=16.8.0" }, "devDependencies": { - "@coord/core": "workspace:*", "@types/lodash-es": "^4.17.7", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", @@ -46,7 +45,8 @@ "vitest": "^0.31.1" }, "dependencies": { - "@use-gesture/react": "^10.2.27" + "@use-gesture/react": "^10.2.27", + "@coord/core": "workspace:*" }, "publishConfig": { "access": "public" diff --git a/packages/graph/src/components/Marker.tsx b/packages/graph/src/components/Marker.tsx index ced9513..70aa988 100644 --- a/packages/graph/src/components/Marker.tsx +++ b/packages/graph/src/components/Marker.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { GraphElement, withGraphContext } from "@/utils"; +import { GraphElement, renderNumber, withGraphContext } from "@/utils"; import { ScalarPoint, Scalar } from "@/types"; import { Vec2, point } from "@coord/core"; import { useGesture } from "@use-gesture/react"; @@ -27,7 +27,7 @@ const DefaultMarker = ({ size, color, interactable }: MarkerContentProps) => ( ( /> )} - + ); @@ -96,13 +96,13 @@ const Component = ({ if (typeof label === "string") { c = ( - + @@ -120,7 +120,9 @@ const Component = ({ return ( =16.8.0", + "react-dom": ">=16.8.0" + }, + "devDependencies": { + "@types/events": "^3.0.0", + "@types/react": "^18.2.12", + "autoprefixer": "10.4.14", + "postcss": "8.4.24", + "tailwindcss": "3.3.2", + "tsconfig": "workspace:*", + "tsup": "^6.7.0", + "typescript": "^5.1.3" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@coord/core": "workspace:*", + "@coord/motion": "workspace:*", + "@use-gesture/react": "^10.2.27", + "clsx": "^1.2.1", + "events": "^3.3.0", + "react-icons": "^4.9.0" + } +} diff --git a/packages/motion-react/postcss.config.js b/packages/motion-react/postcss.config.js new file mode 100644 index 0000000..dd07c71 --- /dev/null +++ b/packages/motion-react/postcss.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: [require("tailwindcss")(), require("autoprefixer")()], +}; diff --git a/packages/motion-react/src/components/Player/MotionPlayer.tsx b/packages/motion-react/src/components/Player/MotionPlayer.tsx new file mode 100644 index 0000000..100aba0 --- /dev/null +++ b/packages/motion-react/src/components/Player/MotionPlayer.tsx @@ -0,0 +1,93 @@ +import { MotionControls } from "@/hooks"; +import React, { useEffect, useState } from "react"; +import { MotionPlayerControls } from "./MotionPlayerControls"; +import clsx from "clsx"; + +type MotionPlayerProps = React.PropsWithChildren<{ + controls: MotionControls; + autoplay?: boolean; + repeat?: boolean; +}> & + React.HTMLAttributes; + +export function MotionPlayer({ + controls, + children, + className, + repeat, + + autoplay = false, + + ...rest +}: MotionPlayerProps) { + const ref = React.useRef(null); + const toggleFullScreen = () => { + if (!document.fullscreenElement) { + ref.current?.requestFullscreen(); + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + } + } + }; + + const [cursorIsIdle, setCursorIsIdle] = useState(true); + + const timeout = React.useRef | null>(null); + + useEffect(() => { + if (autoplay) { + controls.play(); + } + + if (repeat) { + controls.setRepeat(true); + } + }, []); + return ( +
{ + setCursorIsIdle(false); + if (timeout.current) { + clearTimeout(timeout.current); + } + timeout.current = setTimeout(() => { + setCursorIsIdle(true); + }, 1000); + }} + onClick={() => { + if (controls.playing) { + controls.pause(); + return; + } + controls.play(); + }} + {...rest} + > + {children} + +
{ + e.stopPropagation(); + }} + className={clsx( + "absolute bottom-0 w-full transition-opacity duration-300 ", + { + "opacity-0": controls.playing, + "group-hover/player:opacity-100": !cursorIsIdle, + } + )} + > + +
+
+ ); +} diff --git a/packages/motion-react/src/components/Player/MotionPlayerControls.tsx b/packages/motion-react/src/components/Player/MotionPlayerControls.tsx new file mode 100644 index 0000000..0c42241 --- /dev/null +++ b/packages/motion-react/src/components/Player/MotionPlayerControls.tsx @@ -0,0 +1,97 @@ +import React from "react"; +import { MotionControls } from "@/hooks"; +import { + HiPlay, + HiPause, + HiOutlineArrowsPointingOut, + HiArrowPath, +} from "react-icons/hi2"; +import { MotionPlayerProgressBar } from "./MotionPlayerProgressBar"; +import { formatDuration } from "@coord/core/dist"; +import clsx from "clsx"; + +type MotionPlayerControlsProps = { + controls: MotionControls; + toggleFullScreen?: () => void; +}; +const oneHour = 3600_000; + +export function MotionPlayerControls({ + controls, + toggleFullScreen, +}: MotionPlayerControlsProps) { + const { + playing, + play, + pause, + meta, + setFrame, + repeat, + setRepeat, + frame, + currentTime, + duration, + } = controls; + + const longerThanAnHour = duration >= oneHour; + + return ( +
+
+ +
+
+
+ {playing ? ( + + ) : ( + + )} + + +
+ +
+ {formatDuration(currentTime, longerThanAnHour)} + {" / "} + {formatDuration(duration, longerThanAnHour)} +
+
{meta.title}
+ {toggleFullScreen && ( +
+ +
+ )} +
+
+ ); +} diff --git a/packages/motion-react/src/components/Player/MotionPlayerProgressBar.tsx b/packages/motion-react/src/components/Player/MotionPlayerProgressBar.tsx new file mode 100644 index 0000000..9f3af2a --- /dev/null +++ b/packages/motion-react/src/components/Player/MotionPlayerProgressBar.tsx @@ -0,0 +1,119 @@ +import React, { useMemo, useRef } from "react"; +import { MotionControls } from "@/hooks"; +import { MotionContextMeta } from "@coord/motion"; +import { clamp, inverseLerp } from "@coord/core"; +import { useDrag } from "@use-gesture/react"; + +export function MotionPlayerProgressBar({ + controls, +}: { + controls: MotionControls; +}) { + const { playing, play, pause, meta, setFrame, frame } = controls; + const ref = useRef(null); + const { duration: movieDuration } = meta; + + useDrag( + ({ xy: [x], first, last, memo = false }) => { + if (!ref.current) return; + + if (first) { + pause(); + memo = playing; + } + const rect = ref.current.getBoundingClientRect(); + const pos = clamp((x - rect.x) / rect.width, 0, 1); + setFrame(Math.round(movieDuration * pos)); + + if (last && memo) { + play(); + } + return memo; + }, + { + target: ref, + } + ); + + const normalizedMeta = useMemo(() => { + if (Object.keys(meta.scenes).length === 0) { + return { + movie: meta, + }; + } + + const scenes: { + [key: string]: Omit; + } = {}; + + Object.keys(meta.scenes).forEach((key) => { + const value = meta.scenes[key]; + if (!value) return; + + scenes[key] = value; + }); + return scenes; + }, [meta]); + + return ( +
+ {Object.keys(normalizedMeta).map((key) => { + const scene = normalizedMeta[key]; + if (!scene) return null; + + return ( +
+
+

+ {scene.title} +

+
+ +
+
+
+
+ ); + })} + +
+ ); +} diff --git a/packages/motion-react/src/components/Player/index.tsx b/packages/motion-react/src/components/Player/index.tsx new file mode 100644 index 0000000..1ef4e37 --- /dev/null +++ b/packages/motion-react/src/components/Player/index.tsx @@ -0,0 +1,2 @@ +export * from "./MotionPlayer"; +export * from "./MotionPlayerControls"; diff --git a/packages/motion-react/src/components/index.ts b/packages/motion-react/src/components/index.ts new file mode 100644 index 0000000..791293e --- /dev/null +++ b/packages/motion-react/src/components/index.ts @@ -0,0 +1 @@ +export * from "./Player"; diff --git a/packages/motion-react/src/hooks/index.ts b/packages/motion-react/src/hooks/index.ts new file mode 100644 index 0000000..38f606f --- /dev/null +++ b/packages/motion-react/src/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useMotionController"; diff --git a/packages/motion-react/src/hooks/useMotionController.ts b/packages/motion-react/src/hooks/useMotionController.ts new file mode 100644 index 0000000..e753496 --- /dev/null +++ b/packages/motion-react/src/hooks/useMotionController.ts @@ -0,0 +1,67 @@ +import { useLayoutEffect, useState } from "react"; +import { + MotionController, + MotionState, + MotionContextSettings, + MotionScene, +} from "@coord/motion"; + +export function useMotionController( + scene: MotionScene, + contextSettings?: Partial +) { + const [motionPlayer] = useState(() => + MotionController.from(scene, contextSettings) + ); + + const [frame, setFrame] = useState(motionPlayer.currentFrame); + const [playing, setPlaying] = useState(motionPlayer.playing); + const [repeat, setRepeat] = useState(motionPlayer.repeat); + const [playRange, setPlayRange] = useState(motionPlayer.playRange); + + useLayoutEffect(() => { + motionPlayer.on("frame-changed", () => { + setFrame(motionPlayer.currentFrame); + }); + motionPlayer.on("play-range-changed", () => { + setPlayRange(motionPlayer.playRange); + }); + motionPlayer.on("play-status-changed", () => { + setPlaying(motionPlayer.playing); + }); + motionPlayer.on("repeat-changed", () => { + setRepeat(motionPlayer.repeat); + }); + + return () => { + motionPlayer.disconnect(); + }; + }, [motionPlayer]); + + return { + motionPlayer, + + play: motionPlayer.play, + pause: motionPlayer.pause, + stop: motionPlayer.stop, + setPlayRange: motionPlayer.setPlayRange, + setRepeat: motionPlayer.setRepeat, + setTime: motionPlayer.setTime, + setFrame: motionPlayer.setFrame, + + state: motionPlayer.state, + duration: motionPlayer.duration, + durationInFrames: motionPlayer.durationInFrames, + fps: motionPlayer.fps, + currentTime: motionPlayer.currentTime, + meta: motionPlayer.context.meta, + + frame, + playing, + repeat, + playRange, + } as const; +} + +export type MotionControls = + ReturnType>; diff --git a/packages/motion-react/src/index.ts b/packages/motion-react/src/index.ts new file mode 100644 index 0000000..cfdf72b --- /dev/null +++ b/packages/motion-react/src/index.ts @@ -0,0 +1,3 @@ +import "./styles.css"; +export * from "./components"; +export * from "./hooks"; diff --git a/packages/motion-react/src/styles.css b/packages/motion-react/src/styles.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/packages/motion-react/src/styles.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/packages/motion-react/src/types.ts b/packages/motion-react/src/types.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/motion-react/tailwind.config.js b/packages/motion-react/tailwind.config.js new file mode 100644 index 0000000..54331dc --- /dev/null +++ b/packages/motion-react/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/packages/motion-react/tsconfig.json b/packages/motion-react/tsconfig.json new file mode 100644 index 0000000..77160d0 --- /dev/null +++ b/packages/motion-react/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "tsconfig/react-library.json", + "compilerOptions": { + "jsx": "react", + "target": "ESNext", + "noUncheckedIndexedAccess": true, + "noEmit": true, + "isolatedModules": false, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/motion-react/vitest.config.ts b/packages/motion-react/vitest.config.ts new file mode 100644 index 0000000..05a201d --- /dev/null +++ b/packages/motion-react/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineProject } from "vitest/config"; + +export default defineProject({ + test: { + alias: { + "@/": "src/", + }, + }, +}); diff --git a/packages/motion/package.json b/packages/motion/package.json index 23c771a..80b64e7 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -19,18 +19,28 @@ "cjs", "esm" ], - "minify": true, + "minify": false, "entryPoints": [ "src/index.ts" - ] + ], + "sourcemap": true + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" }, "devDependencies": { - "@coord/core": "workspace:*", + "@types/events": "^3.0.0", + "@types/react": "^18.2.12", "tsconfig": "workspace:*", "tsup": "^6.7.0", "typescript": "^5.1.3" }, "publishConfig": { "access": "public" + }, + "dependencies": { + "@coord/core": "workspace:*", + "events": "^3.3.0" } } diff --git a/packages/motion/src/context/create-motion-context.ts b/packages/motion/src/context/create-motion-context.ts index f816104..a9e1412 100644 --- a/packages/motion/src/context/create-motion-context.ts +++ b/packages/motion/src/context/create-motion-context.ts @@ -1,4 +1,4 @@ -import { isObject } from "@coord/core"; +import { MotionScene } from ".."; export const DEFAULT_MOTION_CONTEXT_SETTINGS = { fps: 60, @@ -7,17 +7,14 @@ export const DEFAULT_MOTION_CONTEXT_SETTINGS = { export type MotionContextSettings = typeof DEFAULT_MOTION_CONTEXT_SETTINGS; -export type MotionContextSceneMeta = { - title: string; - description?: string; - frame: number; - duration: number; -}; - export type MotionContextMeta = { title: string; description?: string; - scenes: MotionContextSceneMeta[]; + duration: number; + start: number; + scenes: { + [key: string]: Omit; + }; }; export type MotionState = { @@ -37,20 +34,20 @@ export class MotionContext { settings: MotionContextSettings; constructor( - initialState: TState, - contextSettings: Partial = {}, - meta: Partial = {} + scene: MotionScene, + contextSettings: Partial = {} ) { - this._state = { ...initialState, $frame: 0, $transitionIn: 1 }; + this._state = { ...scene.initialState, $frame: 0, $transitionIn: 1 }; this.frames = []; this.settings = { ...DEFAULT_MOTION_CONTEXT_SETTINGS, ...contextSettings, }; this.meta = { - title: "Untitled", - scenes: [], - ...meta, + ...scene.meta, + start: 0, + duration: 0, + scenes: {}, }; } @@ -67,14 +64,16 @@ export class MotionContext { for (const [key, childContext] of this._childContexts.entries()) { Object.assign(this._state, { [key]: childContext._state }); childContext.pushFrame(); + Object.assign(this.meta.scenes, { [key]: childContext.meta }); } } - createChildContext(key: TKey & string) { - const childContext = new MotionContext( - this._state[key] as MotionState, - this.settings - ); + createChildContext( + key: TKey & string, + scene: MotionScene + ) { + const childContext = new MotionContext(scene, this.settings); + childContext.meta.start = this.frames.length; this._childContexts.set(key, childContext); return childContext; } @@ -91,6 +90,7 @@ export class MotionContext { this.collectChildStates(); this.frames.push(this._state); + this.meta.duration = this.frames.length; this._state = { ...this._state, $frame: this.frames.length, @@ -99,8 +99,8 @@ export class MotionContext { } export function createMotionContext( - initialState: TState, + scene: MotionScene, contextSettings: Partial = {} ) { - return new MotionContext(initialState, contextSettings); + return new MotionContext(scene, contextSettings); } diff --git a/packages/motion/src/context/motion-runner.test.ts b/packages/motion/src/context/motion-runner.test.ts index ba2dc11..40aa310 100644 --- a/packages/motion/src/context/motion-runner.test.ts +++ b/packages/motion/src/context/motion-runner.test.ts @@ -1,11 +1,13 @@ import { test, describe, expect } from "vitest"; -import { createMotion, requestContext, runMotion } from "./motion-runner"; +import { createMotion, requestContext, runScene } from "./motion-runner"; import { MotionContext, createMotionContext } from "./create-motion-context"; import { frameMaker } from "@/test-utils"; +import { makeScene } from ".."; describe("createMotion", async () => { test("should create motion context and execute correctly", async () => { - const [context, runner] = createMotion( + const scene = makeScene( + "Test", { value: "", }, @@ -21,6 +23,7 @@ describe("createMotion", async () => { yield; } ); + const [context, runner] = createMotion(scene); runner.next(); expect(context?.frames.length).toBe(1); @@ -36,9 +39,10 @@ describe("createMotion", async () => { }); }); -describe("runMotion", async () => { +describe("runScene", async () => { test("should create motion context and execute correctly", async () => { - const context = runMotion( + const scene = makeScene( + "Test", { value: "", }, @@ -54,6 +58,7 @@ describe("runMotion", async () => { yield; } ); + const context = runScene(scene); const makeSceneFrame = frameMaker( { @@ -76,7 +81,8 @@ describe("runMotion", async () => { describe("requestContext", async () => { test("should provide context when requested", async () => { - runMotion( + const scene = makeScene( + "Test", { value: "", }, @@ -85,5 +91,6 @@ describe("requestContext", async () => { expect(requestedContext).instanceOf(MotionContext); } ); + runScene(scene); }); }); diff --git a/packages/motion/src/context/motion-runner.ts b/packages/motion/src/context/motion-runner.ts index 37eaaa0..8b94425 100644 --- a/packages/motion/src/context/motion-runner.ts +++ b/packages/motion/src/context/motion-runner.ts @@ -5,6 +5,7 @@ import { MotionState, createMotionContext, } from "./create-motion-context"; +import { MotionScene } from ".."; export type YieldedType any> = ReturnType extends Generator ? U : never; @@ -54,28 +55,23 @@ export function* motionRunner( } export function createMotion( - initialState: TState, - builder: MotionBuilder, - contextSettings: Partial = {} + scene: MotionScene, + contextSettings?: Partial ) { - const context = createMotionContext(initialState, contextSettings); - const runner = motionRunner(context, builder); + const context = createMotionContext(scene, contextSettings); + const runner = motionRunner(context, scene.builder); return [context, runner] as const; } -export function runMotion< - TState extends MotionState, - TBuilder extends MotionBuilder ->( - initialState: TState, - builder: TBuilder, - contextSettings: Partial = {} +export function runScene( + scene: MotionScene, + contextSettings?: Partial ) { - const [context, runner] = createMotion( - initialState, - builder, - contextSettings - ); + const [context, runner] = createMotion(scene, contextSettings); + context.meta = { + ...context.meta, + ...scene.meta, + }; while (!runner.next().done) { continue; } @@ -83,6 +79,26 @@ export function runMotion< return context; } +// export function runMotion< +// TState extends MotionState +// // TBuilder extends MotionBuilder +// >( +// initialState: TState, +// builder: MotionBuilder, +// contextSettings: Partial = {} +// ) { +// const [context, runner] = createMotion( +// initialState, +// builder, +// contextSettings +// ); +// while (!runner.next().done) { +// continue; +// } + +// return context; +// } + export function* requestContext() { const request: { type: "REQUEST_CONTEXT"; diff --git a/packages/motion/src/controls/color-control.ts b/packages/motion/src/controls/color-control.ts new file mode 100644 index 0000000..a8d6797 --- /dev/null +++ b/packages/motion/src/controls/color-control.ts @@ -0,0 +1,109 @@ +import { + MotionBuilder, + MotionContext, + MotionState, + requestContext, +} from "@/context"; +import { tween } from "@/tweening"; +import { + getDeep, + setDeep, + InferPathValueTree, + EasingOptions, +} from "@coord/core"; + +type OnlyKeysOfType = Omit< + T, + { + [K in keyof T]: T[K] extends TType ? never : K; + }[keyof T] +>; + +export function* controlColor< + TState extends MotionState, + TTree extends OnlyKeysOfType, string | undefined>, + TKey extends keyof TTree, + TControl extends ColorControl +>( + key: TKey & string, + fn?: (control: TControl) => ReturnType> +) { + const context = yield* requestContext(); + const control = new ColorControl(context, key) as TControl; + if (fn) { + yield* fn(control); + } + return control; +} + +type RectangularColorSpace = + | "srgb" + | "srgb-linear" + | "lab" + | "oklab" + | "xyz" + | "xyz-d50" + | "xyz-d65"; +type PolarColorSpace = "hsl" | "hwb" | "lch" | "oklch"; + +type ColorMixOptions = { + method: RectangularColorSpace | PolarColorSpace; + interpolationMethod?: "shorter" | "longer" | "increasing" | "decreasing"; +}; + +const isPolarColorSpace = (value: string): value is PolarColorSpace => + ["hsl", "hwb", "lch", "oklch"].includes(value); + +const colorMix = ( + a: string, + b: string, + t: number, + config: Partial = {} +) => { + let { method = "srgb", interpolationMethod = "shorter" } = config; + if (t === 0) return a; + if (t === 1) return b; + const fullMethod = isPolarColorSpace(method) + ? `${method} ${interpolationMethod} hue` + : method; + return `color-mix(in ${fullMethod}, ${a}, ${b} ${(t * 100).toFixed(2)}%)`; +}; + +type ColorTweeningOptions = { + easing: EasingOptions; +} & ColorMixOptions; + +export class ColorControl { + chain: ReturnType>[] = []; + constructor(public context: MotionContext, public key: string) {} + + get() { + return getDeep(this.context._state, this.key) as TType; + } + set(value: string) { + this.context._state = setDeep(this.context._state, this.key, value); + } + + tweenTo( + value: string, + duration: number, + config: Partial = {} + ) { + const { easing, ...colorMixOptions } = config; + const initialValue = this.get() ?? "#ffffff"; + + if (typeof initialValue !== "string") { + throw new Error( + `Cannot tween to a non-string value, got ${initialValue}` + ); + } + + return tween( + duration, + (t) => { + this.set(colorMix(initialValue, value, t, colorMixOptions)); + }, + easing + ); + } +} diff --git a/packages/motion/src/controls/index.ts b/packages/motion/src/controls/index.ts index e69de29..f17763f 100644 --- a/packages/motion/src/controls/index.ts +++ b/packages/motion/src/controls/index.ts @@ -0,0 +1,4 @@ +export * from "./number-control"; +export * from "./point-control"; +export * from "./color-control"; +export * from "./transform-control"; diff --git a/packages/motion/src/controls/number-control.test.ts b/packages/motion/src/controls/number-control.test.ts index 818690e..1304704 100644 --- a/packages/motion/src/controls/number-control.test.ts +++ b/packages/motion/src/controls/number-control.test.ts @@ -1,13 +1,15 @@ import { test, describe, expect } from "vitest"; -import { runMotion } from "@/context"; -import { frameMaker } from "@/test-utils"; +import { runScene } from "@/context"; + import { controlNumber } from "./number-control"; -import { all, chain } from "@/flow"; +import { all } from "@/flow"; +import { makeScene } from "@/movie"; describe("number control", async () => { test("tweens to a value", async () => { - let executed = runMotion( + const scene = makeScene( + "test", { t: 0, b: [0, 2], @@ -20,6 +22,7 @@ describe("number control", async () => { ); } ); + let executed = runScene(scene); expect(executed.frames.length).toBe(60); diff --git a/packages/motion/src/controls/number-control.ts b/packages/motion/src/controls/number-control.ts index 0436fc7..8d48d77 100644 --- a/packages/motion/src/controls/number-control.ts +++ b/packages/motion/src/controls/number-control.ts @@ -11,26 +11,24 @@ import { InferPathValueTree, lerp, EasingOptions, + InferPath, + InferPathValue, + InferPathValueImplementation, + ExtractPathsOfType, } from "@coord/core"; -type OnlyKeysOfType = Omit< - T, - { - [K in keyof T]: T[K] extends TType ? never : K; - }[keyof T] ->; +const isNumber = (value: unknown): value is number => typeof value === "number"; export function* controlNumber< TState extends MotionState, - TTree extends OnlyKeysOfType, number | undefined>, - TKey extends keyof TTree, - TControl extends NumberControl + TKey extends ExtractPathsOfType >( - key: TKey & string, - fn?: (control: TControl) => ReturnType> + key: TKey, + fn?: (control: NumberControl) => ReturnType>, + initialValue?: number ) { const context = yield* requestContext(); - const control = new NumberControl(context, key) as TControl; + const control = new NumberControl(context, key, initialValue); if (fn) { yield* fn(control); } @@ -39,13 +37,24 @@ export function* controlNumber< const isFinite = (value: unknown): value is number => Number.isFinite(value); -export class NumberControl { - chain: ReturnType>[] = []; +export class NumberControl { + _targetValue: number; - constructor(public context: MotionContext, public key: string) {} + constructor( + public context: MotionContext, + public key: string, + private _initialValue = 0 + ) { + this._targetValue = this.get() ?? 0; + } - get() { - return getDeep(this.context._state, this.key) as TType; + get(): number { + let value = getDeep(this.context._state, this.key); + if (!isNumber(value)) { + this.set(this._initialValue); + return this._initialValue; + } + return value; } set(value: number) { this.context._state = setDeep(this.context._state, this.key, value); @@ -76,7 +85,7 @@ export class NumberControl { ); } - return spring( + return spring( initialValue, to, (t) => { @@ -85,4 +94,22 @@ export class NumberControl { parameters ); } + + from(value: number) { + this.set(value); + return this; + } + + to(value: number) { + this._targetValue = value; + return this; + } + + in(duration: number, easing?: EasingOptions) { + return this.tweenTo(this._targetValue, duration, easing); + } + + spring(parameters?: SpringParameters) { + return this.springTo(this._targetValue, parameters); + } } diff --git a/packages/motion/src/controls/point-control.test.ts b/packages/motion/src/controls/point-control.test.ts index fc14e31..6aa6bc1 100644 --- a/packages/motion/src/controls/point-control.test.ts +++ b/packages/motion/src/controls/point-control.test.ts @@ -1,14 +1,16 @@ import { test, describe, expect } from "vitest"; -import { runMotion } from "@/context"; +import { runScene } from "@/context"; import { all } from "@/flow"; import { point } from "@coord/core"; import { controlPoint } from "./point-control"; +import { makeScene } from "@/movie"; describe("point control", async () => { test("tweens to a value", async () => { - let executed = runMotion( + const scene = makeScene( + "Test", { point: point(0, 0), }, @@ -16,6 +18,7 @@ describe("point control", async () => { yield* all(controlPoint("point", (t) => t.tweenTo([10, 10], 1))); } ); + let executed = runScene(scene); expect(executed.frames.length).toBe(60); expect(executed.frames.at(-1)).toEqual({ diff --git a/packages/motion/src/controls/point-control.ts b/packages/motion/src/controls/point-control.ts index 7ec879d..5b4932d 100644 --- a/packages/motion/src/controls/point-control.ts +++ b/packages/motion/src/controls/point-control.ts @@ -12,47 +12,54 @@ import { EasingOptions, Vec2ish, Vec2, + ExtractPathsOfType, } from "@coord/core"; -type OnlyKeysOfType = Omit< - T, - { - [K in keyof T]: T[K] extends TType ? never : K; - }[keyof T] ->; - export function* controlPoint< TState extends MotionState, - TTree extends OnlyKeysOfType, Vec2 | undefined>, - TKey extends keyof TTree, - TControl extends PointControl + TKey extends ExtractPathsOfType >( key: TKey & string, - fn?: (control: TControl) => ReturnType> + fn?: (control: PointControl) => ReturnType>, + initialValue?: Vec2ish ) { const context = yield* requestContext(); - const control = new PointControl(context, key) as TControl; + const control = new PointControl( + context, + key, + initialValue ? Vec2.of(initialValue) : undefined + ); if (fn) { yield* fn(control); } return control; } -export class PointControl { - chain: ReturnType>[] = []; - - constructor(public context: MotionContext, public key: string) {} +export class PointControl { + _targetValue: Vec2; + constructor( + public context: MotionContext, + public key: string, + private _initialValue = Vec2.of([0, 0]) + ) { + this._targetValue = this.get(); + } get() { const value = getDeep(this.context._state, this.key); - return value as TType; + if (value instanceof Vec2) { + return value; + } + this.set(this._initialValue); + return this._initialValue; } set(value: Vec2ish) { this.context._state = setDeep(this.context._state, this.key, value); } tweenTo(value: Vec2ish, duration: number, easing?: EasingOptions) { - const initialValue = this.get(); + const initialValue = Vec2.of(this.get()); + if ( !(initialValue instanceof Vec2) || (initialValue instanceof Vec2 && !initialValue.isFinite()) @@ -73,18 +80,19 @@ export class PointControl { } springTo(to: Vec2ish, parameters?: SpringParameters) { - const initialValue = this.get() ?? 0; + const initialValue = Vec2.of(this.get()); + if ( !(initialValue instanceof Vec2) || (initialValue instanceof Vec2 && !initialValue.isFinite()) ) { throw new Error( - `Cannot tween to a non-finite value, got ${initialValue}` + `Cannot tween to a non-finite value, got ${initialValue.toString()}` ); } const target = Vec2.of(to); - return spring( + return spring( initialValue, target, (t) => { @@ -93,4 +101,22 @@ export class PointControl { parameters ); } + + from(value: Vec2ish) { + this.set(value); + return this; + } + + to(value: Vec2ish) { + this._targetValue = Vec2.of(value); + return this; + } + + in(duration: number, easing?: EasingOptions) { + return this.tweenTo(this._targetValue, duration, easing); + } + + spring(parameters?: SpringParameters) { + return this.springTo(this._targetValue, parameters); + } } diff --git a/packages/motion/src/controls/transform-control.ts b/packages/motion/src/controls/transform-control.ts new file mode 100644 index 0000000..eb9e3bf --- /dev/null +++ b/packages/motion/src/controls/transform-control.ts @@ -0,0 +1,108 @@ +import { + MotionBuilder, + MotionContext, + MotionState, + requestContext, +} from "@/context"; +import { tween, spring, SpringParameters } from "@/tweening"; +import { + getDeep, + setDeep, + InferPathValueTree, + EasingOptions, + Transform, + Vec2ish, + Vec2, + ExtractPathsOfType, +} from "@coord/core"; + +export function* controlTransform< + TState extends MotionState, + TKey extends ExtractPathsOfType +>( + key: TKey & string, + fn?: (control: TransformControl) => ReturnType>, + initialValue?: Transform +) { + const context = yield* requestContext(); + const control = new TransformControl(context, key); + if (fn) { + yield* fn(control); + } + return control; +} + +export class TransformControl { + _nextTarget: Transform = Transform.identity(); + + constructor( + public context: MotionContext, + public key: string, + private _initialValue = Transform.identity() + ) { + this._initialValue = this._initialValue; + this._nextTarget = this.get().copy(); + } + + get() { + const value = getDeep(this.context._state, this.key); + if (value instanceof Transform) { + return value; + } + this.set(this._initialValue); + + return this._initialValue; + } + set(value: Transform) { + this.context._state = setDeep(this.context._state, this.key, value); + } + + tweenTo = (target: Transform, duration: number, easing?: EasingOptions) => { + const initialValue = this.get(); + + return tween( + duration, + (t) => { + this.set(initialValue.lerp(target, t)); + }, + easing + ); + }; + + from(target: Transform) { + this.set(target.copy()); + return this; + } + + positionTo = (pos: Vec2ish) => { + this._nextTarget.setPositionSelf(Vec2.of(pos)); + return this; + }; + scaleTo = (scale: Vec2ish | number) => { + this._nextTarget.setScaleSelf(scale); + return this; + }; + + rotateTo = (angle: number) => { + this._nextTarget.setRotationSelf(angle); + return this; + }; + + translate = (pos: Vec2ish) => { + this._nextTarget.translateSelf(Vec2.of(pos)); + return this; + }; + scale = (scale: Vec2ish | number) => { + this._nextTarget.scaleSelf(scale); + return this; + }; + + rotate = (angle: number) => { + this._nextTarget.rotateSelf(angle); + return this; + }; + + in = (duration: number, easing?: EasingOptions) => { + return this.tweenTo(this._nextTarget, duration, easing); + }; +} diff --git a/packages/motion/src/flow/all.test.ts b/packages/motion/src/flow/all.test.ts index 0d56f08..cb58cc9 100644 --- a/packages/motion/src/flow/all.test.ts +++ b/packages/motion/src/flow/all.test.ts @@ -1,10 +1,11 @@ import { test, describe, expect } from "vitest"; -import { runMotion } from "@/context"; +import { runScene } from "@/context"; import { frameMaker } from "@/test-utils"; import { all } from "./all"; import { wait } from "./wait"; +import { makeScene } from "@/movie"; describe("all", async () => { test("should run threads in parallel", async () => { @@ -16,8 +17,8 @@ describe("all", async () => { }, 0 ); - - const executed = runMotion( + const scene = makeScene( + "Test", { a: "Waiting", b: "Waiting", @@ -45,11 +46,11 @@ describe("all", async () => { }, wait(1) ); - }, - { - fps: 3, } ); + const executed = runScene(scene, { + fps: 3, + }); expect(executed.frames).toEqual([ makeSceneFrame({ diff --git a/packages/motion/src/flow/all.ts b/packages/motion/src/flow/all.ts index 4079531..345c224 100644 --- a/packages/motion/src/flow/all.ts +++ b/packages/motion/src/flow/all.ts @@ -1,6 +1,5 @@ import { MotionBuilder, - MotionBuilderGenerator, MotionBuilderRequest, MotionState, isMotionBuilderRequest, diff --git a/packages/motion/src/flow/chain.test.ts b/packages/motion/src/flow/chain.test.ts index 149d6df..c011c6d 100644 --- a/packages/motion/src/flow/chain.test.ts +++ b/packages/motion/src/flow/chain.test.ts @@ -1,9 +1,10 @@ import { test, describe, expect } from "vitest"; -import { runMotion } from "@/context"; +import { runScene } from "@/context"; import { frameMaker } from "@/test-utils"; import { chain } from "./chain"; import { wait } from "./wait"; +import { makeScene } from ".."; describe("chain", async () => { test("should run threads in sequence", async () => { @@ -14,7 +15,8 @@ describe("chain", async () => { 0 ); - const executed = runMotion( + const scene = makeScene( + "Test", { a: "Waiting", }, @@ -40,11 +42,11 @@ describe("chain", async () => { }, wait(1) ); - }, - { - fps: 3, } ); + const executed = runScene(scene, { + fps: 3, + }); expect(executed.frames.length).toBe(6); expect(executed.frames).toEqual([ diff --git a/packages/motion/src/flow/delay.test.ts b/packages/motion/src/flow/delay.test.ts index 9325e94..39f3529 100644 --- a/packages/motion/src/flow/delay.test.ts +++ b/packages/motion/src/flow/delay.test.ts @@ -1,8 +1,9 @@ import { test, describe, expect } from "vitest"; -import { runMotion } from "@/context"; +import { runScene } from "@/context"; import { frameMaker } from "@/test-utils"; import { delay } from "./delay"; +import { makeScene } from "@/movie"; describe("delay", async () => { test("waits the specified time before continuing", async () => { @@ -12,8 +13,8 @@ describe("delay", async () => { }, 0 ); - - const executed = runMotion( + const scene = makeScene( + "Test", { a: "Waiting", }, @@ -24,12 +25,13 @@ describe("delay", async () => { }); yield; }); - }, - { - fps: 3, } ); + const executed = runScene(scene, { + fps: 3, + }); + expect(executed.frames.length).toBe(4); expect(executed.frames).toEqual([ makeSceneFrame({ diff --git a/packages/motion/src/flow/wait.test.ts b/packages/motion/src/flow/wait.test.ts index 83a4f2a..d15ec03 100644 --- a/packages/motion/src/flow/wait.test.ts +++ b/packages/motion/src/flow/wait.test.ts @@ -1,8 +1,9 @@ import { test, describe, expect } from "vitest"; -import { runMotion } from "@/context"; +import { runScene } from "@/context"; import { frameMaker } from "@/test-utils"; import { wait } from "./wait"; +import { makeScene } from ".."; describe("wait", async () => { test("waits the specified time before continuing", async () => { @@ -13,7 +14,8 @@ describe("wait", async () => { 0 ); - const executed = runMotion( + const scene = makeScene( + "test", { a: "Waiting", }, @@ -23,11 +25,11 @@ describe("wait", async () => { a: "One second later", }); yield; - }, - { - fps: 3, } ); + const executed = runScene(scene, { + fps: 3, + }); expect(executed.frames.length).toBe(4); expect(executed.frames).toEqual([ diff --git a/packages/motion/src/index.ts b/packages/motion/src/index.ts index 8e8cbab..f52e338 100644 --- a/packages/motion/src/index.ts +++ b/packages/motion/src/index.ts @@ -1 +1,7 @@ -console.log("motion"); +export * from "./context"; +export * from "./player"; +export * from "./utils"; +export * from "./movie"; +export * from "./flow"; +export * from "./tweening"; +export * from "./controls"; diff --git a/packages/motion/src/movie/index.ts b/packages/motion/src/movie/index.ts new file mode 100644 index 0000000..b7a7323 --- /dev/null +++ b/packages/motion/src/movie/index.ts @@ -0,0 +1,2 @@ +export * from "./make-movie"; +export * from "./make-scene"; diff --git a/packages/motion/src/movie/make-movie.test.ts b/packages/motion/src/movie/make-movie.test.ts index c03ef7e..d2b2a55 100644 --- a/packages/motion/src/movie/make-movie.test.ts +++ b/packages/motion/src/movie/make-movie.test.ts @@ -1,7 +1,7 @@ import { test, describe, expect } from "vitest"; import { makeMovie } from "./make-movie"; import { makeScene } from "./make-scene"; -import { runMotion } from "@/context"; +import { runScene } from "@/context"; import { frameMaker } from "@/test-utils"; describe("makeMovie", async () => { @@ -44,7 +44,7 @@ describe("makeMovie", async () => { } ); - const executed = runMotion(movie.initialState, movie.builder); + const executed = runScene(movie); expect(executed.frames.length).toBe(3); expect(executed.frames).toEqual([ @@ -116,7 +116,7 @@ describe("makeMovie", async () => { } ); - const executed = runMotion(movie.initialState, movie.builder); + const executed = runScene(movie); expect(executed.frames.length).toBe(4); @@ -201,7 +201,7 @@ describe("makeMovie", async () => { } ); - const executed = runMotion(movie.initialState, movie.builder, { + const executed = runScene(movie, { fps: 1, }); @@ -278,7 +278,7 @@ describe("makeMovie", async () => { } ); - const executed = runMotion(movie.initialState, movie.builder, { + const executed = runScene(movie, { fps: 1, }); @@ -325,7 +325,7 @@ describe("makeMovie", async () => { } ); - const executed = runMotion(movie.initialState, movie.builder, { + const executed = runScene(movie, { fps: 1, }); diff --git a/packages/motion/src/movie/make-movie.ts b/packages/motion/src/movie/make-movie.ts index 426ec8a..476bcaf 100644 --- a/packages/motion/src/movie/make-movie.ts +++ b/packages/motion/src/movie/make-movie.ts @@ -6,17 +6,17 @@ import { requestTransition, } from "@/context"; import { SceneMetaish, generateMeta } from "@/utils"; -import { Scene, makeScene } from "./make-scene"; +import { MotionScene, makeScene } from "./make-scene"; export type MovieSettings = { transitionDuration: number; }; -export type SceneMap = Record> & { +export type SceneMap = Record> & { [key: number]: never; }; export type SceneMapInitialState = { - [K in keyof TSceneMap]: TSceneMap[K] extends Scene + [K in keyof TSceneMap]: TSceneMap[K] extends MotionScene ? TState & { $transitionIn: number } : never; }; @@ -106,15 +106,20 @@ export function createMovieBuilder( return function* (context) { const startNext = (transition = 0) => { - const scene = sceneStack.pop(); - if (!scene) { + const sceneKey = sceneStack.pop(); + + if (!sceneKey || typeof sceneKey !== "string") { return; } - const childContext = context.createChildContext(scene); - const childScene = scenes[scene]; + const scene = scenes[sceneKey]; + if (!scene) { + throw new Error(`Scene ${String(sceneKey)} not found`); + } + const childContext = context.createChildContext(sceneKey, scene); + const childScene = scenes[sceneKey]; if (!childScene) { - throw new Error(`Scene ${String(scene)} not found`); + throw new Error(`Scene ${String(sceneKey)} not found`); } sceneBuilders.push( diff --git a/packages/motion/src/movie/make-scene.ts b/packages/motion/src/movie/make-scene.ts index c3f26b6..8f20f83 100644 --- a/packages/motion/src/movie/make-scene.ts +++ b/packages/motion/src/movie/make-scene.ts @@ -1,18 +1,18 @@ import { MotionBuilder, MotionState } from "@/context"; import { SceneMetaish, generateMeta } from "@/utils"; -export function makeScene< - TMeta extends SceneMetaish, - TState extends MotionState ->(meta: TMeta, initialState: TState, builder: MotionBuilder) { +export function makeScene( + meta: SceneMetaish, + initialState: TState, + builder: MotionBuilder +) { return { - meta: generateMeta(meta), + meta: generateMeta(meta), builder, initialState, }; } -export type Scene< - TMeta extends SceneMetaish, - TState extends MotionState -> = ReturnType>; +export type MotionScene = ReturnType< + typeof makeScene +>; diff --git a/packages/motion/src/player/index.ts b/packages/motion/src/player/index.ts new file mode 100644 index 0000000..109b186 --- /dev/null +++ b/packages/motion/src/player/index.ts @@ -0,0 +1 @@ +export * from "./motion-controller"; diff --git a/packages/motion/src/player/motion-controller.ts b/packages/motion/src/player/motion-controller.ts new file mode 100644 index 0000000..0623996 --- /dev/null +++ b/packages/motion/src/player/motion-controller.ts @@ -0,0 +1,183 @@ +import { + MotionBuilder, + MotionContext, + MotionContextSettings, + MotionState, + runScene, +} from "@/context"; +import { MotionScene } from "@/movie"; +import { clamp } from "@coord/core"; + +import { EventEmitter } from "events"; + +interface MotionControllerEvents { + "frame-changed": MotionController; + "play-status-changed": MotionController; + "play-range-changed": MotionController; + "repeat-changed": MotionController; +} + +export class MotionController extends EventEmitter { + on>( + event: EventName, + listener: (payload: MotionControllerEvents[EventName]) => void + ): this { + return super.on(event, listener); + } + emit>( + event: EventName, + payload: MotionControllerEvents[EventName] + ): boolean { + return super.emit(event, payload); + } + context: MotionContext; + constructor(context: MotionContext) { + super(); + this.context = context; + this._playRangeEnd = (context.frames.length * 1000) / context.settings.fps; + } + + static from( + scene: MotionScene, + settings?: Partial + ): MotionController { + return new MotionController(runScene(scene, settings)); + } + + private _frameRequest = 0; + private _playing = false; + private _currentTime = 0; + private _startTime = 0; + private _repeat = false; + + private _playRangeStart = 0; + private _playRangeEnd: number; + + get playing() { + return this._playing; + } + + get durationInFrames() { + return this.context.frames.length; + } + + get fps() { + return this.context.settings.fps; + } + + get duration() { + return (this.durationInFrames * 1000) / this.fps; + } + + get currentTime() { + return this._currentTime; + } + get state() { + const frame = this.context.frames[this.currentFrame]; + if (!frame) throw new Error("Frame not found"); + return frame; + } + + get repeat() { + return this._repeat; + } + + get playRange() { + return [this._playRangeStart, this._playRangeEnd] as const; + } + + get currentFrame() { + return clamp( + Math.floor((this._currentTime / 1000) * this.fps), + 0, + this.durationInFrames - 1 + ); + } + + tick = (time: number) => { + if (!this.playing) return; + let currentTime = time - this._startTime; + + if (currentTime >= this._playRangeEnd) { + if (this._repeat) { + currentTime = this._playRangeStart; + this._startTime = time; + } else { + this.setTime(this._playRangeEnd); + this.pause(); + return; + } + } + + this.setTime(currentTime); + + this._frameRequest = requestAnimationFrame(this.tick); + }; + + play = (time = this._currentTime) => { + if (this.playing) return; + this.setIsPlaying(true); + if (time >= this._playRangeEnd) { + time = this._playRangeStart; + } + this.setTime(time); + this._startTime = performance.now() - this._currentTime; + this._frameRequest = requestAnimationFrame(this.tick); + }; + + pause = () => { + cancelAnimationFrame(this._frameRequest); + this.setIsPlaying(false); + }; + + stop = () => { + this.pause(); + + this.setTime(0); + }; + + setPlayRange = (start: number, end: number) => { + start = Math.max(0, start); + end = Math.min(this.duration, end); + + if (start > end) { + throw new Error("Play range start is greater than end"); + } + + this._playRangeStart = start; + this._playRangeEnd = end; + + this.emit("play-range-changed", this); + }; + + setTime = (time: number) => { + const prevFrame = this.currentFrame; + this._currentTime = clamp(time, this._playRangeStart, this._playRangeEnd); + + if (this.currentFrame !== prevFrame) { + this.emit("frame-changed", this); + } + }; + + setFrame = (frame: number) => { + this.setTime((frame / this.durationInFrames) * this.duration); + }; + + setRepeat = (repeat: boolean) => { + if (repeat !== this._repeat) { + this._repeat = repeat; + this.emit("repeat-changed", this); + } + }; + + private setIsPlaying = (playing: boolean) => { + if (playing !== this._playing) { + this._playing = playing; + this.emit("play-status-changed", this); + } + }; + disconnect = () => { + this.pause(); + this.removeAllListeners(); + }; +} diff --git a/packages/motion/src/tweening/spring.ts b/packages/motion/src/tweening/spring.ts index e07ca28..9827464 100644 --- a/packages/motion/src/tweening/spring.ts +++ b/packages/motion/src/tweening/spring.ts @@ -66,7 +66,7 @@ export function* spring( if (frameTime >= updateStep) { frameTime -= updateStep; - fn((Array.isArray(from) ? position : position.x) as T); + fn((isNumber(from) ? position.x : position) as T); yield; } } diff --git a/packages/motion/src/tweening/tween.test.ts b/packages/motion/src/tweening/tween.test.ts index 209144b..9218d8f 100644 --- a/packages/motion/src/tweening/tween.test.ts +++ b/packages/motion/src/tweening/tween.test.ts @@ -1,8 +1,9 @@ import { test, describe, expect } from "vitest"; -import { runMotion } from "@/context"; +import { runScene } from "@/context"; import { frameMaker } from "@/test-utils"; import { tween } from "./tween"; +import { makeScene } from "@/movie"; describe("tween", async () => { test("waits the specified time before continuing", async () => { @@ -12,19 +13,20 @@ describe("tween", async () => { }, 0 ); - - const executed = runMotion( + const scene = makeScene( + "test", { t: 0, }, function* (context) { - yield* tween(1, (t) => context.state({ t })); - }, - { - fps: 3, + yield* tween(1, (t) => context.state({ t }), "linear"); } ); + const executed = runScene(scene, { + fps: 3, + }); + expect(executed.frames.length).toBe(3); expect(executed.frames).toEqual([ makeSceneFrame({ diff --git a/packages/motion/src/tweening/tween.ts b/packages/motion/src/tweening/tween.ts index f491f71..f81f003 100644 --- a/packages/motion/src/tweening/tween.ts +++ b/packages/motion/src/tweening/tween.ts @@ -4,7 +4,7 @@ import { EasingOptions, easingsFunctions } from "@coord/core"; export function* tween( duration: number, fn: (t: number) => void, - easing: EasingOptions = "linear" + easing: EasingOptions = "easeInOutCubic" ) { const easingFn = typeof easing === "function" ? easing : easingsFunctions[easing]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33d2281..5d23a54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,8 @@ importers: packages/docs: specifiers: '@coord/graph': workspace:* + '@coord/motion': workspace:* + '@coord/motion-react': workspace:* '@mdx-js/loader': ^2.3.0 '@mdx-js/mdx': ^2.3.0 '@mdx-js/react': ^2.3.0 @@ -73,6 +75,9 @@ importers: tsconfig: workspace:* typescript: 5.1.3 dependencies: + '@coord/graph': link:../graph + '@coord/motion': link:../motion + '@coord/motion-react': link:../motion-react '@mdx-js/loader': 2.3.0_webpack@5.86.0 '@mdx-js/mdx': 2.3.0 '@mdx-js/react': 2.3.0_react@18.2.0 @@ -89,7 +94,6 @@ importers: remark-gfm: 3.0.1 remark-math: 5.1.1 devDependencies: - '@coord/graph': link:../graph '@tailwindcss/typography': 0.5.9_tailwindcss@3.3.2 '@total-typescript/ts-reset': 0.4.2 '@types/node': 20.2.5 @@ -118,11 +122,11 @@ importers: typescript: ^5.1.3 vitest: ^0.31.1 dependencies: + '@coord/core': link:../core '@use-gesture/react': 10.2.27_react@18.2.0 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 devDependencies: - '@coord/core': link:../core '@types/lodash-es': 4.17.7 '@types/react': 18.2.8 '@types/react-dom': 18.2.4 @@ -135,15 +139,63 @@ importers: packages/motion: specifiers: '@coord/core': workspace:* + '@types/events': ^3.0.0 + '@types/react': ^18.2.12 + events: ^3.3.0 + react: '>=16.8.0' + react-dom: '>=16.8.0' tsconfig: workspace:* tsup: ^6.7.0 typescript: ^5.1.3 - devDependencies: + dependencies: '@coord/core': link:../core + events: 3.3.0 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + devDependencies: + '@types/events': 3.0.0 + '@types/react': 18.2.12 tsconfig: link:../tsconfig tsup: 6.7.0_typescript@5.1.3 typescript: 5.1.3 + packages/motion-react: + specifiers: + '@coord/core': workspace:* + '@coord/motion': workspace:* + '@types/events': ^3.0.0 + '@types/react': ^18.2.12 + '@use-gesture/react': ^10.2.27 + autoprefixer: 10.4.14 + clsx: ^1.2.1 + events: ^3.3.0 + postcss: 8.4.24 + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-icons: ^4.9.0 + tailwindcss: 3.3.2 + tsconfig: workspace:* + tsup: ^6.7.0 + typescript: ^5.1.3 + dependencies: + '@coord/core': link:../core + '@coord/motion': link:../motion + '@use-gesture/react': 10.2.27_react@18.2.0 + clsx: 1.2.1 + events: 3.3.0 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-icons: 4.9.0_react@18.2.0 + devDependencies: + '@types/events': 3.0.0 + '@types/react': 18.2.12 + autoprefixer: 10.4.14_postcss@8.4.24 + postcss: 8.4.24 + tailwindcss: 3.3.2 + tsconfig: link:../tsconfig + tsup: 6.7.0_gaitvkukymdrhrycqpfwh3czqm + typescript: 5.1.3 + packages/tsconfig: specifiers: {} @@ -908,6 +960,10 @@ packages: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} dev: false + /@types/events/3.0.0: + resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==} + dev: true + /@types/glob/7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: @@ -1002,6 +1058,14 @@ packages: '@types/react': 18.2.8 dev: true + /@types/react/18.2.12: + resolution: {integrity: sha512-ndmBMLCgn38v3SntMeoJaIrO6tGHYKMEBohCUmw8HoLLQdRMOIGXfeYaBTLe2lsFaSB3MOK1VXscYFnmLtTSmw==} + dependencies: + '@types/prop-types': 15.7.5 + '@types/scheduler': 0.16.3 + csstype: 3.1.2 + dev: true + /@types/react/18.2.8: resolution: {integrity: sha512-lTyWUNrd8ntVkqycEEplasWy2OxNlShj3zqS0LuB1ENUGis5HodmhM7DtCoUGbxj3VW/WsGA0DUhpG6XrM7gPA==} dependencies: @@ -1668,7 +1732,6 @@ packages: /clsx/1.2.1: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} - dev: true /color-convert/1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -4239,6 +4302,23 @@ packages: yaml: 1.10.2 dev: true + /postcss-load-config/3.1.4_postcss@8.4.24: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.24 + yaml: 1.10.2 + dev: true + /postcss-load-config/4.0.1_postcss@8.4.24: resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} engines: {node: '>= 14'} @@ -5298,6 +5378,43 @@ packages: /tslib/2.5.3: resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} + /tsup/6.7.0_gaitvkukymdrhrycqpfwh3czqm: + resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==} + engines: {node: '>=14.18'} + hasBin: true + peerDependencies: + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.1.0' + peerDependenciesMeta: + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + dependencies: + bundle-require: 4.0.1_esbuild@0.17.19 + cac: 6.7.14 + chokidar: 3.5.3 + debug: 4.3.4 + esbuild: 0.17.19 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss: 8.4.24 + postcss-load-config: 3.1.4_postcss@8.4.24 + resolve-from: 5.0.0 + rollup: 3.23.1 + source-map: 0.8.0-beta.0 + sucrase: 3.32.0 + tree-kill: 1.2.2 + typescript: 5.1.3 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + /tsup/6.7.0_typescript@5.1.3: resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==} engines: {node: '>=14.18'} From 4d6bc19efe711bbd27a43d2e6961acb47d3cd8fa Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Sat, 17 Jun 2023 16:55:39 -0300 Subject: [PATCH 06/20] improve controls --- packages/motion-react/package.json | 8 +- packages/motion-react/rollup.config.ts | 81 ++++++ .../src/components/Player/MotionPlayer.css | 243 ++++++++++++++++++ .../src/components/Player/MotionPlayer.tsx | 47 ++-- .../Player/MotionPlayerControls.tsx | 22 +- .../Player/MotionPlayerProgressBar.tsx | 26 +- .../src/components/Player/index.tsx | 1 + packages/motion-react/src/index.ts | 1 - packages/motion-react/tsconfig.json | 1 + 9 files changed, 376 insertions(+), 54 deletions(-) create mode 100644 packages/motion-react/rollup.config.ts create mode 100644 packages/motion-react/src/components/Player/MotionPlayer.css diff --git a/packages/motion-react/package.json b/packages/motion-react/package.json index 7f87017..b4c3755 100644 --- a/packages/motion-react/package.json +++ b/packages/motion-react/package.json @@ -7,10 +7,10 @@ "license": "MIT", "scripts": { "lint": "tsc", - "build": "tsup", "dev": "tsup --watch", "test": "vitest run && vitest typecheck", "test:watch": "vitest", + "build": "tsup", "clean": "rm -rf ./dist ./.turbo ./node_modules" }, "tsup": { @@ -23,6 +23,7 @@ "entryPoints": [ "src/index.ts" ], + "injectStyle": true, "sourcemap": true }, "peerDependencies": { @@ -45,9 +46,12 @@ "dependencies": { "@coord/core": "workspace:*", "@coord/motion": "workspace:*", + "@rollup/plugin-typescript": "^11.1.1", "@use-gesture/react": "^10.2.27", "clsx": "^1.2.1", "events": "^3.3.0", - "react-icons": "^4.9.0" + "react-icons": "^4.9.0", + "rollup": "^3.25.1", + "rollup-plugin-postcss": "^4.0.2" } } diff --git a/packages/motion-react/rollup.config.ts b/packages/motion-react/rollup.config.ts new file mode 100644 index 0000000..35e8b6f --- /dev/null +++ b/packages/motion-react/rollup.config.ts @@ -0,0 +1,81 @@ +import typescript from "@rollup/plugin-typescript"; +import { Plugin, RollupOptions } from "rollup"; +import postcss from "rollup-plugin-postcss"; + +const mergeTailwind = (): Plugin => { + const virtualStyles = new Map(); + return { + name: "rollup-plugin-postcss", + + transform(code, id) { + console.log(id); + + if (id.endsWith("styles.css")) { + console.log(virtualStyles); + code += `@layer components {`; + + for (const [className, styles] of virtualStyles.entries()) { + code += ` + .${className} { + @apply ${styles} + } + `; + } + code += `}`; + + console.log(code); + return code; + } + if (id.endsWith(".tsx")) { + const pattern = + /(["'`])([\w-.\[\]\(\):]+)\s+?<~\s+?([\w-.\s\/\[\]\(\):]+)\1/g; + const modifiedSource = code.replace( + pattern, + (_, quote, className, styles) => { + // Add extracted styles to the CSS content + const existing = virtualStyles.get(className); + if (!existing) { + virtualStyles.set(className, styles); + console.log(className, styles); + } else { + if (existing !== styles) { + throw new Error( + `class name ${className} has conflicting styles: ${existing} and ${styles}` + ); + } + } + return quote + className + quote; + } + ); + + return modifiedSource; + } + + return null; + }, + }; +}; +const config: RollupOptions = { + input: "src/index.ts", + plugins: [ + typescript(), + mergeTailwind(), + postcss({ + extract: true, + }), + ], + output: [ + { + file: "dist/index.js", + format: "cjs", + sourcemap: true, + }, + { + file: "dist/index.mjs", + format: "es", + sourcemap: true, + }, + ], +}; + +export default config; diff --git a/packages/motion-react/src/components/Player/MotionPlayer.css b/packages/motion-react/src/components/Player/MotionPlayer.css new file mode 100644 index 0000000..2cded2a --- /dev/null +++ b/packages/motion-react/src/components/Player/MotionPlayer.css @@ -0,0 +1,243 @@ +.motion-player { + position: relative; + display: flex; + cursor: pointer; + align-items: center; + justify-content: center; +} +.motion-player-progress-bar-area { + position: absolute; + bottom: 0px; + width: 100%; + opacity: 0; + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; +} +.motion-player-progress-bar-area-visible { + opacity: 1; +} +.motion-player-controls { + width: 100%; +} +.motion-player-controls-progress-bar { + width: 100%; +} +.motion-player-controls-actions { + display: flex; + width: 100%; + align-items: center; + gap: 0.5rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} +.motion-player-controls-button { + border-radius: 0.25rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} +.motion-player-controls-button-active { + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity)); +} +.motion-player-controls-time { + font-size: 0.75rem; + line-height: 1rem; +} +.motion-player-controls-title { + flex-grow: 1; + overflow: hidden; + font-size: 0.75rem; + line-height: 1rem; +} +.motion-player-progress-bar { + position: relative; + display: flex; + width: 100%; + touch-action: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + gap: 0.25rem; +} +.motion-player-progress-bar-scene { + position: relative; + height: 0.25rem; + width: 100%; + cursor: pointer; +} +.motion-player-progress-bar-scene-title-area { + position: absolute; + bottom: 0.25rem; + width: 100%; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + opacity: 0; + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.motion-player-progress-bar-scene-title { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-left: 0.25rem; + padding-right: 0.25rem; + font-size: 0.75rem; + line-height: 1rem; +} +.motion-player-progress-bar-scene-bar { + height: 0.25rem; + width: 100%; + background-color: rgb(229 231 235 / 0.1); + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.motion-player-progress-bar-scene-bar-progress { + height: 100%; + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity)); +} +.motion-player-progress-bar-head { + position: absolute; + left: -0.125rem; + top: 0px; + height: 0.25rem; + width: 0.25rem; + border-radius: 9999px; + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity)); + opacity: 0; + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.motion-player { + position: relative; + display: flex; + cursor: pointer; + align-items: center; + justify-content: center; +} +.motion-player-progress-bar-area { + position: absolute; + bottom: 0px; + width: 100%; + opacity: 0; + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; +} +.motion-player-progress-bar-area-visible { + opacity: 1; +} +.motion-player-controls { + width: 100%; +} +.motion-player-controls-progress-bar { + width: 100%; +} +.motion-player-controls-actions { + display: flex; + width: 100%; + align-items: center; + gap: 0.5rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} +.motion-player-controls-button { + border-radius: 0.25rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} +.motion-player-controls-button-active { + background-color: rgb(59 130 246 / 1); +} +.motion-player-controls-time { + font-size: 0.75rem; + line-height: 1rem; +} +.motion-player-controls-title { + flex-grow: 1; + overflow: hidden; + font-size: 0.75rem; + line-height: 1rem; +} +.motion-player-progress-bar { + position: relative; + display: flex; + width: 100%; + touch-action: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + gap: 0.25rem; +} +.motion-player-progress-bar-scene { + position: relative; + height: 0.25rem; + width: 100%; + cursor: pointer; +} +.motion-player-progress-bar-scene-title-area { + position: absolute; + bottom: 0.25rem; + width: 100%; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + opacity: 0; + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.motion-player-progress-bar-scene-title { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-left: 0.25rem; + padding-right: 0.25rem; + font-size: 0.75rem; + line-height: 1rem; +} +.motion-player-progress-bar-scene-bar { + height: 0.25rem; + width: 100%; + background-color: rgb(229 231 235 / 0.1); + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.motion-player-progress-bar-scene-bar-progress { + height: 100%; + background-color: rgb(59 130 246 / 1); +} +.motion-player-progress-bar-head { + position: absolute; + left: -0.125rem; + top: 0px; + height: 0.25rem; + width: 0.25rem; + border-radius: 9999px; + background-color: rgb(59 130 246 / 1); + opacity: 0; + transition-property: transform opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.motion-player-progress-bar:hover .motion-player-progress-bar-scene-title-area { + opacity: 1; +} + +.motion-player-progress-bar:hover .motion-player-progress-bar-head { + opacity: 1; + transform: scale(4); +} diff --git a/packages/motion-react/src/components/Player/MotionPlayer.tsx b/packages/motion-react/src/components/Player/MotionPlayer.tsx index 100aba0..1b16a63 100644 --- a/packages/motion-react/src/components/Player/MotionPlayer.tsx +++ b/packages/motion-react/src/components/Player/MotionPlayer.tsx @@ -20,6 +20,10 @@ export function MotionPlayer({ ...rest }: MotionPlayerProps) { + const [loading, setLoading] = useState(true); + useEffect(() => { + setLoading(false); + }, []); const ref = React.useRef(null); const toggleFullScreen = () => { if (!document.fullscreenElement) { @@ -35,6 +39,8 @@ export function MotionPlayer({ const timeout = React.useRef | null>(null); + const isProgressBarVisible = controls.playing === false || !cursorIsIdle; + useEffect(() => { if (autoplay) { controls.play(); @@ -47,10 +53,7 @@ export function MotionPlayer({ return (
{ setCursorIsIdle(false); if (timeout.current) { @@ -69,25 +72,25 @@ export function MotionPlayer({ }} {...rest} > - {children} + {!loading && ( + <> + {children} -
{ - e.stopPropagation(); - }} - className={clsx( - "absolute bottom-0 w-full transition-opacity duration-300 ", - { - "opacity-0": controls.playing, - "group-hover/player:opacity-100": !cursorIsIdle, - } - )} - > - -
+
{ + e.stopPropagation(); + }} + className={clsx("motion-player-progress-bar-area", { + "motion-player-progress-bar-area-visible": isProgressBarVisible, + })} + > + +
+ + )}
); } diff --git a/packages/motion-react/src/components/Player/MotionPlayerControls.tsx b/packages/motion-react/src/components/Player/MotionPlayerControls.tsx index 0c42241..4489941 100644 --- a/packages/motion-react/src/components/Player/MotionPlayerControls.tsx +++ b/packages/motion-react/src/components/Player/MotionPlayerControls.tsx @@ -7,7 +7,7 @@ import { HiArrowPath, } from "react-icons/hi2"; import { MotionPlayerProgressBar } from "./MotionPlayerProgressBar"; -import { formatDuration } from "@coord/core/dist"; +import { formatDuration } from "@coord/core"; import clsx from "clsx"; type MotionPlayerControlsProps = { @@ -36,14 +36,14 @@ export function MotionPlayerControls({ const longerThanAnHour = duration >= oneHour; return ( -
-
+
+
-
+
{playing ? ( - ) : (
-
+
{formatDuration(currentTime, longerThanAnHour)} {" / "} {formatDuration(duration, longerThanAnHour)}
-
{meta.title}
+
{meta.title}
{toggleFullScreen && (
) : ( )} -
-
+
{formatDuration(currentTime, longerThanAnHour)} {" / "} {formatDuration(duration, longerThanAnHour)} @@ -82,12 +83,12 @@ export function MotionPlayerControls({ {toggleFullScreen && (
)} diff --git a/packages/motion-react/src/components/Player/MotionPlayerProgressBar.css b/packages/motion-react/src/components/Player/MotionPlayerProgressBar.css new file mode 100644 index 0000000..79b75b5 --- /dev/null +++ b/packages/motion-react/src/components/Player/MotionPlayerProgressBar.css @@ -0,0 +1,196 @@ +.motion-player-progress-bar { + position: relative; + display: flex; + width: 100%; + touch-action: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + gap: 0.5em; +} +.motion-player-progress-bar_scene { + position: relative; + height: 0.5em; + width: 100%; + cursor: pointer; +} +.motion-player-progress-bar_scene-title-area { + position: absolute; + bottom: 0.5em; + width: 100%; + padding-top: 1em; + padding-bottom: 1em; + opacity: 0; + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.motion-player-progress-bar_scene-title { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-left: 0.5em; + padding-right: 0.5em; + font-size: 1.5em; + line-height: 1em; +} +.motion-player-progress-bar_scene-bar { + height: 0.5em; + width: 100%; + background-color: rgb(229 231 235 / 0.1); + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.motion-player-progress-bar_scene-bar-progress { + height: 100%; + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity)); +} +.motion-player-progress-bar_head { + position: absolute; + left: -0.125em; + top: 0px; + height: 0.5em; + width: 0.5em; + border-radius: 9999px; + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity)); + opacity: 0; + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.motion-player { + position: relative; + display: flex; + cursor: pointer; + align-items: center; + justify-content: center; +} +.motion-player-progress-bar_area { + position: absolute; + bottom: 0px; + width: 100%; + opacity: 0; + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; +} +.motion-player-progress-bar_area-visible { + opacity: 1; +} +.motion-player-controls { + width: 100%; +} +.motion-player-controls-progress-bar { + width: 100%; +} +.motion-player-controls-actions { + display: flex; + width: 100%; + align-items: center; + gap: 1em; + padding-top: 1em; + padding-bottom: 1em; +} +.motion-player-controls-button { + border-radius: 0.5em; + padding-left: 1.5em; + padding-right: 1.5em; + padding-top: 0.5em; + padding-bottom: 0.5em; +} +.motion-player-controls-button-active { + background-color: var(--motion-player-accent-color); +} +.motion-player-controls-time { + font-size: 1.5em; + line-height: 1em; +} +.motion-player-controls-title { + flex-grow: 1; + overflow: hidden; + font-size: 1.5em; + line-height: 1em; +} +.motion-player-progress-bar { + position: relative; + display: flex; + width: 100%; + touch-action: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + gap: 0.25em; +} +.motion-player-progress-bar_scene { + position: relative; + height: 0.5em; + width: 100%; + cursor: pointer; +} +.motion-player-progress-bar_scene-title-area { + position: absolute; + bottom: 0.5em; + + width: 100%; + padding-top: 1em; + padding-bottom: 1em; + opacity: 0; + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.motion-player-progress-bar_scene-title { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-left: 0.5em; + padding-right: 0.5em; + font-size: 1.2em; + line-height: 1em; +} +.motion-player-progress-bar_scene-bar { + height: 0.5em; + width: 100%; + background-color: rgb(229 231 235 / 0.1); + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.motion-player-progress-bar_scene:hover .motion-player-progress-bar_scene-bar { + transform: scaley(1.5); +} + +.motion-player-progress-bar_scene-bar-progress { + height: 100%; + background-color: var(--motion-player-accent-color); +} + +.motion-player-progress-bar_head { + position: absolute; + left: -0.25em; + top: 0px; + height: 0.5em; + width: 0.5em; + border-radius: 9999px; + background-color: var(--motion-player-accent-color); + opacity: 0; + transition-property: transform opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.motion-player-progress-bar:hover .motion-player-progress-bar_scene-title-area { + opacity: 1; +} + +.motion-player-progress-bar:hover .motion-player-progress-bar_head { + opacity: 1; + transform: scale(4); +} diff --git a/packages/motion-react/src/components/Player/MotionPlayerProgressBar.tsx b/packages/motion-react/src/components/Player/MotionPlayerProgressBar.tsx index 1ab10b8..5d7c081 100644 --- a/packages/motion-react/src/components/Player/MotionPlayerProgressBar.tsx +++ b/packages/motion-react/src/components/Player/MotionPlayerProgressBar.tsx @@ -4,6 +4,7 @@ import { MotionContextMeta } from "@coord/motion"; import { clamp, inverseLerp } from "@coord/core"; import { useDrag } from "@use-gesture/react"; import clsx from "clsx"; +import "./MotionPlayerProgressBar.css"; export function MotionPlayerProgressBar({ controls, @@ -55,32 +56,33 @@ export function MotionPlayerProgressBar({ }); return scenes; }, [meta]); - + const sceneKeys = Object.keys(normalizedMeta); return (
- {Object.keys(normalizedMeta).map((key) => { + {sceneKeys.map((key) => { const scene = normalizedMeta[key]; if (!scene) return null; return (
-
-

- {scene.title} -

-
- -
+ {sceneKeys.length > 1 && ( +
+

+ {scene.title} +

+
+ )} +
= Omit< - T, - { - [K in keyof T]: T[K] extends TType ? never : K; - }[keyof T] ->; - -export function* controlColor< - TState extends MotionState, - TTree extends OnlyKeysOfType, string | undefined>, - TKey extends keyof TTree, - TControl extends ColorControl ->( - key: TKey & string, - fn?: (control: TControl) => ReturnType> -) { - const context = yield* requestContext(); - const control = new ColorControl(context, key) as TControl; - if (fn) { - yield* fn(control); - } - return control; -} +import { EasingOptions } from "@coord/core"; +import { Control, makeControlUtility } from "./control"; type RectangularColorSpace = | "srgb" @@ -73,37 +40,48 @@ type ColorTweeningOptions = { easing: EasingOptions; } & ColorMixOptions; -export class ColorControl { - chain: ReturnType>[] = []; - constructor(public context: MotionContext, public key: string) {} - - get() { - return getDeep(this.context._state, this.key) as TType; - } - set(value: string) { - this.context._state = setDeep(this.context._state, this.key, value); +export class ColorControl extends Control { + assertType(value: unknown): asserts value is string { + if (typeof value !== "string") { + throw new Error(`Expected a color string, got ${typeof value}`); + } } - tweenTo( + private *tweenColor( value: string, duration: number, config: Partial = {} ) { const { easing, ...colorMixOptions } = config; - const initialValue = this.get() ?? "#ffffff"; - - if (typeof initialValue !== "string") { - throw new Error( - `Cannot tween to a non-string value, got ${initialValue}` - ); - } + let initialValue: string; return tween( duration, (t) => { + if (!initialValue) { + initialValue = this.get(); + this.assertType(initialValue); + } + this.set(colorMix(initialValue, value, t, colorMixOptions)); }, easing ); } + + in( + duration: number, + config: Partial + ) { + const next = this.nextTarget; + this._nextTarget = null; + return this.tweenColor(next, duration, config); + } } +const createColorControl = (context: MotionContext, key: string) => + new ColorControl(context, key); + +export const controlColor = makeControlUtility< + string, + typeof createColorControl +>(createColorControl); diff --git a/packages/motion/src/controls/control.ts b/packages/motion/src/controls/control.ts new file mode 100644 index 0000000..bfebf15 --- /dev/null +++ b/packages/motion/src/controls/control.ts @@ -0,0 +1,91 @@ +import { + MotionBuilder, + MotionContext, + MotionState, + requestContext, +} from "@/context"; +import { ExtractPathsOfType, getDeep, setDeep } from "@coord/core"; + +export abstract class Control { + _nextTarget: TValue | null = null; + + constructor(public context: MotionContext, public key: string) {} + + get nextTarget() { + if (this._nextTarget === null) { + const curr = this.get(); + + this._nextTarget = curr; + return curr; + } + return this._nextTarget; + } + + set nextTarget(value: TValue) { + this._nextTarget = value; + } + + assertType(value: unknown): asserts value is TValue { + throw new Error( + `Type assertion for ${this.constructor.name} not implemented` + ); + } + + normalizeValue(value: TValueIn): TValue { + this.assertType(value); + return value; + } + get = () => { + const value = getDeep(this.context._state, this.key); + this.assertType(value); + return value; + }; + + set = (value: TValueIn) => { + this.context._state = setDeep( + this.context._state, + this.key, + this.normalizeValue(value) + ); + }; + + *as(value: TValueIn) { + this._nextTarget = null; + this.set(value); + yield; + } + + from(value: TValueIn) { + this.nextTarget = this.normalizeValue(value); + return this; + } + + to(value: TValueIn) { + this.nextTarget = this.normalizeValue(value); + return this; + } +} + +function hasSet(value: unknown): value is { set: (value: T) => void } { + return typeof value === "object" && value !== null && "set" in value; +} +export function makeControlUtility< + TType, + TFactory extends (context: MotionContext, key: string) => any +>(factory: TFactory) { + return function* controlUtility( + key: ExtractPathsOfType, + fn?: (control: ReturnType) => ReturnType>, + initialValue?: TType + ) { + const context = yield* requestContext(); + const control = factory(context, key); + if (initialValue !== undefined && hasSet(control)) { + control.set(initialValue); + } + if (fn) { + yield* fn(control); + } + return control as ReturnType; + }; +} diff --git a/packages/motion/src/controls/index.ts b/packages/motion/src/controls/index.ts index f17763f..f9665ec 100644 --- a/packages/motion/src/controls/index.ts +++ b/packages/motion/src/controls/index.ts @@ -2,3 +2,5 @@ export * from "./number-control"; export * from "./point-control"; export * from "./color-control"; export * from "./transform-control"; + +export * from "./string-control"; diff --git a/packages/motion/src/controls/number-control.test.ts b/packages/motion/src/controls/number-control.test.ts index 1304704..5bd1d28 100644 --- a/packages/motion/src/controls/number-control.test.ts +++ b/packages/motion/src/controls/number-control.test.ts @@ -17,8 +17,8 @@ describe("number control", async () => { function* () { const t = yield* controlNumber("t"); yield* all( - t.tweenTo(10, 1), - controlNumber("b.0", (t) => t.tweenTo(10, 1)) + t.to(10).in(1), + controlNumber("b.0", (t) => t.to(10).in(1)) ); } ); diff --git a/packages/motion/src/controls/number-control.ts b/packages/motion/src/controls/number-control.ts index 8d48d77..40c0085 100644 --- a/packages/motion/src/controls/number-control.ts +++ b/packages/motion/src/controls/number-control.ts @@ -16,100 +16,61 @@ import { InferPathValueImplementation, ExtractPathsOfType, } from "@coord/core"; +import { Control, makeControlUtility } from "./control"; const isNumber = (value: unknown): value is number => typeof value === "number"; -export function* controlNumber< - TState extends MotionState, - TKey extends ExtractPathsOfType ->( - key: TKey, - fn?: (control: NumberControl) => ReturnType>, - initialValue?: number -) { - const context = yield* requestContext(); - const control = new NumberControl(context, key, initialValue); - if (fn) { - yield* fn(control); - } - return control; -} - const isFinite = (value: unknown): value is number => Number.isFinite(value); -export class NumberControl { - _targetValue: number; - - constructor( - public context: MotionContext, - public key: string, - private _initialValue = 0 - ) { - this._targetValue = this.get() ?? 0; - } - - get(): number { - let value = getDeep(this.context._state, this.key); +export class NumberControl extends Control { + assertType(value: unknown): asserts value is number { if (!isNumber(value)) { - this.set(this._initialValue); - return this._initialValue; + throw new Error(`Expected number, got ${typeof value}`); + } + if (!isFinite(value)) { + throw new Error(`Expected finite number, got ${value}`); } - return value; - } - set(value: number) { - this.context._state = setDeep(this.context._state, this.key, value); } - tweenTo(value: number, duration: number, easing?: EasingOptions) { - const initialValue = this.get() ?? 0; - if (!isFinite(initialValue)) { - throw new Error( - `Cannot tween to a non-finite value, got ${initialValue}` - ); - } + private *tweenNumber( + value: number, + duration: number, + easing?: EasingOptions + ) { + let initialValue: number; - return tween( + yield* tween( duration, (t) => { + if (!initialValue) { + initialValue = this.get(); + this.assertType(initialValue); + } this.set(lerp(initialValue, value, t)); }, easing ); } - springTo(to: number, parameters?: SpringParameters) { - const initialValue = this.get() ?? 0; - if (!isFinite(initialValue)) { - throw new Error( - `Cannot spring to a non-finite value, got ${initialValue}` - ); - } - - return spring( - initialValue, - to, - (t) => { - this.set(t); - }, - parameters - ); + in(duration: number, easing?: EasingOptions) { + const next = this.nextTarget; + this._nextTarget = null; + return this.tweenNumber(next, duration, easing); } - from(value: number) { - this.set(value); - return this; - } - - to(value: number) { - this._targetValue = value; - return this; + spring(parameters?: SpringParameters) { + const next = this.nextTarget; + this._nextTarget = null; + return spring(this.get, next, this.set, parameters); } +} - in(duration: number, easing?: EasingOptions) { - return this.tweenTo(this._targetValue, duration, easing); - } +const createNumberControl = ( + context: MotionContext, + key: string +) => new NumberControl(context, key); - spring(parameters?: SpringParameters) { - return this.springTo(this._targetValue, parameters); - } -} +export const controlNumber = makeControlUtility< + number, + typeof createNumberControl +>(createNumberControl); diff --git a/packages/motion/src/controls/point-control.test.ts b/packages/motion/src/controls/point-control.test.ts index 6aa6bc1..8aac616 100644 --- a/packages/motion/src/controls/point-control.test.ts +++ b/packages/motion/src/controls/point-control.test.ts @@ -15,7 +15,7 @@ describe("point control", async () => { point: point(0, 0), }, function* () { - yield* all(controlPoint("point", (t) => t.tweenTo([10, 10], 1))); + yield* all(controlPoint("point", (t) => t.to([10, 10]).in(1))); } ); let executed = runScene(scene); diff --git a/packages/motion/src/controls/point-control.ts b/packages/motion/src/controls/point-control.ts index 5b4932d..731e961 100644 --- a/packages/motion/src/controls/point-control.ts +++ b/packages/motion/src/controls/point-control.ts @@ -1,122 +1,57 @@ -import { - MotionBuilder, - MotionContext, - MotionState, - requestContext, -} from "@/context"; +import { MotionContext, MotionState } from "@/context"; import { tween, spring, SpringParameters } from "@/tweening"; -import { - getDeep, - setDeep, - InferPathValueTree, - EasingOptions, - Vec2ish, - Vec2, - ExtractPathsOfType, -} from "@coord/core"; +import { EasingOptions, Vec2ish, Vec2 } from "@coord/core"; +import { Control, makeControlUtility } from "./control"; -export function* controlPoint< - TState extends MotionState, - TKey extends ExtractPathsOfType ->( - key: TKey & string, - fn?: (control: PointControl) => ReturnType>, - initialValue?: Vec2ish -) { - const context = yield* requestContext(); - const control = new PointControl( - context, - key, - initialValue ? Vec2.of(initialValue) : undefined - ); - if (fn) { - yield* fn(control); - } - return control; -} - -export class PointControl { - _targetValue: Vec2; - constructor( - public context: MotionContext, - public key: string, - private _initialValue = Vec2.of([0, 0]) - ) { - this._targetValue = this.get(); - } - - get() { - const value = getDeep(this.context._state, this.key); - if (value instanceof Vec2) { - return value; +export class PointControl extends Control { + assertType(value: unknown): asserts value is Vec2 { + if (!(value instanceof Vec2)) { + throw new Error(`Expected Vec2, got ${typeof value}`); + } + if (!value.isFinite()) { + throw new Error(`Expected finite Vec2, got ${value}`); } - this.set(this._initialValue); - return this._initialValue; } - set(value: Vec2ish) { - this.context._state = setDeep(this.context._state, this.key, value); + normalizeValue(value: Vec2ish) { + return Vec2.of(value); } - tweenTo(value: Vec2ish, duration: number, easing?: EasingOptions) { - const initialValue = Vec2.of(this.get()); - - if ( - !(initialValue instanceof Vec2) || - (initialValue instanceof Vec2 && !initialValue.isFinite()) - ) { - throw new Error( - `Cannot tween to a non-finite value, got ${initialValue}` - ); - } - const target = Vec2.of(value); + private *tweenPoint( + value: Vec2, + duration: number, + easing?: EasingOptions + ) { + let initialValue: Vec2; - return tween( + yield* tween( duration, (t) => { - this.set(initialValue.lerp(target, t)); + if (!initialValue) { + initialValue = this.get(); + this.assertType(initialValue); + } + this.set(initialValue.lerp(value, t)); }, easing ); } - springTo(to: Vec2ish, parameters?: SpringParameters) { - const initialValue = Vec2.of(this.get()); - - if ( - !(initialValue instanceof Vec2) || - (initialValue instanceof Vec2 && !initialValue.isFinite()) - ) { - throw new Error( - `Cannot tween to a non-finite value, got ${initialValue.toString()}` - ); - } - const target = Vec2.of(to); - - return spring( - initialValue, - target, - (t) => { - this.set(t); - }, - parameters - ); - } - - from(value: Vec2ish) { - this.set(value); - return this; + in(duration: number, easing?: EasingOptions) { + const next = this.nextTarget; + this._nextTarget = null; + return this.tweenPoint(next, duration, easing); } - to(value: Vec2ish) { - this._targetValue = Vec2.of(value); - return this; + spring(parameters?: SpringParameters) { + const next = this.nextTarget; + this._nextTarget = null; + return spring(this.get, next, this.set, parameters); } +} - in(duration: number, easing?: EasingOptions) { - return this.tweenTo(this._targetValue, duration, easing); - } +const createPointControl = (context: MotionContext, key: string) => + new PointControl(context, key); - spring(parameters?: SpringParameters) { - return this.springTo(this._targetValue, parameters); - } -} +export const controlPoint = makeControlUtility( + createPointControl +); diff --git a/packages/motion/src/controls/string-control.ts b/packages/motion/src/controls/string-control.ts new file mode 100644 index 0000000..bc128d0 --- /dev/null +++ b/packages/motion/src/controls/string-control.ts @@ -0,0 +1,136 @@ +import { + MotionBuilder, + MotionContext, + MotionState, + requestContext, +} from "@/context"; +import { tween } from "@/tweening"; +import { + getDeep, + setDeep, + EasingOptions, + ExtractPathsOfType, +} from "@coord/core"; +import { Control, makeControlUtility } from "./control"; +import { assertType } from "vitest"; + +// export function* controlString< +// TState extends MotionState, +// TKey extends ExtractPathsOfType +// >( +// key: TKey & string, +// fn?: (control: StringControl) => ReturnType>, +// initialValue?: string +// ) { +// const context = yield* requestContext(); +// const control = new StringControl(context, key); +// if (typeof initialValue === "string") { +// control.set(initialValue); +// } +// if (fn) { +// yield* fn(control); +// } +// return control; +// } + +export class StringControl extends Control { + assertType(value: unknown): asserts value is string { + if (typeof value !== "string") { + throw new Error(`Expected string, got ${typeof value}`); + } + } + + private *tweenString( + to: string, + duration: number, + config: Partial<{ + easing: EasingOptions; + mode: keyof typeof TextLerpModes; + }> = {} + ) { + const { easing = "linear", mode = "char-overwrite" } = config; + let fromValue: string; + yield* tween( + duration, + (t) => { + if (!fromValue) { + fromValue = this.get(); + } + this.set(TextLerpModes[mode](fromValue, to, t)); + }, + easing + ); + } + + clear(duration: number, easing?: EasingOptions) { + this._nextTarget = null; + return this.in(duration, "clear", easing); + } + in( + duration: number, + mode?: keyof typeof TextLerpModes, + easing?: EasingOptions + ) { + const next = this.nextTarget; + this._nextTarget = null; + return this.tweenString(next, duration, { + easing, + mode, + }); + } +} + +export const lerpText = (a: string, b: string, t: number, splitter = "") => { + let out = a.split(splitter); + + const length = Math.max(a.length, b.length) * t; + + for (let i = 0; i < length; i++) { + out[i] = b[i] ?? " "; + } + return out.join(splitter); //.trim(); +}; + +export const overwriteString = (a: string, b: string, t: number) => { + return lerpText(a, b, t); +}; + +export const overwriteByWord = (a: string, b: string, t: number) => { + return lerpText(a, b, t, " "); +}; + +export const clear = (a: string, _: string, t: number) => { + return a.slice(0, a.length * (1 - t)); +}; + +export const append = (a: string, b: string, t: number) => { + return a + b.slice(0, b.length * t); +}; + +export const clearThenAppend = (a: string, b: string, t: number) => { + const splitT = a.length / (a.length + b.length); + if (t <= splitT) { + return clear(a, "", t / splitT); + } else { + return append("", b, (t - splitT) / (1 - splitT)); + } +}; + +const TextLerpModes = { + overwrite: overwriteString, + "word-overwrite": overwriteByWord, + "char-overwrite": overwriteString, + clear: clear, + append: append, + "clear-then-append": clearThenAppend, +}; + +const createStringControl = ( + context: MotionContext, + key: string +) => new StringControl(context, key); + +export const controlString = makeControlUtility< + string, + typeof createStringControl +>(createStringControl); diff --git a/packages/motion/src/flow/index.ts b/packages/motion/src/flow/index.ts index b79ea82..b2bf4c0 100644 --- a/packages/motion/src/flow/index.ts +++ b/packages/motion/src/flow/index.ts @@ -2,3 +2,4 @@ export * from "./all"; export * from "./chain"; export * from "./delay"; export * from "./wait"; +export * from "./repeat"; diff --git a/packages/motion/src/flow/repeat.ts b/packages/motion/src/flow/repeat.ts new file mode 100644 index 0000000..2c923d2 --- /dev/null +++ b/packages/motion/src/flow/repeat.ts @@ -0,0 +1,13 @@ +import { MotionState, requestContext } from "@/context"; +import { MotionBuilderish, asIterable } from "@/utils"; + +export function* repeat( + n: number, + factory: (i: number) => MotionBuilderish +) { + const context = yield* requestContext(); + + for (let i = 0; i < n; i++) { + yield* asIterable(factory(i), context); + } +} diff --git a/packages/motion/src/movie/make-movie.ts b/packages/motion/src/movie/make-movie.ts index 476bcaf..54eb04f 100644 --- a/packages/motion/src/movie/make-movie.ts +++ b/packages/motion/src/movie/make-movie.ts @@ -2,6 +2,7 @@ import { MotionBuilder, MotionContext, MotionState, + MotionStateContextProps, isMotionBuilderRequest, requestTransition, } from "@/context"; @@ -17,7 +18,7 @@ export type SceneMap = Record> & { export type SceneMapInitialState = { [K in keyof TSceneMap]: TSceneMap[K] extends MotionScene - ? TState & { $transitionIn: number } + ? TState & MotionStateContextProps : never; }; diff --git a/packages/motion/src/movie/make-scene.ts b/packages/motion/src/movie/make-scene.ts index 8f20f83..d31cebf 100644 --- a/packages/motion/src/movie/make-scene.ts +++ b/packages/motion/src/movie/make-scene.ts @@ -16,3 +16,7 @@ export function makeScene( export type MotionScene = ReturnType< typeof makeScene >; + +const scene = makeScene("scene", { x: 0, y: 0 }, function* () { + // yield { x: 100, y: 100 }; +}); diff --git a/packages/motion/src/tweening/spring.ts b/packages/motion/src/tweening/spring.ts index 9827464..f82e23e 100644 --- a/packages/motion/src/tweening/spring.ts +++ b/packages/motion/src/tweening/spring.ts @@ -2,12 +2,15 @@ import { MotionState, requestContext } from "@/context"; import { Vec2, point } from "@coord/core"; const isNumber = (n: unknown): n is number => typeof n === "number"; +const isFunction = (fn: unknown): fn is Function => typeof fn === "function"; export function* spring( - from: T, - to: T, + intialValue: T | (() => T), + targetValue: T | (() => T), fn: (t: T) => void, spring: SpringParameters = Spring.Plop ) { + const from = isFunction(intialValue) ? intialValue() : intialValue; + const to = isFunction(targetValue) ? targetValue() : targetValue; const { settings } = yield* requestContext(); const { settleTolerance = 0.001 } = spring; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d23a54..43de4d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,6 +163,7 @@ importers: specifiers: '@coord/core': workspace:* '@coord/motion': workspace:* + '@rollup/plugin-typescript': ^11.1.1 '@types/events': ^3.0.0 '@types/react': ^18.2.12 '@use-gesture/react': ^10.2.27 @@ -173,6 +174,8 @@ importers: react: '>=16.8.0' react-dom: '>=16.8.0' react-icons: ^4.9.0 + rollup: ^3.25.1 + rollup-plugin-postcss: ^4.0.2 tailwindcss: 3.3.2 tsconfig: workspace:* tsup: ^6.7.0 @@ -180,12 +183,15 @@ importers: dependencies: '@coord/core': link:../core '@coord/motion': link:../motion + '@rollup/plugin-typescript': 11.1.1_n7ipjawvwu73gcznr6o2d3ryby '@use-gesture/react': 10.2.27_react@18.2.0 clsx: 1.2.1 events: 3.3.0 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-icons: 4.9.0_react@18.2.0 + rollup: 3.25.1 + rollup-plugin-postcss: 4.0.2_postcss@8.4.24 devDependencies: '@types/events': 3.0.0 '@types/react': 18.2.12 @@ -856,6 +862,40 @@ packages: fastq: 1.15.0 dev: true + /@rollup/plugin-typescript/11.1.1_n7ipjawvwu73gcznr6o2d3ryby: + resolution: {integrity: sha512-Ioir+x5Bejv72Lx2Zbz3/qGg7tvGbxQZALCLoJaGrkNXak/19+vKgKYJYM3i/fJxvsb23I9FuFQ8CUBEfsmBRg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.14.0||^3.0.0 + tslib: '*' + typescript: '>=3.7.0' + peerDependenciesMeta: + rollup: + optional: true + tslib: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2_rollup@3.25.1 + resolve: 1.22.2 + rollup: 3.25.1 + typescript: 5.1.3 + dev: false + + /@rollup/pluginutils/5.0.2_rollup@3.25.1: + resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.1 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 3.25.1 + dev: false + /@swc/helpers/0.5.1: resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==} dependencies: @@ -877,6 +917,11 @@ packages: resolution: {integrity: sha512-vqd7ZUDSrXFVT1n8b2kc3LnklncDQFPvR58yUS1kEP23/nHPAO9l1lMjUfnPrXYYk4Hj54rrLKMW5ipwk7k09A==} dev: true + /@trysound/sax/0.2.0: + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + dev: false + /@tsconfig/node10/1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} dev: true @@ -1336,7 +1381,6 @@ packages: engines: {node: '>=8'} dependencies: color-convert: 2.0.1 - dev: true /ansi-styles/5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} @@ -1466,6 +1510,10 @@ packages: resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} dev: true + /boolbase/1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: false + /brace-expansion/1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -1573,6 +1621,15 @@ packages: engines: {node: '>=6'} dev: true + /caniuse-api/3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + dependencies: + browserslist: 4.21.7 + caniuse-lite: 1.0.30001494 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + dev: false + /caniuse-lite/1.0.30001494: resolution: {integrity: sha512-sY2B5Qyl46ZzfYDegrl8GBCzdawSLT4ThM9b9F+aDYUrAG2zCOyMbd2Tq34mS1g4ZKBfjRlzOohQMxx28x6wJg==} @@ -1608,7 +1665,6 @@ packages: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - dev: true /change-case/3.1.0: resolution: {integrity: sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw==} @@ -1744,7 +1800,6 @@ packages: engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 - dev: true /color-name/1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} @@ -1752,7 +1807,10 @@ packages: /color-name/1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true + + /colord/2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + dev: false /comma-separated-tokens/2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -1772,6 +1830,11 @@ packages: engines: {node: '>= 6'} dev: true + /commander/7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: false + /commander/8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} @@ -1781,6 +1844,12 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true + /concat-with-sourcemaps/1.1.0: + resolution: {integrity: sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==} + dependencies: + source-map: 0.6.1 + dev: false + /concordance/5.0.4: resolution: {integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==} engines: {node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14'} @@ -1828,11 +1897,108 @@ packages: which: 2.0.2 dev: true + /css-declaration-sorter/6.4.0_postcss@8.4.24: + resolution: {integrity: sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==} + engines: {node: ^10 || ^12 || >=14} + peerDependencies: + postcss: ^8.0.9 + dependencies: + postcss: 8.4.24 + dev: false + + /css-select/4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + dev: false + + /css-tree/1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + dev: false + + /css-what/6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: false + /cssesc/3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true - dev: true + + /cssnano-preset-default/5.2.14_postcss@8.4.24: + resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + css-declaration-sorter: 6.4.0_postcss@8.4.24 + cssnano-utils: 3.1.0_postcss@8.4.24 + postcss: 8.4.24 + postcss-calc: 8.2.4_postcss@8.4.24 + postcss-colormin: 5.3.1_postcss@8.4.24 + postcss-convert-values: 5.1.3_postcss@8.4.24 + postcss-discard-comments: 5.1.2_postcss@8.4.24 + postcss-discard-duplicates: 5.1.0_postcss@8.4.24 + postcss-discard-empty: 5.1.1_postcss@8.4.24 + postcss-discard-overridden: 5.1.0_postcss@8.4.24 + postcss-merge-longhand: 5.1.7_postcss@8.4.24 + postcss-merge-rules: 5.1.4_postcss@8.4.24 + postcss-minify-font-values: 5.1.0_postcss@8.4.24 + postcss-minify-gradients: 5.1.1_postcss@8.4.24 + postcss-minify-params: 5.1.4_postcss@8.4.24 + postcss-minify-selectors: 5.2.1_postcss@8.4.24 + postcss-normalize-charset: 5.1.0_postcss@8.4.24 + postcss-normalize-display-values: 5.1.0_postcss@8.4.24 + postcss-normalize-positions: 5.1.1_postcss@8.4.24 + postcss-normalize-repeat-style: 5.1.1_postcss@8.4.24 + postcss-normalize-string: 5.1.0_postcss@8.4.24 + postcss-normalize-timing-functions: 5.1.0_postcss@8.4.24 + postcss-normalize-unicode: 5.1.1_postcss@8.4.24 + postcss-normalize-url: 5.1.0_postcss@8.4.24 + postcss-normalize-whitespace: 5.1.1_postcss@8.4.24 + postcss-ordered-values: 5.1.3_postcss@8.4.24 + postcss-reduce-initial: 5.1.2_postcss@8.4.24 + postcss-reduce-transforms: 5.1.0_postcss@8.4.24 + postcss-svgo: 5.1.0_postcss@8.4.24 + postcss-unique-selectors: 5.1.1_postcss@8.4.24 + dev: false + + /cssnano-utils/3.1.0_postcss@8.4.24: + resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + dev: false + + /cssnano/5.1.15_postcss@8.4.24: + resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + cssnano-preset-default: 5.2.14_postcss@8.4.24 + lilconfig: 2.1.0 + postcss: 8.4.24 + yaml: 1.10.2 + dev: false + + /csso/4.2.0: + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} + dependencies: + css-tree: 1.1.3 + dev: false /csstype/3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} @@ -1971,6 +2137,33 @@ packages: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} dev: true + /dom-serializer/1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + dev: false + + /domelementtype/2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: false + + /domhandler/4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + + /domutils/2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + dev: false + /dot-case/2.1.1: resolution: {integrity: sha512-HnM6ZlFqcajLsyudHq7LeeLDr2rFAVYtDv/hV5qchQEidSck8j9OPUsXY9KwJv/lHMtYlX4DjRQqwFYa+0r8Ug==} dependencies: @@ -1999,6 +2192,10 @@ packages: ansi-colors: 4.1.3 dev: true + /entities/2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + dev: false + /entities/4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2186,6 +2383,14 @@ packages: '@types/unist': 2.0.6 dev: false + /estree-walker/0.6.1: + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + dev: false + + /estree-walker/2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: false + /estree-walker/3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} dependencies: @@ -2197,6 +2402,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /eventemitter3/4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: false + /events/3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -2346,12 +2555,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true - dev: true optional: true /function-bind/1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - dev: true /function.prototype.name/1.1.5: resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} @@ -2367,6 +2574,12 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true + /generic-names/4.0.0: + resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} + dependencies: + loader-utils: 3.2.1 + dev: false + /get-caller-file/2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2543,7 +2756,6 @@ packages: engines: {node: '>= 0.4.0'} dependencies: function-bind: 1.1.1 - dev: true /hast-util-from-dom/4.2.0: resolution: {integrity: sha512-t1RJW/OpJbCAJQeKi3Qrj1cAOLA0+av/iPFori112+0X7R3wng+jxLA+kXec8K4szqPRGI8vPxbbpEYvvpwaeQ==} @@ -2668,6 +2880,19 @@ packages: safer-buffer: 2.1.2 dev: true + /icss-replace-symbols/1.1.0: + resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} + dev: false + + /icss-utils/5.1.0_postcss@8.4.24: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.24 + dev: false + /ieee754/1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: true @@ -2677,6 +2902,20 @@ packages: engines: {node: '>= 4'} dev: true + /import-cwd/3.0.0: + resolution: {integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==} + engines: {node: '>=8'} + dependencies: + import-from: 3.0.0 + dev: false + + /import-from/3.0.0: + resolution: {integrity: sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: false + /indent-string/4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -2815,7 +3054,6 @@ packages: resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} dependencies: has: 1.0.3 - dev: true /is-date-object/1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} @@ -3071,7 +3309,6 @@ packages: /lilconfig/2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} - dev: true /lines-and-columns/1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -3097,6 +3334,11 @@ packages: engines: {node: '>=6.11.5'} dev: false + /loader-utils/3.2.1: + resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==} + engines: {node: '>= 12.13.0'} + dev: false + /local-pkg/0.4.3: resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} engines: {node: '>=14'} @@ -3120,6 +3362,10 @@ packages: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} dev: true + /lodash.camelcase/4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: false + /lodash.castarray/4.4.0: resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} dev: true @@ -3132,6 +3378,10 @@ packages: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} dev: true + /lodash.memoize/4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + dev: false + /lodash.merge/4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true @@ -3144,6 +3394,10 @@ packages: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} dev: true + /lodash.uniq/4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + dev: false + /lodash/4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: true @@ -3427,6 +3681,10 @@ packages: '@types/mdast': 3.0.11 dev: false + /mdn-data/2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + dev: false + /meow/6.1.1: resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} engines: {node: '>=8'} @@ -4006,6 +4264,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /normalize-url/6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + dev: false + /npm-run-path/4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -4013,6 +4276,12 @@ packages: path-key: 3.1.1 dev: true + /nth-check/2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: false + /object-assign/4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -4086,6 +4355,11 @@ packages: p-map: 2.1.0 dev: true + /p-finally/1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + dev: false + /p-limit/2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -4133,6 +4407,21 @@ packages: aggregate-error: 3.1.0 dev: true + /p-queue/6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + dev: false + + /p-timeout/3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + dependencies: + p-finally: 1.0.0 + dev: false + /p-try/2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -4203,7 +4492,6 @@ packages: /path-parse/1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true /path-type/4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -4232,7 +4520,6 @@ packages: /picomatch/2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - dev: true /pify/2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} @@ -4244,6 +4531,11 @@ packages: engines: {node: '>=6'} dev: true + /pify/5.0.0: + resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} + engines: {node: '>=10'} + dev: false + /pirates/4.0.5: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} @@ -4264,6 +4556,76 @@ packages: pathe: 1.1.1 dev: true + /postcss-calc/8.2.4_postcss@8.4.24: + resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} + peerDependencies: + postcss: ^8.2.2 + dependencies: + postcss: 8.4.24 + postcss-selector-parser: 6.0.13 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-colormin/5.3.1_postcss@8.4.24: + resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.7 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-convert-values/5.1.3_postcss@8.4.24: + resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.7 + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-discard-comments/5.1.2_postcss@8.4.24: + resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + dev: false + + /postcss-discard-duplicates/5.1.0_postcss@8.4.24: + resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + dev: false + + /postcss-discard-empty/5.1.1_postcss@8.4.24: + resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + dev: false + + /postcss-discard-overridden/5.1.0_postcss@8.4.24: + resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + dev: false + /postcss-import/15.1.0_postcss@8.4.24: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -4317,7 +4679,6 @@ packages: lilconfig: 2.1.0 postcss: 8.4.24 yaml: 1.10.2 - dev: true /postcss-load-config/4.0.1_postcss@8.4.24: resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} @@ -4336,6 +4697,131 @@ packages: yaml: 2.3.1 dev: true + /postcss-merge-longhand/5.1.7_postcss@8.4.24: + resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + stylehacks: 5.1.1_postcss@8.4.24 + dev: false + + /postcss-merge-rules/5.1.4_postcss@8.4.24: + resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.7 + caniuse-api: 3.0.0 + cssnano-utils: 3.1.0_postcss@8.4.24 + postcss: 8.4.24 + postcss-selector-parser: 6.0.13 + dev: false + + /postcss-minify-font-values/5.1.0_postcss@8.4.24: + resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-minify-gradients/5.1.1_postcss@8.4.24: + resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + colord: 2.9.3 + cssnano-utils: 3.1.0_postcss@8.4.24 + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-minify-params/5.1.4_postcss@8.4.24: + resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.7 + cssnano-utils: 3.1.0_postcss@8.4.24 + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-minify-selectors/5.2.1_postcss@8.4.24: + resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + postcss-selector-parser: 6.0.13 + dev: false + + /postcss-modules-extract-imports/3.0.0_postcss@8.4.24: + resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.24 + dev: false + + /postcss-modules-local-by-default/4.0.3_postcss@8.4.24: + resolution: {integrity: sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0_postcss@8.4.24 + postcss: 8.4.24 + postcss-selector-parser: 6.0.13 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-modules-scope/3.0.0_postcss@8.4.24: + resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.24 + postcss-selector-parser: 6.0.13 + dev: false + + /postcss-modules-values/4.0.0_postcss@8.4.24: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0_postcss@8.4.24 + postcss: 8.4.24 + dev: false + + /postcss-modules/4.3.1_postcss@8.4.24: + resolution: {integrity: sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==} + peerDependencies: + postcss: ^8.0.0 + dependencies: + generic-names: 4.0.0 + icss-replace-symbols: 1.1.0 + lodash.camelcase: 4.3.0 + postcss: 8.4.24 + postcss-modules-extract-imports: 3.0.0_postcss@8.4.24 + postcss-modules-local-by-default: 4.0.3_postcss@8.4.24 + postcss-modules-scope: 3.0.0_postcss@8.4.24 + postcss-modules-values: 4.0.0_postcss@8.4.24 + string-hash: 1.1.3 + dev: false + /postcss-nested/6.0.1_postcss@8.4.24: resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} engines: {node: '>=12.0'} @@ -4346,6 +4832,129 @@ packages: postcss-selector-parser: 6.0.13 dev: true + /postcss-normalize-charset/5.1.0_postcss@8.4.24: + resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + dev: false + + /postcss-normalize-display-values/5.1.0_postcss@8.4.24: + resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-positions/5.1.1_postcss@8.4.24: + resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-repeat-style/5.1.1_postcss@8.4.24: + resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-string/5.1.0_postcss@8.4.24: + resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-timing-functions/5.1.0_postcss@8.4.24: + resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-unicode/5.1.1_postcss@8.4.24: + resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.7 + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-url/5.1.0_postcss@8.4.24: + resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + normalize-url: 6.1.0 + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-whitespace/5.1.1_postcss@8.4.24: + resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-ordered-values/5.1.3_postcss@8.4.24: + resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + cssnano-utils: 3.1.0_postcss@8.4.24 + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-reduce-initial/5.1.2_postcss@8.4.24: + resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.7 + caniuse-api: 3.0.0 + postcss: 8.4.24 + dev: false + + /postcss-reduce-transforms/5.1.0_postcss@8.4.24: + resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + dev: false + /postcss-selector-parser/6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} @@ -4360,11 +4969,30 @@ packages: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - dev: true + + /postcss-svgo/5.1.0_postcss@8.4.24: + resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + postcss-value-parser: 4.2.0 + svgo: 2.8.0 + dev: false + + /postcss-unique-selectors/5.1.1_postcss@8.4.24: + resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.24 + postcss-selector-parser: 6.0.13 + dev: false /postcss-value-parser/4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - dev: true /postcss/8.4.14: resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} @@ -4381,7 +5009,6 @@ packages: nanoid: 3.3.6 picocolors: 1.0.0 source-map-js: 1.0.2 - dev: true /preferred-pm/3.0.3: resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==} @@ -4471,6 +5098,11 @@ packages: react: 18.2.0 dev: true + /promise.series/0.2.0: + resolution: {integrity: sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==} + engines: {node: '>=0.12'} + dev: false + /property-information/6.2.0: resolution: {integrity: sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==} dev: false @@ -4747,7 +5379,6 @@ packages: /resolve-from/5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - dev: true /resolve/1.22.2: resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} @@ -4756,7 +5387,6 @@ packages: is-core-module: 2.12.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true /restore-cursor/3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} @@ -4778,6 +5408,36 @@ packages: glob: 7.2.3 dev: true + /rollup-plugin-postcss/4.0.2_postcss@8.4.24: + resolution: {integrity: sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==} + engines: {node: '>=10'} + peerDependencies: + postcss: 8.x + dependencies: + chalk: 4.1.2 + concat-with-sourcemaps: 1.1.0 + cssnano: 5.1.15_postcss@8.4.24 + import-cwd: 3.0.0 + p-queue: 6.6.2 + pify: 5.0.0 + postcss: 8.4.24 + postcss-load-config: 3.1.4_postcss@8.4.24 + postcss-modules: 4.3.1_postcss@8.4.24 + promise.series: 0.2.0 + resolve: 1.22.2 + rollup-pluginutils: 2.8.2 + safe-identifier: 0.4.2 + style-inject: 0.3.0 + transitivePeerDependencies: + - ts-node + dev: false + + /rollup-pluginutils/2.8.2: + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} + dependencies: + estree-walker: 0.6.1 + dev: false + /rollup/3.23.1: resolution: {integrity: sha512-ybRdFVHOoljGEFILHLd2g/qateqUdjE6YS41WXq4p3C/WwD3xtWxV4FYWETA1u9TeXQc5K8L8zHE5d/scOvrOQ==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -4786,6 +5446,13 @@ packages: fsevents: 2.3.2 dev: true + /rollup/3.25.1: + resolution: {integrity: sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + /run-async/2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -4820,6 +5487,10 @@ packages: /safe-buffer/5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + /safe-identifier/0.4.2: + resolution: {integrity: sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==} + dev: false + /safe-regex-test/1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} dependencies: @@ -5004,6 +5675,11 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true + /stable/0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + dev: false + /stackback/0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true @@ -5022,6 +5698,10 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + /string-hash/1.1.3: + resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} + dev: false + /string-width/4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -5104,6 +5784,10 @@ packages: acorn: 8.8.2 dev: true + /style-inject/0.3.0: + resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==} + dev: false + /style-to-object/0.4.1: resolution: {integrity: sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==} dependencies: @@ -5126,6 +5810,17 @@ packages: client-only: 0.0.1 react: 18.2.0 + /stylehacks/5.1.1_postcss@8.4.24: + resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.7 + postcss: 8.4.24 + postcss-selector-parser: 6.0.13 + dev: false + /sucrase/3.32.0: resolution: {integrity: sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==} engines: {node: '>=8'} @@ -5152,7 +5847,6 @@ packages: engines: {node: '>=8'} dependencies: has-flag: 4.0.0 - dev: true /supports-color/8.1.1: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} @@ -5164,7 +5858,20 @@ packages: /supports-preserve-symlinks-flag/1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: true + + /svgo/2.8.0: + resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} + engines: {node: '>=10.13.0'} + hasBin: true + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 4.3.0 + css-tree: 1.1.3 + csso: 4.2.0 + picocolors: 1.0.0 + stable: 0.1.8 + dev: false /swap-case/1.1.2: resolution: {integrity: sha512-BAmWG6/bx8syfc6qXPprof3Mn5vQgf5dwdUNJhsNqU9WdPt5P+ES/wQ5bxfijy8zwZgZZHslC3iAsxsuQMCzJQ==} @@ -5405,7 +6112,7 @@ packages: postcss: 8.4.24 postcss-load-config: 3.1.4_postcss@8.4.24 resolve-from: 5.0.0 - rollup: 3.23.1 + rollup: 3.25.1 source-map: 0.8.0-beta.0 sucrase: 3.32.0 tree-kill: 1.2.2 @@ -5563,7 +6270,6 @@ packages: resolution: {integrity: sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==} engines: {node: '>=14.17'} hasBin: true - dev: true /ufo/1.1.2: resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==} @@ -5700,7 +6406,6 @@ packages: /util-deprecate/1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: true /uvu/0.5.6: resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} @@ -6065,7 +6770,6 @@ packages: /yaml/1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - dev: true /yaml/2.3.1: resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} From 1c6adf0dc6aa39afc6a1f629d1e624e2e4efe01e Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Sat, 17 Jun 2023 18:22:39 -0300 Subject: [PATCH 08/20] configure tests for motion-react --- packages/motion-react/package.json | 5 +- .../src/hooks/useMotionController.test.ts | 25 ++ packages/motion-react/vitest.config.ts | 2 + packages/motion/package.json | 2 +- pnpm-lock.yaml | 223 +++++++++++++++++- 5 files changed, 250 insertions(+), 7 deletions(-) create mode 100644 packages/motion-react/src/hooks/useMotionController.test.ts diff --git a/packages/motion-react/package.json b/packages/motion-react/package.json index b4c3755..3ed13f2 100644 --- a/packages/motion-react/package.json +++ b/packages/motion-react/package.json @@ -8,7 +8,7 @@ "scripts": { "lint": "tsc", "dev": "tsup --watch", - "test": "vitest run && vitest typecheck", + "test": "vitest run", "test:watch": "vitest", "build": "tsup", "clean": "rm -rf ./dist ./.turbo ./node_modules" @@ -31,9 +31,11 @@ "react-dom": ">=16.8.0" }, "devDependencies": { + "@testing-library/react": "^14.0.0", "@types/events": "^3.0.0", "@types/react": "^18.2.12", "autoprefixer": "10.4.14", + "happy-dom": "^9.20.3", "postcss": "8.4.24", "tailwindcss": "3.3.2", "tsconfig": "workspace:*", @@ -47,6 +49,7 @@ "@coord/core": "workspace:*", "@coord/motion": "workspace:*", "@rollup/plugin-typescript": "^11.1.1", + "@testing-library/react-hooks": "^8.0.1", "@use-gesture/react": "^10.2.27", "clsx": "^1.2.1", "events": "^3.3.0", diff --git a/packages/motion-react/src/hooks/useMotionController.test.ts b/packages/motion-react/src/hooks/useMotionController.test.ts new file mode 100644 index 0000000..16e6f19 --- /dev/null +++ b/packages/motion-react/src/hooks/useMotionController.test.ts @@ -0,0 +1,25 @@ +import { test, expect, describe, vi } from "vitest"; +import { useMotionController } from "./useMotionController"; +import { renderHook } from "@testing-library/react-hooks"; +import { MotionScene, makeScene } from "@coord/motion"; + +describe("useMotionController", () => { + test("should return correct values", () => { + const scene = makeScene("Test", { x: 0, y: 0 }, function* () { + yield; + yield; + yield; + }); + + const { result } = renderHook(() => useMotionController(scene)); + expect(result.current.playing).toBe(false); + expect(result.current.frame).toBe(0); + expect(result.current.playRange).toEqual([0, 50]); + expect(result.current.repeat).toBe(false); + + result.current.play(); + expect(result.current.playing).toBe(true); + result.current.pause(); + expect(result.current.playing).toBe(false); + }); +}); diff --git a/packages/motion-react/vitest.config.ts b/packages/motion-react/vitest.config.ts index 05a201d..e1901ca 100644 --- a/packages/motion-react/vitest.config.ts +++ b/packages/motion-react/vitest.config.ts @@ -2,6 +2,8 @@ import { defineProject } from "vitest/config"; export default defineProject({ test: { + environment: "happy-dom", // or 'jsdom', 'node' + alias: { "@/": "src/", }, diff --git a/packages/motion/package.json b/packages/motion/package.json index 80b64e7..697d213 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -9,7 +9,7 @@ "lint": "tsc", "build": "tsup", "dev": "tsup --watch", - "test": "vitest run && vitest typecheck", + "test": "vitest run", "test:watch": "vitest", "clean": "rm -rf ./dist ./.turbo ./node_modules" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43de4d2..3721972 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,12 +164,15 @@ importers: '@coord/core': workspace:* '@coord/motion': workspace:* '@rollup/plugin-typescript': ^11.1.1 + '@testing-library/react': ^14.0.0 + '@testing-library/react-hooks': ^8.0.1 '@types/events': ^3.0.0 '@types/react': ^18.2.12 '@use-gesture/react': ^10.2.27 autoprefixer: 10.4.14 clsx: ^1.2.1 events: ^3.3.0 + happy-dom: ^9.20.3 postcss: 8.4.24 react: '>=16.8.0' react-dom: '>=16.8.0' @@ -184,6 +187,7 @@ importers: '@coord/core': link:../core '@coord/motion': link:../motion '@rollup/plugin-typescript': 11.1.1_n7ipjawvwu73gcznr6o2d3ryby + '@testing-library/react-hooks': 8.0.1_ahpghwkyyxs7a7vinwifjyd45i '@use-gesture/react': 10.2.27_react@18.2.0 clsx: 1.2.1 events: 3.3.0 @@ -193,9 +197,11 @@ importers: rollup: 3.25.1 rollup-plugin-postcss: 4.0.2_postcss@8.4.24 devDependencies: + '@testing-library/react': 14.0.0_biqbaboplfbrettd7655fr4n2y '@types/events': 3.0.0 '@types/react': 18.2.12 autoprefixer: 10.4.14_postcss@8.4.24 + happy-dom: 9.20.3 postcss: 8.4.24 tailwindcss: 3.3.2 tsconfig: link:../tsconfig @@ -246,7 +252,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.13.11 - dev: true /@changesets/apply-release-plan/6.1.3: resolution: {integrity: sha512-ECDNeoc3nfeAe1jqJb5aFQX7CqzQhD2klXRez2JDb/aVpGUbX673HgKrnrgJRuQR/9f2TtLoYIzrGB9qwD77mg==} @@ -913,6 +918,57 @@ packages: tailwindcss: 3.3.2 dev: true + /@testing-library/dom/9.3.1: + resolution: {integrity: sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==} + engines: {node: '>=14'} + dependencies: + '@babel/code-frame': 7.21.4 + '@babel/runtime': 7.22.3 + '@types/aria-query': 5.0.1 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + dev: true + + /@testing-library/react-hooks/8.0.1_ahpghwkyyxs7a7vinwifjyd45i: + resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==} + engines: {node: '>=12'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 + react: ^16.9.0 || ^17.0.0 + react-dom: ^16.9.0 || ^17.0.0 + react-test-renderer: ^16.9.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-dom: + optional: true + react-test-renderer: + optional: true + dependencies: + '@babel/runtime': 7.22.3 + '@types/react': 18.2.12 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-error-boundary: 3.1.4_react@18.2.0 + dev: false + + /@testing-library/react/14.0.0_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==} + engines: {node: '>=14'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@babel/runtime': 7.22.3 + '@testing-library/dom': 9.3.1 + '@types/react-dom': 18.2.4 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: true + /@total-typescript/ts-reset/0.4.2: resolution: {integrity: sha512-vqd7ZUDSrXFVT1n8b2kc3LnklncDQFPvR58yUS1kEP23/nHPAO9l1lMjUfnPrXYYk4Hj54rrLKMW5ipwk7k09A==} dev: true @@ -965,6 +1021,10 @@ packages: '@types/estree': 1.0.1 dev: false + /@types/aria-query/5.0.1: + resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} + dev: true + /@types/chai-subset/1.3.3: resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: @@ -1100,7 +1160,7 @@ packages: /@types/react-dom/18.2.4: resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==} dependencies: - '@types/react': 18.2.8 + '@types/react': 18.2.12 dev: true /@types/react/18.2.12: @@ -1109,7 +1169,6 @@ packages: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.3 csstype: 3.1.2 - dev: true /@types/react/18.2.8: resolution: {integrity: sha512-lTyWUNrd8ntVkqycEEplasWy2OxNlShj3zqS0LuB1ENUGis5HodmhM7DtCoUGbxj3VW/WsGA0DUhpG6XrM7gPA==} @@ -1417,6 +1476,12 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: false + /aria-query/5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + dependencies: + deep-equal: 2.2.1 + dev: true + /array-buffer-byte-length/1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} dependencies: @@ -1929,6 +1994,10 @@ packages: engines: {node: '>= 6'} dev: false + /css.escape/1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + dev: true + /cssesc/3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2069,6 +2138,29 @@ packages: type-detect: 4.0.8 dev: true + /deep-equal/2.2.1: + resolution: {integrity: sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.1 + is-arguments: 1.1.1 + is-array-buffer: 3.0.2 + is-date-object: 1.0.5 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + isarray: 2.0.5 + object-is: 1.1.5 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.0 + side-channel: 1.0.4 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.1 + which-typed-array: 1.1.9 + dev: true + /deep-extend/0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -2137,6 +2229,10 @@ packages: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} dev: true + /dom-accessibility-api/0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dev: true + /dom-serializer/1.4.1: resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} dependencies: @@ -2199,7 +2295,6 @@ packages: /entities/4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - dev: false /error-ex/1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -2247,6 +2342,20 @@ packages: which-typed-array: 1.1.9 dev: true + /es-get-iterator/1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.2 + is-set: 2.0.2 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + dev: true + /es-module-lexer/1.3.0: resolution: {integrity: sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==} dev: false @@ -2710,6 +2819,17 @@ packages: uglify-js: 3.17.4 dev: true + /happy-dom/9.20.3: + resolution: {integrity: sha512-eBsgauT435fXFvQDNcmm5QbGtYzxEzOaX35Ia+h6yP/wwa4xSWZh1CfP+mGby8Hk6Xu59mTkpyf72rUXHNxY7A==} + dependencies: + css.escape: 1.5.1 + entities: 4.5.0 + iconv-lite: 0.6.3 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + dev: true + /hard-rejection/2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} @@ -2880,6 +3000,13 @@ packages: safer-buffer: 2.1.2 dev: true + /iconv-lite/0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + /icss-replace-symbols/1.1.0: resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} dev: false @@ -3000,6 +3127,14 @@ packages: is-decimal: 2.0.1 dev: false + /is-arguments/1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + /is-array-buffer/3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: @@ -3098,6 +3233,10 @@ packages: lower-case: 1.1.4 dev: true + /is-map/2.0.2: + resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + dev: true + /is-negative-zero/2.0.2: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} @@ -3149,6 +3288,10 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-set/2.0.2: + resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + dev: true + /is-shared-array-buffer/1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} dependencies: @@ -3203,17 +3346,32 @@ packages: upper-case: 1.1.3 dev: true + /is-weakmap/2.0.1: + resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} + dev: true + /is-weakref/1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: call-bind: 1.0.2 dev: true + /is-weakset/2.0.2: + resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: true + /is-windows/1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} dev: true + /isarray/2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + /isbinaryfile/4.0.10: resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} engines: {node: '>= 8.0.0'} @@ -3450,6 +3608,11 @@ packages: yallist: 4.0.0 dev: true + /lz-string/1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + dev: true + /magic-string/0.30.0: resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} engines: {node: '>=12'} @@ -4296,6 +4459,14 @@ packages: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: true + /object-is/1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + dev: true + /object-keys/1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -5149,6 +5320,16 @@ packages: react: 18.2.0 scheduler: 0.23.0 + /react-error-boundary/3.1.4_react@18.2.0: + resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13.1' + dependencies: + '@babel/runtime': 7.22.3 + react: 18.2.0 + dev: false + /react-icons/4.9.0_react@18.2.0: resolution: {integrity: sha512-ijUnFr//ycebOqujtqtV9PFS7JjhWg0QU6ykURVHuL4cbofvRCf3f6GMn9+fBktEFQOIVZnuAYLZdiyadRQRFg==} peerDependencies: @@ -5270,7 +5451,6 @@ packages: /regenerator-runtime/0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - dev: true /regexp.prototype.flags/1.5.0: resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} @@ -5688,6 +5868,13 @@ packages: resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} dev: true + /stop-iteration-iterator/1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + dependencies: + internal-slot: 1.0.5 + dev: true + /stream-transform/2.1.3: resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==} dependencies: @@ -6608,6 +6795,11 @@ packages: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true + /webidl-conversions/7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: true + /webpack-sources/3.2.3: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} engines: {node: '>=10.13.0'} @@ -6658,6 +6850,18 @@ packages: engines: {node: '>=6'} dev: true + /whatwg-encoding/2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: true + + /whatwg-mimetype/3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: true + /whatwg-url/7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} dependencies: @@ -6676,6 +6880,15 @@ packages: is-symbol: 1.0.4 dev: true + /which-collection/1.0.1: + resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} + dependencies: + is-map: 2.0.2 + is-set: 2.0.2 + is-weakmap: 2.0.1 + is-weakset: 2.0.2 + dev: true + /which-module/2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} dev: true From da1eadb7e1300f72c28449ec3f63b70fb2d4e05e Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Sat, 17 Jun 2023 19:00:30 -0300 Subject: [PATCH 09/20] adds shuffle string mix --- packages/core/src/utils/index.ts | 1 + packages/core/src/utils/random.ts | 38 +++++++++++++++++++ packages/docs/src/app/motion/page.tsx | 36 +++--------------- .../motion/src/controls/string-control.ts | 19 +++++++++- 4 files changed, 62 insertions(+), 32 deletions(-) create mode 100644 packages/core/src/utils/random.ts diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index e3c62e6..935d024 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -7,3 +7,4 @@ export * from "./setDeep"; export * from "./getDeep"; export * from "./clamp"; export * from "./formatDuration"; +export * from "./random"; diff --git a/packages/core/src/utils/random.ts b/packages/core/src/utils/random.ts new file mode 100644 index 0000000..c8410ec --- /dev/null +++ b/packages/core/src/utils/random.ts @@ -0,0 +1,38 @@ +export class Random { + private _seed: number; + private readonly _initialSeed: number; + + constructor(seed: number | string) { + if (typeof seed === "string") { + seed = this.fromString(seed); + } + this._seed = seed; + this._initialSeed = seed; + } + + public next(): number { + // LCG parameters for Numerical Recipes + const m = Math.pow(2, 32); + const a = 1664525; + const c = 1013904223; + + this._seed = (a * this._seed + c) % m; + return this._seed / m; + } + + public reset(): void { + this._seed = this._initialSeed; + } + + public chance = (chance: number = 0.5): boolean => { + return this.next() <= chance; + }; + + private fromString(str: string): number { + let seed = 0; + for (let i = 0; i < str.length; i++) { + seed += str.charCodeAt(i); + } + return seed; + } +} diff --git a/packages/docs/src/app/motion/page.tsx b/packages/docs/src/app/motion/page.tsx index dac5f0a..e2ee870 100644 --- a/packages/docs/src/app/motion/page.tsx +++ b/packages/docs/src/app/motion/page.tsx @@ -58,7 +58,7 @@ const sceneA = makeScene( "Scene A", { color: "#ffffff", - // shape: transform(), + text: "", }, function* () { @@ -70,37 +70,11 @@ const sceneA = makeScene( wait(1), text.to(", my name is Julia").in(0.6, "append"), wait(1.5), - text.to("I'm a software engineer").in(0.6), - wait(3), - text.clear(0.5) - // wait(1) + text.to("I'm a software engineer").in(0.6, "shuffle"), + wait(2), + text.clear(0.5), + wait(1) ); - - // yield* all( - // text.to("Hello, world!").in(1) - - // // color.tweenTo("red", 1), - // // shape.scale(2).positionTo([3, 3]).in(1) - // ); - - // yield* all( - // color.tweenTo("yellow", 1), - // shape - // .positionTo([5, 5]) - - // .scale(3) - - // .in(1) - // ); - - // yield* all( - // color.tweenTo("blue", 1), - // shape - // .positionTo([0, 0]) - - // .scaleTo(1) - // .in(1) - // ); } ); export default function Page() { diff --git a/packages/motion/src/controls/string-control.ts b/packages/motion/src/controls/string-control.ts index bc128d0..0ed8259 100644 --- a/packages/motion/src/controls/string-control.ts +++ b/packages/motion/src/controls/string-control.ts @@ -10,6 +10,7 @@ import { setDeep, EasingOptions, ExtractPathsOfType, + Random, } from "@coord/core"; import { Control, makeControlUtility } from "./control"; import { assertType } from "vitest"; @@ -88,7 +89,7 @@ export const lerpText = (a: string, b: string, t: number, splitter = "") => { for (let i = 0; i < length; i++) { out[i] = b[i] ?? " "; } - return out.join(splitter); //.trim(); + return out.join(splitter); }; export const overwriteString = (a: string, b: string, t: number) => { @@ -116,8 +117,24 @@ export const clearThenAppend = (a: string, b: string, t: number) => { } }; +export const shuffleCharacters = (a: string, b: string, t: number) => { + const random = new Random(a + b); + const length = Math.max(a.length, b.length); + + const out = a.split(""); + + const chance = 2 * t; + for (let i = 0; i < length; i++) { + if (random.chance(chance)) { + out[i] = b[i] ?? " "; + } + } + return out.join("").trim(); +}; + const TextLerpModes = { overwrite: overwriteString, + shuffle: shuffleCharacters, "word-overwrite": overwriteByWord, "char-overwrite": overwriteString, clear: clear, From 1be1af347be755f4f1aa8031fe21234b1f05e32c Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Wed, 21 Jun 2023 16:33:54 -0300 Subject: [PATCH 10/20] more exploration with controls and starts writing some docs --- packages/core/package.json | 5 +- .../core/src/components/accessibility.tsx | 16 + packages/core/src/components/index.ts | 1 + packages/core/src/hooks/index.ts | 2 + packages/core/src/hooks/safe-hooks.ts | 67 + packages/core/src/hooks/use-client-rect.ts | 82 ++ packages/core/src/index.ts | 2 + packages/core/src/types/deep-paths.ts | 123 ++ packages/core/src/utils/easing-functions.ts | 8 + packages/core/src/utils/index.ts | 6 + packages/core/src/utils/is-function.ts | 12 + packages/core/src/utils/is-generator.ts | 5 + packages/core/src/utils/is-undefined.ts | 3 + packages/core/src/utils/make-id.ts | 4 + packages/core/src/utils/noop.ts | 3 + packages/core/src/utils/random.ts | 16 + packages/core/src/utils/setDeep.test.ts | 8 + packages/core/src/utils/setDeep.ts | 15 +- packages/core/src/utils/video-sizes.ts | 27 + packages/core/tsconfig.json | 9 +- packages/docs/.gitignore | 3 + packages/docs/contentlayer.config.ts | 43 + packages/docs/next.config.mjs | 23 +- packages/docs/package.json | 14 +- packages/docs/src/app/[[...slug]]/layout.tsx | 65 + packages/docs/src/app/[[...slug]]/page.tsx | 45 + .../src/app/graph/docs/[[...slug]]/layout.tsx | 60 - .../src/app/graph/docs/[[...slug]]/page.tsx | 17 - packages/docs/src/app/graph/page.tsx | 17 +- packages/docs/src/app/layout.tsx | 8 +- packages/docs/src/app/motion/page.tsx | 124 +- packages/docs/src/components/CodeBlock.tsx | 100 +- packages/docs/src/components/PreMdx.tsx | 17 +- packages/docs/src/components/Sidebar.tsx | 1 - packages/docs/src/components/index.tsx | 1 + .../docs/src/components/mdx-components.tsx | 18 + .../graph/{docs/about.mdx => docs.mdx} | 0 .../graph/docs/components/bounding-box.mdx | 7 +- .../content/graph/docs/components/graph.mdx | 22 +- .../content/graph/docs/components/label.mdx | 8 +- .../content/graph/docs/components/marker.mdx | 6 +- .../content/graph/docs/components/plot.mdx | 15 +- .../graph/docs/components/polyline.mdx | 3 +- .../demo/find-intersection-of-two-lines.mdx | 37 +- .../{getting-started.mdx => installation.mdx} | 0 .../docs/interactions/dragging-elements.mdx | 13 +- .../graph/docs/interfaces/scalar-types.mdx | 27 +- .../docs/src/content/graph/docs/routeMaps.ts | 209 --- packages/docs/src/content/motion/docs.mdx | 92 ++ .../what-is-state-animation.mdx | 660 ++++++++++ packages/docs/src/routeMaps.ts | 263 ++++ packages/docs/tailwind.config.js | 1 + packages/docs/tsconfig.json | 8 +- packages/graph/src/components/Circle.tsx | 49 + packages/graph/src/components/Label.tsx | 2 +- packages/graph/src/components/Ruler.tsx | 17 +- packages/graph/src/components/Text.tsx | 1 - packages/graph/src/components/TextBlock.tsx | 60 + packages/graph/src/components/index.ts | 2 + packages/graph/src/index.ts | 2 +- packages/graph/src/utils/graphContext.ts | 6 +- .../src/components/Player/MotionPlayer.css | 10 +- .../src/components/Player/MotionPlayer.tsx | 93 +- .../Player/MotionPlayerControls.css | 22 +- .../Player/MotionPlayerControls.tsx | 6 +- .../Player/MotionPlayerProgressBar.css | 97 +- .../components/Player/MotionPlayerView.tsx | 69 + packages/motion-react/src/styles.css | 4 +- .../src/context/create-motion-context.ts | 8 + packages/motion/src/context/motion-runner.ts | 34 +- .../motion/src/controls/boolean-control.ts | 37 + packages/motion/src/controls/color-control.ts | 48 +- packages/motion/src/controls/control.ts | 127 +- packages/motion/src/controls/index.ts | 3 + packages/motion/src/controls/list-control.ts | 249 ++++ .../motion/src/controls/number-control.ts | 79 +- packages/motion/src/controls/point-control.ts | 66 +- .../motion/src/controls/string-control.ts | 101 +- .../motion/src/controls/transform-control.ts | 93 +- packages/motion/src/effects/index.ts | 1 + .../motion/src/effects/time-based/blink.ts | 11 + .../src/effects/time-based/camera-shake.ts | 52 + .../motion/src/effects/time-based/index.ts | 2 + packages/motion/src/flow/all.ts | 10 + packages/motion/src/flow/index.ts | 1 + packages/motion/src/flow/sequence.ts | 33 + packages/motion/src/flow/wait.ts | 22 +- packages/motion/src/index.ts | 1 + packages/motion/src/tweening/tween.ts | 18 +- packages/motion/src/utils/asIterable.ts | 12 +- packages/motion/src/utils/countFrames.ts | 14 + packages/motion/src/utils/index.ts | 1 + pnpm-lock.yaml | 1164 +++++++++++++++-- prettier.config.js | 1 + 94 files changed, 3879 insertions(+), 1090 deletions(-) create mode 100644 packages/core/src/components/accessibility.tsx create mode 100644 packages/core/src/components/index.ts create mode 100644 packages/core/src/hooks/index.ts create mode 100644 packages/core/src/hooks/safe-hooks.ts create mode 100644 packages/core/src/hooks/use-client-rect.ts create mode 100644 packages/core/src/utils/is-function.ts create mode 100644 packages/core/src/utils/is-generator.ts create mode 100644 packages/core/src/utils/is-undefined.ts create mode 100644 packages/core/src/utils/make-id.ts create mode 100644 packages/core/src/utils/noop.ts create mode 100644 packages/core/src/utils/video-sizes.ts create mode 100644 packages/docs/contentlayer.config.ts create mode 100644 packages/docs/src/app/[[...slug]]/layout.tsx create mode 100644 packages/docs/src/app/[[...slug]]/page.tsx delete mode 100644 packages/docs/src/app/graph/docs/[[...slug]]/layout.tsx delete mode 100644 packages/docs/src/app/graph/docs/[[...slug]]/page.tsx create mode 100644 packages/docs/src/components/index.tsx create mode 100644 packages/docs/src/components/mdx-components.tsx rename packages/docs/src/content/graph/{docs/about.mdx => docs.mdx} (100%) rename packages/docs/src/content/graph/docs/{getting-started.mdx => installation.mdx} (100%) delete mode 100644 packages/docs/src/content/graph/docs/routeMaps.ts create mode 100644 packages/docs/src/content/motion/docs.mdx create mode 100644 packages/docs/src/content/motion/docs/state-animation/what-is-state-animation.mdx create mode 100644 packages/docs/src/routeMaps.ts create mode 100644 packages/graph/src/components/Circle.tsx create mode 100644 packages/graph/src/components/TextBlock.tsx create mode 100644 packages/motion-react/src/components/Player/MotionPlayerView.tsx create mode 100644 packages/motion/src/controls/boolean-control.ts create mode 100644 packages/motion/src/controls/list-control.ts create mode 100644 packages/motion/src/effects/index.ts create mode 100644 packages/motion/src/effects/time-based/blink.ts create mode 100644 packages/motion/src/effects/time-based/camera-shake.ts create mode 100644 packages/motion/src/effects/time-based/index.ts create mode 100644 packages/motion/src/flow/sequence.ts create mode 100644 packages/motion/src/utils/countFrames.ts diff --git a/packages/core/package.json b/packages/core/package.json index b64248f..e9e6227 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,11 +25,14 @@ "src/index.ts" ] }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, "devDependencies": { "@types/lodash-es": "^4.17.7", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", - "react": "^17.0.2", "tsconfig": "workspace:*", "tsup": "^6.7.0", "typescript": "^5.1.3", diff --git a/packages/core/src/components/accessibility.tsx b/packages/core/src/components/accessibility.tsx new file mode 100644 index 0000000..c5a1070 --- /dev/null +++ b/packages/core/src/components/accessibility.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +export const screenReaderOnly: React.CSSProperties = { + clip: "rect(1px, 1px, 1px, 1px)", + overflow: "hidden", + position: "absolute", + width: 1, + height: 1, + whiteSpace: "nowrap", + + // https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe +}; + +export function ScreenReaderOnly(props: React.HTMLAttributes) { + return
; +} diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts new file mode 100644 index 0000000..d815d38 --- /dev/null +++ b/packages/core/src/components/index.ts @@ -0,0 +1 @@ +export * from "./accessibility"; diff --git a/packages/core/src/hooks/index.ts b/packages/core/src/hooks/index.ts new file mode 100644 index 0000000..07025c0 --- /dev/null +++ b/packages/core/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from "./safe-hooks"; +export * from "./use-client-rect"; diff --git a/packages/core/src/hooks/safe-hooks.ts b/packages/core/src/hooks/safe-hooks.ts new file mode 100644 index 0000000..fe3a3b3 --- /dev/null +++ b/packages/core/src/hooks/safe-hooks.ts @@ -0,0 +1,67 @@ +import React from "react"; +import { noop } from "../utils/noop"; +import { isFunction } from "../utils/is-function"; +import { makeId } from "../utils/make-id"; + +export const isServerComponent = typeof React?.useState === "undefined"; + +export const useSafeState: typeof React.useState = ( + initialState?: S | (() => S) +) => { + if (isServerComponent) { + return [isFunction(initialState) ? initialState() : initialState, noop]; + } + + const [state, setState] = React.useState(initialState); + + return [state, setState]; +}; + +export const useSafeEffect: typeof React.useEffect = (effect, deps) => { + if (isServerComponent) { + return; + } + + React.useEffect(effect, deps); +}; + +export const useSafeLayoutEffect: typeof React.useEffect = (effect, deps) => { + if (isServerComponent) { + return; + } + + React.useEffect(effect, deps); +}; + +export const useSafeRef: typeof React.useRef = (initialValue?: T | null) => { + if (isServerComponent) { + return initialValue; + } + + return React.useRef(initialValue); +}; + +export const useSafeMemo: typeof React.useMemo = (factory, deps) => { + if (isServerComponent) { + return factory(); + } + + return React.useMemo(factory, deps); +}; + +export const useSafeCallback: typeof React.useCallback = (callback, deps) => { + if (isServerComponent) { + return callback; + } + + return React.useCallback(callback, deps); +}; + +export const useSafeId = (prefix: string) => { + if (isServerComponent) { + return `${prefix}${makeId()}`; + } + return `${prefix}${React.useId().replace(/\:/g, "")}`; +}; + +// TODO: useSafeContext diff --git a/packages/core/src/hooks/use-client-rect.ts b/packages/core/src/hooks/use-client-rect.ts new file mode 100644 index 0000000..62eb458 --- /dev/null +++ b/packages/core/src/hooks/use-client-rect.ts @@ -0,0 +1,82 @@ +import { + useSafeCallback, + useSafeLayoutEffect, + useSafeRef, + useSafeState, +} from "./safe-hooks"; + +const DOMRectClass = + typeof DOMRect === "undefined" + ? class DOMRect { + constructor( + public x: number, + public y: number, + public width: number, + public height: number + ) {} + + get top() { + return this.y; + } + + get left() { + return this.x; + } + + get right() { + return this.x + this.width; + } + + get bottom() { + return this.y + this.height; + } + + toJSON() { + return { + x: this.x, + y: this.y, + width: this.width, + height: this.height, + top: this.top, + left: this.left, + right: this.right, + bottom: this.bottom, + }; + } + } + : DOMRect; +export function useClientRect( + fn: (rect: DOMRect) => void +) { + const ref = useSafeRef(null); + // const [rect, setRect] = useSafeState(() => { + // const { x = 0, y = 0, width = 0, height = 0 } = defaultValue; + // return new DOMRectClass(x, y, width, height); + // }); + // const updateRect = useSafeCallback(() => { + // const newRect = ref.current?.getBoundingClientRect(); + // if (newRect) { + // setRect(newRect); + // } + // }, [ref]); + + useSafeLayoutEffect(() => { + if (!ref.current) { + return; + } + const observer = new ResizeObserver(() => { + const newRect = ref.current?.getBoundingClientRect(); + if (newRect) { + fn(newRect); + } + }); + + observer.observe(ref.current); + + return () => { + observer.disconnect(); + }; + }, [ref]); + + return ref; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3ee64f0..1ab4d52 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,5 @@ export * from "./math"; export * from "./utils"; export * from "./types"; +export * from "./hooks"; +export * from "./components"; diff --git a/packages/core/src/types/deep-paths.ts b/packages/core/src/types/deep-paths.ts index 0bd54d2..731fd28 100644 --- a/packages/core/src/types/deep-paths.ts +++ b/packages/core/src/types/deep-paths.ts @@ -120,3 +120,126 @@ export type ExtractPathsOfType = { ? K : never; }[InferPath]; + +export type KeyOfTree = keyof InferPathValueTree & string; +export type ValueOfTree = InferPathValueTree[KeyOfTree]; +/** + * v2 + */ + +type Join = K extends string | number + ? P extends string | number + ? `${K}${"" extends P ? "" : "."}${P}` + : never + : never; + +type Prev = [ + never, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + ...0[] +]; + +export type Paths = [D] extends [never] + ? never + : T extends object + ? { + [K in keyof T]-?: K extends string | number + ? `${K}` | Join> + : never; + }[keyof T] + : ""; + +type Leaves = [D] extends [never] + ? never + : T extends object + ? { [K in keyof T]-?: Join> }[keyof T] + : ""; + +export type FilteredPaths = [D] extends [never] + ? never + : T extends object + ? { + [K in keyof T]-?: K extends string | number + ? T[K] extends object + ? Join> + : T[K] extends TType + ? `${K}` //| Join> + : never + : never; + }[keyof T] + : ""; + +type TuplesJoin = P extends string | number + ? K extends string | number + ? `${K}${"" extends K ? "" : "."}${P}` + : never + : never; +export type KVPathsImpl = [D] extends [ + never +] + ? never + : T extends object + ? { + [K in keyof T]-?: K extends string | number + ? + | [TuplesJoin, T[K]] + | KVPathsImpl, Prev[D]> // TuplesJoin> + : never; + }[keyof T] + : never; + +export type KVPaths = Extract< + KVPathsImpl, + [string, any] +>; + +type NestedObjectType = { + a: string; + b: number; + nest: { + c: number; + }[]; + otherNest: { + c: string; + }; +}; + +type test = KPaths; +// ^? +type test2 = VPaths; +// ^? +export type KPaths = Extract< + KVPaths, + [any, TTypeFilter] +> extends [infer K, any] + ? K & string + : never; + +export type VPaths = Extract< + KVPaths, + [K, TTypeFilter] +> extends [any, infer V] + ? V extends TTypeFilter + ? V + : never + : never; diff --git a/packages/core/src/utils/easing-functions.ts b/packages/core/src/utils/easing-functions.ts index 691c876..4940d5c 100644 --- a/packages/core/src/utils/easing-functions.ts +++ b/packages/core/src/utils/easing-functions.ts @@ -148,3 +148,11 @@ export const easingsFunctions = { export type EasingKeys = keyof typeof easingsFunctions; export type EasingFunction = (progress: number) => number; export type EasingOptions = EasingKeys | EasingFunction; + +export const applyEasing = (easing: EasingOptions, t: number) => { + if (typeof easing === "function") { + return easing(t); + } else { + return easingsFunctions[easing](t); + } +}; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 935d024..29de4a4 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -8,3 +8,9 @@ export * from "./getDeep"; export * from "./clamp"; export * from "./formatDuration"; export * from "./random"; +export * from "./video-sizes"; +export * from "./is-function"; +export * from "./make-id"; +export * from "./noop"; +export * from "./is-undefined"; +export * from "./is-generator"; diff --git a/packages/core/src/utils/is-function.ts b/packages/core/src/utils/is-function.ts new file mode 100644 index 0000000..e130b36 --- /dev/null +++ b/packages/core/src/utils/is-function.ts @@ -0,0 +1,12 @@ +export function isFunction(value: unknown): value is Function { + return typeof value === "function"; +} + +export const isGeneratorFunction = ( + value: T +): value is T & (() => Generator) => { + return ( + typeof value === "function" && + value.constructor.name === "GeneratorFunction" + ); +}; diff --git a/packages/core/src/utils/is-generator.ts b/packages/core/src/utils/is-generator.ts new file mode 100644 index 0000000..04f8575 --- /dev/null +++ b/packages/core/src/utils/is-generator.ts @@ -0,0 +1,5 @@ +export function isGenerator(value: unknown): value is Generator { + return ( + typeof value === "object" && value !== null && Symbol.iterator in value + ); +} diff --git a/packages/core/src/utils/is-undefined.ts b/packages/core/src/utils/is-undefined.ts new file mode 100644 index 0000000..705c84e --- /dev/null +++ b/packages/core/src/utils/is-undefined.ts @@ -0,0 +1,3 @@ +export function isUndefined(v: unknown): v is undefined { + return typeof v === "undefined"; +} diff --git a/packages/core/src/utils/make-id.ts b/packages/core/src/utils/make-id.ts new file mode 100644 index 0000000..7c8d159 --- /dev/null +++ b/packages/core/src/utils/make-id.ts @@ -0,0 +1,4 @@ +let _id = 0; +export function makeId(prefix = "") { + return `${prefix}${_id++}`; +} diff --git a/packages/core/src/utils/noop.ts b/packages/core/src/utils/noop.ts new file mode 100644 index 0000000..e9a7552 --- /dev/null +++ b/packages/core/src/utils/noop.ts @@ -0,0 +1,3 @@ +export const noop = () => { + // do nothing +}; diff --git a/packages/core/src/utils/random.ts b/packages/core/src/utils/random.ts index c8410ec..325637a 100644 --- a/packages/core/src/utils/random.ts +++ b/packages/core/src/utils/random.ts @@ -20,6 +20,22 @@ export class Random { return this._seed / m; } + public range(min: number, max: number): number { + return min + this.next() * (max - min); + } + + public intRange(min: number, max: number): number { + return Math.floor(this.range(min, max)); + } + + public list(n: number, min: number = 0, max: number = 1): number[] { + const result: number[] = []; + for (let i = 0; i < n; i++) { + result.push(this.range(min, max)); + } + return result; + } + public reset(): void { this._seed = this._initialSeed; } diff --git a/packages/core/src/utils/setDeep.test.ts b/packages/core/src/utils/setDeep.test.ts index 1b585d8..c9f0e0e 100644 --- a/packages/core/src/utils/setDeep.test.ts +++ b/packages/core/src/utils/setDeep.test.ts @@ -6,6 +6,10 @@ describe("setDeep", () => { foo: { bar: { baz: 1, + other: 2, + }, + otherObj: { + baz: 1, }, }, }; @@ -15,6 +19,10 @@ describe("setDeep", () => { foo: { bar: { baz: 2, + other: 2, + }, + otherObj: { + baz: 1, }, }, }); diff --git a/packages/core/src/utils/setDeep.ts b/packages/core/src/utils/setDeep.ts index a8c2fe7..674a8b2 100644 --- a/packages/core/src/utils/setDeep.ts +++ b/packages/core/src/utils/setDeep.ts @@ -47,18 +47,8 @@ export const stringToPath = (input: string): string[] => const copy = | object>(obj: T): T => { return Array.isArray(obj) ? ([...obj] as T) : ({ ...obj } as T); }; -// export const splitPathAtHead = (path: T) => { -// const [head, ...rest] = stringToPath(path); -// return [head, rest.join(".")] as SplitAtHead; -// }; export const setDeep = < - // TFieldValues extends { - // [key: string]: unknown; - // }, - // TFieldPath extends InferPath, - // TValue extends InferPathValue - T extends { [key: string]: unknown; }, @@ -70,9 +60,6 @@ export const setDeep = < value: TTree[TKey] ) => { let index = -1; - // if (path === "") { - // return value; - // } const tempPath = isKey(path) ? [path] : stringToPath(path); const length = tempPath.length; @@ -86,7 +73,7 @@ export const setDeep = < let newValue = value; if (index !== lastIndex) { - const objValue = newObj[key]; + const objValue = tempObj[key]; if (isObject(objValue) || Array.isArray(objValue)) { newValue = copy(objValue) as TTree[TKey]; diff --git a/packages/core/src/utils/video-sizes.ts b/packages/core/src/utils/video-sizes.ts new file mode 100644 index 0000000..c44277a --- /dev/null +++ b/packages/core/src/utils/video-sizes.ts @@ -0,0 +1,27 @@ +import { Vec2, Vec2ish } from "../math"; + +const fullHD = Vec2.of([1920, 1080]); +const HD = Vec2.of([1280, 720]); +const SD = Vec2.of([640, 480]); +const reels = Vec2.of([1080, 1920]); +const square = Vec2.of([1080, 1080]); + +export const videoSizes = { + fullHD: fullHD, + video: fullHD, + HD, + SD, + + reels, + square, +}; + +export type VideoSize = keyof typeof videoSizes; +export type VideoSizeish = VideoSize | Vec2ish; + +export const getVideoSize = (size: VideoSizeish) => { + if (typeof size === "string") { + return videoSizes[size]; + } + return Vec2.of(size); +}; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index b535f9b..0d5181a 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,8 +1,13 @@ { - "extends": "tsconfig/base.json", + "extends": "tsconfig/react-library.json", "compilerOptions": { + "jsx": "react", "noUncheckedIndexedAccess": true, - "noEmit": true + "noEmit": true, + "isolatedModules": false, + "paths": { + "@/*": ["./src/*"] + } }, "include": ["."], "exclude": ["dist", "build", "node_modules"] diff --git a/packages/docs/.gitignore b/packages/docs/.gitignore index 8f322f0..f448901 100644 --- a/packages/docs/.gitignore +++ b/packages/docs/.gitignore @@ -33,3 +33,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# contentlayer +.contentlayer diff --git a/packages/docs/contentlayer.config.ts b/packages/docs/contentlayer.config.ts new file mode 100644 index 0000000..c0c28d2 --- /dev/null +++ b/packages/docs/contentlayer.config.ts @@ -0,0 +1,43 @@ +// contentlayer.config.ts +import { + ComputedFields, + defineDocumentType, + makeSource, +} from "contentlayer/source-files"; + +import rehypeMdxCodeProps from "rehype-mdx-code-props"; +import remarkGfm from "remark-gfm"; +import rehypeKatex from "rehype-katex"; +import remarkMath from "remark-math"; + +const computedFields: ComputedFields = { + slug: { + type: "string", + resolve: (doc) => `/${doc._raw.flattenedPath}`, + }, + slugAsParams: { + type: "string", + resolve: (doc) => doc._raw.flattenedPath.split("/").slice(1).join("/"), + }, +}; + +export const DocPage = defineDocumentType(() => ({ + name: "DocPage", + filePathPattern: `**/*.mdx`, + contentType: "mdx", + + fields: { + title: { type: "string", required: false }, + }, + computedFields, +})); + +export default makeSource({ + contentDirPath: "./src/content", + + mdx: { + remarkPlugins: [remarkMath, remarkGfm], + rehypePlugins: [rehypeMdxCodeProps, rehypeKatex], + }, + documentTypes: [DocPage], +}); diff --git a/packages/docs/next.config.mjs b/packages/docs/next.config.mjs index 442ac5e..b363cbd 100644 --- a/packages/docs/next.config.mjs +++ b/packages/docs/next.config.mjs @@ -1,19 +1,6 @@ -import rehypeMdxCodeProps from "rehype-mdx-code-props"; -import remarkGfm from "remark-gfm"; -import rehypeKatex from "rehype-katex"; -import remarkMath from "remark-math"; +import { withContentlayer } from "next-contentlayer"; + /** @type {import('next').NextConfig} */ -const nextConfig = { - experimental: { - appDir: true, - }, -}; -import mdx from "@next/mdx"; -const withMDX = mdx({ - options: { - remarkPlugins: [remarkGfm, remarkMath], - rehypePlugins: [rehypeMdxCodeProps, rehypeKatex], - useDynamicImport: true, - }, -}); -export default withMDX(nextConfig); +const nextConfig = { reactStrictMode: true, swcMinify: true }; + +export default withContentlayer(nextConfig); diff --git a/packages/docs/package.json b/packages/docs/package.json index dce52d5..d09ebcd 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -10,13 +10,20 @@ "clean": "rm -rf ./.next ./dist ./.turbo ./node_modules" }, "dependencies": { + "@coord/graph": "workspace:*", + "@coord/motion": "workspace:*", + "@coord/core": "workspace:*", + "@coord/motion-react": "workspace:*", "@mdx-js/loader": "^2.3.0", "@mdx-js/mdx": "^2.3.0", "@mdx-js/react": "^2.3.0", "@next/mdx": "^13.4.4", "@types/mdx": "^2.0.5", + "contentlayer": "^0.3.3", + "date-fns": "^2.30.0", "katex": "^0.16.7", - "next": "^13.4.4", + "next": "^13.4.6", + "next-contentlayer": "^0.3.3", "next-mdx-remote": "^4.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -24,10 +31,7 @@ "rehype-katex": "^6.0.3", "rehype-mdx-code-props": "^1.0.0", "remark-gfm": "^3.0.1", - "remark-math": "^5.1.1", - "@coord/graph": "workspace:*", - "@coord/motion": "workspace:*", - "@coord/motion-react": "workspace:*" + "remark-math": "^5.1.1" }, "devDependencies": { "@tailwindcss/typography": "^0.5.9", diff --git a/packages/docs/src/app/[[...slug]]/layout.tsx b/packages/docs/src/app/[[...slug]]/layout.tsx new file mode 100644 index 0000000..e6e4a0a --- /dev/null +++ b/packages/docs/src/app/[[...slug]]/layout.tsx @@ -0,0 +1,65 @@ +import { Sidebar } from "@/components/Sidebar"; +import { getRoute, getRoutes } from "@/routeMaps"; + +import Link from "next/link"; +import "katex/dist/katex.min.css"; + +import { GoChevronRight, GoChevronLeft } from "react-icons/go"; +import { Header } from "@/components/Header"; +import { Footer } from "@/components/Footer"; + +export default function Layout(props: { + params: { slug?: string[] }; + children: React.ReactNode; +}) { + const routeDetails = getRoute([...(props.params.slug ?? [])].join("/")); + + const nextRoute = routeDetails.next ? getRoute(routeDetails.next) : null; + const prevRoute = routeDetails.prev ? getRoute(routeDetails.prev) : null; + + return ( + <> +
+
+
+ +
+
+
+
+ +
+ {props.children} +
+ {prevRoute && ( + + {prevRoute.title} + + )} + {nextRoute && ( + + {nextRoute.title} + + )} +
+
+
+
+
{" "} +
+
+ + ); +} diff --git a/packages/docs/src/app/[[...slug]]/page.tsx b/packages/docs/src/app/[[...slug]]/page.tsx new file mode 100644 index 0000000..26e8dfa --- /dev/null +++ b/packages/docs/src/app/[[...slug]]/page.tsx @@ -0,0 +1,45 @@ +import { allDocPages } from "contentlayer/generated"; +import { notFound } from "next/navigation"; + +import { getMDXComponent } from "next-contentlayer/hooks"; +import { PreMDX } from "@/components"; + +interface DocPageProps { + params: { + slug: string[]; + }; +} +export async function generateStaticParams(): Promise< + DocPageProps["params"][] +> { + return allDocPages.map((post) => ({ + slug: post.slugAsParams.split("/"), + })); +} +function getPostFromParams(params: DocPageProps["params"]) { + const slug = "/" + (params?.slug ?? []).join("/"); + + const post = allDocPages.find((post) => { + return post.slug === slug; + }); + + if (!post) { + null; + } + + return post; +} +const components = { + pre: (props: any) => , +}; + +export default async function Page({ params }: DocPageProps) { + const page = getPostFromParams(params); + if (!page) { + notFound(); + } + + const Mdx = getMDXComponent(page.body.code); + + return ; +} diff --git a/packages/docs/src/app/graph/docs/[[...slug]]/layout.tsx b/packages/docs/src/app/graph/docs/[[...slug]]/layout.tsx deleted file mode 100644 index e77e311..0000000 --- a/packages/docs/src/app/graph/docs/[[...slug]]/layout.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Sidebar } from "@/components/Sidebar"; -import { getRoute, getRoutes } from "@/content/graph/docs/routeMaps"; - -import Link from "next/link"; -import "katex/dist/katex.min.css"; - -import { GoChevronRight, GoChevronLeft } from "react-icons/go"; - -export default function Layout(props: { - params: { slug?: string[] }; - children: React.ReactNode; -}) { - const routeDetails = getRoute( - ["graph", "docs", ...(props.params.slug ?? [])].join("/") - ); - - const nextRoute = routeDetails.next ? getRoute(routeDetails.next) : null; - const prevRoute = routeDetails.prev ? getRoute(routeDetails.prev) : null; - - return ( -
- -
-
-
-
- {/* breadcrumbs */} - -
- {props.children} -
- {prevRoute && ( - - {prevRoute.title} - - )} - {nextRoute && ( - - {nextRoute.title} - - )} -
-
-
-
-
- ); -} diff --git a/packages/docs/src/app/graph/docs/[[...slug]]/page.tsx b/packages/docs/src/app/graph/docs/[[...slug]]/page.tsx deleted file mode 100644 index 9b4a961..0000000 --- a/packages/docs/src/app/graph/docs/[[...slug]]/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { getRouteComponent, routePaths } from "@/content/graph/docs/routeMaps"; - -export async function generateStaticParams() { - return routePaths.map((path) => ({ - slug: path.split("/").slice(2), - })); -} - -export default async function Page(props: { params: { slug?: string[] } }) { - const C = ( - await getRouteComponent( - ["graph", "docs", ...(props.params.slug ?? [])].join("/") - ) - ).default; - - return ; -} diff --git a/packages/docs/src/app/graph/page.tsx b/packages/docs/src/app/graph/page.tsx index 6326247..ec47d0e 100644 --- a/packages/docs/src/app/graph/page.tsx +++ b/packages/docs/src/app/graph/page.tsx @@ -1,5 +1,7 @@ "use client"; import { LiveCodeBlock } from "@/components/CodeBlock"; +import { Footer } from "@/components/Footer"; +import { Header } from "@/components/Header"; import { Graph, @@ -10,15 +12,13 @@ import { Plot, Text, darkTheme, - point, useNavigationState, useStopwatch, - lerp, } from "@coord/graph"; +import { lerp, point } from "@coord/core"; import Link from "next/link"; import { useLayoutEffect, useState } from "react"; - const easeInOut = (t: number) => { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }; @@ -177,7 +177,8 @@ const Hero = () => { export default function Page() { return ( -
+ <> +

@@ -246,10 +247,7 @@ export default function Page() { ), right: ( - {` + {` import { Graph, Grid, @@ -338,6 +336,7 @@ export default function Page() { ))}

-
+
+ ); } diff --git a/packages/docs/src/app/layout.tsx b/packages/docs/src/app/layout.tsx index 961259d..7a8dc5b 100644 --- a/packages/docs/src/app/layout.tsx +++ b/packages/docs/src/app/layout.tsx @@ -18,12 +18,8 @@ export default function RootLayout({ return ( -
-
-
- {children} -
-
+
+ {children}
diff --git a/packages/docs/src/app/motion/page.tsx b/packages/docs/src/app/motion/page.tsx index e2ee870..b4749c3 100644 --- a/packages/docs/src/app/motion/page.tsx +++ b/packages/docs/src/app/motion/page.tsx @@ -1,123 +1,51 @@ "use client"; import { - Graph, - Grid, - Marker, - Plot, - Text, - Transform, - Vec2, - point, - transform, -} from "@coord/graph"; -import { - controlColor, - all, - controlTransform, makeScene, - makeMovie, controlString, - repeat, chain, wait, - controlNumber, + controlList, + tween, } from "@coord/motion"; -import { - useMotionController, - MotionPlayerControls, - MotionPlayer, -} from "@coord/motion-react"; - -const blink = (t: number, blinkDuration = 0.5) => { - const blink = Math.floor(t / (blinkDuration * 1000)) % 2; - return blink === 1; -}; -const cameraShake = (intensity: number = 1, tFactor = 1) => { - const waveLengths = [2.3, 1.5, 1.2, 1.1, 1.05, 1.01, 1.001]; - - return (t: number): Vec2 => { - let x = 0; - let y = 0; - t *= tFactor; - for (let i = 0; i < waveLengths.length; i++) { - const waveLength = waveLengths[i]; - const sinWave = Math.sin(t * waveLength * Math.PI * 2); - const cosWave = Math.cos(t * waveLength * Math.PI * 2); - x += sinWave * intensity; - y += cosWave * intensity; - } - - x /= waveLengths.length; - y /= waveLengths.length; - return point(x, y); - }; -}; +import { useMotionController, MotionPlayer } from "@coord/motion-react"; -const shake = cameraShake(1, 0.001); -const sceneA = makeScene( - "Scene A", +const scene = makeScene( + "Hello World", { - color: "#ffffff", - text: "", + list: [] as number[], }, function* () { const text = yield* controlString("text"); - + const list = yield* controlList("list"); + yield* list.tweenAppend(10, 1, (i) => i); + // return; yield* chain( + wait(0.2), + text.to("Hello World!").in(1), wait(1), - text.to("Hi").in(0.3), + text.append(' I\'m "@coord/motion"').in(1), wait(1), - text.to(", my name is Julia").in(0.6, "append"), - wait(1.5), - text.to("I'm a software engineer").in(0.6, "shuffle"), - wait(2), - text.clear(0.5), - wait(1) + text.clear(0.5) ); } ); -export default function Page() { - const controls = useMotionController( - makeMovie("Finding the intersection of two lines", { - sceneA: sceneA, - sceneB: sceneA, - }), - { - fps: 60, - } - ); - const { sceneA: state } = controls.state; +export default function MyAnimation() { + const controls = useMotionController(scene); - return ( -
-
-
{JSON.stringify(controls.meta, null, 2)}
+ const { state } = controls; - -
-
-              {state.text}
-              {blink(controls.currentTime) ? "|" : ""}
-            
-
- {/* - - - {state.text} - {blink(controls.currentTime) ? "|" : ""} - - - */} -
-
+ return ( +
+ +
+

{state.text}

+

+ {state.list.map((n) => n.toFixed(1)).join(", ")} +

+
+
); } diff --git a/packages/docs/src/components/CodeBlock.tsx b/packages/docs/src/components/CodeBlock.tsx index 1792f1a..531bbcd 100644 --- a/packages/docs/src/components/CodeBlock.tsx +++ b/packages/docs/src/components/CodeBlock.tsx @@ -6,9 +6,13 @@ import { useLiveRunner, CodeEditor, } from "react-live-runner"; +import { HiChevronDoubleDown, HiChevronDoubleUp } from "react-icons/hi"; import * as graph from "@coord/graph"; -import React from "react"; +import * as motion from "@coord/motion"; +import * as motionReact from "@coord/motion-react"; +import * as core from "@coord/core"; +import React, { useEffect } from "react"; import dedent from "ts-dedent"; import clsx from "clsx"; @@ -16,26 +20,32 @@ const scope = { import: { react: React, "@coord/graph": graph, + "@coord/motion": motion, + "@coord/motion-react": motionReact, + "@coord/core": core, }, }; export function LiveCodeBlock({ children, collapsed: collapsedInitialValue = false, - partiallyVisibleWhenCollapsed, + partialCode, }: { children: string; collapsed?: boolean; - partiallyVisibleWhenCollapsed?: boolean; + partialCode?: boolean; }) { const { element, error, code, onChange } = useLiveRunner({ - initialCode: dedent(children), + // initialCode: "", scope, }); + useEffect(() => { + onChange(dedent(children)); + }, []); const [collapsed, setCollapsed] = React.useState(collapsedInitialValue); return ( -
+
{element} @@ -51,50 +61,46 @@ export function LiveCodeBlock({ {error} )} - {!collapsed && ( -
- -
- )} - {collapsed && partiallyVisibleWhenCollapsed && ( -
- +
+ {(!collapsed || partialCode) && (
-
- )} -
)} - + + +
); } diff --git a/packages/docs/src/components/PreMdx.tsx b/packages/docs/src/components/PreMdx.tsx index 9f6f5d1..f8ec9ec 100644 --- a/packages/docs/src/components/PreMdx.tsx +++ b/packages/docs/src/components/PreMdx.tsx @@ -1,17 +1,16 @@ "use client"; import { CodeBlock, LiveCodeBlock } from "./CodeBlock"; -export function PreMdx({ - children, - live, - collapsed, -}: { +export type PreMDXProps = { children: React.ReactNode; live?: boolean; previewOnly?: boolean; collapsed?: boolean; + partial?: boolean; className?: string; -}) { +}; + +export function PreMDX({ children, live, collapsed, partial }: PreMDXProps) { if ( typeof children === "object" && children !== null && @@ -22,7 +21,11 @@ export function PreMdx({ const language = children.props.className?.replace("language-", ""); if (live) - return {code}; + return ( + + {code} + + ); return {code}; } return children; diff --git a/packages/docs/src/components/Sidebar.tsx b/packages/docs/src/components/Sidebar.tsx index cbff581..beab666 100644 --- a/packages/docs/src/components/Sidebar.tsx +++ b/packages/docs/src/components/Sidebar.tsx @@ -12,7 +12,6 @@ export type RouteSection = { export type RouteItem = { title: string; route: string; - file: string; }; export function Sidebar({ items }: { items: Readonly }) { const [open, setOpen] = useState(false); diff --git a/packages/docs/src/components/index.tsx b/packages/docs/src/components/index.tsx new file mode 100644 index 0000000..06c7ac9 --- /dev/null +++ b/packages/docs/src/components/index.tsx @@ -0,0 +1 @@ +export * from "./PreMdx"; diff --git a/packages/docs/src/components/mdx-components.tsx b/packages/docs/src/components/mdx-components.tsx new file mode 100644 index 0000000..edec57e --- /dev/null +++ b/packages/docs/src/components/mdx-components.tsx @@ -0,0 +1,18 @@ +import Image from "next/image"; +import { useMDXComponent } from "next-contentlayer/hooks"; +import { PreMDX } from "@/components"; + +const components = { + Image, + pre: PreMDX, +}; + +interface MdxProps { + code: string; +} + +export function Mdx({ code }: MdxProps) { + const Component = useMDXComponent(code); + + return ; +} diff --git a/packages/docs/src/content/graph/docs/about.mdx b/packages/docs/src/content/graph/docs.mdx similarity index 100% rename from packages/docs/src/content/graph/docs/about.mdx rename to packages/docs/src/content/graph/docs.mdx diff --git a/packages/docs/src/content/graph/docs/components/bounding-box.mdx b/packages/docs/src/content/graph/docs/components/bounding-box.mdx index 71c0172..7387558 100644 --- a/packages/docs/src/content/graph/docs/components/bounding-box.mdx +++ b/packages/docs/src/content/graph/docs/components/bounding-box.mdx @@ -3,7 +3,8 @@ `BoundingBox` is similar to `Rect` but it takes a `BBoxish` object to draw a rect. It's specially usefull when you want to draw a rect around a coordinate area. ```tsx live -import { Graph, Grid, Plot, Rect, Text, BoundingBox } from "@coord/graph"; +import { point } from "@coord/core"; +import { Graph, Grid, Text, BoundingBox } from "@coord/graph"; export default function MyGraph() { const top = 10; @@ -12,8 +13,8 @@ export default function MyGraph() { const left = -10; const coordBox = { - horizontal: [left, right], - vertical: [top, bottom], + horizontal: point(left, right), + vertical: point(top, bottom), }; return ( diff --git a/packages/docs/src/content/graph/docs/components/graph.mdx b/packages/docs/src/content/graph/docs/components/graph.mdx index d064142..8ffe5de 100644 --- a/packages/docs/src/content/graph/docs/components/graph.mdx +++ b/packages/docs/src/content/graph/docs/components/graph.mdx @@ -13,7 +13,8 @@ The `padding` property determines the amount of space in pixels that is added ar The `width` and `height` properties specify the dimensions of the rendering viewport, they accept the same values as CSS `width` and `height` properties. ```tsx live -import { Graph, Grid, Plot, Rect, Text, BoundingBox } from "@coord/graph"; +import { point } from "@coord/core"; +import { Graph, Grid, Text, BoundingBox } from "@coord/graph"; export default function MyGraph() { const top = 10; @@ -22,8 +23,8 @@ export default function MyGraph() { const left = -10; const coordBox = { - horizontal: [left, right], - vertical: [top, bottom], + horizontal: point(left, right), + vertical: point(top, bottom), }; return ( @@ -49,7 +50,8 @@ The `coordStep` property defines the step size of the grid in the horizontal and Sometimes you want the horizontal axis to increment at a different rate than the vertical axis. This is where the `coordStep` property comes in. ```tsx -import { Graph, Grid, Plot, Rect, Text, BoundingBox } from "@coord/graph"; +import { point } from "@coord/core/dist"; +import { Graph, Grid, Text, BoundingBox } from "@coord/graph"; export default function MyGraph() { const top = 10; @@ -58,8 +60,8 @@ export default function MyGraph() { const left = -10; const coordBox = { - horizontal: [left, right], - vertical: [top, bottom], + horizontal: point(left, right), + vertical: point(top, bottom), }; return ( @@ -85,7 +87,7 @@ The theme prop is used to style and customize the appearance of your Graph. It c ### Predefined Themes ```tsx live collapsed -import { Graph, Grid, Text, BoundingBox, Line } from "@coord/graph"; +import { Graph, Grid, Line } from "@coord/graph"; export default function MyGraph() { return ( @@ -105,7 +107,7 @@ export default function MyGraph() { ``` ```tsx live collapsed -import { Graph, Grid, Text, BoundingBox, Line } from "@coord/graph"; +import { Graph, Grid, Line } from "@coord/graph"; export default function MyGraph() { return ( @@ -125,7 +127,7 @@ export default function MyGraph() { ``` ```tsx live collapsed -import { Graph, Grid, Text, BoundingBox, Line } from "@coord/graph"; +import { Graph, Grid, Line } from "@coord/graph"; export default function MyGraph() { return ( @@ -145,7 +147,7 @@ export default function MyGraph() { ``` ```tsx live collapsed -import { Graph, Grid, Text, BoundingBox, Line } from "@coord/graph"; +import { Graph, Grid, Line } from "@coord/graph"; export default function MyGraph() { return ( diff --git a/packages/docs/src/content/graph/docs/components/label.mdx b/packages/docs/src/content/graph/docs/components/label.mdx index 484974b..87e82a6 100644 --- a/packages/docs/src/content/graph/docs/components/label.mdx +++ b/packages/docs/src/content/graph/docs/components/label.mdx @@ -83,16 +83,15 @@ export default function MyGraph() { ## Cardinal directions ```jsx live -import { useState } from "react"; import { Graph, Grid, Label, Marker, useCoordState } from "@coord/graph"; export default function MyGraph() { const [position, setPosition] = useCoordState([0, 0]); - + const directions = ["n", "ne", "e", "se", "s", "sw", "w", "nw"] as const; return ( - {["n", "ne", "e", "se", "s", "sw", "w", "nw"].map((cardinalDir, i) => ( + {directions.map((cardinalDir, i) => ( ); } + ``` This, my dears, is the slope of _Line A - B_, denoted as $m_{AB}$. Why we represent slopes with the letter $m$ is beyound me, but it is what it is. @@ -181,16 +180,15 @@ const findSlope = (a: Vec2, b: Vec2) => { Much better! We have a function that takes two points and returns the slope of the line that connects them. ```tsx live collapsed +import { Vec2 } from "@coord/core"; import { Graph, Grid, - Plot, Line, Marker, Ruler, useCoordState, useNavigationState, - Vec2, } from "@coord/graph"; const findSlope = (a: Vec2, b: Vec2) => { @@ -252,16 +250,14 @@ We will deal with this case later. ## Step 2: Find the y-intercept for each line ```tsx live collapsed +import { Vec2 } from "@coord/core"; import { Graph, Grid, - Plot, Line, Marker, - Ruler, useCoordState, useNavigationState, - Vec2, Label, } from "@coord/graph"; @@ -378,18 +374,14 @@ const findYIntercept = (point: Vec2, slope: number) => { ## Step 3: Find the intersection point of the two lines at infinity ```tsx live collapsed +import { Vec2, point } from "@coord/core"; import { Graph, Grid, - Plot, Line, Marker, - Ruler, useCoordState, useNavigationState, - Vec2, - point, - Label, } from "@coord/graph"; const findSlope = (a: Vec2, b: Vec2) => { @@ -560,26 +552,23 @@ const findIntersection = (a: Vec2, b: Vec2, c: Vec2, d: Vec2) => { y = abSlope * x + abIntercept; } - return point(x, y); + return { x, y }; }; ``` ## Step 4: Find the intersection within the two line segments ```tsx live collapsed +import { Vec2, point } from "@coord/core"; import { Graph, Grid, - Plot, Line, Marker, - Ruler, Text, BoundingBox, useCoordState, useNavigationState, - Vec2, - point, Label, } from "@coord/graph"; @@ -776,6 +765,7 @@ const isWithinBoundingBox = (point: Vec2, a: Vec2, b: Vec2) => { ## Step 5: Putting it all together ```tsx live collapsed +import { Vec2, point } from "@coord/core"; import { Graph, Grid, @@ -783,9 +773,6 @@ import { Line, useCoordState, useNavigationState, - Vec2, - point, - Label, } from "@coord/graph"; const findSlope = (a: Vec2, b: Vec2) => { @@ -948,7 +935,7 @@ const findIntersection = (a: Vec2, b: Vec2, c: Vec2, d: Vec2) => { y = abSlope * x + abIntercept; } - const intersection = point(x, y); + const intersection = { x, y }; // If the intersection point is within the bounding boxes of both lines, then we have an intersection. const isWithinAB = isWithinBoundingBox(intersection, a, b); diff --git a/packages/docs/src/content/graph/docs/getting-started.mdx b/packages/docs/src/content/graph/docs/installation.mdx similarity index 100% rename from packages/docs/src/content/graph/docs/getting-started.mdx rename to packages/docs/src/content/graph/docs/installation.mdx diff --git a/packages/docs/src/content/graph/docs/interactions/dragging-elements.mdx b/packages/docs/src/content/graph/docs/interactions/dragging-elements.mdx index 259bdad..06a30c1 100644 --- a/packages/docs/src/content/graph/docs/interactions/dragging-elements.mdx +++ b/packages/docs/src/content/graph/docs/interactions/dragging-elements.mdx @@ -60,18 +60,7 @@ point; // ✅ [0, 0] ## Example -```jsx live -import { useState } from "react"; -import { - Graph, - Grid, - Label, - Marker, - useCoordState, - Text, - point, -} from "@coord/graph"; - +```jsx liveimport { Graph, Grid, Label, Marker, useCoordState } from "@coord/graph"; export default function MyGraph() { const [position, setPosition] = useCoordState([0, 0]); const [labelPosition, setLabelPosition] = useCoordState([0, 4]); diff --git a/packages/docs/src/content/graph/docs/interfaces/scalar-types.mdx b/packages/docs/src/content/graph/docs/interfaces/scalar-types.mdx index 84888c5..f4a6837 100644 --- a/packages/docs/src/content/graph/docs/interfaces/scalar-types.mdx +++ b/packages/docs/src/content/graph/docs/interfaces/scalar-types.mdx @@ -73,22 +73,29 @@ const scalarPoint: ScalarPoint = Vec2.of(10, 10); - The Vec2 class offers a static method `of` to conveniently create a `Vec2` instance from a `Vec2ish` value. ```tsx live -import { useLayoutEffect } from "react"; +import { CSSProperties } from "react"; +import { point } from "@coord/core"; import { Graph, Grid, Marker, - Vec2, Label, - point, useNavigationState, useStopwatch, + ScalarPoint, } from "@coord/graph"; +const labelStyle: CSSProperties = { + width: 180, + textAlign: "center", + padding: 10, + fontSize: 12, +}; + export default function MyGraph() { - const p0 = ["200vs", "200vs"]; - const p1 = [0, 0]; - const p2 = ["400vs", 0]; + const p0: ScalarPoint = ["200vs", "200vs"]; + const p1: ScalarPoint = [0, 0]; + const p2: ScalarPoint = ["400vs", 0]; const initialCoordBox = { horizontal: point(-10, 10), @@ -97,13 +104,6 @@ export default function MyGraph() { const [coordBox, setCoordBox] = useNavigationState(initialCoordBox); - const labelStyle = { - width: 180, - textAlign: "center", - padding: 10, - fontSize: 12, - }; - const { pause } = useStopwatch( (t) => { const x = Math.sin(t) * 3; @@ -137,7 +137,6 @@ export default function MyGraph() {
-
+
{playing ? ( +
+ ); return ( ); } -export function Pageold() { +function Pageold() { const controls = useMotionController(scene); const [codeString, setCodeString] = useState( dedent(` @@ -117,7 +123,7 @@ export function Pageold() { return (
- + {/* */}
{/* ); } -code` - ${remove("type MyType = boolean;")} - const my${replace("Bool:MyType", "String")} = ${replace("true", '"Hi"')}; -`; diff --git a/packages/docs/src/components/Button.tsx b/packages/docs/src/components/Button.tsx new file mode 100644 index 0000000..5dfc631 --- /dev/null +++ b/packages/docs/src/components/Button.tsx @@ -0,0 +1,22 @@ +import cn from "clsx"; +import type { ComponentProps, ReactElement } from "react"; + +export const Button = ({ + children, + className, + ...props +}: ComponentProps<"button">): ReactElement => { + return ( + + ); +}; diff --git a/packages/docs/src/components/CodeBlock.tsx b/packages/docs/src/components/CodeBlock.tsx index 531bbcd..c06b188 100644 --- a/packages/docs/src/components/CodeBlock.tsx +++ b/packages/docs/src/components/CodeBlock.tsx @@ -1,21 +1,34 @@ "use client"; +import { Runner } from "react-runner"; +import sdk from "@stackblitz/sdk"; import { - CodeBlock as CB, - Language, - useLiveRunner, - CodeEditor, -} from "react-live-runner"; -import { HiChevronDoubleDown, HiChevronDoubleUp } from "react-icons/hi"; - + CodeIcon, + CopyIcon, + CheckIcon, + EyeIcon, + EyeClosedIcon, +} from "@primer/octicons-react"; import * as graph from "@coord/graph"; import * as motion from "@coord/motion"; import * as motionReact from "@coord/motion-react"; import * as core from "@coord/core"; -import React, { useEffect } from "react"; +import React, { + ComponentProps, + PropsWithChildren, + useEffect, + useMemo, +} from "react"; import dedent from "ts-dedent"; import clsx from "clsx"; +import { + CodeBlock as CB, + CodeMorph, + LanguageOptions, + useCode, +} from "@coord/code"; + const scope = { import: { react: React, @@ -26,119 +39,414 @@ const scope = { }, }; -export function LiveCodeBlock({ - children, - collapsed: collapsedInitialValue = false, - partialCode, +const packageJson = { + name: "curves-demo", + version: "0.0.0", + private: true, + dependencies: { + react: "18.2.0", + "react-dom": "18.2.0", + "@types/react": "18.2.14", + "@types/react-dom": "18.2.6", + "@coord/graph": "latest", + "@coord/core": "latest", + }, + stackblitz: { + installDependencies: true, + startCommand: "pnpm install && pnpm start", + }, + scripts: { + start: "react-scripts start", + build: "react-scripts build", + test: "react-scripts test --env=jsdom", + eject: "react-scripts eject", + }, + devDependencies: { + "react-scripts": "latest", + }, +}; + +const files = { + "src/index.tsx": dedent(` + import * as React from 'react'; + + import { StrictMode } from 'react'; + import { createRoot } from 'react-dom/client'; + import './style.css'; + import App from './App'; + + const rootElement = document.getElementById('root'); + const root = createRoot(rootElement); + + root.render( + + + + ); + `), + "public/index.html": '
', + "src/style.css": dedent(` + html, body { + margin: 0; + padding: 0; + font-family: sans-serif; + background-color: #282c34; + overflow: hidden; + } + #root { + height: 100vh; + } + `), + "package.json": JSON.stringify(packageJson, null, 2), +}; + +function CopyButton({ + code, + ...rest +}: { code: string } & ComponentProps<"button">) { + const [copied, setCopied] = React.useState(false); + + useEffect(() => { + if (!copied) return; + const timeout = setTimeout(() => { + setCopied(false); + }, 1000); + return () => { + clearTimeout(timeout); + }; + }, [copied]); + return ( + + ); +} + +export function CodeBlock({ + code, + height, + collapsed = false, + collapsable = true, + preview = false, + editable = false, + morph = false, + language, }: { - children: string; + code: string; + height: number; collapsed?: boolean; - partialCode?: boolean; + collapsable?: boolean; + preview?: boolean; + language?: string; + editable?: boolean; + morph?: boolean; }) { - const { element, error, code, onChange } = useLiveRunner({ - // initialCode: "", - scope, - }); - useEffect(() => { - onChange(dedent(children)); - }, []); - const [collapsed, setCollapsed] = React.useState(collapsedInitialValue); + const codeSections = useMemo(() => { + code = dedent(code); + if (!morph) return [{ type: "option", description: "", code, name: "" }]; + return parseMorphingCode(code); + }, [code]); + + const [activeSectionIndex, setActiveSectionIndex] = React.useState(0); + const activeCode = codeSections[activeSectionIndex]?.code ?? ""; + const [edit, setEdit] = React.useState(false); + const [isCollapsed, setIsCollapsed] = React.useState(collapsed); + const [loading, setLoading] = React.useState(false); + const ref = React.useRef(null); + + useEffect(() => { + if (!edit) return; + if (!ref.current) return; + sdk + .embedProject( + ref.current, + { + title: "Curves demo", + description: "Curves demo", + template: "create-react-app", + dependencies: packageJson.dependencies, + files: { + ...files, + "src/App.tsx": activeCode, + }, + }, + { + openFile: "src/App.tsx", + theme: "dark", + hideExplorer: true, + hideNavigation: true, + hideDevTools: true, + view: "default", + height: 500, + } + ) + .then(() => { + setLoading(false); + }); + }, [edit]); + const steps = codeSections.filter(({ type }) => type === "step"); + const options = codeSections.filter(({ type }) => type === "option"); return ( -
-
-
- {element} -
+
+
+
- - {error && ( -
-          {error}
-        
- )} -
- {(!collapsed || partialCode) && ( +
+ {preview && (
- + {codeSections.map(({ type, description, code, name }, i) => ( +
+ +
+ ))}
)} + {morph && !!steps.length && ( +
+ {steps.map(({ name, description, code }, i) => ( + + ))} +
+ )} + 1 + ? options.map((c, i) => ( + + )) + : undefined + } + buttonsRight={[ + collapsable && ( + + ), - + ), + , + ]} > - {collapsed ? ( - <> - Edit code - - - ) : ( - <> - Hide code - - + {!isCollapsed && !morph && ( + + )} + {!isCollapsed && morph && ( + )} - +
); } -export function CodeBlock({ +function CodeContainer({ children, - language = "typescript", -}: { - language?: Language; - showLineNumbers?: boolean; - children: string; -}) { - const code = React.useMemo(() => dedent(children), []); - const [copied, setCopied] = React.useState(false); + buttonsRight = [], + buttonsLeft = [], +}: PropsWithChildren<{ + buttonsRight?: React.ReactNode[]; + buttonsLeft?: React.ReactNode[]; +}>) { return ( -
-
-
    -
  • - {language} -
  • -
  • - -
  • -
+ {c} +
+ ))} +
- - {code} - +
+
{children}
+
); } +const parseMorphingCode = (code: string) => { + const lines = code.split("\n"); + const codeParts = [ + { + type: "option", + description: "", + code: "", + name: "", + }, + ]; + + for (const line of lines) { + const [_, optionName] = line.split("option:"); + const [__, stepName] = line.split("step:"); + + if (optionName || stepName) { + if (codeParts.at(-1)?.code.trim() === "") { + codeParts.pop(); + } + const [name, description] = (optionName ?? stepName).split(" - "); + + const type = optionName ? "option" : "step"; + codeParts.push({ + type, + code: "", + description: description?.trim() ?? "", + name: name.trim(), + }); + continue; + } + const part = codeParts.at(-1); + if (!part) continue; + + part.code += `${line}\n`; + } + return codeParts.map((c) => { + c.code = dedent(c.code); + return c; + }); +}; +export function MorphingCodeBlock({ + code, + language, +}: { + code: string; + language: LanguageOptions; +}) { + const props = useCode(code, { + duration: 0.8, + language, + }); + return ( + + ); +} diff --git a/packages/docs/src/components/GraphLogo.tsx b/packages/docs/src/components/GraphLogo.tsx index bd28044..9d04209 100644 --- a/packages/docs/src/components/GraphLogo.tsx +++ b/packages/docs/src/components/GraphLogo.tsx @@ -1,4 +1,123 @@ +"use client"; import { Graph, Plot } from "@coord/graph"; +import Link, { LinkProps } from "next/link"; +import { ComponentProps, Fragment, PropsWithChildren, useState } from "react"; +import { GoChevronRight } from "react-icons/go"; +import cn from "clsx"; +import { usePathname } from "next/navigation"; + +function ProjectLink({ + className, + children, + selected, + + ...rest +}: PropsWithChildren<{ + className?: string; + selected: boolean; +}> & + LinkProps) { + return ( + + {children} + + ); +} + +export function Logo() { + const pathname = usePathname(); + const selected = pathname.split("/")[1] ?? ""; + + const [hover, setHover] = useState<"motion" | "editor" | "graph">("motion"); + + const c = { + motion: ( + setHover("motion")} + > + motion + + ), + + graph: ( + setHover("graph")} + > + graph + + ), + editor: ( + setHover("editor")} + > + editor + + ), + } as const; + return ( +
+ + curves + + + + + {selected in c ? ( +
{c[selected as keyof typeof c]}
+ ) : null} +
+ {Object.entries(c) + .filter(([k]) => k !== selected) + .map(([k, v], i) => ( + + {i > 0 && /} + + {v} + + ))} +
+
+ ); +} export const GraphLogo = ({ size = 400, diff --git a/packages/docs/src/components/Header.tsx b/packages/docs/src/components/Header.tsx index 40fc0de..9eed33e 100644 --- a/packages/docs/src/components/Header.tsx +++ b/packages/docs/src/components/Header.tsx @@ -1,37 +1,146 @@ +"use client"; import Link from "next/link"; -import { GoMarkGithub, GoBook } from "react-icons/go"; +import { Menu, Transition } from "@headlessui/react"; +import { MarkGithubIcon, ThreeBarsIcon, XIcon } from "@primer/octicons-react"; +import cn from "clsx"; +import { ReactElement, ReactNode, useEffect, useState } from "react"; +import { PageItem } from "./navigation"; +import { Logo } from "./GraphLogo"; + +export function NavbarMenu({ + className, + menu, + children, +}: { + className?: string; + menu: PageItem; + children: ReactNode; +}): ReactElement { + const { items } = menu; -export function Header() { return ( -
-
-
-

- - @coord/graph - -

-
-
- + + + + - - Docs - + {Object.entries(items || {}).map(([key, item]) => ( + + + {item.title || key} + + + ))} + + + +
+ ); +} - - - GitHub - +type HeaderProps = { + items?: PageItem[]; +}; +export function Header({ items = [] }: HeaderProps): ReactElement { + const [menu, setMenu] = useState(false); + + useEffect(() => { + if (!menu) return; + document.body.classList.add("menu-open"); + + return () => { + document.body.classList.remove("menu-open"); + }; + }, [menu]); + + return ( +
+
+
-
+ {[ + ...items, + { + title: "GitHub", + icon: , + route: + "https://github.com/julia-script/coord/tree/main/packages/graph", + } as PageItem, + ].map((menu, i) => { + const isActive = false; + if (!menu.items?.length ?? false) + return ( + + {menu.icon}{" "} + {menu.title} + + ); + return ( + + {menu.icon} + {menu.title} + + ); + })} + + + +
); } diff --git a/packages/docs/src/components/PreMdx.tsx b/packages/docs/src/components/PreMdx.tsx index f8ec9ec..b0b5009 100644 --- a/packages/docs/src/components/PreMdx.tsx +++ b/packages/docs/src/components/PreMdx.tsx @@ -1,32 +1,71 @@ "use client"; -import { CodeBlock, LiveCodeBlock } from "./CodeBlock"; + +import { CodeBlock } from "./CodeBlock"; +import { get } from "lodash-es"; export type PreMDXProps = { + editable?: boolean; children: React.ReactNode; + height?: number; live?: boolean; - previewOnly?: boolean; collapsed?: boolean; - partial?: boolean; + collapsable?: boolean; className?: string; + preview?: boolean; + morph?: boolean; }; -export function PreMDX({ children, live, collapsed, partial }: PreMDXProps) { - if ( - typeof children === "object" && - children !== null && - "props" in children && - typeof children.props.children === "string" - ) { - const code = children.props.children; - const language = children.props.className?.replace("language-", ""); +const parseProps = (props: PreMDXProps) => { + const code: unknown = get(props, "children.props.children"); + const className: unknown = get(props, "children.props.className"); + const live = !!props.live; + + const { + preview = live, + collapsable = live, + collapsed = false, + editable = live, + morph = false, + } = props; + + return { + height: props.height ?? 400, + code: typeof code === "string" ? code : "", + language: + typeof className === "string" + ? className.replace("language-", "") + : "typescript", + + children: props.children, + collapsable, + collapsed, + editable, + preview, + morph, + }; +}; +export function PreMDX(props: PreMDXProps) { + const { + morph, + editable, + code, + height, + collapsable, + collapsed, + preview, + language, + } = parseProps(props); - if (live) - return ( - - {code} - - ); - return {code}; - } - return children; + return ( + + ); } diff --git a/packages/docs/src/components/mdx-components.tsx b/packages/docs/src/components/mdx-components.tsx deleted file mode 100644 index edec57e..0000000 --- a/packages/docs/src/components/mdx-components.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import Image from "next/image"; -import { useMDXComponent } from "next-contentlayer/hooks"; -import { PreMDX } from "@/components"; - -const components = { - Image, - pre: PreMDX, -}; - -interface MdxProps { - code: string; -} - -export function Mdx({ code }: MdxProps) { - const Component = useMDXComponent(code); - - return ; -} diff --git a/packages/docs/src/components/navigation/Sidebar.tsx b/packages/docs/src/components/navigation/Sidebar.tsx new file mode 100644 index 0000000..4577583 --- /dev/null +++ b/packages/docs/src/components/navigation/Sidebar.tsx @@ -0,0 +1,135 @@ +import { + useState, + useRef, + Component, + ComponentProps, + PropsWithChildren, +} from "react"; +import { PageItem } from "./types"; +import cn from "clsx"; +import Link from "next/link"; +import { GoChevronDown } from "react-icons/go"; +import { isString } from "@coord/core/dist"; +import { usePathname } from "next/navigation"; +import { normalizeRoute } from "@/utils/routes"; + +type MenuProps = { + items: PageItem[]; +} & ComponentProps<"ul">; + +function MenuLabel({ + href, + isPage = true, + ...rest +}: PropsWithChildren<{ + href: string; + isPage?: boolean; + target?: string; + className?: string; +}>) { + if (isPage) { + return ; + } + return ; +} + +function MenuItem({ item }: { item: PageItem }) { + // const [open, setOpen] = useState(true); + const pathname = usePathname(); + const active = normalizeRoute(pathname) === normalizeRoute(item.route); + + return ( +
  • + + {item.title} + {/* */} + + {item.items && ( +
    + +
    + )} +
  • + ); +} +function Menu({ items, className }: MenuProps) { + return ( +
      + {items.map((item) => ( + + ))} +
    + ); +} + +type SidebarProps = { + items: PageItem[]; +}; +export function Sidebar({ items }: SidebarProps) { + const containerRef = useRef(null); + const sidebarRef = useRef(null); + return ( + + ); +} diff --git a/packages/docs/src/components/navigation/index.tsx b/packages/docs/src/components/navigation/index.tsx new file mode 100644 index 0000000..d22203f --- /dev/null +++ b/packages/docs/src/components/navigation/index.tsx @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./Sidebar"; diff --git a/packages/docs/src/components/navigation/types.ts b/packages/docs/src/components/navigation/types.ts new file mode 100644 index 0000000..398c040 --- /dev/null +++ b/packages/docs/src/components/navigation/types.ts @@ -0,0 +1,12 @@ +import { ReactNode } from "react"; + +export type PageItem = { + title: string; + route: string; + + isPage?: boolean; + icon?: ReactNode; + layout?: "full" | "default"; + target?: string; + items?: PageItem[]; +}; diff --git a/packages/docs/src/content/editor.mdx b/packages/docs/src/content/editor.mdx new file mode 100644 index 0000000..b84b4bc --- /dev/null +++ b/packages/docs/src/content/editor.mdx @@ -0,0 +1 @@ +# Wip diff --git a/packages/docs/src/content/graph.mdx b/packages/docs/src/content/graph.mdx new file mode 100644 index 0000000..b84b4bc --- /dev/null +++ b/packages/docs/src/content/graph.mdx @@ -0,0 +1 @@ +# Wip diff --git a/packages/docs/src/content/graph/docs/components/bounding-box.mdx b/packages/docs/src/content/graph/docs/components/bounding-box.mdx index 7387558..6d160a8 100644 --- a/packages/docs/src/content/graph/docs/components/bounding-box.mdx +++ b/packages/docs/src/content/graph/docs/components/bounding-box.mdx @@ -1,8 +1,11 @@ # BoundingBox Component -`BoundingBox` is similar to `Rect` but it takes a `BBoxish` object to draw a rect. It's specially usefull when you want to draw a rect around a coordinate area. +`BoundingBox` is similar to `Rect` but it takes a `BBoxish` object to draw a +rect. It's specially usefull when you want to draw a rect around a coordinate +area. ```tsx live +import * as React from "react"; import { point } from "@coord/core"; import { Graph, Grid, Text, BoundingBox } from "@coord/graph"; @@ -18,7 +21,7 @@ export default function MyGraph() { }; return ( - + + + @@ -107,13 +119,14 @@ export default function MyGraph() { ``` ```tsx live collapsed +import * as React from "react"; import { Graph, Grid, Line } from "@coord/graph"; export default function MyGraph() { return ( @@ -127,13 +140,14 @@ export default function MyGraph() { ``` ```tsx live collapsed +import * as React from "react"; import { Graph, Grid, Line } from "@coord/graph"; export default function MyGraph() { return ( @@ -147,13 +161,14 @@ export default function MyGraph() { ``` ```tsx live collapsed +import * as React from "react"; import { Graph, Grid, Line } from "@coord/graph"; export default function MyGraph() { return ( @@ -168,7 +183,9 @@ export default function MyGraph() { ### Custom Themes -You can also define a custom theme by passing an object to the theme prop. This object should conform to the Theme interface. Here's an example of a custom theme: +You can also define a custom theme by passing an object to the theme prop. This +object should conform to the Theme interface. Here's an example of a custom +theme: ```tsx const customTheme: Theme = { diff --git a/packages/docs/src/content/graph/docs/components/grid.mdx b/packages/docs/src/content/graph/docs/components/grid.mdx index d8b86a7..ad3dbdd 100644 --- a/packages/docs/src/content/graph/docs/components/grid.mdx +++ b/packages/docs/src/content/graph/docs/components/grid.mdx @@ -11,7 +11,7 @@ import { Graph, Grid, Plot } from "@coord/graph"; export default function MyGraph() { return ( - + Math.sin(x) * Math.exp(-x / 10)} strokeColor={2} /> diff --git a/packages/docs/src/content/graph/docs/components/label.mdx b/packages/docs/src/content/graph/docs/components/label.mdx index 87e82a6..b155e5b 100644 --- a/packages/docs/src/content/graph/docs/components/label.mdx +++ b/packages/docs/src/content/graph/docs/components/label.mdx @@ -1,8 +1,9 @@ # Label Component -`Label` component allows you to attach a label with any valid JSX to a specific position on a graph. -The size of the label adapts to its children. -You can also adjust its position, orientation, distance from target, and color, among other properties. +`Label` component allows you to attach a label with any valid JSX to a specific +position on a graph. The size of the label adapts to its children. You can also +adjust its position, orientation, distance from target, and color, among other +properties. ```jsx live import { Graph, Grid, Label, Marker, useCoordState } from "@coord/graph"; @@ -10,7 +11,7 @@ import { Graph, Grid, Label, Marker, useCoordState } from "@coord/graph"; export default function MyGraph() { const [position, setPosition] = useCoordState([0, 0]); return ( - +