├── public ├── .nojekyll ├── .DS_Store └── _headers ├── src ├── splatapus3 │ ├── Inputs.tsx │ ├── index.html │ ├── splatapus3-main.tsx │ ├── Viewport.tsx │ └── model │ │ └── History.tsx ├── lib │ ├── time.tsx │ ├── IdGenerator.tsx │ ├── hooks │ │ ├── usePrevious.tsx │ │ ├── useNonNull.tsx │ │ ├── useMergedRefs.tsx │ │ ├── useMediaQuery.tsx │ │ └── useEvent.tsx │ ├── logger.tsx │ ├── scene │ │ ├── SortOrderProvider.ts │ │ ├── Component.ts │ │ ├── SceneObject.ts │ │ └── SceneSystem.ts │ ├── gl │ │ ├── GlVertexArray.tsx │ │ ├── GlBuffer.tsx │ │ └── GlShader.tsx │ ├── signals │ │ ├── useSignal.tsx │ │ └── startSignalsApp.tsx │ ├── debugPropsToString.tsx │ ├── fakeConsole.ts │ ├── RandomQueue.ts │ ├── EventIterator.tsx │ ├── omitFromStackTrace.tsx │ ├── load.tsx │ ├── createWindowCanvas.tsx │ ├── assert.test.tsx │ ├── assert.ts │ ├── live │ │ ├── LiveMemo.test.tsx │ │ ├── LiveValue.test.tsx │ │ ├── LiveEffect.tsx │ │ └── LiveMemo.tsx │ ├── canvasShapeHelpers.ts │ ├── EventEmitter.tsx │ ├── geom │ │ ├── StraightPathSegment.ts │ │ ├── ApproxQuadraticBezierPathSegment.ts │ │ ├── ApproxCubicBezierPathSegment.ts │ │ ├── bezier.tsx │ │ └── AABB.ts │ ├── findDataElement.tsx │ ├── shader.tsx │ ├── Ticker.tsx │ ├── Stack.tsx │ ├── midi.tsx │ ├── react │ │ └── DebugCanvasComponent.tsx │ └── RingBuffer.ts ├── bees │ ├── constants.tsx │ ├── style.css │ ├── assets │ │ ├── bee-fly.png │ │ ├── bee-flat.png │ │ ├── bee-outline.png │ │ └── bee-shadow.png │ ├── index.html │ ├── AnimatedSpriteStack.tsx │ ├── Sprite.tsx │ ├── driver.tsx │ ├── C.tsx │ └── bees-main2.tsx ├── slomojs │ ├── setup.tsx │ ├── crate │ │ ├── .gitignore │ │ ├── Cargo.toml.d.ts │ │ ├── src │ │ │ ├── display │ │ │ │ ├── mod.rs │ │ │ │ ├── constants.rs │ │ │ │ └── cursor.rs │ │ │ └── dom.rs │ │ ├── tests │ │ │ └── web.rs │ │ └── Cargo.toml │ ├── index.html │ └── main.tsx ├── vite-env.d.ts ├── .DS_Store ├── tailwind.css ├── octopus │ ├── main.css │ └── index.html ├── sim │ ├── tom-thumb.woff2 │ ├── index.html │ ├── crate │ │ └── Cargo.toml │ └── main.tsx ├── emoji │ ├── assets │ │ └── emojis.png │ ├── index.html │ ├── emoji-main.tsx │ ├── Emoji.tsx │ └── Post.tsx ├── infinite-scroll │ ├── .DS_Store │ ├── assets │ │ ├── fox.jpg │ │ └── frog.jpg │ ├── contants.tsx │ ├── infinite-scroll-main.tsx │ └── index.html ├── network │ ├── ConnectionDirection.ts │ ├── colors.ts │ ├── index.html │ ├── index.css │ ├── networkNodes │ │ └── NetworkNode.ts │ ├── ConnectionSet.ts │ └── TravellerFinder.ts ├── terrain │ ├── index.css │ ├── index.html │ ├── TerrainCellEdge.ts │ ├── canvas.ts │ ├── makeInterpolateGradient.ts │ ├── config.ts │ ├── fractalNoise.ts │ ├── Grid2.ts │ └── generatePoisson.ts ├── webgl │ ├── cat-main.tsx │ └── index.html ├── worms │ ├── index.css │ ├── index.html │ ├── canvas.ts │ └── colors.ts ├── wiggle-gradient │ ├── index.css │ ├── index.html │ └── canvas.ts ├── pals │ ├── colors.ts │ ├── index.html │ ├── index.css │ ├── makePal.ts │ └── pals-main.ts ├── gestureland │ ├── constants.tsx │ ├── gestureland-main.tsx │ ├── index.html │ ├── GesturelandStore.tsx │ ├── interactions │ │ └── interactions.tsx │ └── types.tsx ├── platformer │ └── index.html ├── txdraw │ ├── txdraw-main.tsx │ ├── index.html │ ├── shapes │ │ └── shared │ │ │ └── snapShapesToGrid.tsx │ ├── utils │ │ └── texel.ts │ └── components │ │ └── Grid.tsx ├── geometry │ ├── paths.tsx │ ├── index.html │ ├── geometry-main.tsx │ └── GeometryApp.tsx ├── spline-time │ ├── layers │ │ ├── midPointMarkerLayer.tsx │ │ ├── straightLineThroughPointsLayer.tsx │ │ ├── Layer.tsx │ │ └── midPointBezierLayer.tsx │ ├── spline-time-main.tsx │ ├── index.html │ └── SplineTimeLine.tsx ├── lime │ ├── Inputs.tsx │ ├── index.html │ ├── LimeConfig.tsx │ ├── lime-main.tsx │ ├── Viewport.tsx │ └── LimeState.tsx ├── wires │ ├── wires-main.tsx │ ├── index.html │ ├── inputs.tsx │ └── wiresModel.tsx ├── blob-tree │ ├── index.html │ ├── canvas.ts │ └── blob-tree-main.tsx ├── blob-factory │ ├── index.html │ ├── shader.vert │ └── blob-factory-main.tsx ├── splatapus │ ├── model │ │ ├── SplatDocModel.test.tsx │ │ ├── SplatDocData.tsx │ │ ├── findPositionForNewKeyPoint.tsx │ │ ├── ThinPlateSpline.test.tsx │ │ ├── Ids.tsx │ │ ├── SplatDoc.tsx │ │ ├── pathFromCenterPoints.tsx │ │ └── store.tsx │ ├── index.html │ ├── splatapus-main.tsx │ ├── examples │ │ └── examples.test.tsx │ ├── constants.tsx │ ├── editor │ │ ├── modes │ │ │ ├── Mode.tsx │ │ │ └── PlayMode.tsx │ │ ├── modeClassNames.tsx │ │ ├── PreviewPosition.tsx │ │ ├── SplatLocation.tsx │ │ └── Vfx.tsx │ ├── ui │ │ ├── useSquircle.tsx │ │ └── ModePicker.tsx │ └── renderer │ │ └── Positioned.tsx ├── splatapus2 │ ├── index.html │ ├── splatapus2-main.tsx │ ├── app │ │ └── Inputs.tsx │ └── store │ │ └── Records.tsx ├── trees │ ├── index.html │ └── trees-main.tsx └── palette │ ├── index.html │ ├── palette-main.tsx │ ├── support.tsx │ ├── schema.tsx │ └── GamutPicker.tsx ├── .clang-format ├── .DS_Store ├── types ├── crates.d.ts ├── assets.d.ts └── shaders.d.ts ├── .yarnrc.yml ├── .vscode └── settings.json ├── postcss.config.js ├── Cargo.toml ├── .prettierrc ├── .gitignore ├── packages └── vector2.json ├── tsconfig.json ├── .yarn └── patches │ ├── tailwindcss-npm-3.4.10-32443f8dff.patch │ └── @types-culori-npm-2.0.4-129982cb7c.patch ├── .eslintrc.json └── scripts └── writeTailwindConfig.mjs /public/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/splatapus3/Inputs.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | IndentWidth: 4 -------------------------------------------------------------------------------- /src/lib/time.tsx: -------------------------------------------------------------------------------- 1 | export const TIME_MULTIPLIER = 1; 2 | -------------------------------------------------------------------------------- /src/bees/constants.tsx: -------------------------------------------------------------------------------- 1 | export const BG_COLOR = 0xd6f264; 2 | -------------------------------------------------------------------------------- /src/slomojs/setup.tsx: -------------------------------------------------------------------------------- 1 | import "regenerator-runtime/runtime"; 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeHats/toys/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/bees/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #d6f264; 3 | } 4 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeHats/toys/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeHats/toys/HEAD/public/.DS_Store -------------------------------------------------------------------------------- /src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/octopus/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /src/slomojs/crate/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | bin/ 4 | pkg/ 5 | wasm-pack.log 6 | -------------------------------------------------------------------------------- /src/sim/tom-thumb.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeHats/toys/HEAD/src/sim/tom-thumb.woff2 -------------------------------------------------------------------------------- /src/bees/assets/bee-fly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeHats/toys/HEAD/src/bees/assets/bee-fly.png -------------------------------------------------------------------------------- /src/emoji/assets/emojis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeHats/toys/HEAD/src/emoji/assets/emojis.png -------------------------------------------------------------------------------- /src/bees/assets/bee-flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeHats/toys/HEAD/src/bees/assets/bee-flat.png -------------------------------------------------------------------------------- /src/infinite-scroll/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeHats/toys/HEAD/src/infinite-scroll/.DS_Store -------------------------------------------------------------------------------- /src/bees/assets/bee-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeHats/toys/HEAD/src/bees/assets/bee-outline.png -------------------------------------------------------------------------------- /src/bees/assets/bee-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeHats/toys/HEAD/src/bees/assets/bee-shadow.png -------------------------------------------------------------------------------- /src/infinite-scroll/assets/fox.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeHats/toys/HEAD/src/infinite-scroll/assets/fox.jpg -------------------------------------------------------------------------------- /src/slomojs/crate/Cargo.toml.d.ts: -------------------------------------------------------------------------------- 1 | export function start(source: string, element: HTMLElement): Promise; 2 | -------------------------------------------------------------------------------- /public/_headers: -------------------------------------------------------------------------------- 1 | /static/* 2 | Cache-Control: public 3 | Cache-Control: max-age=365000000 4 | Cache-Control: immutable -------------------------------------------------------------------------------- /src/infinite-scroll/assets/frog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeHats/toys/HEAD/src/infinite-scroll/assets/frog.jpg -------------------------------------------------------------------------------- /types/crates.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*/slomojs/Cargo.toml" { 2 | export function start(source: string): void; 3 | } 4 | -------------------------------------------------------------------------------- /src/infinite-scroll/contants.tsx: -------------------------------------------------------------------------------- 1 | export const targetWidthPx = 375; 2 | export const targetHeightPx = 667; 3 | 4 | export const breakpointPx = 786; 5 | -------------------------------------------------------------------------------- /types/assets.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | export default string; 3 | } 4 | 5 | declare module "*.json" { 6 | export default any; 7 | } 8 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.0.2.cjs 8 | -------------------------------------------------------------------------------- /src/network/ConnectionDirection.ts: -------------------------------------------------------------------------------- 1 | enum ConnectionDirection { 2 | IN = "in", 3 | OUT = "out", 4 | } 5 | 6 | export default ConnectionDirection; 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.importModuleSpecifier": "non-relative", 3 | "typescript.tsdk": "./node_modules/typescript/lib" 4 | } 5 | -------------------------------------------------------------------------------- /src/slomojs/crate/src/display/mod.rs: -------------------------------------------------------------------------------- 1 | mod constants; 2 | mod cursor; 3 | mod text; 4 | mod word; 5 | 6 | pub use text::{Text, TextBuilder}; 7 | pub use word::WordId; 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require("tailwindcss")("./tailwind.config.js"), 4 | require("autoprefixer"), 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["src/slomojs/crate", "src/sim/crate"] 3 | 4 | [profile.release] 5 | # Tell `rustc` to optimize for small code size. 6 | opt-level = "s" 7 | lto = true 8 | -------------------------------------------------------------------------------- /src/terrain/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | canvas { 10 | background: black; 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 4, 4 | "trailingComma": "all", 5 | "proseWrap": "always", 6 | "experimentalTernaries": true, 7 | "plugins": ["prettier-plugin-organize-imports"] 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/IdGenerator.tsx: -------------------------------------------------------------------------------- 1 | export class IdGenerator { 2 | private number = 0; 3 | constructor(private readonly prefix: string) {} 4 | 5 | next() { 6 | return `${this.prefix}${this.number++}`; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/webgl/cat-main.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { CatApp } from "@/webgl/CatApp"; 3 | import { createRoot } from "react-dom/client"; 4 | 5 | createRoot(assertExists(document.getElementById("root"))).render(); 6 | -------------------------------------------------------------------------------- /src/worms/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | canvas { 10 | background: #240b36; 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /target 4 | /.cache 5 | /.parcel-cache 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/sdks 12 | !.yarn/versions 13 | 14 | .vitest 15 | *.tsbuildinfo 16 | /packages_out -------------------------------------------------------------------------------- /src/wiggle-gradient/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | canvas { 10 | background: #000; 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | } 15 | -------------------------------------------------------------------------------- /types/shaders.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.frag" { 2 | const fragmentShaderString: string; 3 | export default fragmentShaderString; 4 | } 5 | 6 | declare module "*.vert" { 7 | const vertexShaderString: string; 8 | export default vertexShaderString; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/hooks/usePrevious.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | export function usePrevious(value: T) { 4 | const ref = useRef(value); 5 | useEffect(() => { 6 | ref.current = value; 7 | }, [value]); 8 | return ref.current; 9 | } 10 | -------------------------------------------------------------------------------- /packages/vector2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@somehats/vector2", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/somehats/toys", 5 | "license": "MIT", 6 | "author": "alex dytrych (https://github.com/somehats)", 7 | "main": "../src/lib/geom/Vector2.ts" 8 | } 9 | -------------------------------------------------------------------------------- /src/terrain/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | terrain 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/slomojs/crate/tests/web.rs: -------------------------------------------------------------------------------- 1 | //! Test suite for the Web and headless browsers. 2 | 3 | #![cfg(target_arch = "wasm32")] 4 | 5 | extern crate wasm_bindgen_test; 6 | use wasm_bindgen_test::*; 7 | 8 | wasm_bindgen_test_configure!(run_in_browser); 9 | 10 | #[wasm_bindgen_test] 11 | fn pass() { 12 | assert_eq!(1 + 1, 2); 13 | } 14 | -------------------------------------------------------------------------------- /src/network/colors.ts: -------------------------------------------------------------------------------- 1 | import Color from "color"; 2 | 3 | // https://coolors.co/f8ffe5-06d6a0-1b9aaa-ef476f-ffc43d 4 | export const LIGHT_BG = new Color("#F8FFE5"); 5 | export const TEAL = new Color("#06D6A0"); 6 | export const BLUE = new Color("#1B9AAA"); 7 | export const RED = new Color("#EF476F"); 8 | export const YELLOW = new Color("#FFC43D"); 9 | -------------------------------------------------------------------------------- /src/pals/colors.ts: -------------------------------------------------------------------------------- 1 | import Color from "color"; 2 | 3 | // https://coolors.co/f8ffe5-06d6a0-1b9aaa-ef476f-ffc43d 4 | export const LIGHT_BG = new Color("#F8FFE5"); 5 | export const TEAL = new Color("#06D6A0"); 6 | export const BLUE = new Color("#1B9AAA"); 7 | export const RED = new Color("#EF476F"); 8 | export const YELLOW = new Color("#FFC43D"); 9 | -------------------------------------------------------------------------------- /src/lib/hooks/useNonNull.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | export function useKeepNonNull(value: T | null): T | null { 4 | const ref = useRef(value); 5 | useEffect(() => { 6 | if (value != null) { 7 | ref.current = value; 8 | } 9 | }, [value]); 10 | return value ?? ref.current; 11 | } 12 | -------------------------------------------------------------------------------- /src/pals/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | terrain 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/slomojs/crate/src/display/constants.rs: -------------------------------------------------------------------------------- 1 | pub const CHAR_WIDTH_PX: usize = 11; 2 | pub const CHAR_HEIGHT_PX: usize = 28; 3 | pub const ANIMATION_DURATION_MS: f64 = 1000.; 4 | 5 | lazy_static! { 6 | pub static ref CHAR_WIDTH_CSS: String = format!("{}px", CHAR_WIDTH_PX); 7 | pub static ref CHAR_HEIGHT_CSS: String = format!("{}px", CHAR_HEIGHT_PX); 8 | } 9 | -------------------------------------------------------------------------------- /src/worms/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | worms 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/network/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | terrain 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/sim/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sim 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/wiggle-gradient/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | worms 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/bees/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bees 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/infinite-scroll/infinite-scroll-main.tsx: -------------------------------------------------------------------------------- 1 | import { App } from "@/infinite-scroll/InfiniteScrollApp"; 2 | import { assertExists } from "@/lib/assert"; 3 | import { StrictMode } from "react"; 4 | import { createRoot } from "react-dom/client"; 5 | 6 | createRoot(assertExists(document.getElementById("root"))).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /src/terrain/TerrainCellEdge.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { TerrainCell } from "@/terrain/TerrainCell"; 3 | 4 | export class TerrainCellEdge { 5 | constructor( 6 | public readonly pointA: Vector2, 7 | public readonly pointB: Vector2, 8 | public readonly cellA: TerrainCell, 9 | public readonly cellB: TerrainCell, 10 | ) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/gestureland/constants.tsx: -------------------------------------------------------------------------------- 1 | import { GLPointerType } from "@/gestureland/types"; 2 | 3 | export const POINTER_SENSITIVITY_PX = { 4 | mouse: 8, 5 | pen: 8, 6 | touch: 16, 7 | } as const satisfies Record; 8 | 9 | export const EXPIRE_POINTER_AFTER_MS = 1000; 10 | 11 | export const MAX_TIME_BETWEEN_MULTI_TAP_MS = 200; 12 | export const MAX_DISTANCE_FOR_MULTI_TAP_MS = 40; 13 | -------------------------------------------------------------------------------- /src/lib/logger.tsx: -------------------------------------------------------------------------------- 1 | let callCount = 0; 2 | export function log9(...args: readonly unknown[]) { 3 | callCount++; 4 | if (callCount < 10) { 5 | console.log(`[log9 ${callCount}]`, ...args); 6 | } 7 | } 8 | 9 | export function trace9(...args: readonly unknown[]) { 10 | callCount++; 11 | if (callCount < 10) { 12 | console.trace(`[trace9 ${callCount}]`, ...args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/slomojs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | slomojs 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/platformer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tower defence 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/txdraw/txdraw-main.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { TxdrawApp } from "@/txdraw/App"; 3 | import { createRoot } from "react-dom/client"; 4 | 5 | if (import.meta.hot) { 6 | import.meta.hot.on("vite:beforeUpdate", () => { 7 | window.location.reload(); 8 | }); 9 | } 10 | 11 | const root = assertExists(document.getElementById("root")); 12 | createRoot(root).render(); 13 | -------------------------------------------------------------------------------- /src/gestureland/gestureland-main.tsx: -------------------------------------------------------------------------------- 1 | import { AppWrapper } from "@/gestureland/App"; 2 | import { assertExists } from "@/lib/assert"; 3 | import { createRoot } from "react-dom/client"; 4 | 5 | if (import.meta.hot) { 6 | import.meta.hot.on("vite:beforeUpdate", () => { 7 | window.location.reload(); 8 | }); 9 | } 10 | 11 | const root = assertExists(document.getElementById("root")); 12 | createRoot(root).render(); 13 | -------------------------------------------------------------------------------- /src/octopus/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | octopus 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/scene/SortOrderProvider.ts: -------------------------------------------------------------------------------- 1 | import Component from "@/lib/scene/Component"; 2 | import Entity from "@/lib/scene/Entity"; 3 | 4 | export default class SortOrderProvider extends Component { 5 | constructor( 6 | entity: Entity, 7 | private getSortOrderFn: (entity: Entity) => number, 8 | ) { 9 | super(entity); 10 | } 11 | 12 | getSortOrder(): number { 13 | return this.getSortOrderFn(this.entity); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/gl/GlVertexArray.tsx: -------------------------------------------------------------------------------- 1 | import { Gl } from "@/lib/gl/Gl"; 2 | import { GlBuffer } from "@/lib/gl/GlBuffer"; 3 | 4 | export class GlVertexArray extends GlBuffer { 5 | constructor( 6 | gl: Gl, 7 | readonly vertexArray: WebGLVertexArrayObject, 8 | buffer: WebGLBuffer, 9 | ) { 10 | super(gl, buffer); 11 | } 12 | 13 | bindVao() { 14 | const { gl } = this.gl; 15 | gl.bindVertexArray(this.vertexArray); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/geometry/paths.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { SvgPathBuilder } from "@/lib/svgPathBuilder"; 3 | 4 | export const paths = { 5 | x(position: Vector2, size = 5) { 6 | const path = new SvgPathBuilder() 7 | .moveTo(position.add(size, size)) 8 | .lineTo(position.add(-size, -size)) 9 | .moveTo(position.add(-size, size)) 10 | .lineTo(position.add(size, -size)); 11 | 12 | return path.toString(); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/gl/GlBuffer.tsx: -------------------------------------------------------------------------------- 1 | import { Gl } from "@/lib/gl/Gl"; 2 | import { GlBufferUsage, glEnum } from "@/lib/gl/GlTypes"; 3 | 4 | export class GlBuffer { 5 | constructor( 6 | protected readonly gl: Gl, 7 | readonly buffer: WebGLBuffer, 8 | ) {} 9 | 10 | bufferData(data: BufferSource, usage: GlBufferUsage) { 11 | const { gl } = this.gl; 12 | gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); 13 | gl.bufferData(gl.ARRAY_BUFFER, data, glEnum(usage)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/signals/useSignal.tsx: -------------------------------------------------------------------------------- 1 | import { Signal } from "@/lib/signals/Signals"; 2 | import { useMemo } from "react"; 3 | import { useSubscription } from "use-subscription"; 4 | 5 | export default function useSignal(signal: Signal): number { 6 | return useSubscription( 7 | useMemo( 8 | () => ({ 9 | getCurrentValue: () => signal.read(), 10 | subscribe: (cb) => signal.manager.onUpdate(cb), 11 | }), 12 | [signal], 13 | ), 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/spline-time/layers/midPointMarkerLayer.tsx: -------------------------------------------------------------------------------- 1 | import { windows } from "@/lib/utils"; 2 | import { LineMarker } from "@/spline-time/guides"; 3 | import { LayerProps } from "@/spline-time/layers/Layer"; 4 | 5 | export function midPointMarkerLayer({ line, showExtras }: LayerProps) { 6 | if (!showExtras) return null; 7 | 8 | const midpoints = windows(line.points, 2).map(([p1, p2], i) => ( 9 | 10 | )); 11 | 12 | return <>{midpoints}; 13 | } 14 | -------------------------------------------------------------------------------- /src/pals/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | font-size: 16px; 6 | background: black; 7 | } 8 | 9 | *, 10 | *::before, 11 | *::after { 12 | box-sizing: border-box; 13 | padding: 0; 14 | margin: 0; 15 | } 16 | 17 | #root { 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | width: 100%; 22 | height: 100%; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | } 27 | 28 | canvas { 29 | background-color: #f8ffe5; 30 | } 31 | -------------------------------------------------------------------------------- /src/network/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | font-size: 16px; 6 | background: black; 7 | } 8 | 9 | *, 10 | *::before, 11 | *::after { 12 | box-sizing: border-box; 13 | padding: 0; 14 | margin: 0; 15 | } 16 | 17 | #root { 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | width: 100%; 22 | height: 100%; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | } 27 | 28 | canvas { 29 | background-color: #f8ffe5; 30 | } 31 | -------------------------------------------------------------------------------- /src/webgl/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | cat 6 | 7 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/lime/Inputs.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { memo, reactive } from "@/lib/signia"; 3 | import { Lime } from "@/lime/Lime"; 4 | 5 | export class Inputs { 6 | constructor(private readonly lime: Lime) {} 7 | 8 | @reactive accessor screenPointer = Vector2.ZERO; 9 | 10 | @memo get scenePointer() { 11 | return this.lime.viewport.screenToSlide(this.screenPointer); 12 | } 13 | 14 | updateFromEvent(event: PointerEvent) { 15 | this.screenPointer = Vector2.fromEvent(event); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/wires/wires-main.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { WiresAppRenderer } from "@/wires/WiresApp2"; 3 | import React from "react"; 4 | import { createRoot } from "react-dom/client"; 5 | 6 | if (import.meta.hot) { 7 | import.meta.hot.on("vite:beforeUpdate", () => { 8 | window.location.reload(); 9 | }); 10 | } 11 | 12 | const root = assertExists(document.getElementById("root")); 13 | createRoot(root).render( 14 | 15 | 16 | , 17 | ); 18 | -------------------------------------------------------------------------------- /src/spline-time/spline-time-main.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { SplineTime } from "@/spline-time/SplineTime"; 3 | import React from "react"; 4 | import { createRoot } from "react-dom/client"; 5 | 6 | if (import.meta.hot) { 7 | import.meta.hot.on("vite:beforeUpdate", () => { 8 | window.location.reload(); 9 | }); 10 | } 11 | 12 | const root = assertExists(document.getElementById("root")); 13 | createRoot(root).render( 14 | 15 | 16 | , 17 | ); 18 | -------------------------------------------------------------------------------- /src/blob-tree/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | blob-tree 6 | 7 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/blob-factory/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | blob factory 6 | 7 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/lib/debugPropsToString.tsx: -------------------------------------------------------------------------------- 1 | import { entries, ObjectMap } from "@/lib/utils"; 2 | 3 | export type DebugProps = ObjectMap< 4 | string, 5 | string | number | boolean | null | undefined 6 | >; 7 | 8 | export function debugPropsToString(props: DebugProps) { 9 | return entries(props) 10 | .filter(([k, v]) => v !== undefined) 11 | .map(([k, v]) => (k.startsWith("_") ? v : `${k} = ${v}`)) 12 | .join(", "); 13 | } 14 | 15 | export function debugStateToString(name: string, props: DebugProps = {}) { 16 | return `${name}(${debugPropsToString(props)})`; 17 | } 18 | -------------------------------------------------------------------------------- /src/wires/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | wires 6 | 7 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/splatapus/model/SplatDocModel.test.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { SplatKeyPointId, SplatShapeId } from "@/splatapus/model/SplatDoc"; 3 | import { SplatDocModel } from "@/splatapus/model/SplatDocModel"; 4 | import { test } from "vitest"; 5 | 6 | test("create doc model", () => { 7 | const keyPointId = SplatKeyPointId.generate(); 8 | const shapeId = SplatShapeId.generate(); 9 | SplatDocModel.create() 10 | .addKeyPoint(keyPointId, Vector2.ZERO) 11 | .addShape(shapeId) 12 | .replacePointsForVersion(keyPointId, shapeId, []); 13 | }); 14 | -------------------------------------------------------------------------------- /src/lime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | lime: line motion 6 | 7 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/splatapus/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | splatapus 6 | 7 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/splatapus2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | splatapus 6 | 7 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/splatapus3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | splatapus 6 | 7 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/gestureland/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | splatapus 6 | 7 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/spline-time/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | spline-time 6 | 7 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lime/LimeConfig.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { StrokeOptions } from "@/splatapus/model/perfectFreehand"; 3 | 4 | const LIME_SLIDE_WIDTH_PX = 1080; 5 | const LIME_SLIDE_HEIGHT_PX = LIME_SLIDE_WIDTH_PX / (16 / 9); 6 | export const LIME_SLIDE_SIZE_PX = new Vector2( 7 | LIME_SLIDE_WIDTH_PX, 8 | LIME_SLIDE_HEIGHT_PX, 9 | ); 10 | export const LIME_SLIDE_PADDING_PX = new Vector2(32, 32); 11 | 12 | export const LIME_SIDEBAR_WIDTH_PX = 200; 13 | 14 | export const LIME_FREEHAND: Partial = { 15 | size: 16, 16 | thinning: 0.5, 17 | smoothing: 0.5, 18 | last: true, 19 | }; 20 | -------------------------------------------------------------------------------- /src/emoji/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | emoji 6 | 7 | 11 | 12 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/lib/fakeConsole.ts: -------------------------------------------------------------------------------- 1 | import { noop } from "@/lib/utils"; 2 | 3 | export const fakeConsole: Console = { 4 | assert: noop, 5 | clear: noop, 6 | count: noop, 7 | countReset: noop, 8 | debug: noop, 9 | dir: noop, 10 | dirxml: noop, 11 | error: noop, 12 | group: noop, 13 | groupCollapsed: noop, 14 | groupEnd: noop, 15 | info: noop, 16 | log: noop, 17 | table: noop, 18 | time: noop, 19 | timeEnd: noop, 20 | timeLog: noop, 21 | timeStamp: noop, 22 | trace: noop, 23 | warn: noop, 24 | profile: noop, 25 | profileEnd: noop, 26 | Console: console.Console, 27 | }; 28 | -------------------------------------------------------------------------------- /src/trees/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | trees 6 | 7 | 11 | 12 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/geometry/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | geometry 6 | 7 | 11 | 12 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/palette/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | color palette 6 | 7 | 11 | 12 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/splatapus/splatapus-main.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { App } from "@/splatapus/App"; 3 | import { USE_REACT_STRICT_MODE } from "@/splatapus/constants"; 4 | import React from "react"; 5 | import { createRoot } from "react-dom/client"; 6 | 7 | if (import.meta.hot) { 8 | import.meta.hot.on("vite:beforeUpdate", () => { 9 | window.location.reload(); 10 | }); 11 | } 12 | 13 | const root = assertExists(document.getElementById("root")); 14 | createRoot(root).render( 15 | USE_REACT_STRICT_MODE ? 16 | 17 | 18 | 19 | : , 20 | ); 21 | -------------------------------------------------------------------------------- /src/lime/lime-main.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { LimeApp } from "@/lime/LimeApp"; 3 | import { USE_REACT_STRICT_MODE } from "@/splatapus/constants"; 4 | import React from "react"; 5 | import { createRoot } from "react-dom/client"; 6 | 7 | if (import.meta.hot) { 8 | import.meta.hot.on("vite:beforeUpdate", () => { 9 | window.location.reload(); 10 | }); 11 | } 12 | 13 | const root = assertExists(document.getElementById("root")); 14 | createRoot(root).render( 15 | USE_REACT_STRICT_MODE ? 16 | 17 | 18 | 19 | : , 20 | ); 21 | -------------------------------------------------------------------------------- /src/splatapus2/splatapus2-main.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { USE_REACT_STRICT_MODE } from "@/splatapus/constants"; 3 | import { App } from "@/splatapus2/App"; 4 | import React from "react"; 5 | import { createRoot } from "react-dom/client"; 6 | 7 | if (import.meta.hot) { 8 | import.meta.hot.on("vite:beforeUpdate", () => { 9 | window.location.reload(); 10 | }); 11 | } 12 | 13 | const root = assertExists(document.getElementById("root")); 14 | createRoot(root).render( 15 | USE_REACT_STRICT_MODE ? 16 | 17 | 18 | 19 | : , 20 | ); 21 | -------------------------------------------------------------------------------- /src/splatapus3/splatapus3-main.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { USE_REACT_STRICT_MODE } from "@/splatapus/constants"; 3 | import { App } from "@/splatapus3/App"; 4 | import React from "react"; 5 | import { createRoot } from "react-dom/client"; 6 | 7 | if (import.meta.hot) { 8 | import.meta.hot.on("vite:beforeUpdate", () => { 9 | window.location.reload(); 10 | }); 11 | } 12 | 13 | const root = assertExists(document.getElementById("root")); 14 | createRoot(root).render( 15 | USE_REACT_STRICT_MODE ? 16 | 17 | 18 | 19 | : , 20 | ); 21 | -------------------------------------------------------------------------------- /src/palette/palette-main.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { PaletteApp } from "@/palette/PaletteApp"; 3 | import { SupportProvider } from "@/palette/support"; 4 | import React from "react"; 5 | import { createRoot } from "react-dom/client"; 6 | 7 | if (import.meta.hot) { 8 | import.meta.hot.on("vite:beforeUpdate", () => { 9 | window.location.reload(); 10 | }); 11 | } 12 | 13 | const root = assertExists(document.getElementById("root")); 14 | createRoot(root).render( 15 | 16 | 17 | 18 | 19 | , 20 | ); 21 | -------------------------------------------------------------------------------- /src/splatapus/examples/examples.test.tsx: -------------------------------------------------------------------------------- 1 | import { splatapusStateSchema } from "@/splatapus/model/store"; 2 | import { readFileSync, readdirSync } from "fs"; 3 | import path from "path"; 4 | import { test } from "vitest"; 5 | 6 | const exampleNames = readdirSync(__dirname).filter((file) => 7 | file.endsWith(".json"), 8 | ); 9 | 10 | for (const exampleName of exampleNames) { 11 | console.log({ exampleName }); 12 | test(`example: ${exampleName}`, () => { 13 | const example = JSON.parse( 14 | readFileSync(path.join(__dirname, exampleName), "utf8"), 15 | ); 16 | splatapusStateSchema.parse(example).unwrap(); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/emoji/emoji-main.tsx: -------------------------------------------------------------------------------- 1 | import { EmojiListing } from "@/emoji/EmojiListing"; 2 | import { PostDemo } from "@/emoji/PostDemo"; 3 | import { assertExists } from "@/lib/assert"; 4 | import { createRoot } from "react-dom/client"; 5 | import { RouterProvider, createHashRouter } from "react-router-dom"; 6 | 7 | const router = createHashRouter([ 8 | { 9 | path: "/emoji", 10 | element: , 11 | }, 12 | { 13 | path: "*", 14 | element: , 15 | }, 16 | ]); 17 | console.log(router); 18 | 19 | const root = assertExists(document.getElementById("root")); 20 | createRoot(root).render(); 21 | -------------------------------------------------------------------------------- /src/splatapus/constants.tsx: -------------------------------------------------------------------------------- 1 | import { StrokeOptions } from "@/splatapus/model/perfectFreehand"; 2 | 3 | export const USE_REACT_STRICT_MODE = true; 4 | 5 | export const SIDEBAR_WIDTH_PX = 300; 6 | 7 | export const LOAD_FROM_AUTOSAVE_ENABLED = true; 8 | export const UNDO_ACTIONS = 30; 9 | export const AUTOSAVE_DEBOUNCE_TIME_MS = 500; 10 | 11 | export const MIN_DRAG_GESTURE_DISTANCE_PX = 10; 12 | 13 | export const perfectFreehandOpts: Required = { 14 | size: 16, 15 | streamline: 0.5, 16 | smoothing: 0.5, 17 | thinning: 0.5, 18 | simulatePressure: true, 19 | easing: (t) => t, 20 | start: {}, 21 | end: {}, 22 | last: false, 23 | }; 24 | -------------------------------------------------------------------------------- /src/worms/canvas.ts: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { DebugDraw } from "@/lib/DebugDraw"; 3 | 4 | export const canvasEl = document.createElement("canvas"); 5 | export const ctx = assertExists(canvasEl.getContext("2d")); 6 | export const width = document.body.clientWidth; 7 | export const height = document.body.clientHeight; 8 | export const scale = window.devicePixelRatio; 9 | 10 | canvasEl.width = width * scale; 11 | canvasEl.height = height * scale; 12 | canvasEl.style.width = `${width}px`; 13 | canvasEl.style.height = `${height}px`; 14 | ctx.scale(scale, scale); 15 | export const canvas = new DebugDraw(ctx); 16 | document.body.appendChild(canvasEl); 17 | -------------------------------------------------------------------------------- /src/blob-tree/canvas.ts: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { DebugDraw } from "@/lib/DebugDraw"; 3 | 4 | export const canvasEl = document.createElement("canvas"); 5 | export const ctx = assertExists(canvasEl.getContext("2d")); 6 | export const width = document.body.clientWidth; 7 | export const height = document.body.clientHeight; 8 | export const scale = window.devicePixelRatio; 9 | 10 | canvasEl.width = width * scale; 11 | canvasEl.height = height * scale; 12 | canvasEl.style.width = `${width}px`; 13 | canvasEl.style.height = `${height}px`; 14 | ctx.scale(scale, scale); 15 | export const canvas = new DebugDraw(ctx); 16 | document.body.appendChild(canvasEl); 17 | -------------------------------------------------------------------------------- /src/wiggle-gradient/canvas.ts: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { DebugDraw } from "@/lib/DebugDraw"; 3 | 4 | export const canvasEl = document.createElement("canvas"); 5 | export const ctx = assertExists(canvasEl.getContext("2d")); 6 | export const width = document.body.clientWidth; 7 | export const height = document.body.clientHeight; 8 | export const scale = window.devicePixelRatio; 9 | 10 | canvasEl.width = width * scale; 11 | canvasEl.height = height * scale; 12 | canvasEl.style.width = `${width}px`; 13 | canvasEl.style.height = `${height}px`; 14 | ctx.scale(scale, scale); 15 | export const canvas = new DebugDraw(ctx); 16 | document.body.appendChild(canvasEl); 17 | -------------------------------------------------------------------------------- /src/splatapus3/Viewport.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { reactive } from "@/lib/signia"; 3 | import { Splat } from "@/splatapus3/model/Splat"; 4 | 5 | export class Viewport { 6 | constructor(readonly splat: Splat) {} 7 | 8 | @reactive accessor screenSize = Vector2.ZERO; 9 | 10 | get pan() { 11 | return this.splat.location.position; 12 | } 13 | set pan(newPan: Vector2) { 14 | this.splat.updateLocation({ position: newPan }); 15 | } 16 | 17 | get zoom() { 18 | return this.splat.location.zoom; 19 | } 20 | set zoom(newZoom: number) { 21 | this.splat.updateLocation({ zoom: newZoom }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/terrain/canvas.ts: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { DebugDraw } from "@/lib/DebugDraw"; 3 | import * as config from "@/terrain/config"; 4 | 5 | export const canvasEl = document.createElement("canvas"); 6 | export const ctx = assertExists(canvasEl.getContext("2d")); 7 | export const width = config.SIZE; 8 | export const height = config.SIZE; 9 | export const scale = window.devicePixelRatio; 10 | 11 | canvasEl.width = width * scale; 12 | canvasEl.height = height * scale; 13 | canvasEl.style.width = `${width}px`; 14 | canvasEl.style.height = `${height}px`; 15 | ctx.scale(scale, scale); 16 | export const canvas = new DebugDraw(ctx); 17 | document.body.appendChild(canvasEl); 18 | -------------------------------------------------------------------------------- /src/lib/hooks/useMergedRefs.tsx: -------------------------------------------------------------------------------- 1 | import { Ref, useCallback } from "react"; 2 | 3 | export function assignRef(ref: Ref, instance: T) { 4 | if (!ref) { 5 | return; 6 | } 7 | if (typeof ref === "function") { 8 | ref(instance); 9 | } else { 10 | // @ts-expect-error react's ref types are funky imo 11 | ref.current = instance; 12 | } 13 | } 14 | 15 | export function useMergedRefs( 16 | a: Ref, 17 | b: Ref, 18 | ): (instance: T | null) => void { 19 | return useCallback( 20 | (instance) => { 21 | assignRef(a, instance); 22 | assignRef(b, instance); 23 | }, 24 | [a, b], 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/blob-factory/shader.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | // an attribute is an input (in) to a vertex shader. 4 | // It will receive data from a buffer 5 | in vec2 a_position; 6 | 7 | uniform vec2 u_resolution; 8 | 9 | out vec2 screenPosition; 10 | 11 | // all shaders have a main function 12 | void main() { 13 | screenPosition = a_position; 14 | 15 | // convert the position from pixels to 0.0 to 1.0 16 | vec2 zeroToOne = a_position / u_resolution; 17 | 18 | // convert from 0->1 to 0->2 19 | vec2 zeroToTwo = zeroToOne * 2.0; 20 | 21 | // convert from 0->2 to -1->+1 (clip space) 22 | vec2 clipSpace = zeroToTwo - 1.0; 23 | 24 | gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/RandomQueue.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "@/lib/assert"; 2 | import { random } from "@/lib/utils"; 3 | 4 | export default class RandomQueue { 5 | private items: T[] = []; 6 | 7 | constructor(items: T[] = []) { 8 | this.items = items.slice(); 9 | } 10 | 11 | add(item: T): void { 12 | this.items.push(item); 13 | } 14 | 15 | get size(): number { 16 | return this.items.length; 17 | } 18 | 19 | pop(): T { 20 | assert(this.size, "RandomQueue must not be empty"); 21 | const index = Math.floor(random(this.items.length)); 22 | const item = this.items[index]; 23 | this.items.splice(index, 1); 24 | return item; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/hooks/useMediaQuery.tsx: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useState } from "react"; 2 | 3 | export function useMediaQuery(query: string): boolean | null { 4 | const [matches, setMatches] = useState(null); 5 | 6 | useLayoutEffect(() => { 7 | const mediaQueryList = window.matchMedia(query); 8 | setMatches(mediaQueryList.matches); 9 | 10 | const listener = (event: MediaQueryListEvent) => { 11 | setMatches(event.matches); 12 | }; 13 | 14 | mediaQueryList.addEventListener("change", listener); 15 | 16 | return () => { 17 | mediaQueryList.removeEventListener("change", listener); 18 | }; 19 | }, [query]); 20 | 21 | return matches; 22 | } 23 | -------------------------------------------------------------------------------- /src/infinite-scroll/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | infinite scroll 6 | 7 | 14 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/splatapus/editor/modes/Mode.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { Schema } from "@/lib/schema"; 3 | import { PointerEventContext } from "@/splatapus/editor/EventContext"; 4 | import { Splatapus } from "@/splatapus/editor/useEditor"; 5 | import { ReactNode } from "react"; 6 | 7 | export enum ModeType { 8 | Draw = "draw", 9 | Rig = "rig", 10 | Play = "play", 11 | } 12 | export const modeTypeSchema = Schema.enum(ModeType); 13 | 14 | export interface Mode { 15 | readonly type: Type; 16 | 17 | isIdleLive(): boolean; 18 | toDebugStringLive(): string; 19 | onPointerEvent(ctx: PointerEventContext): void; 20 | getPreviewPositionLive(): Vector2 | null; 21 | renderOverlay(splatapus: Splatapus): ReactNode; 22 | } 23 | -------------------------------------------------------------------------------- /src/splatapus2/app/Inputs.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { atom } from "@tldraw/state"; 3 | 4 | export class Inputs { 5 | private readonly _pointer = atom("Inputs.pointer", Vector2.ZERO); 6 | 7 | onPointerMove(e: React.PointerEvent) { 8 | if (e.isPrimary) { 9 | this._pointer.set(Vector2.fromEvent(e)); 10 | } 11 | } 12 | onPointerDown(e: React.PointerEvent) { 13 | if (e.isPrimary) { 14 | this._pointer.set(Vector2.fromEvent(e)); 15 | } 16 | } 17 | onPointerUp(e: React.PointerEvent) { 18 | if (e.isPrimary) { 19 | this._pointer.set(Vector2.fromEvent(e)); 20 | } 21 | } 22 | 23 | get pointer() { 24 | return this._pointer.get(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/terrain/makeInterpolateGradient.ts: -------------------------------------------------------------------------------- 1 | import { invLerp } from "@/lib/utils"; 2 | import { interpolate } from "d3-interpolate"; 3 | 4 | export default function makeInterpolateGradient( 5 | stops: { color: string; stop: number }[], 6 | ) { 7 | return (n: number): string => { 8 | if (n <= 0) { 9 | return stops[0].color; 10 | } 11 | if (n >= 1) { 12 | return stops[stops.length - 1].color; 13 | } 14 | 15 | const startIndex = stops.findIndex(({ stop }) => stop >= n); 16 | const start = stops[startIndex - 1]; 17 | const end = stops[startIndex]; 18 | 19 | return interpolate( 20 | start.color, 21 | end.color, 22 | )(invLerp(start.stop, end.stop, n)); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/wires/inputs.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { atom } from "@tldraw/state"; 3 | 4 | export class Inputs { 5 | pointer = atom("Inputs.pointer", Vector2.ZERO); 6 | 7 | events = { 8 | onPointerMove: (e: React.PointerEvent) => { 9 | if (e.isPrimary) { 10 | this.pointer.set(Vector2.fromEvent(e)); 11 | } 12 | }, 13 | onPointerDown: (e: React.PointerEvent) => { 14 | if (e.isPrimary) { 15 | this.pointer.set(Vector2.fromEvent(e)); 16 | } 17 | }, 18 | onPointerUp: (e: React.PointerEvent) => { 19 | if (e.isPrimary) { 20 | this.pointer.set(Vector2.fromEvent(e)); 21 | } 22 | }, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/network/networkNodes/NetworkNode.ts: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Vector2 } from "@/lib/geom/Vector2"; 3 | import ConnectionDirection from "@/network/ConnectionDirection"; 4 | import Road from "@/network/Road"; 5 | import Traveller from "@/network/Traveller"; 6 | 7 | export interface NetworkNode { 8 | readonly canConsumeTraveller: boolean; 9 | readonly incomingConnections: readonly Road[]; 10 | readonly isDestination: boolean; 11 | readonly outgoingConnections: readonly Road[]; 12 | readonly position: Vector2; 13 | connectTo(target: Road, direction: ConnectionDirection): void; 14 | consumeTraveller(traveller: Traveller): void; 15 | getAllReachableNodes(visitedNodes?: Set): NetworkNode[]; 16 | getVisualConnectionPointAtAngle(radians: number): Vector2; 17 | } 18 | -------------------------------------------------------------------------------- /src/trees/trees-main.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { LeafPatternApp } from "@/trees/LeafPattern"; 3 | import { TreesApp } from "@/trees/TreesApp"; 4 | import { createRoot } from "react-dom/client"; 5 | import { RouterProvider, createHashRouter } from "react-router-dom"; 6 | 7 | if (import.meta.hot) { 8 | import.meta.hot.on("vite:beforeUpdate", () => { 9 | window.location.reload(); 10 | }); 11 | } 12 | 13 | const router = createHashRouter([ 14 | { 15 | path: "/patterns", 16 | element: , 17 | }, 18 | { 19 | path: "*", 20 | element: , 21 | }, 22 | ]); 23 | 24 | const root = assertExists(document.getElementById("root")); 25 | createRoot(root).render(); 26 | -------------------------------------------------------------------------------- /src/blob-factory/blob-factory-main.tsx: -------------------------------------------------------------------------------- 1 | import { BlobFactoryRenderer } from "@/blob-factory/BlobFactory"; 2 | import { assertExists } from "@/lib/assert"; 3 | import { 4 | sizeFromContentRect, 5 | useResizeObserver, 6 | } from "@/lib/hooks/useResizeObserver"; 7 | import { useState } from "react"; 8 | import { createRoot } from "react-dom/client"; 9 | 10 | createRoot(assertExists(document.getElementById("root"))).render(); 11 | 12 | function App() { 13 | const [container, setContainer] = useState(null); 14 | const size = useResizeObserver(container, sizeFromContentRect); 15 | 16 | return ( 17 |
18 | {size && } 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/bees/AnimatedSpriteStack.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatedSpriteStackSheet } from "@/bees/AnimatedSpriteStackSheet"; 2 | import { Driver } from "@/bees/driver"; 3 | import { Sprite } from "pixi.js"; 4 | 5 | export class AnimatedSpriteStack extends Sprite { 6 | public heading = 0; 7 | 8 | constructor( 9 | public readonly sheet: AnimatedSpriteStackSheet, 10 | private readonly driver: Driver, 11 | ) { 12 | super(sheet.getFrameAtAngle(0, 0)); 13 | driver.addUpdate(this); 14 | } 15 | 16 | updateTick(elapsedTimeMs: number) { 17 | const newTexture = this.sheet.getElapsedTimeAtAngle( 18 | this.heading, 19 | elapsedTimeMs, 20 | ); 21 | if (newTexture !== this.texture) { 22 | this.texture = newTexture; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/hooks/useEvent.tsx: -------------------------------------------------------------------------------- 1 | import { assert } from "@/lib/assert"; 2 | import { useCallback, useDebugValue, useLayoutEffect, useRef } from "react"; 3 | 4 | export function useEvent( 5 | handler: (...args: Args) => Result, 6 | ): (...args: Args) => Result { 7 | const handlerRef = useRef<(...args: Args) => Result>(); 8 | 9 | // In a real implementation, this would run before layout effects 10 | useLayoutEffect(() => { 11 | handlerRef.current = handler; 12 | }); 13 | 14 | useDebugValue(handler); 15 | 16 | return useCallback((...args: Args) => { 17 | // In a real implementation, this would throw if called during render 18 | const fn = handlerRef.current; 19 | assert(fn, "fn does not exist"); 20 | return fn(...args); 21 | }, []); 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/scene/Component.ts: -------------------------------------------------------------------------------- 1 | import Entity from "@/lib/scene/Entity"; 2 | import Scene from "@/lib/scene/Scene"; 3 | 4 | export default abstract class Component { 5 | readonly entity: Entity; 6 | constructor(entity: Entity) { 7 | this.entity = entity; 8 | } 9 | 10 | onRemove() {} 11 | 12 | onAddedToScene(scene: Scene) {} 13 | 14 | onRemovedFromScene(scene: Scene) {} 15 | 16 | beforeUpdate(delta: number) {} 17 | 18 | update(delta: number) {} 19 | 20 | afterUpdate(delta: number) {} 21 | 22 | beforeDraw(ctx: CanvasRenderingContext2D, time: number) {} 23 | 24 | draw(ctx: CanvasRenderingContext2D, time: number) {} 25 | 26 | afterDraw(ctx: CanvasRenderingContext2D, time: number) {} 27 | 28 | getScene(): Scene { 29 | return this.entity.getScene(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/terrain/config.ts: -------------------------------------------------------------------------------- 1 | export const SIZE = Math.max(window.innerWidth, window.innerHeight); 2 | 3 | export const POINT_SPACING = 5; 4 | export const TARGET_CELLS_PER_PLATE = 1600; 5 | 6 | export const MIN_PLATE_BASE_HEIGHT = -0.9; 7 | export const MAX_PLATE_BASE_HEIGHT = 0.45; 8 | 9 | export const CELL_HEIGHT_NOISE_AMT = 0.3; 10 | export const CELL_NOISE_SCALE = (1 / POINT_SPACING) * 0.1; 11 | 12 | export const CELL_SMOOTH_MIN_RADIUS = POINT_SPACING * 10; 13 | export const CELL_SMOOTH_MAX_RADIUS = POINT_SPACING * 20; 14 | export const CELL_SMOOTH_NOISE_SCALE = (1 / POINT_SPACING) * 0.9; 15 | 16 | export const MIN_TECTONIC_DRIFT = 0.5; 17 | export const MAX_TECTONIC_DRIFT = 1; 18 | export const MIN_DRIFT_MAGNITUDE_TO_PROPAGATE = 0.005; 19 | export const DRIFT_NOISE_SCALE = (1 / POINT_SPACING) * 0.1; 20 | export const DRIFT_NOISE_AMT = 1; 21 | -------------------------------------------------------------------------------- /src/lib/EventIterator.tsx: -------------------------------------------------------------------------------- 1 | import EventEmitter from "@/lib/EventEmitter"; 2 | 3 | type Event = { type: "event"; value: T } | { type: "end" }; 4 | export class EventIterator implements AsyncIterable { 5 | private events = new EventEmitter<[Event]>(); 6 | private isDone = false; 7 | 8 | push(event: T) { 9 | this.events.emit({ type: "event", value: event }); 10 | } 11 | 12 | end() { 13 | this.events.emit({ type: "end" }); 14 | this.isDone = true; 15 | } 16 | 17 | async *[Symbol.asyncIterator]() { 18 | if (this.isDone) return; 19 | for await (const event of this.events) { 20 | if (event.type === "end") { 21 | return; 22 | } 23 | if (this.isDone) return; 24 | yield event.value; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/txdraw/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | txdraw 6 | 7 | 11 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "downlevelIteration": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "isolatedModules": true, 7 | "jsx": "preserve", 8 | "lib": ["es2019", "dom"], 9 | "module": "es2020", 10 | "moduleResolution": "node", 11 | "noEmit": true, 12 | "noImplicitOverride": true, 13 | "skipLibCheck": true, 14 | "strict": true, 15 | "target": "es2019", 16 | "paths": { 17 | "@/*": ["./src/*"] 18 | }, 19 | "types": ["vitest/importMeta", "webgl-strict-types", "react/canary"] 20 | }, 21 | "include": [ 22 | "src/**/*", 23 | "types/**/*", 24 | "vite.config.ts", 25 | "tailwind.config.js", 26 | "scripts/**/*" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/signals/startSignalsApp.tsx: -------------------------------------------------------------------------------- 1 | import { assert } from "@/lib/assert"; 2 | import { getListenToMidiInput } from "@/lib/midi"; 3 | import { SignalManager } from "@/lib/signals/Signals"; 4 | import SignalsCanvas, { SignalsCanvasScene } from "@/lib/signals/SignalsCanvas"; 5 | import { createRoot } from "react-dom/client"; 6 | 7 | export default async function startSignalsApp( 8 | scene: SignalsCanvasScene, 9 | debuggerEnabled: boolean, 10 | ) { 11 | const listenToMidi = await getListenToMidiInput(); 12 | 13 | const s = new SignalManager(); 14 | const root = document.getElementById("root"); 15 | assert(root); 16 | 17 | createRoot(root).render( 18 | , 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/worms/colors.ts: -------------------------------------------------------------------------------- 1 | import Color from "color"; 2 | 3 | // // https://coolors.co/f2dc5d-f2a359-db9065-a4031f-240b36 4 | // export const BG = new Color('#240b36'); 5 | // export const SNAKES = [ 6 | // new Color('#f2dc5d'), 7 | // new Color('#f2a359'), 8 | // new Color('#db9065'), 9 | // new Color('#a4031f'), 10 | // ]; 11 | 12 | // https://coolors.co/ba2d0b-d5f2e3-73ba9b-003e1f-01110a 13 | // export const BG = new Color('#01110a'); 14 | 15 | // export const SNAKES = [ 16 | // new Color('#ba2d0b'), 17 | // new Color('#d5f2e3'), 18 | // new Color('#73ba9b'), 19 | // new Color('#003e1f'), 20 | // ]; 21 | 22 | // https://coolors.co/f4e04d-f2ed6f-cee397-8db1ab-587792 23 | export const BG = new Color("#E2ECC9"); 24 | export const SNAKES = [ 25 | new Color("#C33C54"), 26 | new Color("#bfb915"), 27 | new Color("#96ba3b"), 28 | new Color("#39847f"), 29 | new Color("#12416b"), 30 | ]; 31 | -------------------------------------------------------------------------------- /src/bees/Sprite.tsx: -------------------------------------------------------------------------------- 1 | import { C, SpriteOpts } from "@/bees/C"; 2 | import { loadImage } from "@/lib/load"; 3 | 4 | export interface SpriteManifest { 5 | src: URL; 6 | scale: number; 7 | originX: number; 8 | originY: number; 9 | } 10 | 11 | export class Sprite { 12 | static async load(manifest: SpriteManifest) { 13 | const image = await loadImage(manifest.src); 14 | return new Sprite(image, manifest); 15 | } 16 | 17 | constructor( 18 | private readonly image: HTMLImageElement, 19 | private readonly manifest: Omit, 20 | ) {} 21 | 22 | draw(c: C, opts?: Omit) { 23 | c.drawSprite( 24 | this.image, 25 | -this.image.naturalWidth * this.manifest.originX, 26 | -this.image.naturalHeight * this.manifest.originY, 27 | { ...opts, pixelScale: this.manifest.scale }, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/omitFromStackTrace.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * When a function is wrapped in `omitFromStackTrace`, if it throws an error the 3 | * stack trace won't include the function itself or any stack frames above it. 4 | * Useful for assertion-style function where the error will ideally originate 5 | * from the call-site rather than within the implementation of the assert fn. 6 | * 7 | * Only works in platforms that support `Error.captureStackTrace` (ie v8). 8 | */ 9 | export function omitFromStackTrace( 10 | fn: (...args: Args) => Return, 11 | ): (...args: Args) => Return { 12 | const wrappedFn = (...args: Args) => { 13 | try { 14 | return fn(...args); 15 | } catch (error) { 16 | if (error instanceof Error && Error.captureStackTrace) { 17 | Error.captureStackTrace(error, wrappedFn); 18 | } 19 | throw error; 20 | } 21 | }; 22 | 23 | return wrappedFn; 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/gl/GlShader.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists, fail } from "@/lib/assert"; 2 | import { Gl } from "@/lib/gl/Gl"; 3 | import { GlShaderType, glEnum } from "@/lib/gl/GlTypes"; 4 | 5 | export class GlShader { 6 | readonly shader: WebGLShader; 7 | private readonly gl: Gl; 8 | 9 | constructor( 10 | _gl: Gl, 11 | readonly type: GlShaderType, 12 | source: string, 13 | ) { 14 | this.gl = _gl; 15 | const { gl } = _gl; 16 | const shader = assertExists(gl.createShader(glEnum(type))); 17 | gl.shaderSource(shader, source); 18 | gl.compileShader(shader); 19 | const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); 20 | if (!success) { 21 | const error = `Failed to compile shader: ${gl.getShaderInfoLog( 22 | shader, 23 | )}`; 24 | gl.deleteShader(shader); 25 | fail(error); 26 | } 27 | this.shader = shader; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/load.tsx: -------------------------------------------------------------------------------- 1 | import { assert } from "@/lib/assert"; 2 | import { Schema } from "@/lib/schema"; 3 | import { promiseFromEvents } from "@/lib/utils"; 4 | 5 | export async function loadImage(src: URL): Promise { 6 | const image = new Image(); 7 | const promise = promiseFromEvents( 8 | (resolve) => image.addEventListener("load", resolve, { once: true }), 9 | (reject) => image.addEventListener("error", reject, { once: true }), 10 | ); 11 | image.src = src.toString(); 12 | await promise; 13 | return image; 14 | } 15 | 16 | export async function loadJson(src: URL): Promise { 17 | console.log("url"); 18 | const response = await fetch(src.toString()); 19 | console.log({ src, response }); 20 | assert(response.ok, `${src.toString()} OK`); 21 | return response.json(); 22 | } 23 | 24 | export async function loadAndParseJson( 25 | src: URL, 26 | schema: Schema, 27 | ): Promise { 28 | return schema.parse(await loadJson(src)).unwrap(src.toString()); 29 | } 30 | -------------------------------------------------------------------------------- /.yarn/patches/tailwindcss-npm-3.4.10-32443f8dff.patch: -------------------------------------------------------------------------------- 1 | diff --git a/lib/css/preflight.css b/lib/css/preflight.css 2 | index 7a0d82d46e040f2bf7b133e47380b1dc599e855c..d6a4897ebec3d830320518b5763c9c65ae786ee7 100644 3 | --- a/lib/css/preflight.css 4 | +++ b/lib/css/preflight.css 5 | @@ -340,10 +340,10 @@ textarea::placeholder { 6 | Set the default cursor for buttons. 7 | */ 8 | 9 | -button, 10 | +/* button, 11 | [role="button"] { 12 | cursor: pointer; 13 | -} 14 | +} */ 15 | 16 | /* 17 | Make sure disabled buttons don't get the pointer cursor. 18 | diff --git a/src/css/preflight.css b/src/css/preflight.css 19 | index 7a0d82d46e040f2bf7b133e47380b1dc599e855c..d6a4897ebec3d830320518b5763c9c65ae786ee7 100644 20 | --- a/src/css/preflight.css 21 | +++ b/src/css/preflight.css 22 | @@ -340,10 +340,10 @@ textarea::placeholder { 23 | Set the default cursor for buttons. 24 | */ 25 | 26 | -button, 27 | +/* button, 28 | [role="button"] { 29 | cursor: pointer; 30 | -} 31 | +} */ 32 | 33 | /* 34 | Make sure disabled buttons don't get the pointer cursor. 35 | -------------------------------------------------------------------------------- /src/blob-tree/blob-tree-main.tsx: -------------------------------------------------------------------------------- 1 | import { BlobTree } from "@/blob-tree/BlobTree"; 2 | import { BlobTreeEditor } from "@/blob-tree/BlobTreeEditor"; 3 | import { canvas } from "@/blob-tree/canvas"; 4 | import { Vector2 } from "@/lib/geom/Vector2"; 5 | import { frameLoop } from "@/lib/utils"; 6 | 7 | const editor = new BlobTreeEditor(canvas, new BlobTree()); 8 | 9 | document.addEventListener("mousemove", (event) => { 10 | editor.onMouseMove(new Vector2(event.clientX, event.clientY)); 11 | }); 12 | document.addEventListener("mousedown", (event) => { 13 | editor.onMouseDown(new Vector2(event.clientX, event.clientY)); 14 | }); 15 | document.addEventListener("mouseup", (event) => { 16 | editor.onMouseUp(new Vector2(event.clientX, event.clientY)); 17 | }); 18 | document.addEventListener("keydown", (event) => { 19 | editor.onKeyDown(event.key); 20 | }); 21 | 22 | frameLoop(() => { 23 | editor.tick(); 24 | editor.draw(); 25 | }); 26 | 27 | if ((module as any).hot) { 28 | (module as any).hot.dispose(() => window.location.reload()); 29 | } 30 | -------------------------------------------------------------------------------- /src/palette/support.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { useMediaQuery } from "@/lib/hooks/useMediaQuery"; 3 | import { createContext, useContext } from "react"; 4 | 5 | interface Support { 6 | p3Gamut: boolean; 7 | rec2020Gamut: boolean; 8 | oklchSyntax: boolean; 9 | } 10 | 11 | const SupportContext = createContext(null); 12 | 13 | export function SupportProvider({ children }: { children: React.ReactNode }) { 14 | const p3Gamut = useMediaQuery("(color-gamut: p3)"); 15 | const rec2020Gamut = useMediaQuery("(color-gamut: rec2020)"); 16 | const oklchSyntax = CSS.supports("color", "oklch(0 0 0)"); 17 | 18 | if (p3Gamut === null || rec2020Gamut === null) { 19 | return null; 20 | } 21 | 22 | return ( 23 | 24 | {children} 25 | 26 | ); 27 | } 28 | 29 | export function useSupport(): Support { 30 | return assertExists(useContext(SupportContext)); 31 | } 32 | -------------------------------------------------------------------------------- /src/terrain/fractalNoise.ts: -------------------------------------------------------------------------------- 1 | import { mapRange, times } from "@/lib/utils"; 2 | import { makeNoise2D } from "open-simplex-noise"; 3 | 4 | export type Noise2D = (x: number, y: number) => number; 5 | 6 | export function mapNoise2d( 7 | scale: number, 8 | min: number, 9 | max: number, 10 | noise: Noise2D, 11 | ): Noise2D { 12 | return (x: number, y: number) => 13 | mapRange(-1, 1, min, max, noise(x * scale, y * scale)); 14 | } 15 | 16 | export function makeFractalNoise2d( 17 | count: number, 18 | scaleDropOff = 0.5, 19 | sizeScale = 2, 20 | ): Noise2D { 21 | const levels = times(count, () => makeNoise2D(Math.random())); 22 | return (x, y) => { 23 | let result = 0; 24 | let scale = 1; 25 | let size = 1; 26 | let max = 0; 27 | 28 | for (const level of levels) { 29 | result += level(x * size, y * size) * scale; 30 | max += scale; 31 | scale *= scaleDropOff; 32 | size *= sizeScale; 33 | } 34 | 35 | return result / max; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/sim/crate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sim" 3 | version = "0.1.0" 4 | authors = ["✨ Alex ✨ "] 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [features] 10 | default = ["console_error_panic_hook"] 11 | 12 | [dependencies] 13 | wasm-bindgen = { version = "0.2.61", features = [] } 14 | js-sys = "0.3.45" 15 | web-log = "1.0.1" 16 | rand = "0.8.5" 17 | getrandom = { version = "0.2.15", features = ["js"] } 18 | web-sys = { version = "0.3.45", features = [ 19 | 'console', 20 | 'Document', 21 | 'Window', 22 | 'CanvasRenderingContext2d', 23 | 'Element', 24 | 'HtmlCanvasElement', 25 | 'Window', 26 | ] } 27 | itertools = "0.13.0" 28 | 29 | # The `console_error_panic_hook` crate provides better debugging of panics by 30 | # logging them with `console.error`. This is great for development, but requires 31 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 32 | # code size when deploying. 33 | console_error_panic_hook = { version = "0.1.6", optional = true } 34 | 35 | [dev-dependencies] 36 | wasm-bindgen-test = "0.3.13" 37 | -------------------------------------------------------------------------------- /src/lib/createWindowCanvas.tsx: -------------------------------------------------------------------------------- 1 | export function createWindowCanvas(): [ 2 | HTMLCanvasElement, 3 | { 4 | width: number; 5 | height: number; 6 | scale: number; 7 | }, 8 | ] { 9 | const canvas = document.createElement("canvas"); 10 | const size = { 11 | width: window.innerWidth, 12 | height: window.innerHeight, 13 | scale: window.devicePixelRatio, 14 | }; 15 | 16 | canvas.width = size.width * size.scale; 17 | canvas.height = size.height * size.scale; 18 | canvas.style.width = `${size.width}px`; 19 | canvas.style.height = `${size.height}px`; 20 | 21 | window.addEventListener("resize", () => { 22 | size.width = window.innerWidth; 23 | size.height = window.innerHeight; 24 | size.scale = window.devicePixelRatio; 25 | 26 | canvas.width = size.width * size.scale; 27 | canvas.height = size.height * size.scale; 28 | canvas.style.width = `${size.width}px`; 29 | canvas.style.height = `${size.height}px`; 30 | }); 31 | 32 | return [canvas, size]; 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/assert.test.tsx: -------------------------------------------------------------------------------- 1 | import { assert, assertExists } from "@/lib/assert"; 2 | import { expect, test } from "vitest"; 3 | 4 | test("normalizes stack traces", () => { 5 | function myFunctionThatThrows() { 6 | assert(2 + 2 === 5, "it failed!"); 7 | } 8 | 9 | let thrownError: Error | undefined = undefined; 10 | try { 11 | myFunctionThatThrows(); 12 | } catch (err) { 13 | thrownError = err as Error; 14 | } 15 | 16 | assert(thrownError); 17 | assert( 18 | thrownError.stack?.startsWith( 19 | `Error: it failed!\n at myFunctionThatThrows`, 20 | ), 21 | ); 22 | }); 23 | 24 | test("automatically inserts an error message", () => { 25 | // this is actually testing code in `vite.config.ts` 26 | expect(() => assert(2 + 2 === 5)).toThrowErrorMatchingInlineSnapshot( 27 | `[Error: Assertion Error: 2 + 2 === 5]`, 28 | ); 29 | 30 | expect(() => 31 | assertExists((() => undefined)()), 32 | ).toThrowErrorMatchingInlineSnapshot( 33 | `[Error: Assertion Error: (() => undefined)()]`, 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /src/network/ConnectionSet.ts: -------------------------------------------------------------------------------- 1 | import { exhaustiveSwitchError, sample } from "@/lib/utils"; 2 | import ConnectionDirection from "@/network/ConnectionDirection"; 3 | import Road from "@/network/Road"; 4 | 5 | export default class ConnectionSet { 6 | incoming: Road[] = []; 7 | outgoing: Road[] = []; 8 | 9 | add(target: Road, direction: ConnectionDirection) { 10 | switch (direction) { 11 | case ConnectionDirection.IN: 12 | this.addIncoming(target); 13 | break; 14 | case ConnectionDirection.OUT: 15 | this.addOutgoing(target); 16 | break; 17 | default: 18 | exhaustiveSwitchError(direction); 19 | } 20 | } 21 | 22 | addIncoming(target: Road) { 23 | this.incoming.push(target); 24 | } 25 | 26 | addOutgoing(target: Road) { 27 | this.outgoing.push(target); 28 | } 29 | 30 | sampleIncoming(): Road { 31 | return sample(this.incoming); 32 | } 33 | 34 | sampleOutgoing(): Road { 35 | return sample(this.outgoing); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/splatapus/ui/useSquircle.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { 3 | sizeFromBorderBox, 4 | useResizeObserver, 5 | } from "@/lib/hooks/useResizeObserver"; 6 | import { useMemo } from "react"; 7 | 8 | export function useSquircleClipPath( 9 | element: Element | null, 10 | maxRadius = Infinity, 11 | ): string { 12 | const size = useResizeObserver(element, sizeFromBorderBox) ?? Vector2.ZERO; 13 | return useMemo(() => { 14 | if (!size) { 15 | return "none"; 16 | } 17 | const rounding = Math.min(size.x * 0.5, size.y * 0.5, maxRadius); 18 | const clipPath = [ 19 | `M 0,${rounding}`, 20 | `Q 0,0 ${rounding},0`, 21 | `T ${size.x - rounding},0`, 22 | `Q ${size.x},0 ${size.x},${rounding}`, 23 | `T ${size.x},${size.y - rounding}`, 24 | `Q ${size.x},${size.y} ${size.x - rounding},${size.y}`, 25 | `T ${rounding},${size.y}`, 26 | `Q 0,${size.y} 0,${size.y - rounding}`, 27 | "Z", 28 | ].join(" "); 29 | 30 | return `path('${clipPath}')`; 31 | }, [size, maxRadius]); 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/assert.ts: -------------------------------------------------------------------------------- 1 | import { omitFromStackTrace } from "@/lib/omitFromStackTrace"; 2 | 3 | export const fail: (message: string) => never = omitFromStackTrace( 4 | (message) => { 5 | throw new Error(message); 6 | }, 7 | ); 8 | 9 | export const assert: ( 10 | value: unknown, 11 | message?: string, 12 | debug?: boolean, 13 | ) => asserts value = omitFromStackTrace((value, message, debug) => { 14 | if (!value) { 15 | if ( 16 | process.env.NODE_ENV !== "production" && 17 | !import.meta.vitest && 18 | debug 19 | ) { 20 | // eslint-disable-next-line no-debugger 21 | debugger; 22 | } 23 | fail(message ?? "Assertion Error"); 24 | } 25 | }); 26 | 27 | export function assertNumber(value: unknown): number { 28 | assert(typeof value === "number", "value must be number"); 29 | return value; 30 | } 31 | 32 | export const assertExists = omitFromStackTrace( 33 | (value: T, message?: string): NonNullable => { 34 | if (value == null) { 35 | fail(message ?? "value must be defined"); 36 | } 37 | return value; 38 | }, 39 | ); 40 | -------------------------------------------------------------------------------- /src/sim/main.tsx: -------------------------------------------------------------------------------- 1 | import { frameLoop } from "@/lib/utils"; 2 | import initSim, { create } from "@/sim/crate/pkg/sim"; 3 | 4 | const PX_SIZE = 4; 5 | 6 | async function main() { 7 | const fontUrl = new URL("./tom-thumb.woff2", import.meta.url); 8 | const font = await new FontFace( 9 | "tomThumb", 10 | `url(${fontUrl.toString()})`, 11 | ).load(); 12 | (document.fonts as any).add(font); 13 | 14 | await initSim( 15 | new URL("./crate/pkg/sim_bg.wasm", import.meta.url).toString(), 16 | ); 17 | 18 | const canvas = document.createElement("canvas"); 19 | canvas.width = window.innerWidth / PX_SIZE; 20 | canvas.height = window.innerHeight / PX_SIZE; 21 | canvas.style.width = `${window.innerWidth}px`; 22 | canvas.style.height = `${window.innerHeight}px`; 23 | canvas.style.imageRendering = "pixelated"; 24 | canvas.getContext("2d")!.textRendering = "geometricPrecision"; 25 | document.body.appendChild(canvas); 26 | 27 | const app = create(canvas); 28 | console.log(app); 29 | 30 | frameLoop((dt) => { 31 | app.update(dt); 32 | app.draw(); 33 | }); 34 | } 35 | 36 | main().catch(console.error); 37 | -------------------------------------------------------------------------------- /src/spline-time/layers/straightLineThroughPointsLayer.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import { SvgPathBuilder } from "@/lib/svgPathBuilder"; 3 | import { windows } from "@/lib/utils"; 4 | import { DottedGuideLine, FinalLine } from "@/spline-time/guides"; 5 | import { LayerProps } from "@/spline-time/layers/Layer"; 6 | 7 | export function straightLineThroughPointsLayer({ 8 | style, 9 | }: { 10 | style: "final" | "dotted"; 11 | }) { 12 | if (style === "final") { 13 | return ({ line }: LayerProps) => { 14 | if (!line.points.length) { 15 | return null; 16 | } 17 | const path = new SvgPathBuilder(); 18 | path.moveTo(line.points[0]); 19 | for (let i = 1; i < line.points.length; i++) { 20 | path.lineTo(line.points[i]); 21 | } 22 | return ; 23 | }; 24 | } 25 | 26 | return ({ line }: LayerProps) => { 27 | const segments = windows(line.points, 2).map(([from, to], i) => ( 28 | 29 | )); 30 | 31 | return <>{segments}; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/txdraw/shapes/shared/snapShapesToGrid.tsx: -------------------------------------------------------------------------------- 1 | import { snapPxHeightToTexel, snapPxWidthToTexel } from "@/txdraw/utils/texel"; 2 | import { Editor } from "tldraw"; 3 | 4 | export function snapShapesToGrid(editor: Editor) { 5 | editor.sideEffects.registerBeforeCreateHandler("shape", (shape) => { 6 | const snappedX = snapPxWidthToTexel(editor, shape.x); 7 | const snappedY = snapPxHeightToTexel(editor, shape.y); 8 | if (snappedX !== shape.x || snappedY !== shape.y) { 9 | return { 10 | ...shape, 11 | x: snappedX, 12 | y: snappedY, 13 | }; 14 | } 15 | 16 | return shape; 17 | }); 18 | 19 | editor.sideEffects.registerBeforeChangeHandler( 20 | "shape", 21 | (prevShape, nextShape) => { 22 | if (prevShape.x !== nextShape.x || prevShape.y !== nextShape.y) { 23 | return { 24 | ...nextShape, 25 | x: snapPxWidthToTexel(editor, nextShape.x), 26 | y: snapPxHeightToTexel(editor, nextShape.y), 27 | }; 28 | } 29 | 30 | return nextShape; 31 | }, 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/live/LiveMemo.test.tsx: -------------------------------------------------------------------------------- 1 | import { LiveMemo } from "@/lib/live/LiveMemo"; 2 | import { LiveValue } from "@/lib/live/LiveValue"; 3 | import { expect, test, vi } from "vitest"; 4 | 5 | test("computes derived values", () => { 6 | const v1 = new LiveValue(1); 7 | const v2 = new LiveValue(2); 8 | const memo = new LiveMemo(() => v1.live() + v2.live()); 9 | 10 | expect(memo.getOnce()).toBe(3); 11 | v1.update(2); 12 | expect(memo.getOnce()).toBe(4); 13 | }); 14 | 15 | test("compose memos execute lazily", () => { 16 | const v1 = new LiveValue(1); 17 | const v2 = new LiveValue(2); 18 | const v3 = new LiveValue(3); 19 | const compute1 = vi.fn(() => v1.live() + v2.live()); 20 | const m1 = new LiveMemo(compute1); 21 | const compute2 = vi.fn(() => m1.live() + v3.live()); 22 | const m2 = new LiveMemo(compute2); 23 | 24 | expect(m2.getOnce()).toBe(6); 25 | expect(compute1).toBeCalledTimes(1); 26 | expect(compute2).toBeCalledTimes(1); 27 | 28 | // values change, result is the same: 29 | v1.update(2); 30 | v2.update(1); 31 | expect(m2.getOnce()).toBe(6); 32 | expect(compute1).toBeCalledTimes(2); 33 | expect(compute2).toBeCalledTimes(1); 34 | }); 35 | -------------------------------------------------------------------------------- /src/lib/scene/SceneObject.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "@/lib/assert"; 2 | import Scene from "@/lib/scene/Scene"; 3 | 4 | const constructorIdCounts = {} as Record; 5 | 6 | const getNextCount = (name: string): string => { 7 | if (!constructorIdCounts[name]) constructorIdCounts[name] = 0; 8 | return `${name}@${constructorIdCounts[name]++}`; 9 | }; 10 | 11 | export default abstract class SceneObject { 12 | id: string = getNextCount(this.constructor.name); 13 | private scene: Scene | null = null; 14 | 15 | hasScene(): boolean { 16 | return this.scene !== null; 17 | } 18 | 19 | getScene(): Scene { 20 | assert(this.scene, "scene must be present"); 21 | return this.scene; 22 | } 23 | 24 | draw(ctx: CanvasRenderingContext2D, elapsedTime: number): void {} 25 | update(delta: number): void {} 26 | 27 | addTo(scene: Scene): this { 28 | scene.addChild(this); 29 | return this; 30 | } 31 | 32 | onAddedToScene(scene: Scene) { 33 | this.scene = scene; 34 | } 35 | 36 | onRemovedFromScene() { 37 | this.scene = null; 38 | } 39 | 40 | getSortOrder(): number { 41 | return 0; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/splatapus/editor/modeClassNames.tsx: -------------------------------------------------------------------------------- 1 | import { useLive } from "@/lib/live"; 2 | import { ModeType } from "@/splatapus/editor/modes/Mode"; 3 | import { Splatapus } from "@/splatapus/editor/useEditor"; 4 | 5 | export type ModeClassNames = Record; 10 | 11 | export const modeClassNames: ModeClassNames = { 12 | [ModeType.Draw]: { 13 | gradient500: "from-fuchsia-500 to-violet-500", 14 | border500: "border-purple-500", 15 | ring500: "ring-purple-500", 16 | }, 17 | [ModeType.Rig]: { 18 | gradient500: "from-cyan-500 to-blue-500", 19 | border500: "border-sky-500", 20 | ring500: "ring-sky-500", 21 | }, 22 | [ModeType.Play]: { 23 | gradient500: "from-lime-500 to-emerald-500", 24 | border500: "border-green-500", 25 | ring500: "ring-green-500", 26 | }, 27 | }; 28 | 29 | export function useModeClassNames(splatapus: Splatapus) { 30 | const selectedModeType = useLive( 31 | () => splatapus.interaction.activeMode.live().type, 32 | [splatapus], 33 | ); 34 | return modeClassNames[selectedModeType]; 35 | } 36 | -------------------------------------------------------------------------------- /src/palette/schema.tsx: -------------------------------------------------------------------------------- 1 | import { Result } from "@/lib/Result"; 2 | import { Schema, SchemaType } from "@/lib/schema"; 3 | import { Oklch } from "culori"; 4 | 5 | export const lchColorSchema = Schema.object({ 6 | l: Schema.number, 7 | c: Schema.number, 8 | h: Schema.number.optional(), 9 | }) 10 | .indexed({ l: 0, c: 1, h: 2 }) 11 | .transform( 12 | ({ l, c, h }) => Result.ok({ mode: "oklch", l, c, h }), 13 | Schema.cannotValidate("Oklch"), 14 | ({ l, c, h }) => ({ 15 | l: Number(l.toFixed(4)), 16 | c: Number(c.toFixed(4)), 17 | h: h != null ? Number(h.toFixed(2)) : h, 18 | }), 19 | ); 20 | 21 | export const paletteSchema = Schema.object({ 22 | families: Schema.arrayOf(Schema.string), 23 | shades: Schema.arrayOf(Schema.string), 24 | colors: Schema.arrayOf(Schema.arrayOf(lchColorSchema)), 25 | }); 26 | export type Palette = SchemaType; 27 | 28 | export const gamutSchema = Schema.valueUnion("srgb", "p3", "rec2020"); 29 | export type Gamut = SchemaType; 30 | 31 | export const axisSchema = Schema.valueUnion("shades", "families"); 32 | export type Axis = SchemaType; 33 | -------------------------------------------------------------------------------- /src/spline-time/layers/Layer.tsx: -------------------------------------------------------------------------------- 1 | import { SplineTimeLine } from "@/spline-time/SplineTimeLine"; 2 | import { ReactNode } from "react"; 3 | import { createPortal } from "react-dom"; 4 | 5 | export interface LayerProps { 6 | line: SplineTimeLine; 7 | showExtras: boolean; 8 | uiTarget: HTMLDivElement; 9 | } 10 | export type Layer = (props: LayerProps) => ReactNode; 11 | 12 | export function LayerUi({ 13 | label, 14 | uiTarget, 15 | children, 16 | }: { 17 | label: string; 18 | uiTarget: HTMLElement; 19 | children?: ReactNode; 20 | }) { 21 | return createPortal( 22 |
e.stopPropagation()} 25 | onPointerMove={(e) => e.stopPropagation()} 26 | onPointerUp={(e) => e.stopPropagation()} 27 | onPointerCancel={(e) => e.stopPropagation()} 28 | > 29 |
30 | {label} 31 |
32 |
33 | {children} 34 |
35 |
, 36 | uiTarget, 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/spline-time/layers/midPointBezierLayer.tsx: -------------------------------------------------------------------------------- 1 | import { SvgPathBuilder } from "@/lib/svgPathBuilder"; 2 | import { FinalLine } from "@/spline-time/guides"; 3 | import { LayerProps, LayerUi } from "@/spline-time/layers/Layer"; 4 | 5 | export function midPointBezierLayer({ line, uiTarget }: LayerProps) { 6 | const path = new SvgPathBuilder(); 7 | switch (line.points.length) { 8 | case 0: 9 | case 1: 10 | return null; 11 | case 2: { 12 | path.moveTo(line.points[0]); 13 | path.lineTo(line.points[1]); 14 | break; 15 | } 16 | default: 17 | path.moveTo(line.points[0]); 18 | path.lineTo(line.points[0].lerp(line.points[1], 0.5)); 19 | for (let i = 1; i < line.points.length - 1; i++) { 20 | path.quadraticCurveTo( 21 | line.points[i], 22 | line.points[i].lerp(line.points[i + 1], 0.5), 23 | ); 24 | } 25 | path.lineTo(line.points[line.points.length - 1]); 26 | } 27 | 28 | return ( 29 | <> 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/pals/makePal.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import Entity from "@/lib/scene/Entity"; 3 | import { generateRandomPalConfig } from "@/pals/PalConfig"; 4 | import { 5 | PalAbsoluteController, 6 | PalTargetController, 7 | } from "@/pals/PalController"; 8 | import PalGeom from "@/pals/PalGeom"; 9 | import PalRenderer from "@/pals/PalRenderer"; 10 | import PalWalkAnimationController from "@/pals/PalWalkAnimationController"; 11 | 12 | export function makeTargetPal(position: Vector2): Entity { 13 | const pal = new Entity(); 14 | pal.addComponent(PalTargetController, position); 15 | const config = generateRandomPalConfig(); 16 | const geom = pal.addComponent(PalGeom, config); 17 | geom.setAnimationController(new PalWalkAnimationController(config)); 18 | pal.addComponent(PalRenderer, config); 19 | return pal; 20 | } 21 | 22 | export function makeAbsolutePal(position: Vector2): Entity { 23 | const pal = new Entity(); 24 | pal.addComponent(PalAbsoluteController, position); 25 | const config = generateRandomPalConfig(); 26 | const geom = pal.addComponent(PalGeom, config); 27 | geom.setAnimationController(new PalWalkAnimationController(config)); 28 | pal.addComponent(PalRenderer, config); 29 | return pal; 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/scene/SceneSystem.ts: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { assert } from "@/lib/assert"; 3 | import Scene from "@/lib/scene/Scene"; 4 | 5 | const DEFAULT_NAME = "$$AbstractSceneSystem$$"; 6 | 7 | export default abstract class SceneSystem { 8 | static systemName = DEFAULT_NAME; 9 | private scene: Scene | null = null; 10 | 11 | constructor() { 12 | assert( 13 | this.constructor !== SceneSystem, 14 | "SceneSystem is an abstract class that must be extended", 15 | ); 16 | assert( 17 | (this.constructor as typeof SceneSystem).systemName !== 18 | DEFAULT_NAME, 19 | "classes extending SceneSystem must override SceneSystem.systemName", 20 | ); 21 | } 22 | 23 | getScene(): Scene { 24 | assert(this.scene, "scene is required"); 25 | return this.scene; 26 | } 27 | 28 | afterAddToScene(scene: Scene) { 29 | this.scene = scene; 30 | } 31 | 32 | beforeRemoveFromScene(scene: Scene) { 33 | this.scene = null; 34 | } 35 | 36 | beforeUpdate(delta: number) {} 37 | 38 | afterUpdate(delta: number) {} 39 | 40 | beforeDraw(ctx: CanvasRenderingContext2D, time: number) {} 41 | 42 | afterDraw(ctx: CanvasRenderingContext2D, time: number) {} 43 | } 44 | -------------------------------------------------------------------------------- /src/splatapus/model/SplatDocData.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | SplatDocId, 3 | SplatKeyPoint, 4 | SplatKeyPointId, 5 | SplatShape, 6 | SplatShapeId, 7 | SplatShapeVersion, 8 | SplatShapeVersionId, 9 | } from "@/splatapus/model/SplatDoc"; 10 | import { Index, Table, UniqueIndex } from "@/splatapus/model/Table"; 11 | import { NormalizedShapeVersionState } from "@/splatapus/model/normalizedShape"; 12 | 13 | type ShapeVersionLookupIndex = UniqueIndex< 14 | SplatShapeVersion, 15 | SplatShapeVersionId, 16 | [SplatShapeId, SplatKeyPointId] 17 | >; 18 | type ShapeVersionIdsByShapeIndex = Index< 19 | SplatShapeVersion, 20 | SplatShapeVersionId, 21 | SplatShapeId 22 | >; 23 | type ShapeVersionIdsByKeyPointIndex = Index< 24 | SplatShapeVersion, 25 | SplatShapeVersionId, 26 | SplatKeyPointId 27 | >; 28 | 29 | export interface SplatDocData { 30 | readonly id: SplatDocId; 31 | readonly keyPoints: Table; 32 | readonly shapes: Table; 33 | readonly shapeVersions: Table; 34 | readonly shapeVersionLookup: ShapeVersionLookupIndex; 35 | readonly shapeVersionIdsByShape: ShapeVersionIdsByShapeIndex; 36 | readonly shapeVersionIdsByKeyPoint: ShapeVersionIdsByKeyPointIndex; 37 | readonly normalizedShapeVersions: Table; 38 | } 39 | -------------------------------------------------------------------------------- /src/network/TravellerFinder.ts: -------------------------------------------------------------------------------- 1 | // @flow 2 | import QuadTree from "@/lib/QuadTree"; 3 | import AABB from "@/lib/geom/AABB"; 4 | import Circle from "@/lib/geom/Circle"; 5 | import Scene from "@/lib/scene/Scene"; 6 | import SceneSystem from "@/lib/scene/SceneSystem"; 7 | import Traveller from "@/network/Traveller"; 8 | 9 | export default class TravellerFinder extends SceneSystem { 10 | static override systemName = "TravellerFinder"; 11 | 12 | _quadTree!: QuadTree; 13 | 14 | removeTraveller(traveller: Traveller) { 15 | this._quadTree.remove(traveller); 16 | } 17 | 18 | override afterAddToScene(scene: Scene) { 19 | super.afterAddToScene(scene); 20 | this._quadTree = new QuadTree( 21 | AABB.fromLeftTopRightBottom(0, 0, scene.width, scene.height), 22 | (traveller) => traveller.position, 23 | ); 24 | } 25 | 26 | override beforeUpdate() { 27 | const scene = this.getScene(); 28 | this._quadTree.clear(); 29 | scene.children.forEach((child) => { 30 | if (child instanceof Traveller) { 31 | this._quadTree.insert(child); 32 | } 33 | }); 34 | // this._quadTree.debugDraw('red'); 35 | } 36 | 37 | findTravellersInCircle(circle: Circle) { 38 | return this._quadTree.findItemsInCircle(circle); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/slomojs/crate/src/display/cursor.rs: -------------------------------------------------------------------------------- 1 | use super::constants::{CHAR_HEIGHT_PX, CHAR_WIDTH_PX}; 2 | use crate::display_list::Spacing; 3 | 4 | /// (line, column) 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct CursorPosition(usize, usize); 7 | impl CursorPosition { 8 | pub fn new(line: usize, column: usize) -> Self { 9 | CursorPosition(line, column) 10 | } 11 | pub fn inc_spacing_before(&mut self, spacing: Spacing) { 12 | if spacing == Spacing::SpaceAround { 13 | self.1 += 1 14 | } 15 | } 16 | 17 | pub fn inc_columns(&mut self, columns: usize) { 18 | self.1 += columns 19 | } 20 | 21 | pub fn inc_spacing_after(&mut self, spacing: Spacing) { 22 | match spacing { 23 | Spacing::BreakAfter => { 24 | self.0 += 1; 25 | self.1 = 0; 26 | } 27 | Spacing::SpaceAfter => self.1 += 1, 28 | Spacing::SpaceAround => self.1 += 1, 29 | Spacing::None => (), 30 | } 31 | } 32 | 33 | pub fn line(&self) -> usize { 34 | self.0 35 | } 36 | 37 | pub fn column(&self) -> usize { 38 | self.1 39 | } 40 | 41 | pub fn pixel_coords(&self) -> (f64, f64) { 42 | ( 43 | (self.column() * CHAR_WIDTH_PX) as f64, 44 | (self.line() * CHAR_HEIGHT_PX) as f64, 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/canvasShapeHelpers.ts: -------------------------------------------------------------------------------- 1 | // @flow 2 | import CirclePathSegment from "@/lib/geom/CirclePathSegment"; 3 | import { Path } from "@/lib/geom/Path"; 4 | import StraightPathSegment from "@/lib/geom/StraightPathSegment"; 5 | 6 | export function circle( 7 | ctx: CanvasRenderingContext2D, 8 | x: number, 9 | y: number, 10 | radius: number, 11 | ) { 12 | ctx.arc(x, y, radius, 0, 2 * Math.PI, false); 13 | } 14 | 15 | export function path(ctx: CanvasRenderingContext2D, path: Path) { 16 | if (path.segments.length) { 17 | ctx.moveTo( 18 | path.segments[0].getStart().x, 19 | path.segments[0].getStart().y, 20 | ); 21 | } 22 | 23 | for (const segment of path.segments) { 24 | if (segment instanceof StraightPathSegment) { 25 | ctx.lineTo(segment.getEnd().x, segment.getEnd().y); 26 | } else if (segment instanceof CirclePathSegment) { 27 | ctx.arc( 28 | segment.circle.center.x, 29 | segment.circle.center.y, 30 | segment.circle.radius, 31 | segment.startAngle, 32 | segment.endAngle, 33 | segment.isAnticlockwise, 34 | ); 35 | } else { 36 | // eslint-disable-next-line @typescript-eslint/no-base-to-string 37 | throw new Error(`Unknown path segment type: ${String(segment)}`); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/splatapus/editor/PreviewPosition.tsx: -------------------------------------------------------------------------------- 1 | import { debugStateToString } from "@/lib/debugPropsToString"; 2 | import { Vector2 } from "@/lib/geom/Vector2"; 3 | import { exhaustiveSwitchError } from "@/lib/utils"; 4 | import { SplatKeyPointId } from "@/splatapus/model/SplatDoc"; 5 | 6 | export type PreviewPosition = 7 | | { 8 | readonly type: "keyPointId"; 9 | readonly keyPointId: SplatKeyPointId; 10 | } 11 | | { 12 | readonly type: "interpolated"; 13 | readonly scenePosition: Vector2; 14 | }; 15 | 16 | export const PreviewPosition = { 17 | interpolated: (scenePosition: Vector2): PreviewPosition => ({ 18 | type: "interpolated", 19 | scenePosition, 20 | }), 21 | keyPointId: (keyPointId: SplatKeyPointId): PreviewPosition => ({ 22 | type: "keyPointId", 23 | keyPointId, 24 | }), 25 | toDebugString: (position: PreviewPosition): string => { 26 | switch (position.type) { 27 | case "interpolated": 28 | return debugStateToString(position.type, { 29 | _: position.scenePosition.toString(2), 30 | }); 31 | case "keyPointId": 32 | return debugStateToString(position.type, { 33 | _: position.keyPointId, 34 | }); 35 | default: 36 | exhaustiveSwitchError(position); 37 | } 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/geometry/geometry-main.tsx: -------------------------------------------------------------------------------- 1 | import { Entry, GeometryApp } from "@/geometry/GeometryApp"; 2 | import { TickerProvider } from "@/lib/Ticker"; 3 | import { assertExists } from "@/lib/assert"; 4 | import { createRoot } from "react-dom/client"; 5 | import { RouterProvider, createHashRouter } from "react-router-dom"; 6 | 7 | if (import.meta.hot) { 8 | import.meta.hot.on("vite:beforeUpdate", () => { 9 | window.location.reload(); 10 | }); 11 | } 12 | 13 | const pages = import.meta.glob("./experiments/*.tsx", { eager: true }); 14 | 15 | const entries = Object.entries(pages).map( 16 | ([path, page]: [string, any]): Entry => { 17 | const id = path.slice("./experiments/".length, -".tsx".length); 18 | return { 19 | id, 20 | component: page.default as React.FC, 21 | name: (page.name as string) || id, 22 | }; 23 | }, 24 | ); 25 | 26 | console.log(entries); 27 | 28 | const router = createHashRouter([ 29 | ...entries.map((entry) => ({ 30 | path: `/${entry.id}`, 31 | element: , 32 | })), 33 | { 34 | path: "*", 35 | element: , 36 | }, 37 | ]); 38 | 39 | const root = assertExists(document.getElementById("root")); 40 | createRoot(root).render( 41 | 42 | 43 | , 44 | ); 45 | -------------------------------------------------------------------------------- /src/lib/EventEmitter.tsx: -------------------------------------------------------------------------------- 1 | import { promiseWithResolve } from "@/lib/utils"; 2 | import { unstable_batchedUpdates } from "react-dom"; 3 | 4 | export type Unsubscribe = () => void; 5 | 6 | type Listener = (...args: Args) => void; 7 | 8 | export default class EventEmitter 9 | implements AsyncIterable 10 | { 11 | private handlers = new Set>(); 12 | 13 | listen(listener: Listener) { 14 | const handler: Listener = (...args) => listener(...args); 15 | this.handlers.add(handler); 16 | return () => { 17 | this.handlers.delete(handler); 18 | }; 19 | } 20 | 21 | emit(...args: Args) { 22 | unstable_batchedUpdates(() => { 23 | for (const handler of this.handlers) { 24 | handler(...args); 25 | } 26 | }); 27 | } 28 | 29 | listenerCount(): number { 30 | return this.handlers.size; 31 | } 32 | 33 | hasListeners(): boolean { 34 | return this.handlers.size > 0; 35 | } 36 | 37 | async *[Symbol.asyncIterator]() { 38 | while (true) { 39 | const promise = promiseWithResolve(); 40 | const unsubscribe = this.listen((...args) => { 41 | promise.resolve(args[0]); 42 | unsubscribe(); 43 | }); 44 | yield await promise; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/live/LiveValue.test.tsx: -------------------------------------------------------------------------------- 1 | import { LiveValue } from "@/lib/live/LiveValue"; 2 | import { beforeEach, expect, test, vi } from "vitest"; 3 | 4 | const addOne = vi.fn((n: number) => n + 1); 5 | beforeEach(() => { 6 | vi.restoreAllMocks(); 7 | }); 8 | 9 | test("read value", () => { 10 | const value = new LiveValue(1); 11 | expect(value.getOnce()).toBe(1); 12 | }); 13 | 14 | test("can read written value", () => { 15 | const value = new LiveValue(1); 16 | value.update(2); 17 | value.update(addOne); 18 | expect(value.getOnce()).toBe(3); 19 | }); 20 | 21 | test("eagerly evaluates updates", () => { 22 | const value = new LiveValue(1); 23 | value.update(addOne); 24 | value.update(addOne); 25 | value.update(addOne); 26 | expect(addOne).toBeCalledTimes(3); 27 | expect(value.getOnce()).toBe(4); 28 | expect(addOne).toBeCalledTimes(3); 29 | }); 30 | 31 | test("notifies once of invalidation", () => { 32 | const value = new LiveValue(1); 33 | const listener = vi.fn(); 34 | value.addEagerInvalidateListener(listener); 35 | 36 | value.update(addOne); 37 | expect(listener).toBeCalledTimes(1); 38 | value.update(addOne); 39 | expect(listener).toBeCalledTimes(1); 40 | 41 | value.getOnce(); 42 | expect(listener).toBeCalledTimes(1); 43 | value.update(addOne); 44 | expect(listener).toBeCalledTimes(2); 45 | value.update(addOne); 46 | expect(listener).toBeCalledTimes(2); 47 | }); 48 | -------------------------------------------------------------------------------- /src/splatapus/renderer/Positioned.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { useLive } from "@/lib/live"; 3 | import { Splatapus } from "@/splatapus/editor/useEditor"; 4 | import classNames from "classnames"; 5 | import { ComponentProps } from "react"; 6 | 7 | interface PositionedDivProps extends ComponentProps<"div"> { 8 | position: Vector2; 9 | } 10 | 11 | export function PositionedDiv({ 12 | position, 13 | children, 14 | className, 15 | style, 16 | ...props 17 | }: PositionedDivProps) { 18 | return ( 19 |
27 | {children} 28 |
29 | ); 30 | } 31 | 32 | interface ScenePositionedDivProps extends PositionedDivProps { 33 | screenOffset?: Vector2; 34 | splatapus: Splatapus; 35 | } 36 | 37 | export function ScenePositionedDiv({ 38 | position, 39 | screenOffset = Vector2.ZERO, 40 | splatapus, 41 | ...props 42 | }: ScenePositionedDivProps) { 43 | const screenPosition = useLive( 44 | () => splatapus.viewport.sceneToScreenLive(position).add(screenOffset), 45 | [position, screenOffset, splatapus.viewport], 46 | ); 47 | return ; 48 | } 49 | -------------------------------------------------------------------------------- /src/bees/driver.tsx: -------------------------------------------------------------------------------- 1 | import { Ticker } from "pixi.js"; 2 | 3 | interface OnDestroyed { 4 | on(event: "destroyed", callback: () => void): void; 5 | } 6 | 7 | interface FixedUpdater extends OnDestroyed { 8 | fixedUpdateTick(): void; 9 | } 10 | 11 | interface Updater extends OnDestroyed { 12 | updateTick(elapsedTimeMs: number, deltaTimeMs: number): void; 13 | } 14 | 15 | export class Driver { 16 | public readonly ticker = new Ticker(); 17 | private readonly fixedUpdaters = new Set(); 18 | private readonly updaters = new Set(); 19 | private elapsedTimeMs = 0; 20 | 21 | constructor() { 22 | this.ticker.add(() => this.tick(this.ticker.deltaMS)); 23 | this.ticker.start(); 24 | } 25 | 26 | private tick(dtMs: number) { 27 | this.elapsedTimeMs += dtMs; 28 | for (const fixedUpdater of this.fixedUpdaters) { 29 | fixedUpdater.fixedUpdateTick(); 30 | } 31 | for (const updater of this.updaters) { 32 | updater.updateTick(this.elapsedTimeMs, dtMs); 33 | } 34 | } 35 | 36 | addFixedUpdate(updater: FixedUpdater) { 37 | this.fixedUpdaters.add(updater); 38 | updater.on("destroyed", () => this.fixedUpdaters.delete(updater)); 39 | } 40 | 41 | addUpdate(updater: Updater) { 42 | this.updaters.add(updater); 43 | updater.on("destroyed", () => this.updaters.delete(updater)); 44 | } 45 | } 46 | 47 | export const driver = new Driver(); 48 | -------------------------------------------------------------------------------- /src/emoji/Emoji.tsx: -------------------------------------------------------------------------------- 1 | import { useDrawEmoji } from "@/emoji/drawEmoji"; 2 | import { DebugCanvas } from "@/lib/react/DebugCanvasComponent"; 3 | import { CSSProperties } from "react"; 4 | 5 | export const characters = ["yeti", "cat", "tree"] as const; 6 | export const emotions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as const; 7 | // export const colors = { 8 | // auto: null, 9 | // red: tailwindColors.cyberRed, 10 | // orange: tailwindColors.cyberOrange, 11 | // yellow: tailwindColors.cyberYellow, 12 | // green: tailwindColors.cyberGreen, 13 | // blue: tailwindColors.cyberBlue, 14 | // purple: tailwindColors.cyberPurple, 15 | // pink: tailwindColors.cyberPink, 16 | // }; 17 | 18 | export interface Emoji { 19 | character: (typeof characters)[number]; 20 | emotion: (typeof emotions)[number]; 21 | // color: { name: keyof typeof colors; level: 30 | 40 | 50 | 70 }; 22 | } 23 | 24 | export function Emoji({ 25 | sizePx, 26 | emoji, 27 | style, 28 | className, 29 | }: { 30 | sizePx: number; 31 | emoji: Emoji; 32 | style?: CSSProperties; 33 | className?: string; 34 | }) { 35 | const drawEmoji = useDrawEmoji(); 36 | if (!drawEmoji) return null; 37 | 38 | return ( 39 | { 43 | drawEmoji(c.ctx, emoji, sizePx); 44 | }} 45 | style={style} 46 | className={className} 47 | /> 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/gestureland/GesturelandStore.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2, Vector2Model } from "@/lib/geom/Vector2"; 2 | import { 3 | BaseRecord, 4 | RecordId, 5 | Store, 6 | StoreSchema, 7 | createRecordType, 8 | } from "@tldraw/store"; 9 | 10 | export type CameraId = RecordId; 11 | export interface Camera extends BaseRecord<"camera", CameraId> { 12 | readonly center: Vector2Model; 13 | readonly zoom: number; 14 | } 15 | export const Camera = createRecordType("camera", { 16 | scope: "session", 17 | }).withDefaultProperties(() => ({ center: Vector2.ZERO.toJson(), zoom: 1 })); 18 | export const CAMERA_ID = Camera.createId("camera"); 19 | 20 | export type StrokeId = RecordId; 21 | export interface Stroke extends BaseRecord<"stroke", StrokeId> { 22 | readonly points: Vector2Model[]; 23 | } 24 | export const Stroke = createRecordType("stroke", { 25 | scope: "document", 26 | }).withDefaultProperties(() => ({ points: [] })); 27 | 28 | export type GLRecord = Camera | Stroke; 29 | 30 | export const schema = StoreSchema.create({ 31 | camera: Camera, 32 | stroke: Stroke, 33 | }); 34 | 35 | export class GesturelandStore extends Store { 36 | constructor() { 37 | super({ 38 | schema, 39 | props: undefined, 40 | }); 41 | } 42 | 43 | put1(record: GLRecord) { 44 | return this.put([record]); 45 | } 46 | 47 | delete(id: GLRecord["id"]) { 48 | return this.remove([id]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/terrain/Grid2.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { times } from "@/lib/utils"; 3 | 4 | export class Grid2 { 5 | public size: Vector2; 6 | private rows: (T | undefined)[][]; 7 | 8 | constructor(size: Vector2) { 9 | this.size = size.floor(); 10 | this.rows = times(this.size.y, () => 11 | times(this.size.y, () => undefined), 12 | ); 13 | } 14 | 15 | areCoordsInBounds(coords: Vector2): boolean { 16 | return ( 17 | 0 <= coords.x && 18 | coords.x < this.size.x && 19 | 0 <= coords.y && 20 | coords.y < this.size.y 21 | ); 22 | } 23 | 24 | vectorToGridCoords(vector: Vector2, cellSize: number): Vector2 { 25 | return vector.div(cellSize).floor(); 26 | } 27 | 28 | get({ x, y }: Vector2): T | undefined { 29 | return this.rows[y][x]; 30 | } 31 | 32 | set({ x, y }: Vector2, item: T | undefined) { 33 | this.rows[y][x] = item; 34 | } 35 | 36 | squareAroundCell(cell: Vector2, size: number): Vector2[] { 37 | const result = []; 38 | 39 | const squareOrigin = cell.sub(new Vector2(size, size).div(2)).round(); 40 | 41 | for (let x = 0; x < size; x++) { 42 | for (let y = 0; y < size; y++) { 43 | const point = new Vector2(x, y).add(squareOrigin); 44 | if (this.areCoordsInBounds(point)) { 45 | result.push(point); 46 | } 47 | } 48 | } 49 | 50 | return result; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/geom/StraightPathSegment.ts: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Line2 } from "@/lib/geom/Line2"; 3 | import { PathSegment } from "@/lib/geom/Path"; 4 | import { Vector2 } from "@/lib/geom/Vector2"; 5 | import { SvgPathBuilder } from "@/lib/svgPathBuilder"; 6 | import { constrain } from "@/lib/utils"; 7 | 8 | export default class StraightPathSegment implements PathSegment { 9 | readonly line: Line2; 10 | private readonly delta: Vector2; 11 | 12 | constructor(start: Vector2, end: Vector2) { 13 | this.line = new Line2(start, end); 14 | this.delta = this.line.getDelta(); 15 | Object.freeze(this); 16 | } 17 | 18 | getStart(): Vector2 { 19 | return this.line.start; 20 | } 21 | 22 | getEnd(): Vector2 { 23 | return this.line.end; 24 | } 25 | 26 | getDelta(): Vector2 { 27 | return this.delta; 28 | } 29 | 30 | getLength(): number { 31 | return this.delta.magnitude(); 32 | } 33 | 34 | angle(): number { 35 | return this.delta.angle(); 36 | } 37 | 38 | getPointAtPosition(position: number): Vector2 { 39 | const constrainedPosition = constrain(0, this.getLength(), position); 40 | return this.delta 41 | .withMagnitude(constrainedPosition) 42 | .add(this.line.start); 43 | } 44 | 45 | getAngleAtPosition(): number { 46 | return this.delta.angle(); 47 | } 48 | 49 | appendToSvgPathBuilder(pathBuilder: SvgPathBuilder): void { 50 | pathBuilder.moveToIfNeeded(this.line.start); 51 | pathBuilder.lineTo(this.line.end); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/geom/ApproxQuadraticBezierPathSegment.ts: -------------------------------------------------------------------------------- 1 | import { Path, PathSegment } from "@/lib/geom/Path"; 2 | import StraightPathSegment from "@/lib/geom/StraightPathSegment"; 3 | import { Vector2 } from "@/lib/geom/Vector2"; 4 | import { SvgPathBuilder } from "@/lib/svgPathBuilder"; 5 | 6 | export default class ApproxQuadraticBezierPathSegment implements PathSegment { 7 | private readonly path: Path; 8 | constructor( 9 | start: Vector2, 10 | control: Vector2, 11 | end: Vector2, 12 | resolution = 100, 13 | ) { 14 | const path = new Path(); 15 | let lastPoint = start; 16 | for (let i = 1; i <= resolution; i++) { 17 | const t = i / resolution; 18 | 19 | const point = start.lerp(control, t).lerp(control.lerp(end, t), t); 20 | path.addSegment(new StraightPathSegment(lastPoint, point)); 21 | lastPoint = point; 22 | } 23 | this.path = path; 24 | } 25 | 26 | getStart(): Vector2 { 27 | return this.path.getStart(); 28 | } 29 | 30 | getEnd(): Vector2 { 31 | return this.path.getEnd(); 32 | } 33 | 34 | getLength(): number { 35 | return this.path.getLength(); 36 | } 37 | 38 | getPointAtPosition(position: number): Vector2 { 39 | return this.path.getPointAtPosition(position); 40 | } 41 | 42 | getAngleAtPosition(position: number): number { 43 | return this.path.getAngleAtPosition(position); 44 | } 45 | 46 | appendToSvgPathBuilder(pathBuilder: SvgPathBuilder): void { 47 | this.path.appendToSvgPathBuilder(pathBuilder); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/splatapus/model/findPositionForNewKeyPoint.tsx: -------------------------------------------------------------------------------- 1 | import AABB from "@/lib/geom/AABB"; 2 | import { Vector2 } from "@/lib/geom/Vector2"; 3 | import { runOnce } from "@/lib/live"; 4 | import { random } from "@/lib/utils"; 5 | import { Viewport } from "@/splatapus/editor/Viewport"; 6 | import { SplatDocModel } from "@/splatapus/model/SplatDocModel"; 7 | 8 | export function findPositionForNewKeyPoint( 9 | document: SplatDocModel, 10 | viewport: Viewport, 11 | ) { 12 | const bounds = runOnce(() => viewport.visibleSceneBoundsLive()); 13 | const idealBounds = AABB.fromLeftTopRightBottom( 14 | bounds.left + bounds.width / 10, 15 | bounds.top + bounds.height / 10, 16 | bounds.right - bounds.width / 10, 17 | bounds.bottom - bounds.height / 10, 18 | ); 19 | const otherPoints = Array.from( 20 | document.keyPoints, 21 | (keyPoint) => keyPoint.position, 22 | ); 23 | const minDimension = Math.min(bounds.width, bounds.height); 24 | const threshold = (minDimension / 10) ** 2; 25 | 26 | let candidate = idealBounds.getCenter(); 27 | for (let i = 0; i < 500; i++) { 28 | if ( 29 | otherPoints.every( 30 | (otherPoint) => 31 | !otherPoint || 32 | otherPoint.distanceToSquared(candidate) > threshold, 33 | ) 34 | ) { 35 | return candidate; 36 | } 37 | candidate = new Vector2( 38 | random(idealBounds.left, idealBounds.right), 39 | random(idealBounds.top, idealBounds.bottom), 40 | ); 41 | } 42 | return candidate; 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/findDataElement.tsx: -------------------------------------------------------------------------------- 1 | import { assert } from "@/lib/assert"; 2 | import { Schema } from "@/lib/schema"; 3 | import { entries, ObjectMap } from "@/lib/utils"; 4 | 5 | export function findDataElementParent< 6 | Props extends Record>, 7 | >( 8 | element: EventTarget, 9 | search: Props, 10 | ): null | { 11 | element: HTMLElement; 12 | data: { 13 | [K in keyof Props]: Props[K] extends string ? Props[K] 14 | : Props[K] extends Schema ? P 15 | : never; 16 | }; 17 | } { 18 | assert(element instanceof HTMLElement); 19 | 20 | const data: ObjectMap = {}; 21 | let failed = false; 22 | for (const [key, value] of entries(search)) { 23 | const dataValue = element.dataset[key]; 24 | if (typeof value === "string") { 25 | if (value === dataValue) { 26 | data[key] = value; 27 | } else { 28 | failed = true; 29 | break; 30 | } 31 | } else { 32 | const parsed = value.parse(dataValue); 33 | if (parsed.ok) { 34 | data[key] = parsed.value; 35 | } else { 36 | failed = true; 37 | break; 38 | } 39 | } 40 | } 41 | 42 | if (failed) { 43 | if (element.parentElement) { 44 | return findDataElementParent(element.parentElement, search); 45 | } 46 | 47 | return null; 48 | } 49 | 50 | return { 51 | element, 52 | // @ts-expect-error this is fine 53 | data, 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/shader.tsx: -------------------------------------------------------------------------------- 1 | export type ScalarGlslTypeName = "bool" | "int" | "uint" | "float" | "double"; 2 | interface JsTypeByScalarGlslTypeName { 3 | bool: boolean; 4 | int: number; 5 | uint: number; 6 | float: number; 7 | double: number; 8 | } 9 | export interface ScalarGlslType { 10 | readonly type: "scalar"; 11 | readonly name: ScalarGlslTypeName; 12 | } 13 | 14 | export interface BoolGlslType { 15 | readonly type: "scalar"; 16 | readonly name: "bool"; 17 | } 18 | export interface IntGlslType { 19 | readonly type: "scalar"; 20 | readonly name: "int"; 21 | } 22 | export interface UintGlslType { 23 | readonly type: "scalar"; 24 | readonly name: "uint"; 25 | } 26 | export interface FloatGlslType { 27 | readonly type: "scalar"; 28 | readonly name: "float"; 29 | } 30 | export interface DoubleGlslType { 31 | readonly type: "scalar"; 32 | readonly name: "double"; 33 | } 34 | 35 | export type GlslType = ScalarGlslType; 36 | 37 | export type GlslTypeToJsType = 38 | JsTypeByScalarGlslTypeName[T["name"]]; 39 | 40 | export abstract class GlslExpressionBase<_Type extends GlslType> {} 41 | 42 | export class GlslScalar< 43 | Type extends ScalarGlslType, 44 | > extends GlslExpressionBase { 45 | constructor(readonly value: GlslTypeToJsType) { 46 | super(); 47 | } 48 | } 49 | 50 | export const Glsl = { 51 | bool: (value: boolean) => new GlslScalar(value), 52 | int: (value: number) => new GlslScalar(value), 53 | uint: (value: number) => new GlslScalar(value), 54 | float: (value: number) => new GlslScalar(value), 55 | double: (value: number) => new GlslScalar(value), 56 | }; 57 | -------------------------------------------------------------------------------- /src/splatapus/model/ThinPlateSpline.test.tsx: -------------------------------------------------------------------------------- 1 | import { Tps } from "@/splatapus/model/ThinPlateSpline"; 2 | import { expect, test } from "vitest"; 3 | 4 | const testInputs = [ 5 | [91, 28, 9], 6 | [3, 83, 40], 7 | [90, 92, 16], 8 | [45, 82, 87], 9 | [51, 91, 9], 10 | [63, 60, 30], 11 | [16, 97, 68], 12 | [49, 90, 47], 13 | [34, 93, 59], 14 | [39, 25, 26], 15 | ]; 16 | 17 | test("ThinPlateSpline", function () { 18 | const tps = new Tps( 19 | testInputs.map((input) => [input[0], input[1]]), 20 | testInputs.map((input) => input[2]), 21 | ); 22 | expect(tps.getValue([53, 28]).toPrecision(10)).toMatchInlineSnapshot( 23 | '"22.96223273"', 24 | ); 25 | expect(tps.getValue([84, 6]).toPrecision(10)).toMatchInlineSnapshot( 26 | '"2.847564993"', 27 | ); 28 | expect(tps.getValue([25, 42]).toPrecision(10)).toMatchInlineSnapshot( 29 | '"45.74377405"', 30 | ); 31 | expect(tps.getValue([21, 1]).toPrecision(10)).toMatchInlineSnapshot( 32 | '"6.556757854"', 33 | ); 34 | expect(tps.getValue([2, 82]).toPrecision(10)).toMatchInlineSnapshot( 35 | '"37.86663024"', 36 | ); 37 | expect(tps.getValue([91, 40]).toPrecision(10)).toMatchInlineSnapshot( 38 | '"14.26136062"', 39 | ); 40 | expect(tps.getValue([8, 86]).toPrecision(10)).toMatchInlineSnapshot( 41 | '"52.78258480"', 42 | ); 43 | expect(tps.getValue([29, 19]).toPrecision(10)).toMatchInlineSnapshot( 44 | '"21.39156005"', 45 | ); 46 | expect(tps.getValue([90, 36]).toPrecision(10)).toMatchInlineSnapshot( 47 | '"12.35507995"', 48 | ); 49 | expect(tps.getValue([97, 69]).toPrecision(10)).toMatchInlineSnapshot( 50 | '"27.20192286"', 51 | ); 52 | }); 53 | -------------------------------------------------------------------------------- /src/slomojs/crate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "slomojs" 3 | version = "0.1.0" 4 | authors = ["✨ Alex ✨ "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [features] 11 | default = ["console_error_panic_hook"] 12 | 13 | [dependencies] 14 | wasm-bindgen = { version = "0.2.100", features = ["serde-serialize"] } 15 | js-sys = "0.3.77" 16 | web-sys = { version = "0.3.45", features = [ 17 | "console", 18 | "Document", 19 | "Element", 20 | "HtmlElement", 21 | "Node", 22 | "Window", 23 | "CssStyleDeclaration", 24 | "Animation", 25 | "AnimationEffect", 26 | "KeyframeEffect", 27 | "KeyframeEffectOptions", 28 | "FillMode", 29 | ] } 30 | swc_ecma_parser = "0.42.0" 31 | swc_common = { version = "0.10.5", features = ["tty-emitter"] } 32 | swc_ecma_ast = "0.34.0" 33 | serde = { version = "1.0", features = ["derive"] } 34 | itertools = "0.9.0" 35 | paste = "1.0" 36 | wasm-bindgen-futures = "0.4.18" 37 | async-trait = "0.1.42" 38 | futures = "0.3.8" 39 | lazy_static = "1.4.0" 40 | unzip-n = "0.1.2" 41 | # The `console_error_panic_hook` crate provides better debugging of panics by 42 | # logging them with `console.error`. This is great for development, but requires 43 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 44 | # code size when deploying. 45 | console_error_panic_hook = { version = "0.1.6", optional = true } 46 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 47 | # compared to the default allocator's ~10K. It is slower than the default 48 | # allocator, however. 49 | # 50 | # Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. 51 | wee_alloc = { version = "0.4.5", optional = true } 52 | 53 | [dev-dependencies] 54 | wasm-bindgen-test = "0.3.13" 55 | -------------------------------------------------------------------------------- /src/splatapus/editor/modes/PlayMode.tsx: -------------------------------------------------------------------------------- 1 | import { debugStateToString } from "@/lib/debugPropsToString"; 2 | import { Vector2 } from "@/lib/geom/Vector2"; 3 | import { LiveValue } from "@/lib/live"; 4 | import { exhaustiveSwitchError } from "@/lib/utils"; 5 | import { 6 | PointerEventContext, 7 | PointerEventType, 8 | } from "@/splatapus/editor/EventContext"; 9 | import { Mode, ModeType } from "@/splatapus/editor/modes/Mode"; 10 | import { ReactNode } from "react"; 11 | 12 | export class PlayMode implements Mode { 13 | readonly type = ModeType.Play; 14 | readonly previewPosition = new LiveValue( 15 | null, 16 | "PlayMode.previewPosition", 17 | ); 18 | 19 | isIdleLive(): boolean { 20 | return true; 21 | } 22 | toDebugStringLive(): string { 23 | return debugStateToString("play", { 24 | _: this.previewPosition.live()?.toString(2) ?? null, 25 | }); 26 | } 27 | onPointerEvent({ 28 | event, 29 | eventType, 30 | splatapus, 31 | }: PointerEventContext): void { 32 | switch (eventType) { 33 | case "move": 34 | case "down": 35 | if (event.isPrimary) { 36 | this.previewPosition.update( 37 | splatapus.viewport.eventSceneCoords(event), 38 | ); 39 | } 40 | return; 41 | case "cancel": 42 | case "up": 43 | return; 44 | default: 45 | exhaustiveSwitchError(eventType); 46 | } 47 | } 48 | getPreviewPositionLive(): Vector2 | null { 49 | return this.previewPosition.live(); 50 | } 51 | renderOverlay(): ReactNode { 52 | return null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/bees/C.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | 3 | export interface SpriteOpts { 4 | pixelScale?: number; 5 | opacity?: number; 6 | } 7 | 8 | export class C { 9 | constructor(public readonly ctx: CanvasRenderingContext2D) {} 10 | 11 | do(cb: () => void) { 12 | this.ctx.save(); 13 | cb(); 14 | this.ctx.restore(); 15 | } 16 | 17 | scale(x: number, y: number = x) { 18 | this.ctx.scale(x, y); 19 | } 20 | 21 | translate(position: Vector2) { 22 | this.ctx.translate(position.x, position.y); 23 | } 24 | 25 | drawSpriteFromSheet( 26 | image: HTMLImageElement, 27 | sx: number, 28 | sy: number, 29 | sw: number, 30 | sh: number, 31 | dx: number, 32 | dy: number, 33 | { pixelScale = 1, opacity = 1 }: SpriteOpts = {}, 34 | ) { 35 | this.do(() => { 36 | console.log({ sx, sy, sw, sh, dx, dy }); 37 | this.ctx.imageSmoothingEnabled = false; 38 | this.ctx.globalAlpha = opacity; 39 | this.ctx.drawImage( 40 | image, 41 | sx, 42 | sy, 43 | sw, 44 | sh, 45 | Math.round(dx) * pixelScale, 46 | Math.round(dy) * pixelScale, 47 | sw * pixelScale, 48 | sh * pixelScale, 49 | ); 50 | }); 51 | } 52 | 53 | drawSprite( 54 | image: HTMLImageElement, 55 | x: number, 56 | y: number, 57 | opts?: SpriteOpts, 58 | ) { 59 | this.drawSpriteFromSheet( 60 | image, 61 | 0, 62 | 0, 63 | image.naturalWidth, 64 | image.naturalHeight, 65 | x, 66 | y, 67 | opts, 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lime/Viewport.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { memo, reactive } from "@/lib/signia"; 3 | import { 4 | LIME_SIDEBAR_WIDTH_PX, 5 | LIME_SLIDE_PADDING_PX, 6 | LIME_SLIDE_SIZE_PX, 7 | } from "@/lime/LimeConfig"; 8 | 9 | export class Viewport { 10 | @reactive accessor screenSize = Vector2.ZERO; 11 | 12 | @memo get canvasOffset() { 13 | return new Vector2(LIME_SIDEBAR_WIDTH_PX, 0); 14 | } 15 | 16 | @memo get canvasSize() { 17 | return this.screenSize.sub(this.canvasOffset); 18 | } 19 | 20 | @memo get availableCanvasSize() { 21 | return this.canvasSize.sub(LIME_SLIDE_PADDING_PX.scale(2)); 22 | } 23 | 24 | @memo get scaleFactor() { 25 | return Math.min( 26 | this.availableCanvasSize.x / LIME_SLIDE_SIZE_PX.x, 27 | this.availableCanvasSize.y / LIME_SLIDE_SIZE_PX.y, 28 | ); 29 | } 30 | 31 | @memo get slideSize() { 32 | return LIME_SLIDE_SIZE_PX.scale(this.scaleFactor); 33 | } 34 | 35 | @memo get slideOffset() { 36 | return new Vector2( 37 | Math.max( 38 | LIME_SLIDE_PADDING_PX.x, 39 | (this.canvasSize.x - this.slideSize.x) / 2, 40 | ), 41 | Math.max( 42 | LIME_SLIDE_PADDING_PX.y, 43 | (this.canvasSize.y - this.slideSize.y) / 2, 44 | ), 45 | ); 46 | } 47 | 48 | screenToSlide(screenCoords: Vector2) { 49 | return screenCoords 50 | .sub(this.canvasOffset) 51 | .sub(this.slideOffset) 52 | .div(this.scaleFactor); 53 | } 54 | 55 | slideToScreen(slideCoords: Vector2) { 56 | return slideCoords 57 | .scale(this.scaleFactor) 58 | .add(this.slideOffset) 59 | .add(this.canvasOffset); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/slomojs/main.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { has } from "@/lib/utils"; 3 | import initSlomojs, * as slomojs from "@/slomojs/crate/pkg/slomojs"; 4 | import "regenerator-runtime/runtime"; 5 | 6 | // Chrome does not seem to expose the Animation constructor globally 7 | if (typeof Animation === "undefined") { 8 | // @ts-expect-error oohhh its all spooky like 9 | window.Animation = document.body.animate({}).constructor; 10 | } 11 | 12 | if (!has(Animation.prototype, "finished")) { 13 | console.log("add finished polyfill"); 14 | Object.defineProperty(Animation.prototype, "finished", { 15 | get() { 16 | this._finished ??= 17 | this.playState === "finished" ? 18 | Promise.resolve() 19 | : new Promise((resolve, reject) => { 20 | this.addEventListener("finish", resolve, { 21 | once: true, 22 | }); 23 | this.addEventListener("cancel", reject, { 24 | once: true, 25 | }); 26 | }); 27 | return this._finished; 28 | }, 29 | }); 30 | } 31 | 32 | const source = ` 33 | let a = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10; 34 | let b = "hello" + " " + "world"; 35 | let x = 1 + 2000 + 3 + "hiii", a = 1, b = 2, c; 36 | let y = "hello" + x; 37 | log(y, x, a, b + b + a, log); 38 | let str = "hello, world"; 39 | log(str); 40 | __debugScope(); 41 | log(__debugScope); 42 | `.trim(); 43 | 44 | initSlomojs(new URL("./crate/pkg/slomojs_bg.wasm", import.meta.url)) 45 | .then(() => 46 | slomojs.start(source, assertExists(document.getElementById("root"))), 47 | ) 48 | .then((result: unknown) => console.log("success", result)) 49 | .catch((err: unknown) => console.log("error", err)); 50 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "plugins": [ 7 | "react", 8 | "@typescript-eslint", 9 | "react-hooks", 10 | "no-relative-import-paths" 11 | ], 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:react-hooks/recommended", 15 | "plugin:react/recommended", 16 | "plugin:react/jsx-runtime", 17 | "plugin:@typescript-eslint/recommended-type-checked", 18 | "plugin:@typescript-eslint/stylistic-type-checked" 19 | ], 20 | "parser": "@typescript-eslint/parser", 21 | "parserOptions": { 22 | "ecmaFeatures": { 23 | "jsx": true 24 | }, 25 | "ecmaVersion": "latest", 26 | "sourceType": "module", 27 | "project": true, 28 | "tsconfigRootDir": "." 29 | }, 30 | "rules": { 31 | "@typescript-eslint/no-unused-vars": [ 32 | "warn", 33 | { "varsIgnorePattern": "^_", "args": "none" } 34 | ], 35 | "@typescript-eslint/no-explicit-any": "off", 36 | "@typescript-eslint/no-empty-function": "off", 37 | "@typescript-eslint/no-unsafe-member-access": "off", 38 | "@typescript-eslint/no-unsafe-call": "off", 39 | "@typescript-eslint/no-unsafe-argument": "off", 40 | "@typescript-eslint/no-unsafe-return": "off", 41 | "@typescript-eslint/no-unsafe-assignment": "off", 42 | "no-constant-condition": ["error", { "checkLoops": false }], 43 | "prefer-const": ["error", { "destructuring": "all" }], 44 | "no-relative-import-paths/no-relative-import-paths": ["error"], 45 | "react-hooks/exhaustive-deps": [ 46 | "error", 47 | { "additionalHooks": "useLive" } 48 | ] 49 | }, 50 | "settings": { 51 | "react": { 52 | "version": "detect" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/palette/GamutPicker.tsx: -------------------------------------------------------------------------------- 1 | import { Gamut } from "@/palette/schema"; 2 | import { useSupport } from "@/palette/support"; 3 | import { RadioGroup } from "@headlessui/react"; 4 | import { FaExclamationTriangle } from "react-icons/fa"; 5 | 6 | export function GamutPicker({ 7 | value, 8 | onChange, 9 | }: { 10 | value: Gamut; 11 | onChange: (gamut: Gamut) => void; 12 | }) { 13 | const support = useSupport(); 14 | return ( 15 | 20 | 21 | 26 | 31 | 32 | ); 33 | } 34 | 35 | function GamutPickerOption({ 36 | value, 37 | label, 38 | supported, 39 | }: { 40 | value: Gamut; 41 | label: string; 42 | supported: boolean; 43 | }) { 44 | return ( 45 | 54 | {!supported && } 55 | {label} 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/Ticker.tsx: -------------------------------------------------------------------------------- 1 | import EventEmitter, { Unsubscribe } from "@/lib/EventEmitter"; 2 | import { reactive } from "@/lib/signia"; 3 | import { frameLoop } from "@/lib/utils"; 4 | import { createContext, useContext, useEffect, useState } from "react"; 5 | import { generateUUID } from "three/src/math/MathUtils"; 6 | 7 | export class Ticker { 8 | id = generateUUID(); 9 | private event = new EventEmitter<[ticker: Ticker]>(); 10 | 11 | @reactive accessor elapsedMs = 0; 12 | @reactive accessor deltaMs = 1000 / 60; 13 | 14 | private _stop: Unsubscribe | null = null; 15 | start() { 16 | if (this._stop) return this; 17 | let lastT: number | null = null; 18 | this._stop = frameLoop((t) => { 19 | if (lastT !== null) { 20 | const delta = t - lastT; 21 | this.elapsedMs += delta; 22 | this.deltaMs = delta; 23 | this.event.emit(this); 24 | } 25 | lastT = t; 26 | }); 27 | 28 | return this; 29 | } 30 | 31 | stop() { 32 | this._stop?.(); 33 | this._stop = null; 34 | return this; 35 | } 36 | 37 | listen(cb: (ticker: Ticker) => void) { 38 | return this.event.listen(cb); 39 | } 40 | } 41 | 42 | const TickerContext = createContext(null); 43 | export function TickerProvider({ children }: { children: React.ReactNode }) { 44 | const [ticker] = useState(() => new Ticker()); 45 | 46 | useEffect(() => { 47 | ticker.start(); 48 | return () => { 49 | ticker.stop(); 50 | }; 51 | }, [ticker]); 52 | 53 | return ( 54 | 55 | {children} 56 | 57 | ); 58 | } 59 | 60 | export function useTicker() { 61 | const ticker = useContext(TickerContext); 62 | if (!ticker) throw new Error("Ticker not found"); 63 | return ticker; 64 | } 65 | -------------------------------------------------------------------------------- /src/splatapus/model/Ids.tsx: -------------------------------------------------------------------------------- 1 | import { Result } from "@/lib/Result"; 2 | import { Schema, SchemaParseError } from "@/lib/schema"; 3 | import { identity, sample, times } from "@/lib/utils"; 4 | 5 | const ALPHABET = 6 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split(""); 7 | const USE_DEBUG_IDS = process.env.NODE_ENV !== "production"; 8 | 9 | export class IdGenerator { 10 | private readonly prefix: Prefix; 11 | readonly Id: `${Prefix}.${string}`; 12 | private debugIdCounter = 0; 13 | 14 | constructor( 15 | prefix: Prefix, 16 | readonly randomLength = 16, 17 | ) { 18 | this.prefix = prefix; 19 | this.Id = `${prefix}.SAMPLE`; 20 | const re = new RegExp(`^${this.prefix}\\.([a-zA-Z0-9_]+)$`); 21 | const validate = (input: string) => { 22 | if (re.test(input)) { 23 | return Result.ok(input as this["Id"]); 24 | } else { 25 | return Result.error( 26 | new SchemaParseError( 27 | `Expected ${this.prefix}.*, got ${input}`, 28 | [], 29 | ), 30 | ); 31 | } 32 | }; 33 | this.schema = Schema.string.transform(validate, validate, identity); 34 | } 35 | 36 | generate(debugPrefix?: string): this["Id"] { 37 | const randomSection = times(this.randomLength, () => 38 | sample(ALPHABET), 39 | ).join(""); 40 | if (USE_DEBUG_IDS) { 41 | let debugSection = debugPrefix ?? `${this.debugIdCounter++}`; 42 | debugSection = debugSection.slice(0, this.randomLength - 5); 43 | return `${this.prefix}.${debugSection}_${randomSection.slice( 44 | 0, 45 | this.randomLength - (debugSection.length + 1), 46 | )}`; 47 | } 48 | return `${this.prefix}.${randomSection}`; 49 | } 50 | 51 | schema: Schema; 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/Stack.tsx: -------------------------------------------------------------------------------- 1 | import { assertExists } from "@/lib/assert"; 2 | import { EMPTY_ARRAY } from "@tldraw/state"; 3 | 4 | export type Stack = StackItem | EmptyStackItem; 5 | 6 | export function stack(items?: T[]): Stack { 7 | if (items) { 8 | let result = EMPTY_STACK_ITEM as Stack; 9 | while (items.length) { 10 | result = result.push(assertExists(items.pop())); 11 | } 12 | return result; 13 | } 14 | return EMPTY_STACK_ITEM as any; 15 | } 16 | 17 | class EmptyStackItem implements Iterable { 18 | readonly length = 0; 19 | readonly head = null; 20 | readonly tail: Stack = this; 21 | 22 | push(head: T): Stack { 23 | return new StackItem(head, this); 24 | } 25 | 26 | toArray() { 27 | return EMPTY_ARRAY; 28 | } 29 | 30 | [Symbol.iterator]() { 31 | return { 32 | next() { 33 | return { value: undefined, done: true as const }; 34 | }, 35 | }; 36 | } 37 | } 38 | 39 | const EMPTY_STACK_ITEM = new EmptyStackItem(); 40 | 41 | class StackItem implements Iterable { 42 | length: number; 43 | constructor( 44 | public readonly head: T, 45 | public readonly tail: Stack, 46 | ) { 47 | this.length = tail.length + 1; 48 | } 49 | 50 | push(head: T): Stack { 51 | return new StackItem(head, this); 52 | } 53 | 54 | toArray() { 55 | return Array.from(this); 56 | } 57 | 58 | [Symbol.iterator]() { 59 | let stack = this as Stack; 60 | return { 61 | next() { 62 | if (stack.length) { 63 | const value = assertExists(stack.head); 64 | stack = stack.tail; 65 | return { value, done: false as const }; 66 | } else { 67 | return { value: undefined, done: true as const }; 68 | } 69 | }, 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/splatapus/model/SplatDoc.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { Schema, SchemaType } from "@/lib/schema"; 3 | import { IdGenerator } from "@/splatapus/model/Ids"; 4 | 5 | export const SplatKeyPointId = new IdGenerator("key"); 6 | export type SplatKeyPointId = (typeof SplatKeyPointId)["Id"]; 7 | export const splatKeyPointSchema = Schema.object({ 8 | id: SplatKeyPointId.schema, 9 | position: Vector2.schema.nullable(), 10 | }); 11 | export type SplatKeyPoint = SchemaType; 12 | 13 | export const SplatShapeId = new IdGenerator("shp"); 14 | export type SplatShapeId = (typeof SplatShapeId)["Id"]; 15 | export const splatShapeSchema = Schema.object({ 16 | id: SplatShapeId.schema, 17 | }); 18 | export type SplatShape = SchemaType; 19 | 20 | export const SplatShapeVersionId = new IdGenerator("shv"); 21 | export type SplatShapeVersionId = (typeof SplatShapeVersionId)["Id"]; 22 | export const splatShapeVersionSchema = Schema.object({ 23 | id: SplatShapeVersionId.schema, 24 | keyPointId: SplatKeyPointId.schema, 25 | shapeId: SplatShapeId.schema, 26 | rawPoints: Schema.arrayOf(Vector2.schema), 27 | }); 28 | export type SplatShapeVersion = SchemaType; 29 | 30 | export const SplatDocId = new IdGenerator("doc"); 31 | export type SplatDocId = (typeof SplatDocId)["Id"]; 32 | export const splatDocSchema = Schema.object({ 33 | id: SplatDocId.schema, 34 | keyPoints: Schema.objectMap(SplatKeyPointId.schema, splatKeyPointSchema), 35 | shapes: Schema.objectMap(SplatShapeId.schema, splatShapeSchema), 36 | shapeVersions: Schema.objectMap( 37 | SplatShapeVersionId.schema, 38 | splatShapeVersionSchema, 39 | ), 40 | }); 41 | export type SplatDoc = SchemaType; 42 | 43 | export function createSplatDoc(): SplatDoc { 44 | return { 45 | id: SplatDocId.generate(), 46 | keyPoints: {}, 47 | shapes: {}, 48 | shapeVersions: {}, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/lib/midi.tsx: -------------------------------------------------------------------------------- 1 | import EventEmitter, { Unsubscribe } from "@/lib/EventEmitter"; 2 | import { assertNumber } from "@/lib/assert"; 3 | import { mapRange } from "@/lib/utils"; 4 | import { WebMidi } from "webmidi"; 5 | 6 | export async function enableMidi(): Promise { 7 | return await WebMidi.enable(); 8 | } 9 | 10 | export interface MidiInputChangeEvent { 11 | id: string; 12 | value: number; 13 | } 14 | export type ListenToMidiInputFn = ( 15 | cb: (event: MidiInputChangeEvent) => void, 16 | ) => Unsubscribe; 17 | 18 | export async function getListenToMidiInput(): Promise { 19 | try { 20 | await enableMidi(); 21 | } catch (err) { 22 | console.log(err); 23 | return () => () => {}; 24 | } 25 | 26 | console.log(WebMidi); 27 | 28 | const midiEventEmitter = new EventEmitter< 29 | [{ id: string; value: number }] 30 | >(); 31 | for (const input of WebMidi.inputs) { 32 | input.addListener("controlchange", (event) => { 33 | midiEventEmitter.emit({ 34 | id: `${input.id}.${event.type}.${event.controller.number}`, 35 | value: assertNumber(event.value), 36 | }); 37 | }); 38 | input.addListener("pitchbend", (event) => { 39 | midiEventEmitter.emit({ 40 | id: `${input.id}.${event.type}`, 41 | value: mapRange(-1, 1, 0, 1, assertNumber(event.value)), 42 | }); 43 | }); 44 | input.addListener("noteon", (event) => { 45 | midiEventEmitter.emit({ 46 | id: `${input.id}.note.${event.note.number}`, 47 | value: assertNumber(event.value), 48 | }); 49 | }); 50 | input.addListener("noteoff", (event) => { 51 | midiEventEmitter.emit({ 52 | id: `${input.id}.note.${event.note.number}`, 53 | value: 0, 54 | }); 55 | }); 56 | } 57 | 58 | return (cb) => midiEventEmitter.listen(cb); 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/live/LiveEffect.tsx: -------------------------------------------------------------------------------- 1 | import { Unsubscribe } from "@/lib/EventEmitter"; 2 | import { LiveComputation } from "@/lib/live/LiveComputation"; 3 | import { noop } from "@/lib/utils"; 4 | 5 | export type LiveEffectScheduleFn = (callback: () => void) => Unsubscribe; 6 | 7 | const requestIdleCallback = 8 | globalThis.requestIdleCallback ?? globalThis.requestAnimationFrame; 9 | const cancelIdleCallback = 10 | globalThis.cancelIdleCallback ?? globalThis.cancelAnimationFrame; 11 | 12 | export class LiveEffect { 13 | private readonly computation: LiveComputation; 14 | private cancelScheduledCallback: Unsubscribe | null = null; 15 | private unsubscribe: Unsubscribe | null = null; 16 | 17 | static eager: LiveEffectScheduleFn = (cb) => { 18 | cb(); 19 | return noop; 20 | }; 21 | static frame: LiveEffectScheduleFn = (cb) => { 22 | const id = requestAnimationFrame(cb); 23 | return () => cancelAnimationFrame(id); 24 | }; 25 | static idle: LiveEffectScheduleFn = (cb) => { 26 | const id = requestIdleCallback(cb); 27 | return () => cancelIdleCallback(id); 28 | }; 29 | 30 | constructor( 31 | run: () => void, 32 | private readonly schedule: LiveEffectScheduleFn, 33 | debugName?: string, 34 | ) { 35 | this.computation = new LiveComputation(run, debugName, "LiveEffect"); 36 | this.cancelScheduledCallback = schedule(() => { 37 | this.cancelScheduledCallback = null; 38 | this.unsubscribe = this.computation.addEagerInvalidateListener( 39 | this.invalidate, 40 | ); 41 | }); 42 | } 43 | 44 | private invalidate = () => { 45 | this.cancelScheduledCallback = this.schedule(this.recompute); 46 | }; 47 | 48 | private recompute = () => { 49 | this.cancelScheduledCallback = null; 50 | this.computation.computeIfNeeded(); 51 | }; 52 | 53 | cancel() { 54 | this.cancelScheduledCallback?.(); 55 | this.unsubscribe?.(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/splatapus/ui/ModePicker.tsx: -------------------------------------------------------------------------------- 1 | import { useLive } from "@/lib/live"; 2 | import { modeClassNames } from "@/splatapus/editor/modeClassNames"; 3 | import { ModeType } from "@/splatapus/editor/modes/Mode"; 4 | import { Splatapus } from "@/splatapus/editor/useEditor"; 5 | import { ActionButton } from "@/splatapus/ui/Button"; 6 | import classNames from "classnames"; 7 | import React, { ReactNode } from "react"; 8 | 9 | export const ModePicker = React.memo(function ModePicker({ 10 | splatapus, 11 | }: { 12 | splatapus: Splatapus; 13 | }) { 14 | return ( 15 | <> 16 | 17 | draw 18 | 19 | 20 | rig 21 | 22 | 23 | play 24 | 25 | 26 | ); 27 | }); 28 | 29 | const ModeButton = function ModeButton({ 30 | modeType, 31 | splatapus, 32 | children, 33 | }: { 34 | modeType: ModeType; 35 | splatapus: Splatapus; 36 | children: ReactNode; 37 | }) { 38 | const selectedModeType = useLive( 39 | () => splatapus.interaction.activeMode.live().type, 40 | [splatapus.interaction], 41 | ); 42 | 43 | const isActive = selectedModeType === modeType; 44 | return ( 45 | { 49 | splatapus.vfx.triggerAnimation(modeType); 50 | splatapus.interaction.requestSetActiveMode(modeType); 51 | }} 52 | className={classNames( 53 | isActive && 54 | `pointer-events-none bg-gradient-to-br ${modeClassNames[modeType].gradient500} !text-white`, 55 | )} 56 | disabled={isActive} 57 | > 58 | {children} 59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/lib/react/DebugCanvasComponent.tsx: -------------------------------------------------------------------------------- 1 | import { DebugDraw } from "@/lib/DebugDraw"; 2 | import { assert } from "@/lib/assert"; 3 | import * as React from "react"; 4 | import useResizeObserver from "use-resize-observer"; 5 | 6 | type DrawFn = (canvas: DebugDraw) => void; 7 | 8 | export function DebugCanvas({ 9 | draw, 10 | width, 11 | height, 12 | style = {}, 13 | className, 14 | }: { 15 | draw: DrawFn; 16 | width: number; 17 | height: number; 18 | style?: React.CSSProperties; 19 | className?: string; 20 | }) { 21 | const pxWidth = width * window.devicePixelRatio; 22 | const pxHeight = height * window.devicePixelRatio; 23 | const canvasRef = React.useRef(null); 24 | 25 | React.useLayoutEffect(() => { 26 | assert(canvasRef.current); 27 | const ctx = canvasRef.current.getContext("2d"); 28 | assert(ctx); 29 | ctx.scale(window.devicePixelRatio, window.devicePixelRatio); 30 | const debugDraw = new DebugDraw(ctx); 31 | debugDraw.clear(); 32 | draw(debugDraw); 33 | ctx.resetTransform(); 34 | }); 35 | 36 | return ( 37 | 44 | ); 45 | } 46 | 47 | export function OverlayDebugCanvas({ draw }: { draw: DrawFn }) { 48 | const { ref, width, height } = useResizeObserver(); 49 | 50 | return ( 51 |
63 | {width && height ? 64 | 65 | : null} 66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/slomojs/crate/src/dom.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::JsCast; 2 | pub use web_sys::{Document, Element, HtmlElement}; 3 | 4 | fn create_el(document: &Document, name: &str) -> HtmlElement { 5 | document.create_element(name).unwrap().dyn_into().unwrap() 6 | } 7 | 8 | pub fn div(document: &Document) -> DomBuilder { 9 | DomBuilder { 10 | el: create_el(document, "div"), 11 | } 12 | } 13 | 14 | pub fn set_styles(element: &HtmlElement, styles: &[(&str, &str)]) { 15 | styles 16 | .iter() 17 | .for_each(|(name, value)| element.style().set_property(name, value).unwrap()); 18 | } 19 | 20 | pub fn append_child(element: &HtmlElement, child_el: &HtmlElement) { 21 | element.append_child(child_el).unwrap(); 22 | } 23 | 24 | pub struct DomBuilder { 25 | el: HtmlElement, 26 | } 27 | impl DomBuilder { 28 | pub fn styles(self, styles: &[(&str, &str)]) -> Self { 29 | set_styles(&self.el, styles); 30 | self 31 | } 32 | 33 | pub fn data_attr(self, name: &str, value: &str) -> Self { 34 | self.el 35 | .set_attribute(&format!("data-{}", name), value) 36 | .unwrap(); 37 | self 38 | } 39 | 40 | pub fn class_name(self, name: &str) -> Self { 41 | self.el.set_class_name(name); 42 | self 43 | } 44 | 45 | pub fn child(self, child_el: &HtmlElement) -> Self { 46 | self.el.append_child(child_el).unwrap(); 47 | self 48 | } 49 | 50 | pub fn children<'a, I: Iterator>(self, items: I) -> Self { 51 | items.for_each(|child_el| append_child(&self.el, child_el)); 52 | self 53 | } 54 | 55 | pub fn children_into>(self, items: I) -> Self { 56 | items.for_each(|child_el| append_child(&self.el, &child_el)); 57 | self 58 | } 59 | 60 | pub fn text_content(self, text: &str) -> Self { 61 | self.el.set_text_content(Some(text)); 62 | self 63 | } 64 | } 65 | impl Into for DomBuilder { 66 | fn into(self) -> HtmlElement { 67 | self.el 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/spline-time/SplineTimeLine.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { Result } from "@/lib/Result"; 3 | import { Schema } from "@/lib/schema"; 4 | import { copyArrayAndInsert, copyArrayAndReplace } from "@/lib/utils"; 5 | 6 | export class SplineTimeLine { 7 | static schema = Schema.arrayOf(Vector2.schema).transform( 8 | (points) => Result.ok(new SplineTimeLine(points)), 9 | Schema.cannotValidate("SplineTimeLine"), 10 | (line) => line.points, 11 | ); 12 | 13 | constructor(readonly points: readonly Vector2[]) {} 14 | 15 | insertPointAtIndex(index: number, point: Vector2) { 16 | return new SplineTimeLine( 17 | copyArrayAndInsert(this.points, index, point), 18 | ); 19 | } 20 | 21 | updatePointAtIndex(index: number, point: Vector2) { 22 | return new SplineTimeLine( 23 | copyArrayAndReplace(this.points, index, point), 24 | ); 25 | } 26 | 27 | getClosestPointTo( 28 | target: Vector2, 29 | ): { index: number; point: Vector2; distance: number } | null { 30 | if (this.points.length === 0) return null; 31 | 32 | let closestPointIndex = 0; 33 | let closestPointDistance = this.points[0].distanceTo(target); 34 | for (let i = 1; i < this.points.length; i++) { 35 | const point = this.points[i]; 36 | const distance = point.distanceTo(target); 37 | if (distance < closestPointDistance) { 38 | closestPointIndex = i; 39 | closestPointDistance = distance; 40 | } 41 | } 42 | return { 43 | index: closestPointIndex, 44 | point: this.points[closestPointIndex], 45 | distance: closestPointDistance, 46 | }; 47 | } 48 | 49 | getClosestPointIndexWithinRadius(point: Vector2, radius: number) { 50 | const closestPoint = this.getClosestPointTo(point); 51 | if (closestPoint && closestPoint.distance <= radius) { 52 | return closestPoint.index; 53 | } 54 | return null; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/pals/pals-main.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "@/lib/assert"; 2 | import { Vector2 } from "@/lib/geom/Vector2"; 3 | import Entity from "@/lib/scene/Entity"; 4 | import Scene from "@/lib/scene/Scene"; 5 | import { generateRandomPalConfig } from "@/pals/PalConfig"; 6 | import { PalTargetController } from "@/pals/PalController"; 7 | import PalGeom from "@/pals/PalGeom"; 8 | import PalRenderer from "@/pals/PalRenderer"; 9 | import PalWalkAnimationController from "@/pals/PalWalkAnimationController"; 10 | 11 | const root = document.getElementById("root"); 12 | assert(root); 13 | 14 | const scene = new Scene(800, 600, window.devicePixelRatio); 15 | scene.appendTo(root); 16 | 17 | // const pal = new Pal(100, 50); 18 | const pal = new Entity(); 19 | pal.addComponent(PalTargetController, new Vector2(100, 50)); 20 | const config = generateRandomPalConfig(); 21 | const geom = pal.addComponent(PalGeom, config); 22 | geom.setAnimationController(new PalWalkAnimationController(config)); 23 | pal.addComponent(PalRenderer, config); 24 | scene.addChild(pal); 25 | 26 | root.addEventListener("mousemove", (e) => { 27 | const x = e.clientX - scene.canvas.offsetLeft; 28 | const y = e.clientY - scene.canvas.offsetTop; 29 | pal.getComponent(PalTargetController).setTarget(new Vector2(x - 50, y)); 30 | }); 31 | 32 | scene.start(); 33 | 34 | // const scenario5 = () => { 35 | // const pal = new Pal(100, 50); 36 | // scene.addChild(pal); 37 | 38 | // const root = document.getElementById('root'); 39 | // invariant(root, '#root must be present'); 40 | 41 | // root.addEventListener('mousemove', e => { 42 | // pal.setTarget(e.offsetX / scene.scaleFactor, e.offsetY / scene.scaleFactor); 43 | // }); 44 | // }; 45 | 46 | // const go = () => { 47 | // if (window.scene) return; 48 | // scene = new Scene(800, 600, window.devicePixelRatio); 49 | // window.scene = scene; 50 | // const root = document.getElementById('root'); 51 | // invariant(root, '#root must be present'); 52 | // scene.appendTo(root); 53 | 54 | // scene.addSystem(new DebugOverlay()); 55 | // scene.addSystem(new TravellerFinder()); 56 | 57 | // scenario3(); 58 | 59 | // scene.start(); 60 | // }; 61 | -------------------------------------------------------------------------------- /src/geometry/GeometryApp.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { 3 | sizeFromBorderBox, 4 | useResizeObserver, 5 | } from "@/lib/hooks/useResizeObserver"; 6 | import { Button } from "@/splatapus/ui/Button"; 7 | import classNames from "classnames"; 8 | import { useState } from "react"; 9 | 10 | export interface Entry { 11 | id: string; 12 | name: string; 13 | component: React.ComponentType<{ size: Vector2 }>; 14 | } 15 | 16 | export function GeometryApp({ 17 | entries, 18 | current, 19 | }: { 20 | entries: Entry[]; 21 | current: Entry; 22 | }) { 23 | const [container, setContainer] = useState(null); 24 | const size = useResizeObserver(container, sizeFromBorderBox); 25 | 26 | return ( 27 |
28 |
29 |

30 | geometries 31 |

32 |
33 | {entries.map((entry) => { 34 | const isActive = entry.id === current.id; 35 | return ( 36 | 46 | ); 47 | })} 48 |
49 |
50 |
54 | {size && } 55 |
56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/gestureland/interactions/interactions.tsx: -------------------------------------------------------------------------------- 1 | import { Gestureland } from "@/gestureland/Gestureland"; 2 | import { Stroke } from "@/gestureland/GesturelandStore"; 3 | import { GLTarget } from "@/gestureland/types"; 4 | 5 | export function drawOnCanvas(editor: Gestureland): GLTarget { 6 | return { 7 | id: "draw on canvas", 8 | index: -Infinity, 9 | distanceTo() { 10 | return 0; 11 | }, 12 | onDrag(start) { 13 | const shape = Stroke.create({}); 14 | editor.store.put1(shape); 15 | 16 | return { 17 | onUpdate(event) { 18 | editor.store.update(shape.id, (s) => ({ 19 | ...s, 20 | points: [...s.points, event.pagePosition.toJson()], 21 | })); 22 | }, 23 | onCancel() { 24 | editor.store.delete(shape.id); 25 | }, 26 | }; 27 | }, 28 | }; 29 | } 30 | 31 | export function interactWithShapes(editor: Gestureland): GLTarget[] { 32 | return editor.shapes.map((initialShape) => ({ 33 | id: `drag shape ${initialShape.id}`, 34 | index: 1, 35 | distanceTo(testPoint) { 36 | return Math.min( 37 | ...initialShape.points.map((p) => testPoint.distanceTo(p)), 38 | ); 39 | }, 40 | onDrag(start) { 41 | return { 42 | onUpdate(event) { 43 | const delta = event.pagePosition.sub(start.pagePosition); 44 | editor.store.update(initialShape.id, (s) => ({ 45 | ...s, 46 | points: initialShape.points.map((p) => 47 | delta.add(p).toJson(), 48 | ), 49 | })); 50 | }, 51 | onCancel(event) { 52 | editor.store.update(initialShape.id, (s) => ({ 53 | ...s, 54 | points: initialShape.points, 55 | })); 56 | }, 57 | }; 58 | }, 59 | })); 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/geom/ApproxCubicBezierPathSegment.ts: -------------------------------------------------------------------------------- 1 | import { Path, PathSegment } from "@/lib/geom/Path"; 2 | import StraightPathSegment from "@/lib/geom/StraightPathSegment"; 3 | import { Vector2 } from "@/lib/geom/Vector2"; 4 | import { SvgPathBuilder } from "@/lib/svgPathBuilder"; 5 | 6 | export function interpolateCubicBezier( 7 | start: Vector2, 8 | control1: Vector2, 9 | control2: Vector2, 10 | end: Vector2, 11 | n: number, 12 | ) { 13 | const a1 = start.lerp(control1, n); 14 | const a2 = control1.lerp(control2, n); 15 | const a3 = control2.lerp(end, n); 16 | const b1 = a1.lerp(a2, n); 17 | const b2 = a2.lerp(a3, n); 18 | return b1.lerp(b2, n); 19 | } 20 | 21 | export default class ApproxCubicBezierPathSegment implements PathSegment { 22 | private readonly path: Path; 23 | constructor( 24 | start: Vector2, 25 | control1: Vector2, 26 | control2: Vector2, 27 | end: Vector2, 28 | resolution = 100, 29 | ) { 30 | const path = new Path(); 31 | let lastPoint = start; 32 | for (let i = 1; i <= resolution; i++) { 33 | const t = i / resolution; 34 | 35 | const point = interpolateCubicBezier( 36 | start, 37 | control1, 38 | control2, 39 | end, 40 | t, 41 | ); 42 | path.addSegment(new StraightPathSegment(lastPoint, point)); 43 | lastPoint = point; 44 | } 45 | this.path = path; 46 | } 47 | 48 | getStart(): Vector2 { 49 | return this.path.getStart(); 50 | } 51 | 52 | getEnd(): Vector2 { 53 | return this.path.getEnd(); 54 | } 55 | 56 | getLength(): number { 57 | return this.path.getLength(); 58 | } 59 | 60 | getPointAtPosition(position: number): Vector2 { 61 | return this.path.getPointAtPosition(position); 62 | } 63 | 64 | getAngleAtPosition(position: number): number { 65 | return this.path.getAngleAtPosition(position); 66 | } 67 | 68 | appendToSvgPathBuilder(pathBuilder: SvgPathBuilder): void { 69 | this.path.appendToSvgPathBuilder(pathBuilder); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/emoji/Post.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export function Post({ 4 | name = "Charlie Yeti", 5 | funZone, 6 | }: { 7 | name?: string; 8 | funZone?: ReactNode; 9 | }) { 10 | return ( 11 |
12 |
13 |
14 | {initials(name)} 15 |
16 |
17 |
{name}
18 |
Jan 2 at 7:46pm
19 |
20 |
21 |
22 |

23 | A spectre is haunting Europe — the spectre of communism. All 24 | the powers of old Europe have entered into a holy alliance 25 | to exorcise this spectre: Pope and Tsar, Metternich and 26 | Guizot, French Radicals and German police-spies. 27 |

28 |

29 | Where is the party in opposition that has not been decried 30 | as communistic by its opponents in power? Where is the 31 | opposition that has not hurled back the branding reproach of 32 | communism, against the more advanced opposition parties, as 33 | well as against its reactionary adversaries? 34 |

35 |
36 |
37 | {funZone} 38 | 41 |
42 |
43 | ); 44 | } 45 | 46 | function initials(name: string) { 47 | return name 48 | .split(" ") 49 | .filter(Boolean) 50 | .map((word) => word[0].toUpperCase()) 51 | .join("") 52 | .slice(0, 2); 53 | } 54 | -------------------------------------------------------------------------------- /src/wires/wiresModel.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { Schema, SchemaType } from "@/lib/schema"; 3 | import { applyUpdateWithin } from "@/lib/utils"; 4 | import { IdGenerator } from "@/splatapus/model/Ids"; 5 | 6 | export const WireNodeId = new IdGenerator("node"); 7 | export type WireNodeId = (typeof WireNodeId)["Id"]; 8 | export const outWireNodeSchema = Schema.object({ 9 | type: Schema.value("out"), 10 | id: WireNodeId.schema, 11 | position: Vector2.schema, 12 | }); 13 | export type OutWireNode = SchemaType; 14 | 15 | export const inWireNodeSchema = Schema.object({ 16 | type: Schema.value("in"), 17 | id: WireNodeId.schema, 18 | position: Vector2.schema, 19 | }); 20 | export type InWireNodeSchema = SchemaType; 21 | 22 | export const joinWireNodeSchema = Schema.object({ 23 | type: Schema.value("join"), 24 | id: WireNodeId.schema, 25 | position: Vector2.schema, 26 | }); 27 | export type JoinWireNodeSchema = SchemaType; 28 | 29 | export const wireNodeSchema = Schema.union("type", { 30 | out: outWireNodeSchema, 31 | in: inWireNodeSchema, 32 | join: joinWireNodeSchema, 33 | }); 34 | export type WireNode = SchemaType; 35 | 36 | export const WireId = new IdGenerator("wire"); 37 | export type WireId = (typeof WireId)["Id"]; 38 | export const wireSchema = Schema.object({ 39 | id: WireId.schema, 40 | startNodeId: WireNodeId.schema, 41 | endNodeId: WireNodeId.schema, 42 | midPoints: Schema.arrayOf(Vector2.schema), 43 | }); 44 | export type Wire = SchemaType; 45 | 46 | export const wiresModelSchema = Schema.object({ 47 | nodes: Schema.objectMap(WireNodeId.schema, wireNodeSchema), 48 | wires: Schema.objectMap(WireId.schema, wireSchema), 49 | }); 50 | export type WiresModel = SchemaType; 51 | 52 | export function insert< 53 | K extends keyof WiresModel, 54 | Id extends string, 55 | T extends { readonly id: Id }, 56 | >(model: WiresModel, key: K, item: T): WiresModel { 57 | return applyUpdateWithin(model, key, (table) => ({ 58 | ...table, 59 | [item.id]: item, 60 | })); 61 | } 62 | -------------------------------------------------------------------------------- /.yarn/patches/@types-culori-npm-2.0.4-129982cb7c.patch: -------------------------------------------------------------------------------- 1 | diff --git a/fn/index.d.ts b/fn/index.d.ts 2 | index d8427ad5732c487b7e078763bdafc2f2aa19789c..a080370ccc06ee22182f58fafebdd2203e1389a2 100644 3 | --- a/fn/index.d.ts 4 | +++ b/fn/index.d.ts 5 | @@ -70,7 +70,7 @@ export { 6 | interpolatorSplineMonotoneClosed, 7 | } from "../src/interpolate/splineMonotone"; 8 | 9 | -export { clampChroma, clampRgb } from "../src/clamp"; 10 | +export { clampChroma, clampRgb, toGamut, clampGamut } from "../src/clamp"; 11 | export { default as displayable } from "../src/displayable"; 12 | export { default as lerp } from "../src/interpolate/lerp"; 13 | export { getMode, removeParser, useMode, useParser } from "../src/modes"; 14 | diff --git a/index.d.ts b/index.d.ts 15 | index c71d73f7b6c809029b35da1af2b0ac2afe9179d1..7a3dabd695c96253c661a593dcde72008c31188c 100644 16 | --- a/index.d.ts 17 | +++ b/index.d.ts 18 | @@ -72,7 +72,7 @@ export { 19 | interpolatorSplineMonotoneClosed, 20 | } from "./src/interpolate/splineMonotone"; 21 | 22 | -export { clampChroma, clampRgb } from "./src/clamp"; 23 | +export { clampChroma, clampRgb, toGamut, clampGamut } from "./src/clamp"; 24 | export { default as displayable } from "./src/displayable"; 25 | export { default as lerp } from "./src/interpolate/lerp"; 26 | export { getMode, removeParser, useMode, useParser } from "./src/modes"; 27 | diff --git a/src/clamp.d.ts b/src/clamp.d.ts 28 | index e61d3a09805f62d6ce3bc2e8e2c2e3d687db92ec..3ed62131fd731998cd549b4e7273e5208e241c6a 100644 29 | --- a/src/clamp.d.ts 30 | +++ b/src/clamp.d.ts 31 | @@ -1,7 +1,16 @@ 32 | import { Color, Mode } from "./common"; 33 | +import { DiffFn } from "./difference"; 34 | 35 | export function clampRgb(color: string): Color | undefined; 36 | export function clampRgb(color: C): C; 37 | 38 | export function clampChroma(color: string, mode?: Mode): Color | undefined; 39 | export function clampChroma(color: C, mode?: Mode): C; 40 | + 41 | +export function toGamut( 42 | + dest?: Mode, 43 | + mode?: Mode, 44 | + delta?: DiffFn, 45 | + jnd?: number 46 | +): (color: string | Color) => Color; 47 | +export function clampGamut(mode?: Mode): (color: string | Color) => Color; 48 | \ No newline at end of file 49 | -------------------------------------------------------------------------------- /src/bees/bees-main2.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatedSpriteStack } from "@/bees/AnimatedSpriteStack"; 2 | import { assets } from "@/bees/assets/assets"; 3 | import { BG_COLOR } from "@/bees/constants"; 4 | import { driver } from "@/bees/driver"; 5 | import { Vector2 } from "@/lib/geom/Vector2"; 6 | import { random, times } from "@/lib/utils"; 7 | import { 8 | Application, 9 | InteractionManager, 10 | Renderer, 11 | SCALE_MODES, 12 | settings, 13 | } from "pixi.js"; 14 | 15 | Renderer.registerPlugin("interaction", InteractionManager); 16 | console.log({ Renderer, InteractionManager }); 17 | 18 | settings.STRICT_TEXTURE_CACHE = true; 19 | settings.SCALE_MODE = SCALE_MODES.NEAREST; 20 | 21 | const application = new Application({ 22 | resizeTo: window, 23 | autoDensity: true, 24 | resolution: window.devicePixelRatio, 25 | backgroundColor: BG_COLOR, 26 | }); 27 | 28 | document.body.appendChild(application.view); 29 | 30 | void assets.loadAll().then(() => { 31 | times(20, makeBee); 32 | }); 33 | 34 | function makeBee() { 35 | const sp = random(1, 5); 36 | const rsp = random(0.01, 0.15); 37 | 38 | const bee = new AnimatedSpriteStack(assets.get("beeFly"), driver); 39 | application.stage.addChild(bee); 40 | bee.x = Math.random() * 1000; 41 | bee.y = Math.random() * 1000; 42 | 43 | driver.addFixedUpdate({ 44 | on() {}, 45 | fixedUpdateTick() { 46 | const beePosition = Vector2.from(bee.getGlobalPosition()); 47 | const mousePosition = Vector2.from( 48 | ( 49 | application.renderer.plugins 50 | .interaction as InteractionManager 51 | ).mouse.getLocalPosition(application.stage), 52 | ); 53 | 54 | const headingVec = Vector2.fromPolar(bee.heading, 1); 55 | const targetVec = mousePosition.sub(beePosition).normalize(); 56 | const newVec = headingVec.lerp(targetVec, rsp); 57 | 58 | bee.heading = newVec.angle(); 59 | bee.position.copyFrom( 60 | Vector2.from(bee.position).add( 61 | Vector2.fromPolar(bee.heading, sp), 62 | ), 63 | ); 64 | }, 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /src/splatapus/editor/SplatLocation.tsx: -------------------------------------------------------------------------------- 1 | import { LiveMemoWritable, LiveWritable } from "@/lib/live"; 2 | import { Schema, SchemaType } from "@/lib/schema"; 3 | import { applyUpdateWithin } from "@/lib/utils"; 4 | import { ViewportState } from "@/splatapus/editor/Viewport"; 5 | import { ModeType, modeTypeSchema } from "@/splatapus/editor/modes/Mode"; 6 | import { SplatKeyPointId, SplatShapeId } from "@/splatapus/model/SplatDoc"; 7 | 8 | const splatLocationSchema = Schema.object({ 9 | keyPointId: SplatKeyPointId.schema, 10 | shapeId: SplatShapeId.schema, 11 | viewport: ViewportState.schema, 12 | mode: modeTypeSchema, 13 | }); 14 | export type SplatLocationState = SchemaType; 15 | 16 | export const SplatLocationState = { 17 | initialize: ( 18 | keyPointId: SplatKeyPointId, 19 | shapeId: SplatShapeId, 20 | ): SplatLocationState => ({ 21 | keyPointId, 22 | shapeId, 23 | mode: ModeType.Draw, 24 | viewport: ViewportState.initialize(), 25 | }), 26 | schema: splatLocationSchema, 27 | }; 28 | 29 | export class SplatLocation { 30 | constructor(readonly state: LiveWritable) {} 31 | 32 | readonly keyPointId = new LiveMemoWritable( 33 | () => this.state.live().keyPointId, 34 | (update) => 35 | this.state.update((state) => 36 | applyUpdateWithin(state, "keyPointId", update), 37 | ), 38 | ); 39 | readonly shapeId = new LiveMemoWritable( 40 | () => this.state.live().shapeId, 41 | (update) => 42 | this.state.update((state) => 43 | applyUpdateWithin(state, "shapeId", update), 44 | ), 45 | ); 46 | readonly mode = new LiveMemoWritable( 47 | () => this.state.live().mode, 48 | (update) => 49 | this.state.update((state) => 50 | applyUpdateWithin(state, "mode", update), 51 | ), 52 | ); 53 | readonly viewportState = new LiveMemoWritable( 54 | () => this.state.live().viewport, 55 | (update) => 56 | this.state.update((state) => 57 | applyUpdateWithin(state, "viewport", update), 58 | ), 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/txdraw/utils/texel.ts: -------------------------------------------------------------------------------- 1 | import { Editor, VecModel, WeakCache } from "tldraw"; 2 | 3 | const texelSizeCache = new WeakCache(); 4 | export function getTexelSize(editor: Editor) { 5 | return texelSizeCache.get(editor, () => { 6 | const box = editor.textMeasure.measureText("╋", { 7 | maxWidth: null, 8 | fontFamily: "tldraw_mono, monospace", 9 | fontSize: 14, 10 | fontStyle: "normal", 11 | fontWeight: "normal", 12 | lineHeight: 1, 13 | padding: "0", 14 | }); 15 | 16 | return { x: box.w, y: box.h }; 17 | }); 18 | } 19 | 20 | export function pxToTxWidth(editor: Editor, px: number): number { 21 | const { x } = getTexelSize(editor); 22 | return Math.round(px / x); 23 | } 24 | 25 | export function pxToTxHeight(editor: Editor, px: number): number { 26 | const { y } = getTexelSize(editor); 27 | return Math.round(px / y); 28 | } 29 | 30 | export function pxToTxWidthCeil(editor: Editor, px: number): number { 31 | const { x } = getTexelSize(editor); 32 | return Math.ceil(px / x); 33 | } 34 | 35 | export function pxToTxHeightCeil(editor: Editor, px: number): number { 36 | const { y } = getTexelSize(editor); 37 | return Math.ceil(px / y); 38 | } 39 | 40 | export function pxToTxWidthFloor(editor: Editor, px: number): number { 41 | const { x } = getTexelSize(editor); 42 | return Math.floor(px / x); 43 | } 44 | 45 | export function pxToTxHeightFloor(editor: Editor, px: number): number { 46 | const { y } = getTexelSize(editor); 47 | return Math.floor(px / y); 48 | } 49 | 50 | export function txToPxWidth(editor: Editor, tx: number): number { 51 | const { x } = getTexelSize(editor); 52 | return tx * x; 53 | } 54 | 55 | export function txToPxHeight(editor: Editor, tx: number): number { 56 | const { y } = getTexelSize(editor); 57 | return tx * y; 58 | } 59 | 60 | export function snapPxWidthToTexel(editor: Editor, px: number): number { 61 | const { x } = getTexelSize(editor); 62 | return Math.round(px / x) * x; 63 | } 64 | 65 | export function snapPxHeightToTexel(editor: Editor, px: number): number { 66 | const { y } = getTexelSize(editor); 67 | return Math.round(px / y) * y; 68 | } 69 | -------------------------------------------------------------------------------- /src/lib/live/LiveMemo.tsx: -------------------------------------------------------------------------------- 1 | import { Unsubscribe } from "@/lib/EventEmitter"; 2 | import { Live, LiveWritable } from "@/lib/live"; 3 | import { LiveComputation, trackRead } from "@/lib/live/LiveComputation"; 4 | import { Result } from "@/lib/Result"; 5 | import { UpdateAction } from "@/lib/utils"; 6 | 7 | export class LiveMemo implements Live { 8 | private readonly computation: LiveComputation; 9 | private completion: Result | null = null; 10 | 11 | constructor(compute: () => T, debugName?: string, __t = "LiveMemo") { 12 | this.computation = new LiveComputation(compute, debugName, __t); 13 | } 14 | 15 | getDebugName(): string { 16 | return this.computation.getDebugName(); 17 | } 18 | 19 | getOnce(): T { 20 | this.completion = this.computation.computeIfNeeded(); 21 | return this.completion.unwrap(); 22 | } 23 | 24 | live(): T { 25 | this.completion = this.computation.computeIfNeeded(); 26 | trackRead(this); 27 | return this.completion.unwrap(); 28 | } 29 | 30 | addEagerInvalidateListener(callback: () => void): Unsubscribe { 31 | return this.computation.addEagerInvalidateListener(callback); 32 | } 33 | addBatchInvalidateListener(callback: () => void): Unsubscribe { 34 | return this.computation.addBatchInvalidateListener(callback); 35 | } 36 | } 37 | 38 | export class LiveMemoWritable 39 | extends LiveMemo 40 | implements LiveWritable 41 | { 42 | constructor( 43 | read: () => T, 44 | private readonly write: ( 45 | update: UpdateAction, 46 | ...args: Args | [] 47 | ) => void, 48 | debugName?: string, 49 | ) { 50 | super(read, debugName, "LiveMemoWritable"); 51 | } 52 | 53 | update(update: UpdateAction, ...args: Args | []) { 54 | this.write(update, ...args); 55 | // // we have to eagerly evaluate memo updates because otherwise we don't know what to invalidate :( 56 | // const initialValue = this.getWithoutListening(); 57 | // const newValue = applyUpdate(initialValue, update); 58 | // if (!Object.is(newValue, initialValue)) { 59 | // this.write(newValue); 60 | // } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/txdraw/components/Grid.tsx: -------------------------------------------------------------------------------- 1 | import { getTexelSize } from "@/txdraw/utils/texel"; 2 | import { 3 | modulate, 4 | suffixSafeId, 5 | TLGridProps, 6 | useEditor, 7 | useUniqueSafeId, 8 | } from "tldraw"; 9 | 10 | export function Grid({ x, y, z }: TLGridProps) { 11 | const id = useUniqueSafeId("grid"); 12 | const editor = useEditor(); 13 | const { gridSteps } = editor.options; 14 | const size = getTexelSize(editor); 15 | return ( 16 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/splatapus2/store/Records.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { SchemaType } from "@/lib/schema"; 3 | import { IdGenerator } from "@/splatapus/model/Ids"; 4 | import { 5 | IncrementalDiff, 6 | IncrementalValue, 7 | incrementalArrayOf, 8 | incrementalAtom, 9 | incrementalObject, 10 | incrementalStatic, 11 | incrementalTable, 12 | } from "@/splatapus2/store/Incremental"; 13 | 14 | export const KeyPointId = new IdGenerator("key"); 15 | export type KeyPointId = (typeof KeyPointId)["Id"]; 16 | export const KeyPoint = incrementalObject({ 17 | id: incrementalStatic(KeyPointId.schema), 18 | position: incrementalAtom(Vector2.schema.nullable()), 19 | }); 20 | export type KeyPoint = SchemaType; 21 | 22 | export const KeyPointTable = incrementalTable(KeyPointId.schema, KeyPoint); 23 | export type KeyPointTable = SchemaType; 24 | 25 | export const ShapeId = new IdGenerator("shp"); 26 | export type ShapeId = (typeof ShapeId)["Id"]; 27 | export const Shape = incrementalObject({ 28 | id: incrementalStatic(ShapeId.schema), 29 | }); 30 | export type Shape = SchemaType; 31 | 32 | export const ShapeTable = incrementalTable(ShapeId.schema, Shape); 33 | export type ShapeTable = SchemaType; 34 | 35 | export const ShapeVersionId = new IdGenerator("shv"); 36 | export type ShapeVersionId = (typeof ShapeVersionId)["Id"]; 37 | export const ShapeVersion = incrementalObject({ 38 | id: incrementalStatic(ShapeVersionId.schema), 39 | keyPointId: incrementalAtom(KeyPointId.schema), 40 | shapeId: incrementalAtom(ShapeId.schema), 41 | rawPoints: incrementalArrayOf(Vector2.schema), 42 | }); 43 | export type ShapeVersion = SchemaType; 44 | 45 | export const ShapeVersionTable = incrementalTable( 46 | ShapeVersionId.schema, 47 | ShapeVersion, 48 | ); 49 | export type ShapeVersionTable = SchemaType< 50 | typeof ShapeVersionTable.valueSchema 51 | >; 52 | 53 | export const Doc = incrementalObject({ 54 | keyPoints: KeyPointTable, 55 | shapes: ShapeTable, 56 | shapeVersions: ShapeVersionTable, 57 | }); 58 | export type Doc = IncrementalValue; 59 | export type DocDiff = IncrementalDiff; 60 | -------------------------------------------------------------------------------- /src/splatapus3/model/History.tsx: -------------------------------------------------------------------------------- 1 | import { assert } from "@/lib/assert"; 2 | import { reactive } from "@/lib/signia"; 3 | import { values } from "@/lib/utils"; 4 | import { IdGenerator } from "@/splatapus/model/Ids"; 5 | import { Splat } from "@/splatapus3/model/Splat"; 6 | import { SplatSerializedStore } from "@/splatapus3/model/schema"; 7 | import { transact } from "@tldraw/state"; 8 | 9 | export const HistoryEntryId = new IdGenerator("undo"); 10 | export type HistoryEntryId = typeof HistoryEntryId.Id; 11 | 12 | interface HistoryEntry { 13 | readonly id: HistoryEntryId; 14 | readonly store: SplatSerializedStore; 15 | } 16 | 17 | export class SplatHistory { 18 | @reactive private accessor undos = [] as readonly HistoryEntry[]; 19 | @reactive private accessor redos = [] as readonly HistoryEntry[]; 20 | 21 | constructor(private readonly splat: Splat) {} 22 | 23 | get canUndo() { 24 | return this.undos.length > 0; 25 | } 26 | 27 | get canRedo() { 28 | return this.redos.length > 0; 29 | } 30 | 31 | private restore(entry: HistoryEntry) { 32 | transact(() => { 33 | this.splat.store.clear(); 34 | this.splat.store.put(values(entry.store)); 35 | this.splat.ensureStoreIsUsable(); 36 | }); 37 | } 38 | 39 | mark() { 40 | const entry = { 41 | id: HistoryEntryId.generate(), 42 | store: this.splat.store.serialize(), 43 | }; 44 | this.undos = [entry, ...this.undos]; 45 | this.redos = []; 46 | return entry.id; 47 | } 48 | 49 | undo() { 50 | if (this.canUndo) { 51 | const [entry, ...rest] = this.undos; 52 | this.undos = rest; 53 | this.redos = [entry, ...this.redos]; 54 | this.restore(entry); 55 | } 56 | } 57 | 58 | redo() { 59 | if (this.canRedo) { 60 | const [entry, ...rest] = this.redos; 61 | this.redos = rest; 62 | this.undos = [entry, ...this.undos]; 63 | this.restore(entry); 64 | } 65 | } 66 | 67 | bailToMark(mark: HistoryEntryId) { 68 | const lastUndo = this.undos[0]; 69 | assert(lastUndo.id === mark); 70 | this.undos = this.undos.slice(0, -1); 71 | this.restore(lastUndo); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/splatapus/model/pathFromCenterPoints.tsx: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | import { 3 | getSvgPathFromStroke, 4 | StrokeCenterPoint, 5 | } from "@/splatapus/model/perfectFreehand"; 6 | 7 | export function pathFromCenterPoints( 8 | path: readonly StrokeCenterPoint[], 9 | ): Vector2[] { 10 | const leftPoints: Vector2[] = []; 11 | const rightPoints: Vector2[] = []; 12 | 13 | if (path.length === 0) { 14 | return []; 15 | } 16 | 17 | if (path.length === 1) { 18 | const dot: Vector2[] = []; 19 | 20 | for (let i = 0; i < 12; i++) { 21 | dot.push( 22 | path[0].center.add( 23 | Vector2.fromPolar((i * Math.PI) / 6, path[0].radius), 24 | ), 25 | ); 26 | } 27 | 28 | return dot; 29 | } 30 | 31 | const p0 = path[0]; 32 | const p1 = path[1]; 33 | const startDirection = p1.center.sub(p0.center).normalize(); 34 | const startOffset = startDirection.perpendicular().scale(p0.radius); 35 | leftPoints.push(p0.center.sub(startDirection.scale(p0.radius))); 36 | leftPoints.push(p0.center.sub(startOffset)); 37 | rightPoints.push(p0.center.add(startOffset)); 38 | 39 | for (let i = 1; i < path.length - 1; i++) { 40 | const last = path[i - 1]; 41 | const current = path[i]; 42 | const next = path[i + 1]; 43 | 44 | const direction = next.center.sub(last.center).normalize(); 45 | const offset = direction.perpendicular().scale(current.radius); 46 | leftPoints.push(current.center.sub(offset)); 47 | rightPoints.push(current.center.add(offset)); 48 | } 49 | 50 | const last = path[path.length - 2]; 51 | const current = path[path.length - 1]; 52 | const endDirection = current.center.sub(last.center).normalize(); 53 | const endOffset = endDirection.perpendicular().scale(current.radius); 54 | leftPoints.push(current.center.sub(endOffset)); 55 | leftPoints.push(current.center.add(endDirection.scale(current.radius))); 56 | rightPoints.push(current.center.add(endOffset)); 57 | 58 | return leftPoints.concat(rightPoints.reverse()); 59 | } 60 | 61 | export function svgPathFromCenterPoints( 62 | path: readonly StrokeCenterPoint[], 63 | ): string { 64 | return getSvgPathFromStroke(pathFromCenterPoints(path)); 65 | } 66 | -------------------------------------------------------------------------------- /src/lib/geom/bezier.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2016 Roger Veciana i Rovira 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | import { Vector2 } from "@/lib/geom/Vector2"; 26 | 27 | export function getQuadraticArcLength( 28 | start: Vector2, 29 | control: Vector2, 30 | end: Vector2, 31 | t = 1, 32 | ) { 33 | const ax = start.x - 2 * control.x + end.x; 34 | const ay = start.y - 2 * control.y + end.y; 35 | const bx = 2 * control.x - 2 * start.x; 36 | const by = 2 * control.y - 2 * start.y; 37 | 38 | const A = 4 * (ax * ax + ay * ay); 39 | const B = 4 * (ax * bx + ay * by); 40 | const C = bx * bx + by * by; 41 | 42 | if (A === 0) { 43 | return ( 44 | t * 45 | Math.sqrt( 46 | Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2), 47 | ) 48 | ); 49 | } 50 | const b = B / (2 * A); 51 | const c = C / A; 52 | const u = t + b; 53 | const k = c - b * b; 54 | 55 | const uuk = u * u + k > 0 ? Math.sqrt(u * u + k) : 0; 56 | const bbk = b * b + k > 0 ? Math.sqrt(b * b + k) : 0; 57 | const term = 58 | b + Math.sqrt(b * b + k) !== 0 ? 59 | k * Math.log(Math.abs((u + uuk) / (b + bbk))) 60 | : 0; 61 | 62 | return (Math.sqrt(A) / 2) * (u * uuk - b * bbk + term); 63 | } 64 | -------------------------------------------------------------------------------- /src/splatapus/model/store.tsx: -------------------------------------------------------------------------------- 1 | import { Result } from "@/lib/Result"; 2 | import { Vector2 } from "@/lib/geom/Vector2"; 3 | import { Schema, SchemaType } from "@/lib/schema"; 4 | import { 5 | getLocalStorageItemUnchecked, 6 | setLocalStorageItemUnchecked, 7 | } from "@/lib/storage"; 8 | import { debounce } from "@/lib/utils"; 9 | import { AUTOSAVE_DEBOUNCE_TIME_MS } from "@/splatapus/constants"; 10 | import { SplatLocationState } from "@/splatapus/editor/SplatLocation"; 11 | import { SplatKeyPointId, SplatShapeId } from "@/splatapus/model/SplatDoc"; 12 | import { SplatDocModel } from "@/splatapus/model/SplatDocModel"; 13 | 14 | export const splatapusStateSchema = Schema.object({ 15 | document: SplatDocModel.schema, 16 | location: SplatLocationState.schema, 17 | }); 18 | 19 | export type SplatapusState = SchemaType; 20 | 21 | export function loadSaved(key: string): Result { 22 | const item = getLocalStorageItemUnchecked(`splatapus.${key}`); 23 | if (!item) { 24 | return Result.error("No saved data found"); 25 | } 26 | return splatapusStateSchema.parse(item).mapErr((err) => err.toString()); 27 | } 28 | 29 | export function writeSaved(key: string, state: SplatapusState) { 30 | // @ts-expect-error this is fine 31 | window.splatSerializedDoc = state; 32 | setLocalStorageItemUnchecked( 33 | `splatapus.${key}`, 34 | splatapusStateSchema.serialize(state), 35 | ); 36 | } 37 | 38 | export const writeSavedDebounced = debounce( 39 | AUTOSAVE_DEBOUNCE_TIME_MS, 40 | writeSaved, 41 | ); 42 | 43 | export function getDefaultLocationForDocument( 44 | document: SplatDocModel, 45 | screenSize: Vector2, 46 | ) { 47 | const keyPointId = [...document.keyPoints][0].id; 48 | const shapeId = [...document.shapes][0].id; 49 | return SplatLocationState.initialize(keyPointId, shapeId); 50 | } 51 | export function makeEmptySaveState(screenSize: Vector2): SplatapusState { 52 | const keyPointId = SplatKeyPointId.generate(); 53 | const shapeId = SplatShapeId.generate(); 54 | const document = SplatDocModel.create() 55 | .addKeyPoint(keyPointId, Vector2.ZERO) 56 | .addShape(shapeId) 57 | .replacePointsForVersion(keyPointId, shapeId, []); 58 | return { 59 | document, 60 | location: getDefaultLocationForDocument(document, screenSize), 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/gestureland/types.tsx: -------------------------------------------------------------------------------- 1 | import { Gestureland } from "@/gestureland/Gestureland"; 2 | import { Vector2 } from "@/lib/geom/Vector2"; 3 | 4 | export type GLPointerType = "mouse" | "pen" | "touch"; 5 | 6 | export type GLGestureType = "drag" | "twoFingerDrag" | "tap" | "doubleTap"; 7 | 8 | export interface GLTarget { 9 | readonly id: string; 10 | 11 | index: number; 12 | 13 | distanceTo(point: Vector2): number; 14 | 15 | onDrag?(event: GLPointerEvent): GLDragGesture; 16 | onTwoFingerDrag?(event: GLPointerEvent): GLDragGesture; 17 | 18 | onTap?(event: GLPointerEvent): GLTapGesture; 19 | onDoubleTap?(event: GLPointerEvent): GLTapGesture; 20 | } 21 | 22 | export interface GLDragGesture { 23 | onUpdate: (event: GLPointerEvent) => void; 24 | onEnd?: (event: GLPointerEvent) => void; 25 | onCancel: (event: GLPointerEvent) => void; 26 | } 27 | 28 | export interface GLTwoFingerDragGesture { 29 | onUpdate: (event: GLPointerEvent) => void; 30 | onEnd?: (event: GLPointerEvent) => void; 31 | onCancel: (event: GLPointerEvent) => void; 32 | } 33 | 34 | export interface GLTapGesture { 35 | onConfirm?: (event: GLPointerEvent) => void; 36 | onCancel: (event: GLPointerEvent) => void; 37 | } 38 | 39 | export type GLTargetFn = (editor: Gestureland) => null | GLTarget | GLTarget[]; 40 | 41 | export type GLPointerId = number; 42 | 43 | export interface GLPointer { 44 | readonly pointerId: GLPointerId; 45 | readonly viewportPosition: Vector2; 46 | readonly isDown: boolean; 47 | readonly type: GLPointerType; 48 | readonly state: PointerState; 49 | readonly lastUpdatedAt: number; 50 | } 51 | 52 | export interface GLPointerEvent extends Omit { 53 | readonly pagePosition: Vector2; 54 | } 55 | 56 | export interface IdlePointerState { 57 | readonly type: "idle"; 58 | } 59 | // export interface DownPointerState { 60 | // readonly type: "down"; 61 | // readonly drag: { target: GLTarget; gesture: GLDragGesture } | null; 62 | // } 63 | // export interface DraggingPointerState { 64 | // readonly type: 'dragging'; 65 | // readonly target: GLTarget 66 | // readonly gesture: GLDragGesture 67 | // } 68 | 69 | export interface DragPointerState { 70 | readonly type: "drag"; 71 | readonly isConfirmed: boolean; 72 | readonly target: GLTarget; 73 | readonly drag: GLDragGesture; 74 | } 75 | 76 | export type PointerState = IdlePointerState | DragPointerState; 77 | -------------------------------------------------------------------------------- /src/terrain/generatePoisson.ts: -------------------------------------------------------------------------------- 1 | import RandomQueue from "@/lib/RandomQueue"; 2 | import AABB from "@/lib/geom/AABB"; 3 | import { Vector2 } from "@/lib/geom/Vector2"; 4 | import { random } from "@/lib/utils"; 5 | import { Grid2 } from "@/terrain/Grid2"; 6 | 7 | export function generatePoisson( 8 | bounds: AABB, 9 | minimumDistance: number, 10 | pointCount: number, 11 | ): Vector2[] { 12 | const boundsAtZero = new AABB(Vector2.ZERO, bounds.size); 13 | const cellSize = minimumDistance / Math.SQRT2; 14 | 15 | const grid = new Grid2(bounds.size.div(cellSize).ceil()); 16 | const processList = new RandomQueue(); 17 | const samplePoints = []; 18 | 19 | const firstPoint = new Vector2( 20 | random(bounds.size.x), 21 | random(bounds.size.y), 22 | ); 23 | processList.add(firstPoint); 24 | samplePoints.push(firstPoint.add(bounds.origin)); 25 | 26 | while (processList.size) { 27 | const point = processList.pop(); 28 | 29 | for (let i = 0; i < pointCount; i++) { 30 | const newPoint = generateRandomPointAround(point, minimumDistance); 31 | 32 | if ( 33 | boundsAtZero.contains(newPoint) && 34 | !isInNeighbourhood(grid, newPoint, minimumDistance, cellSize) 35 | ) { 36 | processList.add(newPoint); 37 | samplePoints.push(newPoint.add(bounds.origin)); 38 | grid.set(grid.vectorToGridCoords(newPoint, cellSize), newPoint); 39 | } 40 | } 41 | } 42 | 43 | return samplePoints; 44 | } 45 | 46 | function generateRandomPointAround( 47 | point: Vector2, 48 | minimumDistance: number, 49 | ): Vector2 { 50 | const radius = random(minimumDistance, 2 * minimumDistance); 51 | const angle = random(Math.PI * 2); 52 | return Vector2.fromPolar(angle, radius).add(point); 53 | } 54 | 55 | function isInNeighbourhood( 56 | grid: Grid2, 57 | point: Vector2, 58 | minimumDistance: number, 59 | cellSize: number, 60 | ): boolean { 61 | const gridPoint = grid.vectorToGridCoords(point, cellSize); 62 | 63 | const cellsAroundPoint = grid.squareAroundCell(gridPoint, 5); 64 | for (const cell of cellsAroundPoint) { 65 | const pointInCell = grid.get(cell); 66 | if (pointInCell && pointInCell.distanceTo(point) < minimumDistance) { 67 | return true; 68 | } 69 | } 70 | 71 | return false; 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/geom/AABB.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "@/lib/geom/Vector2"; 2 | 3 | export default class AABB { 4 | static ZERO = new AABB(Vector2.ZERO, Vector2.ZERO); 5 | 6 | static fromLeftTopRightBottom( 7 | left: number, 8 | top: number, 9 | right: number, 10 | bottom: number, 11 | ): AABB { 12 | return new AABB( 13 | new Vector2(left, top), 14 | new Vector2(right - left, bottom - top), 15 | ); 16 | } 17 | 18 | static fromLeftTopWidthHeight( 19 | left: number, 20 | top: number, 21 | width: number, 22 | height: number, 23 | ): AABB { 24 | return new AABB(new Vector2(left, top), new Vector2(width, height)); 25 | } 26 | 27 | static from({ 28 | x, 29 | y, 30 | width, 31 | height, 32 | }: { 33 | x: number; 34 | y: number; 35 | width: number; 36 | height: number; 37 | }) { 38 | return AABB.fromLeftTopWidthHeight(x, y, width, height); 39 | } 40 | 41 | constructor( 42 | public readonly origin: Vector2, 43 | public readonly size: Vector2, 44 | ) { 45 | Object.freeze(this); 46 | } 47 | 48 | contains({ x, y }: Vector2): boolean { 49 | return ( 50 | this.left <= x && 51 | x <= this.right && 52 | this.top <= y && 53 | y <= this.bottom 54 | ); 55 | } 56 | 57 | intersects(other: AABB): boolean { 58 | return !( 59 | this.right < other.left || 60 | this.left > other.right || 61 | this.bottom < other.top || 62 | this.top > other.bottom 63 | ); 64 | } 65 | 66 | getCenter(): Vector2 { 67 | return this.origin.add(this.size.scale(0.5)); 68 | } 69 | 70 | equals(other: AABB): boolean { 71 | return this.origin.equals(other.origin) && this.size.equals(other.size); 72 | } 73 | 74 | get left(): number { 75 | return this.origin.x; 76 | } 77 | 78 | get right(): number { 79 | return this.origin.x + this.size.x; 80 | } 81 | 82 | get top(): number { 83 | return this.origin.y; 84 | } 85 | 86 | get bottom(): number { 87 | return this.origin.y + this.size.y; 88 | } 89 | 90 | get width(): number { 91 | return this.size.x; 92 | } 93 | 94 | get height(): number { 95 | return this.size.y; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/lime/LimeState.tsx: -------------------------------------------------------------------------------- 1 | import { reactive } from "@/lib/signia"; 2 | import { Lime } from "@/lime/Lime"; 3 | import { SlideId } from "@/lime/LimeStore"; 4 | 5 | export abstract class StateNode { 6 | abstract name: string; 7 | child?: { toDebugString(): string }; 8 | readonly lime: Lime; 9 | 10 | constructor(readonly parent: Parent) { 11 | this.lime = parent.lime; 12 | } 13 | 14 | toDebugString() { 15 | if (this.child) { 16 | return `${this.name}.${this.child.toDebugString()}`; 17 | } else { 18 | return this.name; 19 | } 20 | } 21 | 22 | onPointerDown?(event: PointerEvent): void; 23 | onPointerMove?(event: PointerEvent): void; 24 | onPointerUp?(event: PointerEvent): void; 25 | } 26 | 27 | export class LimeState { 28 | constructor(readonly lime: Lime) {} 29 | @reactive accessor child: Idle | Drawing = new Idle(this); 30 | 31 | toDebugString() { 32 | return `lime.${this.child.toDebugString()}`; 33 | } 34 | 35 | onPointerDown = (event: PointerEvent) => { 36 | this.child.onPointerDown?.(event); 37 | }; 38 | onPointerMove = (event: PointerEvent) => { 39 | this.child.onPointerMove?.(event); 40 | }; 41 | onPointerUp = (event: PointerEvent) => { 42 | this.child.onPointerUp?.(event); 43 | }; 44 | } 45 | 46 | export class Idle extends StateNode { 47 | override name = "idle"; 48 | 49 | override onPointerDown(event: PointerEvent) { 50 | const slideId = this.lime.session.slideId; 51 | this.lime.updateSlide(slideId, (slide) => ({ 52 | ...slide, 53 | rawPoints: [this.lime.inputs.scenePointer], 54 | })); 55 | this.parent.child = new Drawing(this.parent, slideId); 56 | } 57 | } 58 | 59 | export class Drawing extends StateNode { 60 | override name = "drawing"; 61 | constructor( 62 | parent: LimeState, 63 | private readonly slideId: SlideId, 64 | ) { 65 | super(parent); 66 | } 67 | 68 | override onPointerMove(event: PointerEvent): void { 69 | this.lime.updateSlide(this.slideId, (slide) => ({ 70 | ...slide, 71 | rawPoints: [...slide.rawPoints, this.lime.inputs.scenePointer], 72 | })); 73 | } 74 | 75 | override onPointerUp(event: PointerEvent): void { 76 | this.parent.child = new Idle(this.parent); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /scripts/writeTailwindConfig.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable no-relative-import-paths/no-relative-import-paths */ 3 | 4 | import fs from "fs"; 5 | import path from "path"; 6 | import prettier from "prettier"; 7 | import resolveConfig from "tailwindcss/resolveConfig.js"; 8 | import url from "url"; 9 | import rawTailwindConfig from "../tailwind.config.js"; 10 | 11 | const tailwindConfig = resolveConfig(rawTailwindConfig); 12 | const { colors, transitionTimingFunction: easings } = tailwindConfig.theme; 13 | 14 | const scriptDir = url.fileURLToPath(new URL(".", import.meta.url)); 15 | 16 | /** 17 | * @param {string} name 18 | */ 19 | function camelCase(name) { 20 | return name.replace( 21 | /([a-z])-([a-z])/g, 22 | (/** @type {any} */ _, /** @type {any} */ a, /** @type {string} */ b) => 23 | `${a}${b.toUpperCase()}`, 24 | ); 25 | } 26 | 27 | const code = [ 28 | "// GENERATED FILE, DO NOT EDIT DIRECTLY", 29 | "// To edit, change `tailwind.config.js` and run `./scripts/writeTailwindConfig.mjs`.", 30 | "function makeColor>(colors: T) {", 31 | "return function get(key: K): T[K] {", 32 | "return colors[key];", 33 | "}", 34 | "}", 35 | "export const tailwindColors = {", 36 | ...Object.entries(colors).flatMap(([name, colors]) => { 37 | if (typeof colors !== "object") { 38 | return []; 39 | } 40 | const niceName = camelCase(name); 41 | return [ 42 | ...Object.entries(colors).map( 43 | ([num, v]) => `${niceName}${num}: ${JSON.stringify(v)},`, 44 | ), 45 | `${niceName}: makeColor({`, 46 | ...Object.entries(colors).map( 47 | ([num, v]) => `${num}: ${JSON.stringify(v)},`, 48 | ), 49 | `}),`, 50 | ]; 51 | }), 52 | "} as const;", 53 | "export const tailwindEasings = {", 54 | ...Object.entries(easings).map(([name, value]) => { 55 | const niceName = camelCase(name); 56 | return `${niceName}: ${JSON.stringify(value)},`; 57 | }), 58 | "} as const;", 59 | ].join("\n"); 60 | 61 | const prettierConfig = await prettier.resolveConfig(scriptDir); 62 | const formattedCode = await prettier.format(code, { 63 | ...prettierConfig, 64 | parser: "typescript", 65 | }); 66 | fs.writeFileSync( 67 | path.resolve(scriptDir, "../src/lib/theme.tsx"), 68 | formattedCode, 69 | "utf-8", 70 | ); 71 | -------------------------------------------------------------------------------- /src/splatapus/editor/Vfx.tsx: -------------------------------------------------------------------------------- 1 | import { useEvent } from "@/lib/hooks/useEvent"; 2 | import { LiveValue, useLive } from "@/lib/live"; 3 | import { ReadonlyObjectMap, get } from "@/lib/utils"; 4 | import { ModeType } from "@/splatapus/editor/modes/Mode"; 5 | import React, { useEffect, useRef } from "react"; 6 | 7 | export type VfxActionName = "undo" | "redo" | ModeType; 8 | let nextAnimationIdx = 1; 9 | 10 | export class Vfx { 11 | activeAnimations = new LiveValue>( 12 | {}, 13 | "vfx.activeAnimations", 14 | ); 15 | 16 | triggerAnimation(name: VfxActionName) { 17 | this.activeAnimations.update((prev) => ({ 18 | ...prev, 19 | [name]: nextAnimationIdx++, 20 | })); 21 | } 22 | 23 | getActiveAnimationIdLive(name: VfxActionName): number | null { 24 | return get(this.activeAnimations.live(), name) ?? null; 25 | } 26 | 27 | acknowledgeAnimation(name: VfxActionName, activeAnimationVersion: number) { 28 | this.activeAnimations.update((activeAnimations) => { 29 | const { [name]: foundVersion, ...remaining } = activeAnimations; 30 | if (foundVersion !== activeAnimationVersion) { 31 | return activeAnimations; 32 | } 33 | return remaining; 34 | }); 35 | } 36 | } 37 | 38 | export function useVfxAnimation( 39 | vfx: Vfx, 40 | name: VfxActionName, 41 | animate: () => KeyframeAnimationOptions & { 42 | keyFrames: Keyframe[] | PropertyIndexedKeyframes; 43 | }, 44 | ): React.RefObject { 45 | const ref = useRef(null); 46 | const activeAnimationVersion = useLive( 47 | () => vfx.getActiveAnimationIdLive(name), 48 | [name, vfx], 49 | ); 50 | 51 | const getAnimationConfig = useEvent(animate); 52 | useEffect(() => { 53 | const element = ref.current; 54 | if (!element || !activeAnimationVersion) { 55 | return undefined; 56 | } 57 | 58 | const config = getAnimationConfig(); 59 | const animation = element.animate(config.keyFrames, config); 60 | animation.addEventListener("finish", () => { 61 | vfx.acknowledgeAnimation(name, activeAnimationVersion); 62 | }); 63 | animation.play(); 64 | return () => { 65 | animation.cancel(); 66 | }; 67 | }, [activeAnimationVersion, getAnimationConfig, name, vfx]); 68 | 69 | return ref; 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/RingBuffer.ts: -------------------------------------------------------------------------------- 1 | import { times } from "@/lib/utils"; 2 | 3 | export default class RingBuffer { 4 | private start = 0; 5 | private end = 0; 6 | private buffer: (T | null)[]; 7 | 8 | constructor(size = 32) { 9 | this.buffer = times(Math.max(size, 1), () => null); 10 | } 11 | 12 | private resize(size: number) { 13 | const length = this.length; 14 | this.buffer = times(size, (i) => (i < length ? this.get(i) : null)); 15 | this.start = 0; 16 | this.end = length; 17 | } 18 | 19 | private growForInsertIfNeeded() { 20 | if (this.length >= this.capacity - 1) { 21 | this.resize(this.capacity * 2); 22 | } 23 | } 24 | 25 | get length(): number { 26 | if (this.start <= this.end) { 27 | return this.end - this.start; 28 | } else { 29 | return this.end + this.buffer.length - this.start; 30 | } 31 | } 32 | 33 | get capacity(): number { 34 | return this.buffer.length; 35 | } 36 | 37 | get(index: number): T | null { 38 | return this.buffer[(this.start + index) % this.capacity]; 39 | } 40 | 41 | push(item: T) { 42 | this.growForInsertIfNeeded(); 43 | this.buffer[this.end] = item; 44 | this.end = (this.end + 1) % this.capacity; 45 | } 46 | 47 | pop(): T | null { 48 | if (this.end === this.start) { 49 | return null; 50 | } 51 | 52 | if (this.end === 0) { 53 | this.end = this.capacity - 1; 54 | } else { 55 | this.end = this.end - 1; 56 | } 57 | 58 | const value = this.buffer[this.end]; 59 | this.buffer[this.end] = null; 60 | return value; 61 | } 62 | 63 | unshift(item: T) { 64 | this.growForInsertIfNeeded(); 65 | if (this.start === 0) { 66 | this.start = this.capacity - 1; 67 | } else { 68 | this.start = this.start - 1; 69 | } 70 | this.buffer[this.start] = item; 71 | } 72 | 73 | shift(): T | null { 74 | if (this.end === this.start) { 75 | return null; 76 | } 77 | 78 | const value = this.buffer[this.start]; 79 | this.buffer[this.start] = null; 80 | 81 | this.start = (this.start + 1) % this.capacity; 82 | return value; 83 | } 84 | 85 | first(): T | null { 86 | return this.get(0); 87 | } 88 | 89 | last(): T | null { 90 | return this.get(this.length - 1); 91 | } 92 | } 93 | --------------------------------------------------------------------------------