├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── check.yml │ ├── publish.yml │ └── release-gif.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── Colors.ts ├── Slide.ts ├── Util.ts ├── app │ ├── app.ts │ └── rendering.test.ts ├── components │ ├── Body.tsx │ ├── CodeLineNumbers.tsx │ ├── DistortedCurve.tsx │ ├── ErrorBox.tsx │ ├── FileTree.tsx │ ├── Glow.tsx │ ├── ImgWindow.tsx │ ├── Layout.tsx │ ├── LinePlot.tsx │ ├── MotionCanvasLogo.tsx │ ├── Plot.tsx │ ├── ScatterPlot.tsx │ ├── Scrollable.tsx │ ├── Table.tsx │ ├── Terminal.tsx │ ├── Window.tsx │ ├── WindowsButton.tsx │ └── index.ts ├── highlightstyle │ ├── Catppuccin.ts │ ├── MaterialPaleNight.ts │ └── index.ts └── index.ts ├── test ├── motion-canvas.d.ts ├── project.meta ├── project.ts ├── public │ └── .gitkeep └── scenes │ ├── example.meta │ └── example.tsx ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,ts,json}] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "ignorePatterns": [ 7 | "**/*.js", 8 | "**/*.d.ts", 9 | "packages/template", 10 | "packages/create/template-*" 11 | ], 12 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": "latest", 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["@typescript-eslint", "eslint-plugin-tsdoc"], 19 | "rules": { 20 | "require-yield": "off", 21 | "@typescript-eslint/explicit-member-accessibility": "error", 22 | "@typescript-eslint/no-explicit-any": "off", 23 | "@typescript-eslint/ban-ts-comment": "off", 24 | "@typescript-eslint/no-non-null-assertion": "off", 25 | "@typescript-eslint/no-namespace": "off", 26 | "tsdoc/syntax": "warn" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | on: 3 | pull_request: 4 | types: [opened, reopened, synchronize, edited] 5 | branches-ignore: 6 | - 'nobuild**' 7 | push: 8 | branches: 9 | - main 10 | tags: 11 | - 'v*.*.*' 12 | 13 | jobs: 14 | UnitTests: 15 | name: 'Tests and Builds' 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-node@v1 20 | with: 21 | node-version: 18 22 | - name: Install 23 | run: | 24 | npm ci 25 | - name: Build Succeeds 26 | run: | 27 | npm run build 28 | - name: Format Check 29 | run: | 30 | npm run lint 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | branches: main 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: '20' 14 | - run: npm ci 15 | - name: Format Check 16 | run: | 17 | npm run lint 18 | - name: Build Succeeds 19 | run: | 20 | npm run build 21 | - uses: JS-DevTools/npm-publish@v3 22 | with: 23 | token: ${{ secrets.NPM_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/release-gif.yml: -------------------------------------------------------------------------------- 1 | name: 'Generate Preview GIF' 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | pre-release: 10 | name: 'Generate Preview GIF' 11 | runs-on: 'ubuntu-latest' 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | id: setup-node 17 | with: 18 | node-version: 20 19 | cache: 'npm' 20 | - uses: browser-actions/setup-chrome@v1 21 | id: setup-chrome 22 | - uses: FedericoCarboni/setup-ffmpeg@v2 23 | id: setup-ffmpeg 24 | - run: npm ci 25 | - run: npm test 26 | - if: always() 27 | run: 28 | ffmpeg -framerate 15 -i output/project/%06d.png -vf 29 | "fps=15,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" 30 | output-big.gif 31 | - uses: 'marvinpinto/action-automatic-releases@latest' 32 | with: 33 | repo_token: '${{ secrets.GITHUB_TOKEN }}' 34 | automatic_release_tag: 'latest' 35 | prerelease: true 36 | title: 'Development Build' 37 | files: | 38 | output-big.gif 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | node_modules 3 | output 4 | dist 5 | lib 6 | 7 | # Editor directories and files 8 | .vscode/* 9 | !.vscode/extensions.json 10 | .idea 11 | .DS_Store 12 | *.suo 13 | *.ntvs* 14 | *.njsproj 15 | *.sln 16 | *.sw? 17 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .husky 3 | .editorconfig 4 | .eslintignore 5 | .eslintrc.json 6 | .prettierignore 7 | .prettierrc 8 | tsconfig.json 9 | vite.config.ts 10 | test 11 | *.mp4 12 | output 13 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "trailingComma": "all", 6 | "arrowParens": "avoid", 7 | "proseWrap": "always" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hunter Henrichsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Canvas Commons 2 | 3 | [![npm](https://img.shields.io/npm/v/%40hhenrichsen%2Fcanvas-commons?style=for-the-badge&logo=npm)](https://www.npmjs.com/package/@hhenrichsen/canvas-commons) 4 | [![GitHub](https://img.shields.io/github/v/tag/hhenrichsen/canvas-commons?style=for-the-badge&logo=github&label=GitHub) ](https://github.com/hhenrichsen/canvas-commons) 5 | [![Static Badge](https://img.shields.io/badge/Donate-Kofi?style=for-the-badge&label=KoFi&color=%23FF5722)](https://ko-fi.com/hhenrichsen) 6 | 7 | 8 | > **Warning** 9 | ⚠️ This library is still under construction. Breaking changes are possible until I release version 1.0.0. Update versions with caution and only after reading the commit log. ⚠️ 10 | 11 | If you use this in your videos, I would appreciate credit via a link to this 12 | repo, or a mention by name. I would also love to see them; feel free to show me 13 | on the motion canvas discord (I'm `@Hunter` on there). 14 | 15 | If you want to support the development of this and other libraries, feel free to 16 | donate on [Ko-fi](https://ko-fi.com/hhenrichsen). 17 | 18 | ## Preview 19 | 20 | ![](https://github.com/hhenrichsen/canvas-commons/releases/download/latest/output-big.gif) 21 | 22 | Code for this GIF can be found 23 | [here](https://github.com/hhenrichsen/canvas-commons/blob/main/test/src/scenes/example.tsx) 24 | 25 | ## Using this library 26 | 27 | ### From git 28 | 29 | 1. Clone this repo. 30 | 1. Run `npm install ` in your motion canvas project 31 | 32 | ### From npm 33 | 34 | 1. Run `npm install @hhenrichsen/canvas-commons` 35 | 36 | ## Components 37 | 38 | ### Scrollable 39 | 40 | The `Scrollable` node is a custom component designed to allow for scrolling 41 | within a container. Its size represents the viewports size, and it can be 42 | scrolled to any position within its content. 43 | 44 | #### Props 45 | 46 | - `activeOpacity` - the opacity of the scrollbars when they are active 47 | - `handleFadeoutDuration` - how long it takes for the scrollbars to fade out 48 | - `handleFadeoutHang` - how long the scrollbars stay visible after the last 49 | scroll event 50 | - `handleInset` - the amount to inset the scrollbar handles 51 | - `handleProps` - the props to pass to the scrollbar handles 52 | - `handleWidth` - the width of the scrollbar handles 53 | - `inactiveOpacity` - the opacity of the scrollbars when they are inactive 54 | - `scrollOffset` - the initial offset to use for the scrollable 55 | - `scrollPadding` - the amount of extra space to add when scrolling to preset 56 | positions 57 | - `zoom` - the zoom level of the scrollable 58 | 59 | #### Example 60 | 61 | ```tsx 62 | import {Scrollable} from '@hhenrichsen/canvas-commons'; 63 | import {makeScene2D, Rect} from '@motion-canvas/2d'; 64 | import {createRef, waitFor} from '@motion-canvas/core'; 65 | 66 | export default makeScene2D(function* (view) { 67 | const scrollable = createRef(); 68 | const rect = createRef(); 69 | view.add( 70 | 71 | 72 | , 73 | ); 74 | 75 | yield* scrollable().scrollTo([150, 150], 2); 76 | yield* scrollable().scrollToLeft(1); 77 | yield* scrollable().scrollToTop(1); 78 | yield* scrollable().scrollTo(0, 1); 79 | yield* waitFor(1); 80 | 81 | yield rect().fill('seagreen', 1); 82 | yield* rect().size(600, 2); 83 | yield* waitFor(1); 84 | 85 | yield* scrollable().scrollToBottom(1); 86 | yield* scrollable().scrollToRight(1); 87 | yield* scrollable().scrollBy(-100, 1); 88 | yield* waitFor(5); 89 | }); 90 | ``` 91 | 92 | ### Window 93 | 94 | The `Window` node is custom component designed to look like a window on either a 95 | MacOS system or a Windows 98 system. 96 | 97 | #### Props 98 | 99 | - `bodyColor` - the color of the body 100 | - `headerColor` - the color of the header 101 | - `titleProps` - the props to pass to the title's `` node 102 | - `title` - the title of the window 103 | - `windowStyle` - the style of the window, either `WindowStyle.Windows98` or 104 | `WindowStyle.MacOS` 105 | 106 | #### Example 107 | 108 | ```tsx 109 | import {Window, Scrollable, WindowStyle} from '@hhenrichsen/canvas-commons'; 110 | import {makeScene2D, Rect} from '@motion-canvas/2d'; 111 | import {createRef, waitFor} from '@motion-canvas/core'; 112 | 113 | export default makeScene2D(function* (view) { 114 | const window = createRef(); 115 | const rect = createRef(); 116 | view.add( 117 | <> 118 | 119 | 120 | 121 | , 122 | ); 123 | 124 | yield* window.open(view, 1); 125 | yield* waitFor(1); 126 | }); 127 | ``` 128 | 129 | ### FileTree 130 | 131 | The `FileTree` node is a custom component designed to look like a file tree. It 132 | supports highlighting and selection of files and folders. 133 | 134 | #### Props 135 | 136 | - `assetColor` - the color of the asset icon 137 | - `fileColor` - the color of the file icon 138 | - `folderColor` - the color of the folder icon 139 | - `indentAmount` - the amount to indent each level of the tree 140 | - `labelColor` - the color of the label 141 | - `rowSize` - the size of each row in the tree 142 | - `structure` - the structure of the file tree 143 | 144 | #### Example 145 | 146 | ```tsx 147 | import {FileTree, FileType} from '@hhenrichsen/canvas-commons'; 148 | import {makeScene2D} from '@motion-canvas/2d'; 149 | import {createRef, waitFor} from '@motion-canvas/core'; 150 | 151 | export default makeScene2D(function* (view) { 152 | const fileStructure = createRef(); 153 | view.add( 154 | <> 155 | 226 | , 227 | ); 228 | 229 | yield* fileStructure().emphasize('db', 1); 230 | }); 231 | ``` 232 | 233 | ### Functional Components 234 | 235 | I also have a collection of functional components that I use to automate using 236 | some of these components: 237 | 238 | - `ImgWindow` - a window that contains an image 239 | - `Body` - a `Txt` component that wraps text 240 | - `Title` - a `Txt` component that is bold and large 241 | - `Em` - a `Txt` component that is emphasized 242 | - `Bold` - a `Txt` component that is bold 243 | - `ErrorBox` - a Windows 98-style error message 244 | - `Windows98Button` - a button with a bevel, like in Windows 98 245 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hhenrichsen/canvas-commons", 3 | "description": "Common utilities for working with Motion Canvas", 4 | "repository": "https://github.com/hhenrichsen/canvas-commons", 5 | "version": "0.10.2", 6 | "main": "lib/index.js", 7 | "types": "lib/index.d.ts", 8 | "scripts": { 9 | "prepare": "husky install", 10 | "build:dev": "rollup -c rollup.config.mjs", 11 | "watch": "rollup -c rollup.config.mjs -w", 12 | "build": "rollup -c rollup.config.mjs", 13 | "prebuild": "rimraf ./lib", 14 | "lint-staged": "lint-staged", 15 | "lint": "npm run eslint && npm run prettier", 16 | "format": "npm run eslint:fix && npm run prettier:fix", 17 | "eslint": "eslint \"**/*.ts?(x)\"", 18 | "eslint:fix": "eslint --fix \"**/*.ts?(x)\"", 19 | "prettier": "prettier --check .", 20 | "prettier:fix": "prettier --write .", 21 | "serve": "vite", 22 | "test": "vitest" 23 | }, 24 | "dependencies": { 25 | "@codemirror/language": "^6.10.1", 26 | "@lezer/highlight": "^1.2.0", 27 | "@motion-canvas/2d": "^3.17.1", 28 | "@motion-canvas/core": "^3.17.0", 29 | "@motion-canvas/ffmpeg": "^3.17.0" 30 | }, 31 | "devDependencies": { 32 | "@lezer/javascript": "^1.4.16", 33 | "@motion-canvas/ui": "^3.17.0", 34 | "@motion-canvas/vite-plugin": "^3.17.0", 35 | "@rollup/plugin-terser": "^0.4.4", 36 | "@rollup/plugin-typescript": "^11.1.6", 37 | "@typescript-eslint/eslint-plugin": "^7.2.0", 38 | "@typescript-eslint/parser": "^7.2.0", 39 | "cross-env": "^7.0.3", 40 | "eslint": "^8.57.0", 41 | "eslint-plugin-tsdoc": "^0.2.17", 42 | "husky": "^9.0.11", 43 | "lint-staged": "^15.2.2", 44 | "prettier": "^3.2.5", 45 | "puppeteer": "^22.11.0", 46 | "rimraf": "^5.0.5", 47 | "rollup": "^4.13.0", 48 | "rollup-plugin-dts": "^6.1.0", 49 | "rollup-plugin-node-externals": "^7.0.1", 50 | "tslib": "^2.6.2", 51 | "typescript": "^5.4.2", 52 | "vite": "^4.0.0", 53 | "vite-tsconfig-paths": "^4.3.1", 54 | "vitest": "^1.6.0" 55 | }, 56 | "lint-staged": { 57 | "*.{ts,tsx}": "eslint --fix", 58 | "*.{js,jsx,ts,tsx,md,scss,json,mjs}": "prettier --write" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import dts from 'rollup-plugin-dts'; 3 | import externals from 'rollup-plugin-node-externals'; 4 | import terser from '@rollup/plugin-terser'; 5 | 6 | /** @type {import('rollup').RollupOptions} */ 7 | const config = [ 8 | { 9 | input: 'src/index.ts', 10 | output: { 11 | file: 'lib/index.min.js', 12 | format: 'es', 13 | }, 14 | plugins: [externals(), typescript(), terser()], 15 | external: [/^@motion-canvas\/core/, /^@motion-canvas\/2d/], 16 | }, 17 | { 18 | input: 'src/index.ts', 19 | output: { 20 | file: 'lib/index.js', 21 | format: 'es', 22 | }, 23 | plugins: [externals(), typescript()], 24 | external: [/^@motion-canvas\/core/, /^@motion-canvas\/2d/], 25 | }, 26 | { 27 | input: 'src/index.ts', 28 | output: { 29 | file: 'lib/index.d.ts', 30 | format: 'es', 31 | }, 32 | plugins: [dts()], 33 | }, 34 | ]; 35 | 36 | export default config; 37 | -------------------------------------------------------------------------------- /src/Colors.ts: -------------------------------------------------------------------------------- 1 | export const Colors = { 2 | Tailwind: { 3 | Slate: { 4 | '50': '#f8fafc', 5 | '100': '#f1f5f9', 6 | '200': '#e2e8f0', 7 | '300': '#cbd5e1', 8 | '400': '#94a3b8', 9 | '500': '#64748b', 10 | '600': '#475569', 11 | '700': '#334155', 12 | '800': '#1e293b', 13 | '900': '#0f172a', 14 | '950': '#020617', 15 | }, 16 | Gray: { 17 | '50': '#f9fafb', 18 | '100': '#f3f4f6', 19 | '200': '#e5e7eb', 20 | '300': '#d1d5db', 21 | '400': '#9ca3af', 22 | '500': '#6b7280', 23 | '600': '#4b5563', 24 | '700': '#374151', 25 | '800': '#1f2937', 26 | '900': '#111827', 27 | '950': '#030712', 28 | }, 29 | Zinc: { 30 | '50': '#fafafa', 31 | '100': '#f4f4f5', 32 | '200': '#e4e4e7', 33 | '300': '#d4d4d8', 34 | '400': '#a1a1aa', 35 | '500': '#71717a', 36 | '600': '#52525b', 37 | '700': '#3f3f46', 38 | '800': '#27272a', 39 | '900': '#18181b', 40 | '950': '#09090b', 41 | }, 42 | Neutral: { 43 | '50': '#fafafa', 44 | '100': '#f5f5f5', 45 | '200': '#e5e5e5', 46 | '300': '#d4d4d4', 47 | '400': '#a3a3a3', 48 | '500': '#737373', 49 | '600': '#525252', 50 | '700': '#404040', 51 | '800': '#262626', 52 | '900': '#171717', 53 | '950': '#0a0a0a', 54 | }, 55 | Stone: { 56 | '50': '#fafaf9', 57 | '100': '#f5f5f4', 58 | '200': '#e7e5e4', 59 | '300': '#d6d3d1', 60 | '400': '#a8a29e', 61 | '500': '#78716c', 62 | '600': '#57534e', 63 | '700': '#44403c', 64 | '800': '#292524', 65 | '900': '#1c1917', 66 | '950': '#0c0a09', 67 | }, 68 | Red: { 69 | '50': '#fef2f2', 70 | '100': '#fee2e2', 71 | '200': '#fecaca', 72 | '300': '#fca5a5', 73 | '400': '#f87171', 74 | '500': '#ef4444', 75 | '600': '#dc2626', 76 | '700': '#b91c1c', 77 | '800': '#991b1b', 78 | '900': '#7f1d1d', 79 | '950': '#450a0a', 80 | }, 81 | Orange: { 82 | '50': '#fff7ed', 83 | '100': '#ffedd5', 84 | '200': '#fed7aa', 85 | '300': '#fdba74', 86 | '400': '#fb923c', 87 | '500': '#f97316', 88 | '600': '#ea580c', 89 | '700': '#c2410c', 90 | '800': '#9a3412', 91 | '900': '#7c2d12', 92 | '950': '#431407', 93 | }, 94 | Amber: { 95 | '50': '#fffbeb', 96 | '100': '#fef3c7', 97 | '200': '#fde68a', 98 | '300': '#fcd34d', 99 | '400': '#fbbf24', 100 | '500': '#f59e0b', 101 | '600': '#d97706', 102 | '700': '#b45309', 103 | '800': '#92400e', 104 | '900': '#78350f', 105 | '950': '#451a03', 106 | }, 107 | 108 | Yellow: { 109 | '50': '#fefce8', 110 | '100': '#fef9c3', 111 | '200': '#fef08a', 112 | '300': '#fde047', 113 | '400': '#facc15', 114 | '500': '#eab308', 115 | '600': '#ca8a04', 116 | '700': '#a16207', 117 | '800': '#854d0e', 118 | '900': '#713f12', 119 | '950': '#422006', 120 | }, 121 | Lime: { 122 | '50': '#f7fee7', 123 | '100': '#ecfccb', 124 | '200': '#d9f99d', 125 | '300': '#bef264', 126 | '400': '#a3e635', 127 | '500': '#84cc16', 128 | '600': '#65a30d', 129 | '700': '#4d7c0f', 130 | '800': '#3f6212', 131 | '900': '#365314', 132 | '950': '#1a2e05', 133 | }, 134 | Green: { 135 | '50': '#f0fdf4', 136 | '100': '#dcfce7', 137 | '200': '#bbf7d0', 138 | '300': '#86efac', 139 | '400': '#4ade80', 140 | '500': '#22c55e', 141 | '600': '#16a34a', 142 | '700': '#15803d', 143 | '800': '#166534', 144 | '900': '#14532d', 145 | '950': '#052e16', 146 | }, 147 | Emerald: { 148 | '50': '#ecfdf5', 149 | '100': '#d1fae5', 150 | '200': '#a7f3d0', 151 | '300': '#6ee7b7', 152 | '400': '#34d399', 153 | '500': '#10b981', 154 | '600': '#059669', 155 | '700': '#047857', 156 | '800': '#065f46', 157 | '900': '#064e3b', 158 | '950': '#022c22', 159 | }, 160 | Teal: { 161 | '50': '#f0fdfa', 162 | '100': '#ccfbf1', 163 | '200': '#99f6e4', 164 | '300': '#5eead4', 165 | '400': '#2dd4bf', 166 | '500': '#14b8a6', 167 | '600': '#0d9488', 168 | '700': '#0f766e', 169 | '800': '#115e59', 170 | '900': '#134e4a', 171 | '950': '#042f2e', 172 | }, 173 | Cyan: { 174 | '50': '#ecfeff', 175 | '100': '#cffafe', 176 | '200': '#a5f3fc', 177 | '300': '#67e8f9', 178 | '400': '#22d3ee', 179 | '500': '#06b6d4', 180 | '600': '#0891b2', 181 | '700': '#0e7490', 182 | '800': '#155e75', 183 | '900': '#164e63', 184 | '950': '#083344', 185 | }, 186 | Sky: { 187 | '50': '#f0f9ff', 188 | '100': '#e0f2fe', 189 | '200': '#bae6fd', 190 | '300': '#7dd3fc', 191 | '400': '#38bdf8', 192 | '500': '#0ea5e9', 193 | '600': '#0284c7', 194 | '700': '#0369a1', 195 | '800': '#075985', 196 | '900': '#0c4a6e', 197 | '950': '#082f49', 198 | }, 199 | Blue: { 200 | '50': '#eff6ff', 201 | '100': '#dbeafe', 202 | '200': '#bfdbfe', 203 | '300': '#93c5fd', 204 | '400': '#60a5fa', 205 | '500': '#3b82f6', 206 | '600': '#2563eb', 207 | '700': '#1d4ed8', 208 | '800': '#1e40af', 209 | '900': '#1e3a8a', 210 | '950': '#172554', 211 | }, 212 | Indigo: { 213 | '50': '#eef2ff', 214 | '100': '#e0e7ff', 215 | '200': '#c7d2fe', 216 | '300': '#a5b4fc', 217 | '400': '#818cf8', 218 | '500': '#6366f1', 219 | '600': '#4f46e5', 220 | '700': '#4338ca', 221 | '800': '#3730a3', 222 | '900': '#312e81', 223 | '950': '#1e1b4b', 224 | }, 225 | Violet: { 226 | '50': '#f5f3ff', 227 | '100': '#ede9fe', 228 | '200': '#ddd6fe', 229 | '300': '#c4b5fd', 230 | '400': '#a78bfa', 231 | '500': '#8b5cf6', 232 | '600': '#7c3aed', 233 | '700': '#6d28d9', 234 | '800': '#5b21b6', 235 | '900': '#4c1d95', 236 | '950': '#2e1065', 237 | }, 238 | Purple: { 239 | '50': '#faf5ff', 240 | '100': '#f3e8ff', 241 | '200': '#e9d5ff', 242 | '300': '#d8b4fe', 243 | '400': '#c084fc', 244 | '500': '#a855f7', 245 | '600': '#9333ea', 246 | '700': '#7e22ce', 247 | '800': '#6b21a8', 248 | '900': '#581c87', 249 | '950': '#3b0764', 250 | }, 251 | Fuchsia: { 252 | '50': '#fdf4ff', 253 | '100': '#fae8ff', 254 | '200': '#f5d0fe', 255 | '300': '#f0abfc', 256 | '400': '#e879f9', 257 | '500': '#d946ef', 258 | '600': '#c026d3', 259 | '700': '#a21caf', 260 | '800': '#86198f', 261 | '900': '#701a75', 262 | '950': '#4a044e', 263 | }, 264 | Pink: { 265 | '50': '#fdf2f8', 266 | '100': '#fce7f3', 267 | '200': '#fbcfe8', 268 | '300': '#f9a8d4', 269 | '400': '#f472b6', 270 | '500': '#ec4899', 271 | '600': '#db2777', 272 | '700': '#be185d', 273 | '800': '#9d174d', 274 | '900': '#831843', 275 | '950': '#500724', 276 | }, 277 | Rose: { 278 | '50': '#fff1f2', 279 | '100': '#ffe4e6', 280 | '200': '#fecdd3', 281 | '300': '#fda4af', 282 | '400': '#fb7185', 283 | '500': '#f43f5e', 284 | '600': '#e11d48', 285 | '700': '#be123c', 286 | '800': '#9f1239', 287 | '900': '#881337', 288 | '950': '#4c0519', 289 | }, 290 | }, 291 | Nord: { 292 | PolarNight: { 293 | '1': '#2e3440', 294 | '2': '#3b4252', 295 | '3': '#434c5e', 296 | '4': '#4c566a', 297 | }, 298 | SnowStorm: { 299 | '1': '#d8dee9', 300 | '2': '#e5e9f0', 301 | '3': '#eceff4', 302 | }, 303 | Frost: { 304 | '1': '#8fbcbb', 305 | '2': '#88c0d0', 306 | '3': '#81a1c1', 307 | '4': '#5e81ac', 308 | }, 309 | Aurora: { 310 | '1': '#bf616a', 311 | '2': '#d08770', 312 | '3': '#ebcb8b', 313 | '4': '#a3be8c', 314 | '5': '#b48ead', 315 | }, 316 | }, 317 | Catppuccin: { 318 | Mocha: { 319 | Rosewater: '#f5e0dc', 320 | Flamingo: '#f2cdcd', 321 | Pink: '#f5c2e7', 322 | Mauve: '#cba6f7', 323 | Red: '#f38ba8', 324 | Maroon: '#eba0ac', 325 | Peach: '#fab387', 326 | Yellow: '#f9e2af', 327 | Green: '#a6e3a1', 328 | Teal: '#94e2d5', 329 | Sky: '#89dceb', 330 | Sapphire: '#74c7ec', 331 | Blue: '#89b4fa', 332 | Lavender: '#b4befe', 333 | Text: '#cdd6f4', 334 | Subtext1: '#bac2de', 335 | Subtext0: '#a6adc8', 336 | Overlay2: '#9399b2', 337 | Overlay1: '#7f849c', 338 | Overlay0: '#6c7086', 339 | Surface2: '#585b70', 340 | Surface1: '#45475a', 341 | Surface0: '#313244', 342 | Base: '#1e1e2e', 343 | Mantle: '#181825', 344 | Crust: '#11111b', 345 | }, 346 | }, 347 | }; 348 | -------------------------------------------------------------------------------- /src/Slide.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LoopCallback, 3 | PlaybackState, 4 | ThreadGenerator, 5 | beginSlide, 6 | cancel, 7 | loop, 8 | usePlayback, 9 | } from '@motion-canvas/core'; 10 | 11 | export function* loopSlide( 12 | name: string, 13 | setup: undefined | (() => ThreadGenerator), 14 | frame: (() => ThreadGenerator) | LoopCallback, 15 | cleanup: undefined | (() => ThreadGenerator), 16 | ): ThreadGenerator { 17 | if (usePlayback().state !== PlaybackState.Presenting) { 18 | if (setup) yield* setup(); 19 | // Run the loop once if it's in preview mode 20 | // @ts-ignore 21 | yield* frame(0); 22 | yield* beginSlide(name); 23 | if (cleanup) yield* cleanup(); 24 | return; 25 | } 26 | if (setup) yield* setup(); 27 | const task = yield loop(Infinity, frame); 28 | yield* beginSlide(name); 29 | if (cleanup) yield* cleanup(); 30 | cancel(task); 31 | } 32 | -------------------------------------------------------------------------------- /src/Util.ts: -------------------------------------------------------------------------------- 1 | import {Curve, Layout, PossibleCanvasStyle, View2D} from '@motion-canvas/2d'; 2 | import { 3 | Reference, 4 | SignalValue, 5 | SimpleSignal, 6 | Vector2, 7 | all, 8 | createSignal, 9 | delay, 10 | unwrap, 11 | } from '@motion-canvas/core'; 12 | 13 | export function belowScreenPosition( 14 | view: View2D, 15 | node: SignalValue, 16 | ): Vector2 { 17 | const n = unwrap(node); 18 | return new Vector2({ 19 | x: n.position().x, 20 | y: view.size().y / 2 + n.height() / 2, 21 | }); 22 | } 23 | 24 | export function signalRef(): {ref: Reference; signal: SimpleSignal} { 25 | const s = createSignal(); 26 | // @ts-ignore 27 | return {ref: s, signal: s}; 28 | } 29 | 30 | export function signum(value: number): number { 31 | return value > 0 ? 1 : value < 0 ? -1 : 0; 32 | } 33 | 34 | export function* drawIn( 35 | nodeOrRef: SignalValue, 36 | stroke: PossibleCanvasStyle, 37 | fill: PossibleCanvasStyle, 38 | duration: number, 39 | restoreStroke: boolean = false, 40 | defaultStrokeWidth: number = 4, 41 | ) { 42 | const node = unwrap(nodeOrRef); 43 | const prevStroke = node.stroke(); 44 | const oldStrokeWidth = node.lineWidth(); 45 | const strokeWidth = 46 | node.lineWidth() > 0 ? node.lineWidth() : defaultStrokeWidth; 47 | node.end(0); 48 | node.lineWidth(strokeWidth); 49 | node.stroke(stroke); 50 | yield* node.end(1, duration * 0.7); 51 | yield* node.fill(fill, duration * 0.3); 52 | if (restoreStroke) { 53 | yield delay( 54 | duration * 0.1, 55 | all( 56 | node.lineWidth(oldStrokeWidth, duration * 0.7), 57 | node.stroke(prevStroke, duration * 0.7), 58 | ), 59 | ); 60 | } 61 | } 62 | 63 | function getLinkPoints(node: Layout) { 64 | return [node.left(), node.right(), node.top(), node.bottom()]; 65 | } 66 | 67 | export function getClosestLinkPoints( 68 | a: SignalValue, 69 | b: SignalValue, 70 | ): [Vector2, Vector2] { 71 | const aPoints = getLinkPoints(unwrap(a)); 72 | const bPoints = getLinkPoints(unwrap(b)); 73 | 74 | const aClosest = aPoints.map(aPoint => { 75 | return bPoints.map(bPoint => { 76 | return { 77 | aPoint, 78 | bPoint, 79 | distanceSq: 80 | Math.pow(aPoint.x - bPoint.x, 2) + Math.pow(aPoint.y - bPoint.y, 2), 81 | }; 82 | }); 83 | }); 84 | 85 | const min = aClosest.reduce( 86 | (a, b) => { 87 | const bMin = b.reduce((a, b) => { 88 | return a.distanceSq < b.distanceSq ? a : b; 89 | }); 90 | return a.distanceSq < bMin.distanceSq ? a : bMin; 91 | }, 92 | {aPoint: {x: 0, y: 0}, bPoint: {x: 0, y: 0}, distanceSq: Infinity}, 93 | ); 94 | delete min.distanceSq; 95 | return [new Vector2(min.aPoint), new Vector2(min.bPoint)]; 96 | } 97 | -------------------------------------------------------------------------------- /src/app/app.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import puppeteer, {Page} from 'puppeteer'; 3 | import {fileURLToPath} from 'url'; 4 | import {createServer} from 'vite'; 5 | 6 | const Root = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | export interface App { 9 | page: Page; 10 | stop: () => Promise; 11 | } 12 | 13 | export async function start(): Promise { 14 | const [browser, server] = await Promise.all([ 15 | puppeteer.launch({ 16 | headless: true, 17 | protocolTimeout: 15 * 60 * 1000, 18 | }), 19 | createServer({ 20 | root: path.resolve(Root, '../../'), 21 | configFile: path.resolve(Root, '../../vite.config.ts'), 22 | server: { 23 | port: 9000, 24 | }, 25 | }), 26 | ]); 27 | 28 | const portPromise = new Promise(resolve => { 29 | server.httpServer.once('listening', async () => { 30 | const port = (server.httpServer.address() as any).port; 31 | resolve(port); 32 | }); 33 | }); 34 | await server.listen(); 35 | const port = await portPromise; 36 | const page = await browser.newPage(); 37 | await page.goto(`http://localhost:${port}`, { 38 | waitUntil: 'networkidle0', 39 | }); 40 | 41 | return { 42 | page, 43 | async stop() { 44 | await Promise.all([browser.close(), server.close()]); 45 | }, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/app/rendering.test.ts: -------------------------------------------------------------------------------- 1 | import {afterAll, beforeAll, describe, expect, test} from 'vitest'; 2 | import {App, start} from './app'; 3 | 4 | describe('Rendering', () => { 5 | let app: App; 6 | 7 | beforeAll(async () => { 8 | app = await start(); 9 | }); 10 | 11 | afterAll(async () => { 12 | await app.stop(); 13 | }); 14 | 15 | test( 16 | 'Animation renders correctly', 17 | { 18 | timeout: 15 * 60 * 1000, 19 | }, 20 | async () => { 21 | await app.page.evaluateHandle('document.fonts.ready'); 22 | await new Promise(resolve => setTimeout(resolve, 5_000)); 23 | await app.page.screenshot(); 24 | const rendering = await app.page.waitForSelector( 25 | "::-p-xpath(//div[contains(text(), 'Video Settings')])", 26 | ); 27 | if (rendering) { 28 | const tab = await app.page.evaluateHandle( 29 | el => el.parentElement, 30 | rendering, 31 | ); 32 | await tab.click(); 33 | } 34 | await new Promise(resolve => setTimeout(resolve, 1_000)); 35 | 36 | const frameRateLabel = await app.page.waitForSelector( 37 | "::-p-xpath(//div[contains(text(), 'Rendering')]/parent::div//label[contains(text(), 'frame rate')]/parent::div//input)", 38 | ); 39 | expect(frameRateLabel).toBeDefined(); 40 | expect(frameRateLabel).toBeDefined(); 41 | await frameRateLabel.click({clickCount: 3}); 42 | await frameRateLabel.type('15'); 43 | 44 | const scaleLabel = await app.page.waitForSelector( 45 | "::-p-xpath(//div[contains(text(), 'Rendering')]/parent::div//label[contains(text(), 'scale')])", 46 | ); 47 | expect(scaleLabel).toBeDefined(); 48 | const scale = await app.page.evaluateHandle( 49 | el => el.parentElement.children[1], 50 | scaleLabel, 51 | ); 52 | 53 | await app.page.select( 54 | "::-p-xpath(//div[contains(text(), 'Rendering')]/parent::div//label[contains(text(), 'exporter')]/parent::div//select)", 55 | 'Image sequence', 56 | ); 57 | 58 | await scale.select('1'); 59 | 60 | const render = await app.page.waitForSelector('#render'); 61 | await render.click(); 62 | await app.page.waitForSelector('#render[data-rendering="true"]', { 63 | timeout: 2 * 1000, 64 | }); 65 | await app.page.waitForSelector('#render:not([data-rendering="true"])', { 66 | timeout: 15 * 60 * 1000, 67 | }); 68 | 69 | expect(true).toBe(true); 70 | }, 71 | ); 72 | }); 73 | -------------------------------------------------------------------------------- /src/components/Body.tsx: -------------------------------------------------------------------------------- 1 | import {Layout, LayoutProps, Txt, TxtProps} from '@motion-canvas/2d'; 2 | import {Colors} from '../Colors'; 3 | import {SignalValue, createComputed, unwrap} from '@motion-canvas/core'; 4 | 5 | export const Text = { 6 | fontFamily: 'Montserrat', 7 | fill: Colors.Tailwind.Slate['100'], 8 | fontSize: 36, 9 | }; 10 | 11 | export const Title: TxtProps = { 12 | ...Text, 13 | fontSize: 64, 14 | fontWeight: 700, 15 | }; 16 | 17 | export const Bold = (props: TxtProps) => ; 18 | 19 | export const Em = (props: TxtProps) => ; 20 | 21 | export const Body = ( 22 | props: LayoutProps & { 23 | text: string; 24 | wrapAt?: SignalValue; 25 | txtProps?: TxtProps; 26 | }, 27 | ) => { 28 | const wrapAt = props.wrapAt ?? 20; 29 | const lines = createComputed(() => 30 | props.text.split(' ').reduce((acc, word) => { 31 | if (acc.length === 0) { 32 | return [word]; 33 | } 34 | if (acc[acc.length - 1].length + word.length > unwrap(wrapAt)) { 35 | return [...acc, word]; 36 | } 37 | return [...acc.slice(0, -1), `${acc[acc.length - 1]} ${word}`]; 38 | }, []), 39 | ); 40 | 41 | const children = createComputed(() => 42 | lines().map(line => ( 43 | 44 | {line} 45 | 46 | )), 47 | ); 48 | 49 | return ( 50 | 51 | {children()} 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/CodeLineNumbers.tsx: -------------------------------------------------------------------------------- 1 | import {Code, Layout, LayoutProps, Txt, TxtProps} from '@motion-canvas/2d'; 2 | import {Reference, range} from '@motion-canvas/core'; 3 | 4 | export interface CodeLineNumbersProps extends LayoutProps { 5 | code: Reference; 6 | numberProps?: TxtProps; 7 | rootLayoutProps?: LayoutProps; 8 | columnLayoutProps?: LayoutProps; 9 | } 10 | 11 | export class CodeLineNumbers extends Layout { 12 | public constructor(props: CodeLineNumbersProps) { 13 | super({ 14 | ...props, 15 | layout: true, 16 | justifyContent: 'space-evenly', 17 | direction: 'column', 18 | }); 19 | this.children(() => 20 | range(props.code().parsed().split('\n').length).map(i => ( 21 | 28 | )), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/DistortedCurve.tsx: -------------------------------------------------------------------------------- 1 | import {Curve, Line, LineProps} from '@motion-canvas/2d'; 2 | import { 3 | SignalValue, 4 | createComputed, 5 | range, 6 | unwrap, 7 | useRandom, 8 | } from '@motion-canvas/core'; 9 | 10 | export function DistortedCurve(props: { 11 | curve: SignalValue; 12 | displacement?: number; 13 | count?: number; 14 | samples?: number; 15 | lineProps: LineProps; 16 | }) { 17 | const c = unwrap(props.curve); 18 | const points = createComputed(() => 19 | range((props.samples ?? 100) + 1).map( 20 | i => c.getPointAtPercentage(i / props.samples).position, 21 | ), 22 | ); 23 | const dpl = props.displacement ?? 10; 24 | const displacementMaps: [number, number][][] = range(props.count ?? 1).map( 25 | () => 26 | range(1 + (props.samples ?? 100)).map(() => [ 27 | useRandom().nextFloat(-dpl, dpl), 28 | useRandom().nextFloat(-dpl, dpl), 29 | ]), 30 | ); 31 | return ( 32 | <> 33 | {range(props.count ?? 1).map(ci => { 34 | const displaced = createComputed(() => 35 | points().map((p, pi) => p.add(displacementMaps[ci][pi])), 36 | ); 37 | return ; 38 | })} 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/ErrorBox.tsx: -------------------------------------------------------------------------------- 1 | import {Layout, Txt, View2D} from '@motion-canvas/2d'; 2 | import {Windows98Button} from './WindowsButton'; 3 | import {Window, WindowProps, WindowStyle} from './Window'; 4 | import { 5 | PossibleVector2, 6 | Reference, 7 | SignalValue, 8 | Vector2, 9 | all, 10 | beginSlide, 11 | chain, 12 | createComputed, 13 | createRef, 14 | sequence, 15 | unwrap, 16 | useRandom, 17 | } from '@motion-canvas/core'; 18 | import {Colors} from '../Colors'; 19 | import {Body} from './Body'; 20 | import {belowScreenPosition} from '../Util'; 21 | 22 | export const ErrorBox = ( 23 | props: Omit & { 24 | error: string; 25 | size?: SignalValue>; 26 | wrapAt?: SignalValue; 27 | }, 28 | ) => { 29 | const sz = createComputed( 30 | () => new Vector2(unwrap(props.size ?? [500, 300])), 31 | ); 32 | return ( 33 | sz().addY(50)} 39 | > 40 | <> 41 | 42 | 49 | 50 | 51 | OK 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | export function* errorBoxes(messages: string[], view: View2D, prefix: string) { 61 | const refs = messages.map(message => [message, createRef()]) as [ 62 | string, 63 | Reference, 64 | ][]; 65 | const random = useRandom(); 66 | yield* chain( 67 | ...refs.map(([message, ref], i) => { 68 | view.add( 69 | , 79 | ); 80 | const p = ref().position(); 81 | ref().position(belowScreenPosition(view, ref)); 82 | return all( 83 | ref().position(p, 1), 84 | ref().scale(1, 1), 85 | beginSlide(`${prefix}-${i}`), 86 | ); 87 | }), 88 | ); 89 | return { 90 | refs, 91 | closeAll: function* () { 92 | yield* sequence( 93 | 0.2, 94 | ...refs.reverse().map(([, ref]) => ref().close(view, 1)), 95 | ); 96 | }, 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /src/components/FileTree.tsx: -------------------------------------------------------------------------------- 1 | import {Colors} from '@Colors'; 2 | import { 3 | Icon, 4 | Layout, 5 | Rect, 6 | RectProps, 7 | Txt, 8 | colorSignal, 9 | initial, 10 | signal, 11 | } from '@motion-canvas/2d'; 12 | import { 13 | Color, 14 | ColorSignal, 15 | DEFAULT, 16 | PossibleColor, 17 | SignalValue, 18 | SimpleSignal, 19 | makeRef, 20 | } from '@motion-canvas/core'; 21 | 22 | export enum FileType { 23 | File = 'File', 24 | Folder = 'Folder', 25 | Asset = 'Asset', 26 | } 27 | 28 | export interface FileStructure { 29 | name: SignalValue; 30 | type: FileType; 31 | id?: string; 32 | children?: FileStructure[]; 33 | } 34 | 35 | export interface FileTreeProps extends RectProps { 36 | structure?: FileStructure; 37 | folderColor?: SignalValue; 38 | fileColor?: SignalValue; 39 | assetColor?: SignalValue; 40 | labelColor?: SignalValue; 41 | indentAmount?: SignalValue; 42 | rowSize?: SignalValue; 43 | } 44 | 45 | export class FileTree extends Rect { 46 | public declare readonly structure: FileStructure; 47 | 48 | @initial(Colors.Tailwind.Amber['500']) 49 | @colorSignal() 50 | public declare readonly folderColor: ColorSignal; 51 | 52 | @initial(Colors.Tailwind.Purple['500']) 53 | @colorSignal() 54 | public declare readonly assetColor: ColorSignal; 55 | 56 | @initial(Colors.Tailwind.Slate['500']) 57 | @colorSignal() 58 | public declare readonly fileColor: ColorSignal; 59 | 60 | @initial(Colors.Tailwind.Slate['100']) 61 | @colorSignal() 62 | public declare readonly labelColor: ColorSignal; 63 | 64 | @initial(40) 65 | @signal() 66 | public declare readonly rowSize: SimpleSignal; 67 | 68 | @signal() 69 | public declare readonly indentAmount: SimpleSignal; 70 | 71 | private readonly refs: Record< 72 | string, 73 | { 74 | txt: Txt; 75 | icon: Icon; 76 | container: Rect; 77 | } 78 | > = {}; 79 | 80 | public constructor(props: FileTreeProps) { 81 | super({indentAmount: props.rowSize ?? 40, ...props}); 82 | this.structure = props.structure || {name: '/', type: FileType.Folder}; 83 | this.add(this.createRow(this.structure)); 84 | } 85 | 86 | private getIconProps(structure: FileStructure) { 87 | switch (structure.type) { 88 | case FileType.Folder: 89 | return {icon: 'ic:baseline-folder', color: this.folderColor()}; 90 | case FileType.File: 91 | return {icon: 'ic:round-insert-drive-file', color: this.fileColor()}; 92 | case FileType.Asset: 93 | return {icon: 'ic:baseline-image', color: this.assetColor()}; 94 | } 95 | } 96 | 97 | public getRef(id: string) { 98 | return this.refs[id]; 99 | } 100 | 101 | private createRow(structure: FileStructure, depth: number = 0) { 102 | if (structure.id) { 103 | this.refs[structure.id] = { 104 | icon: null as Icon, 105 | txt: null as Txt, 106 | container: null as Rect, 107 | }; 108 | } 109 | return ( 110 | 111 | this.rowSize() * 0.1} 117 | ref={ 118 | structure.id 119 | ? makeRef(this.refs[structure.id], 'container') 120 | : undefined 121 | } 122 | > 123 | {depth ? : null} 124 | this.rowSize() * 0.8} 132 | marginRight={() => this.rowSize() / 2} 133 | /> 134 | this.rowSize() * 0.6} 136 | fill={this.labelColor} 137 | ref={ 138 | structure.id ? makeRef(this.refs[structure.id], 'txt') : undefined 139 | } 140 | text={structure.name} 141 | /> 142 | 143 | {structure.children?.map(child => this.createRow(child, depth + 1))} 144 | 145 | ); 146 | } 147 | 148 | public *emphasize(id: string, duration: number, modifier = 1.3) { 149 | const dbRefs = this.getRef(id); 150 | yield dbRefs.icon.size(() => this.rowSize() * 0.8 * modifier, duration); 151 | yield dbRefs.txt.fill(this.folderColor, duration); 152 | yield dbRefs.container.fill( 153 | new Color(Colors.Tailwind.Slate['500']).alpha(0.5), 154 | 1, 155 | ); 156 | yield* dbRefs.txt.fontSize(() => this.rowSize() * 0.6 * modifier, duration); 157 | } 158 | 159 | public *reset(id: string, duration: number) { 160 | const dbRefs = this.getRef(id); 161 | yield dbRefs.icon.size(() => this.rowSize() * 0.8, duration); 162 | yield dbRefs.txt.fill(this.labelColor, duration); 163 | yield dbRefs.container.fill(DEFAULT, 1); 164 | yield* dbRefs.txt.fontSize(() => this.rowSize() * 0.6, duration); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/components/Glow.tsx: -------------------------------------------------------------------------------- 1 | import {Layout, LayoutProps, initial, signal} from '@motion-canvas/2d'; 2 | import {SignalValue, SimpleSignal, clamp} from '@motion-canvas/core'; 3 | 4 | export interface GlowProps extends LayoutProps { 5 | amount?: SignalValue; 6 | copyOpacity?: SignalValue; 7 | } 8 | 9 | export class Glow extends Layout { 10 | @initial(10) 11 | @signal() 12 | public declare readonly amount: SimpleSignal; 13 | 14 | @initial(1) 15 | @signal() 16 | public declare readonly copyOpacity: SimpleSignal; 17 | 18 | public constructor(props: GlowProps) { 19 | super({...props}); 20 | } 21 | 22 | protected draw(context: CanvasRenderingContext2D): void { 23 | super.draw(context); 24 | 25 | context.save(); 26 | context.globalAlpha = clamp(0, 1, this.copyOpacity()); 27 | context.filter = `blur(${this.amount()}px)`; 28 | context.globalCompositeOperation = 'overlay'; 29 | this.children().forEach(child => child.render(context)); 30 | context.restore(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/ImgWindow.tsx: -------------------------------------------------------------------------------- 1 | import {Rect, Img} from '@motion-canvas/2d'; 2 | import { 3 | Reference, 4 | SignalValue, 5 | PossibleVector2, 6 | createComputed, 7 | Vector2, 8 | unwrap, 9 | } from '@motion-canvas/core'; 10 | import {Window, WindowProps, WindowStyle} from './Window'; 11 | import {Colors} from '../Colors'; 12 | 13 | export const ImgWindow = ( 14 | props: Omit & { 15 | src: string; 16 | ref: Reference; 17 | padding?: number; 18 | size?: SignalValue>; 19 | }, 20 | ) => { 21 | const sz = createComputed(() => new Vector2(unwrap(props.size))); 22 | return ( 23 | sz().addY(50)} 31 | > 32 | 33 | sz().sub(props.padding)}> 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import {Layout, LayoutProps} from '@motion-canvas/2d'; 2 | 3 | export const Row = (props: LayoutProps) => ( 4 | 5 | ); 6 | 7 | export const Column = (props: LayoutProps) => ( 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /src/components/LinePlot.tsx: -------------------------------------------------------------------------------- 1 | import {signal, Line, LineProps} from '@motion-canvas/2d'; 2 | import {BBox, SimpleSignal, Vector2, useLogger} from '@motion-canvas/core'; 3 | import {Plot} from './Plot'; 4 | 5 | export interface LinePlotProps extends LineProps { 6 | data?: [number, number][]; 7 | points?: never; 8 | } 9 | 10 | export class LinePlot extends Line { 11 | @signal() 12 | public declare readonly data: SimpleSignal<[number, number][], this>; 13 | 14 | public constructor(props?: LinePlotProps) { 15 | super({ 16 | ...props, 17 | points: props.data, 18 | }); 19 | } 20 | 21 | public override parsedPoints(): Vector2[] { 22 | const parent = this.parent(); 23 | if (!(parent instanceof Plot)) { 24 | useLogger().warn( 25 | 'Using a LinePlot outside of a Plot is the same as a Line', 26 | ); 27 | return super.parsedPoints(); 28 | } 29 | const data = this.data().map(point => parent.getPointFromPlotSpace(point)); 30 | return data; 31 | } 32 | 33 | protected childrenBBox(): BBox { 34 | return BBox.fromPoints(...this.parsedPoints()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/MotionCanvasLogo.tsx: -------------------------------------------------------------------------------- 1 | import {Layout, LayoutProps, Node, Rect} from '@motion-canvas/2d'; 2 | import { 3 | Reference, 4 | range, 5 | all, 6 | linear, 7 | loop, 8 | createRef, 9 | unwrap, 10 | Vector2, 11 | chain, 12 | } from '@motion-canvas/core'; 13 | 14 | const YELLOW = '#FFC66D'; 15 | const RED = '#FF6470'; 16 | const GREEN = '#99C47A'; 17 | const BLUE = '#68ABDF'; 18 | 19 | const Trail = (props: LayoutProps) => ( 20 | 21 | ); 22 | 23 | export class MotionCanvasLogo extends Layout { 24 | private readonly star: Reference; 25 | private readonly trail1: Reference; 26 | private readonly trail2: Reference; 27 | private readonly trail3: Reference; 28 | private readonly dot: Reference; 29 | 30 | public constructor(props: LayoutProps) { 31 | super({ 32 | ...props, 33 | size: () => (new Vector2(unwrap(props.scale)).x ?? 1) * 300, 34 | }); 35 | this.star = createRef(); 36 | this.trail1 = createRef(); 37 | this.trail2 = createRef(); 38 | this.trail3 = createRef(); 39 | this.dot = createRef(); 40 | 41 | this.add( 42 | 43 | 44 | 45 | 46 | {range(3).map(() => ( 47 | 48 | ))} 49 | 50 | 58 | 59 | 60 | 61 | {range(3).map(() => ( 62 | 63 | ))} 64 | 65 | 73 | 74 | 75 | 76 | {range(4).map(i => ( 77 | 85 | ))} 86 | 87 | 96 | 97 | 98 | {range(5).map(i => ( 99 | 108 | ))} 109 | {range(5).map(i => ( 110 | 118 | ))} 119 | 120 | 121 | , 122 | ); 123 | } 124 | 125 | public animate() { 126 | // eslint-disable-next-line @typescript-eslint/no-this-alias -- need this for generator functions to work 127 | const that = this; 128 | return loop(() => 129 | all( 130 | chain(that.star().rotation(360, 4, linear), that.star().rotation(0, 0)), 131 | loop(4, function* () { 132 | yield* that.trail1().position.y(-150, 1, linear); 133 | that.trail1().position.y(0); 134 | }), 135 | loop(2, function* () { 136 | yield* that.trail2().position.y(-150, 2, linear); 137 | that.trail2().position.y(0); 138 | }), 139 | loop(2, function* () { 140 | yield* all( 141 | that.trail3().position.y(-130, 2, linear), 142 | that.dot().fill(GREEN, 2, linear), 143 | ); 144 | that.dot().fill(BLUE); 145 | that.trail3().position.y(0); 146 | }), 147 | ), 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/components/Plot.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CanvasStyleSignal, 3 | Layout, 4 | LayoutProps, 5 | canvasStyleSignal, 6 | computed, 7 | drawRect, 8 | initial, 9 | resolveCanvasStyle, 10 | signal, 11 | vector2Signal, 12 | } from '@motion-canvas/2d'; 13 | import { 14 | BBox, 15 | PossibleColor, 16 | PossibleVector2, 17 | SignalValue, 18 | SimpleSignal, 19 | Vector2, 20 | Vector2Signal, 21 | range, 22 | } from '@motion-canvas/core'; 23 | 24 | export interface PlotProps extends LayoutProps { 25 | minX?: SignalValue; 26 | minY?: SignalValue; 27 | min?: SignalValue; 28 | 29 | maxX?: SignalValue; 30 | maxY?: SignalValue; 31 | max?: SignalValue; 32 | 33 | ticksX?: SignalValue; 34 | ticksY?: SignalValue; 35 | ticks?: SignalValue; 36 | 37 | labelSizeX?: SignalValue; 38 | labelSizeY?: SignalValue; 39 | labelSize?: SignalValue; 40 | 41 | labelPaddingX?: SignalValue; 42 | labelPaddingY?: SignalValue; 43 | labelPadding?: SignalValue; 44 | 45 | tickPaddingX?: SignalValue; 46 | tickPaddingY?: SignalValue; 47 | tickPadding?: SignalValue; 48 | 49 | tickLabelSizeX?: SignalValue; 50 | tickLabelSizeY?: SignalValue; 51 | tickLabelSize?: SignalValue; 52 | 53 | tickOverflowX?: SignalValue; 54 | tickOverflowY?: SignalValue; 55 | tickOverflow?: SignalValue; 56 | 57 | gridStrokeWidth?: SignalValue; 58 | axisStrokeWidth?: SignalValue; 59 | 60 | labelX?: SignalValue; 61 | axisColorX?: SignalValue; 62 | axisTextColorX?: SignalValue; 63 | 64 | labelY?: SignalValue; 65 | axisColorY?: SignalValue; 66 | axisTextColorY?: SignalValue; 67 | 68 | labelFormatterX?: (x: number) => string; 69 | labelFormatterY?: (y: number) => string; 70 | } 71 | 72 | export class Plot extends Layout { 73 | @initial(Vector2.zero) 74 | @vector2Signal('min') 75 | public declare readonly min: Vector2Signal; 76 | 77 | @initial(Vector2.one.mul(100)) 78 | @vector2Signal('max') 79 | public declare readonly max: Vector2Signal; 80 | 81 | @initial(Vector2.one.mul(10)) 82 | @vector2Signal('ticks') 83 | public declare readonly ticks: Vector2Signal; 84 | 85 | @initial(Vector2.one.mul(30)) 86 | @vector2Signal('labelSize') 87 | public declare readonly labelSize: Vector2Signal; 88 | 89 | @initial(Vector2.one.mul(5)) 90 | @vector2Signal('labelPadding') 91 | public declare readonly labelPadding: Vector2Signal; 92 | 93 | @initial(Vector2.one.mul(10)) 94 | @vector2Signal('tickLabelSize') 95 | public declare readonly tickLabelSize: Vector2Signal; 96 | 97 | @initial(Vector2.one.mul(5)) 98 | @vector2Signal('tickOverflow') 99 | public declare readonly tickOverflow: Vector2Signal; 100 | 101 | @initial(Vector2.one.mul(6)) 102 | @vector2Signal('tickPadding') 103 | public declare readonly tickPadding: Vector2Signal; 104 | 105 | @initial(Vector2.one.mul(1)) 106 | @vector2Signal('gridStrokeWidth') 107 | public declare readonly gridStrokeWidth: Vector2Signal; 108 | 109 | @initial(Vector2.one.mul(2)) 110 | @vector2Signal('axisStrokeWidth') 111 | public declare readonly axisStrokeWidth: Vector2Signal; 112 | 113 | @initial('white') 114 | @canvasStyleSignal() 115 | public declare readonly axisColorX: CanvasStyleSignal; 116 | 117 | @initial('white') 118 | @canvasStyleSignal() 119 | public declare readonly axisTextColorX: CanvasStyleSignal; 120 | 121 | @initial('') 122 | @signal() 123 | public declare readonly labelX: SimpleSignal; 124 | 125 | @initial('white') 126 | @canvasStyleSignal() 127 | public declare readonly axisColorY: CanvasStyleSignal; 128 | 129 | @initial('white') 130 | @canvasStyleSignal() 131 | public declare readonly axisTextColorY: CanvasStyleSignal; 132 | 133 | @initial('') 134 | @signal() 135 | public declare readonly labelY: SimpleSignal; 136 | 137 | public readonly labelFormatterX: (x: number) => string; 138 | public readonly labelFormatterY: (y: number) => string; 139 | 140 | @computed() 141 | private edgePadding() { 142 | return this.labelSize() 143 | .add(this.labelPadding()) 144 | .add(this.tickLabelSize().mul([Math.log10(this.max().y) + 1, 2])) 145 | .add(this.tickOverflow()) 146 | .add(this.axisStrokeWidth()); 147 | } 148 | 149 | public constructor(props?: PlotProps) { 150 | super(props); 151 | this.labelFormatterX = props.labelFormatterX ?? (x => x.toFixed(0)); 152 | this.labelFormatterY = props.labelFormatterY ?? (y => y.toFixed(0)); 153 | } 154 | 155 | public cacheBBox(): BBox { 156 | return BBox.fromSizeCentered(this.size().add(this.edgePadding().mul(2))); 157 | } 158 | 159 | protected draw(context: CanvasRenderingContext2D): void { 160 | const halfSize = this.computedSize().mul(-0.5); 161 | 162 | for (let i = 0; i <= this.ticks().floored.x; i++) { 163 | const startPosition = halfSize.add( 164 | this.computedSize().mul([i / this.ticks().x, 1]), 165 | ); 166 | 167 | context.beginPath(); 168 | context.moveTo( 169 | startPosition.x, 170 | startPosition.y + 171 | this.tickOverflow().x + 172 | this.axisStrokeWidth().x / 2 + 173 | this.axisStrokeWidth().x / 2, 174 | ); 175 | context.lineTo(startPosition.x, halfSize.y); 176 | context.strokeStyle = resolveCanvasStyle(this.axisColorX(), context); 177 | context.lineWidth = this.gridStrokeWidth().x; 178 | context.stroke(); 179 | 180 | context.fillStyle = resolveCanvasStyle(this.axisTextColorX(), context); 181 | context.font = `${this.tickLabelSize().y}px sans-serif`; 182 | context.textAlign = 'center'; 183 | context.textBaseline = 'top'; 184 | context.fillText( 185 | `${this.labelFormatterX(this.mapToX(i / this.ticks().x))}`, 186 | startPosition.x, 187 | startPosition.y + 188 | this.axisStrokeWidth().x + 189 | this.tickOverflow().x + 190 | Math.floor(this.tickPadding().x / 2), 191 | ); 192 | } 193 | 194 | for (let i = 0; i <= this.ticks().floored.y; i++) { 195 | const startPosition = halfSize.add( 196 | this.computedSize().mul([1, 1 - i / this.ticks().y]), 197 | ); 198 | 199 | context.beginPath(); 200 | context.moveTo(startPosition.x, startPosition.y); 201 | context.lineTo(halfSize.x - this.tickOverflow().y, startPosition.y); 202 | context.strokeStyle = resolveCanvasStyle(this.axisColorY(), context); 203 | context.lineWidth = this.gridStrokeWidth().y; 204 | context.stroke(); 205 | 206 | context.fillStyle = resolveCanvasStyle(this.axisTextColorY(), context); 207 | context.font = `${this.tickLabelSize().y}px ${this.fontFamily()}`; 208 | context.textAlign = 'right'; 209 | context.textBaseline = 'middle'; 210 | context.fillText( 211 | `${this.labelFormatterY(this.mapToY(i / this.ticks().y))}`, 212 | halfSize.x - 213 | this.axisStrokeWidth().y - 214 | this.tickOverflow().y - 215 | Math.floor(this.tickPadding().y / 2), 216 | startPosition.y, 217 | ); 218 | } 219 | 220 | context.beginPath(); 221 | const yAxisStartPoint = this.getPointFromPlotSpace([0, this.min().y]); 222 | const yAxisEndPoint = this.getPointFromPlotSpace([0, this.max().y]); 223 | context.moveTo( 224 | yAxisStartPoint.x - this.gridStrokeWidth().y / 2, 225 | yAxisStartPoint.y - this.gridStrokeWidth().y / 2, 226 | ); 227 | context.lineTo( 228 | yAxisEndPoint.x - this.gridStrokeWidth().y / 2, 229 | yAxisEndPoint.y + this.gridStrokeWidth().y / 2, 230 | ); 231 | context.strokeStyle = resolveCanvasStyle(this.axisColorX(), context); 232 | context.lineWidth = this.axisStrokeWidth().x; 233 | context.stroke(); 234 | 235 | context.beginPath(); 236 | const xAxisStartPoint = this.getPointFromPlotSpace([this.min().x, 0]); 237 | const xAxisEndPoint = this.getPointFromPlotSpace([this.max().x, 0]); 238 | context.moveTo( 239 | xAxisStartPoint.x - this.gridStrokeWidth().x / 2, 240 | xAxisStartPoint.y + this.gridStrokeWidth().x / 2, 241 | ); 242 | context.lineTo( 243 | xAxisEndPoint.x + this.gridStrokeWidth().x / 2, 244 | xAxisEndPoint.y + this.gridStrokeWidth().x / 2, 245 | ); 246 | context.strokeStyle = resolveCanvasStyle(this.axisColorY(), context); 247 | context.lineWidth = this.axisStrokeWidth().y; 248 | context.stroke(); 249 | 250 | // Draw X axis label 251 | context.fillStyle = resolveCanvasStyle(this.axisTextColorX(), context); 252 | context.font = `${this.labelSize().y}px ${this.fontFamily()}`; 253 | context.textAlign = 'center'; 254 | context.textBaseline = 'alphabetic'; 255 | context.fillText( 256 | this.labelX(), 257 | 0, 258 | -halfSize.y + 259 | this.axisStrokeWidth().x + 260 | this.tickOverflow().x + 261 | this.tickLabelSize().x + 262 | this.tickPadding().x + 263 | Math.floor(this.labelPadding().x) + 264 | this.labelSize().x, 265 | ); 266 | 267 | // Draw rotated Y axis label 268 | context.fillStyle = resolveCanvasStyle(this.axisTextColorY(), context); 269 | context.font = `${this.labelSize().y}px ${this.fontFamily()}`; 270 | context.textAlign = 'center'; 271 | context.textBaseline = 'alphabetic'; 272 | context.save(); 273 | context.translate( 274 | halfSize.x - 275 | this.axisStrokeWidth().y - 276 | this.tickOverflow().y - 277 | this.tickLabelSize().y - 278 | this.tickPadding().y - 279 | Math.floor(this.labelPadding().y / 2) - 280 | this.labelSize().y, 281 | 0, 282 | ); 283 | context.rotate(-Math.PI / 2); 284 | context.fillText(this.labelY(), 0, 0); 285 | context.restore(); 286 | 287 | if (this.clip()) { 288 | context.clip(this.getPath()); 289 | } 290 | this.drawChildren(context); 291 | } 292 | 293 | public getPath(): Path2D { 294 | const path = new Path2D(); 295 | const box = BBox.fromSizeCentered(this.size()); 296 | drawRect(path, box); 297 | 298 | return path; 299 | } 300 | 301 | public getPointFromPlotSpace(point: PossibleVector2) { 302 | const bottomLeft = this.computedSize().mul([-0.5, 0.5]); 303 | 304 | return this.toRelativeGridSize(point) 305 | .mul([1, -1]) 306 | .mul(this.computedSize()) 307 | .add(bottomLeft); 308 | } 309 | 310 | private mapToX(value: number) { 311 | return this.min().x + value * (this.max().x - this.min().x); 312 | } 313 | 314 | private mapToY(value: number) { 315 | return this.min().y + value * (this.max().y - this.min().y); 316 | } 317 | 318 | private toRelativeGridSize(p: PossibleVector2) { 319 | return new Vector2(p).sub(this.min()).div(this.max().sub(this.min())); 320 | } 321 | 322 | public makeGraphData( 323 | resolution: number, 324 | f: (x: number) => number, 325 | ): [number, number][] { 326 | return range(this.min().x, this.max().x + resolution, resolution).map(x => [ 327 | x, 328 | f(x), 329 | ]); 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /src/components/ScatterPlot.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | initial, 3 | signal, 4 | resolveCanvasStyle, 5 | canvasStyleSignal, 6 | CanvasStyleSignal, 7 | PossibleCanvasStyle, 8 | computed, 9 | Layout, 10 | LayoutProps, 11 | parser, 12 | } from '@motion-canvas/2d'; 13 | import { 14 | PossibleVector2, 15 | SignalValue, 16 | SimpleSignal, 17 | clamp, 18 | useLogger, 19 | } from '@motion-canvas/core'; 20 | import {Plot} from './Plot'; 21 | 22 | export interface ScatterPlotProps extends LayoutProps { 23 | pointRadius?: number; 24 | pointColor?: PossibleCanvasStyle; 25 | data?: SignalValue; 26 | start?: SignalValue; 27 | end?: SignalValue; 28 | } 29 | 30 | export class ScatterPlot extends Layout { 31 | @initial(5) 32 | @signal() 33 | public declare readonly pointRadius: SimpleSignal; 34 | 35 | @initial('white') 36 | @canvasStyleSignal() 37 | public declare readonly pointColor: CanvasStyleSignal; 38 | 39 | @signal() 40 | public declare readonly data: SimpleSignal<[number, number][], this>; 41 | 42 | @initial(0) 43 | @parser((value: number) => clamp(0, 1, value)) 44 | @signal() 45 | public declare readonly start: SimpleSignal; 46 | 47 | @initial(1) 48 | @parser((value: number) => clamp(0, 1, value)) 49 | @signal() 50 | public declare readonly end: SimpleSignal; 51 | 52 | @computed() 53 | private firstIndex() { 54 | return Math.ceil(this.data().length * this.start() + 1); 55 | } 56 | 57 | @computed() 58 | private firstPointProgress() { 59 | return this.firstIndex() - this.start() * this.data().length; 60 | } 61 | 62 | @computed() 63 | private lastIndex() { 64 | return Math.floor(this.data().length * this.end() - 1); 65 | } 66 | 67 | @computed() 68 | private pointProgress() { 69 | return this.end() * this.data().length - this.lastIndex(); 70 | } 71 | 72 | public constructor(props?: ScatterPlotProps) { 73 | super({ 74 | ...props, 75 | }); 76 | } 77 | 78 | protected draw(context: CanvasRenderingContext2D): void { 79 | context.save(); 80 | context.fillStyle = resolveCanvasStyle(this.pointColor(), context); 81 | 82 | const parent = this.parent(); 83 | if (!(parent instanceof Plot)) { 84 | useLogger().warn('Using a ScatterPlot outside of a Plot does nothing'); 85 | return; 86 | } 87 | 88 | if (this.firstIndex() < this.lastIndex()) { 89 | const firstPoint = this.data()[this.firstIndex() - 1]; 90 | 91 | const coord = parent.getPointFromPlotSpace(firstPoint); 92 | 93 | context.beginPath(); 94 | context.arc( 95 | coord.x, 96 | coord.y, 97 | this.pointRadius() * this.firstPointProgress(), 98 | 0, 99 | Math.PI * 2, 100 | ); 101 | context.fill(); 102 | } 103 | 104 | const data = this.data(); 105 | data.slice(this.firstIndex(), this.lastIndex()).forEach(point => { 106 | const coord = parent.getPointFromPlotSpace(point); 107 | 108 | context.beginPath(); 109 | context.arc(coord.x, coord.y, this.pointRadius(), 0, Math.PI * 2); 110 | context.fill(); 111 | }); 112 | 113 | if (this.lastIndex() > this.firstIndex()) { 114 | const lastPoint = data[this.lastIndex()]; 115 | 116 | const lastCoord = parent.getPointFromPlotSpace(lastPoint); 117 | 118 | context.beginPath(); 119 | context.arc( 120 | lastCoord.x, 121 | lastCoord.y, 122 | this.pointRadius() * this.pointProgress(), 123 | 0, 124 | Math.PI * 2, 125 | ); 126 | context.fill(); 127 | } 128 | 129 | context.restore(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/components/Scrollable.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Rect, 3 | RectProps, 4 | Node, 5 | Layout, 6 | computed, 7 | vector2Signal, 8 | initial, 9 | nodeName, 10 | signal, 11 | Curve, 12 | } from '@motion-canvas/2d'; 13 | import { 14 | BBox, 15 | InterpolationFunction, 16 | PossibleVector2, 17 | SignalValue, 18 | SimpleSignal, 19 | TimingFunction, 20 | Vector2, 21 | Vector2Signal, 22 | all, 23 | clampRemap, 24 | createRef, 25 | createSignal, 26 | delay, 27 | easeInOutCubic, 28 | map, 29 | unwrap, 30 | } from '@motion-canvas/core'; 31 | import {Colors} from '../Colors'; 32 | import {signum} from '@Util'; 33 | 34 | export interface ScrollableProps extends RectProps { 35 | activeOpacity?: SignalValue; 36 | inactiveOpacity?: SignalValue; 37 | handleProps?: RectProps; 38 | scrollHandleDelay?: SignalValue; 39 | scrollHandleDuration?: SignalValue; 40 | scrollOffset?: SignalValue; 41 | scrollPadding?: SignalValue; 42 | handleWidth?: SignalValue; 43 | handleInset?: SignalValue; 44 | zoom?: SignalValue; 45 | } 46 | 47 | @nodeName('Scrollable') 48 | export class Scrollable extends Rect { 49 | @initial(0) 50 | @vector2Signal('scrollOffset') 51 | public declare readonly scrollOffset: Vector2Signal; 52 | 53 | @initial(4) 54 | @vector2Signal('scrollPadding') 55 | public declare readonly scrollPadding: Vector2Signal; 56 | 57 | @initial(0.5) 58 | @vector2Signal('inactiveOpacity') 59 | public declare readonly inactiveOpacity: Vector2Signal; 60 | 61 | @initial(1) 62 | @vector2Signal('activeOpacity') 63 | public declare readonly activeOpacity: Vector2Signal; 64 | 65 | @initial(8) 66 | @signal() 67 | public declare readonly handleWidth: SimpleSignal; 68 | 69 | @initial(16) 70 | @signal() 71 | public declare readonly handleInset: SimpleSignal; 72 | 73 | @initial(1) 74 | @signal() 75 | public declare readonly zoom: SimpleSignal; 76 | 77 | @initial(-0.2) 78 | @signal() 79 | public declare readonly scrollHandleDelay: SimpleSignal; 80 | 81 | @signal() 82 | public declare readonly scrollHandleDuration: SimpleSignal; 83 | 84 | private readonly scrollOpacity = Vector2.createSignal(); 85 | 86 | @signal() 87 | private readonly pathProgress = createSignal(0); 88 | 89 | @signal() 90 | private readonly path = createSignal(); 91 | 92 | @computed() 93 | private inverseZoom() { 94 | return 1 / this.zoom(); 95 | } 96 | 97 | @computed() 98 | public contentsBox() { 99 | return this.contents() 100 | .childrenAs() 101 | .reduce( 102 | (b, child) => { 103 | if (!(child instanceof Layout)) { 104 | return b; 105 | } 106 | const combinedLowX = Math.min( 107 | b.x, 108 | child.position().x - child.size().x / 2, 109 | ); 110 | const combinedLowY = Math.min( 111 | b.y, 112 | child.position().y - child.size().y / 2, 113 | ); 114 | const combinedHighX = Math.max( 115 | b.x + b.width, 116 | child.position().x + child.size().x / 2, 117 | ); 118 | const combinedHighY = Math.max( 119 | b.y + b.height, 120 | child.position().y + child.size().y / 2, 121 | ); 122 | 123 | return new BBox( 124 | combinedLowX, 125 | combinedLowY, 126 | combinedHighX - combinedLowX, 127 | combinedHighY - combinedLowY, 128 | ); 129 | }, 130 | // Always start with the scrollable's size. 131 | new BBox( 132 | -this.size().x / 2, 133 | -this.size().y / 2, 134 | this.size().x, 135 | this.size().y, 136 | ), 137 | ); 138 | } 139 | 140 | @computed() 141 | private contentsSize() { 142 | return this.contentsBox().size; 143 | } 144 | 145 | @computed() 146 | private contentsProportion() { 147 | return this.size() 148 | .mul(this.inverseZoom()) 149 | .div([this.contentsSize().width, this.contentsSize().height]); 150 | } 151 | 152 | @computed() 153 | private scrollOpacityY() { 154 | if (this.contentsProportion().y > 1.05) { 155 | return 0; 156 | } 157 | if (this.contentsProportion().y > 1) { 158 | return (1.05 - this.contentsProportion().y) * 10; 159 | } 160 | return this.scrollOpacity().y; 161 | } 162 | 163 | @computed() 164 | private scrollOpacityX() { 165 | if (this.contentsProportion().x > 1) { 166 | return 0; 167 | } 168 | if (this.contentsProportion().x > 0.95) { 169 | return (0.95 - this.contentsProportion().x) * 10; 170 | } 171 | return this.scrollOpacity().x; 172 | } 173 | 174 | @computed() 175 | private handleSize() { 176 | return this.contentsProportion() 177 | .mul(this.size()) 178 | .sub(this.handleInset() + this.handleWidth() / 2); 179 | } 180 | 181 | @computed() 182 | private handlePosition() { 183 | const halfHandleSize = this.handleSize().div(2); 184 | // Map the contents box to the scrollable's size, ensuring that they don't 185 | // get clipped by going out of bounds. 186 | return new Vector2( 187 | clampRemap( 188 | this.contentsBox().x + this.size().x / 2, 189 | this.contentsBox().x + this.contentsBox().width - this.size().x / 2, 190 | -this.size().x / 2 + this.handleInset() / 2 + halfHandleSize.x, 191 | this.size().x / 2 - 192 | this.handleWidth() / 2 - 193 | this.handleInset() - 194 | halfHandleSize.x, 195 | this.scrollOffset().x, 196 | ), 197 | clampRemap( 198 | this.contentsBox().y + this.size().y / 2, 199 | this.contentsBox().y + this.contentsBox().height - this.size().y / 2, 200 | -this.size().y / 2 + this.handleInset() / 2 + halfHandleSize.y, 201 | this.size().y / 2 - 202 | this.handleWidth() / 2 - 203 | this.handleInset() - 204 | halfHandleSize.y, 205 | this.scrollOffset().y, 206 | ), 207 | ); 208 | } 209 | 210 | public readonly contents; 211 | 212 | public constructor(props: ScrollableProps) { 213 | super({...props, clip: true}); 214 | this.scrollOpacity(this.inactiveOpacity); 215 | this.scrollHandleDuration( 216 | props.scrollHandleDuration ?? 217 | (props.scrollHandleDelay ? -props.scrollHandleDelay : 0.2), 218 | ); 219 | this.contents = createRef(); 220 | 221 | this.add( 222 | 223 | this.scrollOffset().mul(-1).mul(this.zoom())} 225 | scale={this.zoom} 226 | > 227 | {props.children} 228 | 229 | this.size().x / 2 - this.handleInset()} 234 | height={() => this.handleSize().y} 235 | y={() => this.handlePosition().y} 236 | width={this.handleWidth} 237 | opacity={this.scrollOpacityY} 238 | > 239 | this.size().y / 2 - this.handleInset()} 244 | height={this.handleWidth} 245 | width={() => this.handleSize().x} 246 | x={() => this.handlePosition().x} 247 | opacity={this.scrollOpacityX} 248 | > 249 | , 250 | ); 251 | this.scrollOffset( 252 | props.scrollOffset ?? 253 | (() => this.contentsSize().mul(-0.5).add(this.size().mul(0.5))), 254 | ); 255 | } 256 | 257 | public *tweenZoom( 258 | v: SignalValue, 259 | duration: number, 260 | timingFunction?: TimingFunction, 261 | interpolationFunction?: InterpolationFunction, 262 | ) { 263 | yield this.scrollOpacity(this.activeOpacity, 0.1); 264 | yield* all( 265 | delay( 266 | duration + this.scrollHandleDelay(), 267 | this.scrollOpacity(this.inactiveOpacity, this.scrollHandleDuration()), 268 | ), 269 | this.zoom.context.tweener( 270 | v, 271 | duration, 272 | timingFunction ?? easeInOutCubic, 273 | interpolationFunction ?? map, 274 | ), 275 | ); 276 | } 277 | 278 | public *tweenScrollOffset( 279 | offset: SignalValue, 280 | duration: number, 281 | timingFunction?: TimingFunction, 282 | interpolationFunction?: InterpolationFunction, 283 | ) { 284 | const _offset = new Vector2(unwrap(offset)); 285 | yield this.scrollOpacity( 286 | () => [ 287 | _offset.x != this.scrollOffset().x 288 | ? this.activeOpacity().x 289 | : this.inactiveOpacity().x, 290 | _offset.y != this.scrollOffset().y 291 | ? this.activeOpacity().y 292 | : this.inactiveOpacity().y, 293 | ], 294 | 0.1, 295 | ); 296 | yield* all( 297 | delay( 298 | duration + this.scrollHandleDelay(), 299 | this.scrollOpacity(this.inactiveOpacity, this.scrollHandleDuration()), 300 | ), 301 | this.scrollOffset.context.tweener( 302 | offset, 303 | duration, 304 | timingFunction, 305 | interpolationFunction, 306 | ), 307 | ); 308 | } 309 | 310 | public *scrollTo( 311 | offset: PossibleVector2, 312 | duration: number, 313 | timingFunction?: TimingFunction, 314 | interpolationFunction?: InterpolationFunction, 315 | ) { 316 | yield* this.scrollOffset( 317 | offset, 318 | duration, 319 | timingFunction, 320 | interpolationFunction, 321 | ); 322 | } 323 | 324 | public *scrollBy( 325 | offset: PossibleVector2, 326 | duration: number, 327 | timingFunction?: TimingFunction, 328 | interpolationFunction?: InterpolationFunction, 329 | ) { 330 | yield* this.scrollTo( 331 | this.scrollOffset().add(offset), 332 | duration, 333 | timingFunction, 334 | interpolationFunction, 335 | ); 336 | } 337 | 338 | public *scrollToScaled( 339 | x: number | undefined, 340 | y: number | undefined, 341 | duration: number, 342 | timingFunction?: TimingFunction, 343 | interpolationFunction?: InterpolationFunction, 344 | ) { 345 | const xSign = signum(0.5 - x); 346 | const ySign = signum(0.5 - y); 347 | const viewOffsetX = 348 | xSign * (this.size().x / 2 - this.scrollPadding().x) * this.inverseZoom(); 349 | const viewOffsetY = 350 | ySign * (this.size().y / 2 - this.scrollPadding().y) * this.inverseZoom(); 351 | yield* this.scrollTo( 352 | { 353 | x: 354 | x != undefined 355 | ? this.contentsBox().x + x * this.contentsBox().width + viewOffsetX 356 | : this.scrollOffset().x, 357 | y: 358 | y != undefined 359 | ? this.contentsBox().y + y * this.contentsBox().height + viewOffsetY 360 | : this.scrollOffset().y, 361 | }, 362 | duration, 363 | timingFunction, 364 | interpolationFunction, 365 | ); 366 | } 367 | 368 | public *scrollToTop( 369 | duration: number, 370 | timingFunction?: TimingFunction, 371 | interpolationFunction?: InterpolationFunction, 372 | ) { 373 | yield* this.scrollToScaled( 374 | undefined, 375 | 0, 376 | duration, 377 | timingFunction, 378 | interpolationFunction, 379 | ); 380 | } 381 | 382 | public *scrollToTopCenter( 383 | duration: number, 384 | timingFunction?: TimingFunction, 385 | interpolationFunction?: InterpolationFunction, 386 | ) { 387 | yield* this.scrollToScaled( 388 | 0.5, 389 | 0, 390 | duration, 391 | timingFunction, 392 | interpolationFunction, 393 | ); 394 | } 395 | 396 | public *scrollToBottom( 397 | duration: number, 398 | timingFunction?: TimingFunction, 399 | interpolationFunction?: InterpolationFunction, 400 | ) { 401 | yield* this.scrollToScaled( 402 | undefined, 403 | 1, 404 | duration, 405 | timingFunction, 406 | interpolationFunction, 407 | ); 408 | } 409 | 410 | public *scrollToBottomCenter( 411 | duration: number, 412 | timingFunction?: TimingFunction, 413 | interpolationFunction?: InterpolationFunction, 414 | ) { 415 | yield* this.scrollToScaled( 416 | 0.5, 417 | 1, 418 | duration, 419 | timingFunction, 420 | interpolationFunction, 421 | ); 422 | } 423 | 424 | public *scrollToLeft( 425 | duration: number, 426 | timingFunction?: TimingFunction, 427 | interpolationFunction?: InterpolationFunction, 428 | ) { 429 | yield* this.scrollToScaled( 430 | 0, 431 | undefined, 432 | duration, 433 | timingFunction, 434 | interpolationFunction, 435 | ); 436 | } 437 | 438 | public *scrollToLeftCenter( 439 | duration: number, 440 | timingFunction?: TimingFunction, 441 | interpolationFunction?: InterpolationFunction, 442 | ) { 443 | yield* this.scrollToScaled( 444 | 0, 445 | 0.5, 446 | duration, 447 | timingFunction, 448 | interpolationFunction, 449 | ); 450 | } 451 | 452 | public *scrollToRight( 453 | duration: number, 454 | timingFunction?: TimingFunction, 455 | interpolationFunction?: InterpolationFunction, 456 | ) { 457 | yield* this.scrollToScaled( 458 | 1, 459 | undefined, 460 | duration, 461 | timingFunction, 462 | interpolationFunction, 463 | ); 464 | } 465 | 466 | public *scrollToRightCenter( 467 | duration: number, 468 | timingFunction?: TimingFunction, 469 | interpolationFunction?: InterpolationFunction, 470 | ) { 471 | yield* this.scrollToScaled( 472 | 1, 473 | 0.5, 474 | duration, 475 | timingFunction, 476 | interpolationFunction, 477 | ); 478 | } 479 | 480 | public *scrollToCenter( 481 | duration: number, 482 | timingFunction?: TimingFunction, 483 | interpolationFunction?: InterpolationFunction, 484 | ) { 485 | yield* this.scrollToScaled( 486 | 0.5, 487 | 0.5, 488 | duration, 489 | timingFunction, 490 | interpolationFunction, 491 | ); 492 | } 493 | 494 | public *scrollToTopLeft( 495 | duration: number, 496 | timingFunction?: TimingFunction, 497 | interpolationFunction?: InterpolationFunction, 498 | ) { 499 | yield* this.scrollToScaled( 500 | 0, 501 | 0, 502 | duration, 503 | timingFunction, 504 | interpolationFunction, 505 | ); 506 | } 507 | 508 | public *scrollToTopRight( 509 | duration: number, 510 | timingFunction?: TimingFunction, 511 | interpolationFunction?: InterpolationFunction, 512 | ) { 513 | yield* this.scrollToScaled( 514 | 1, 515 | 0, 516 | duration, 517 | timingFunction, 518 | interpolationFunction, 519 | ); 520 | } 521 | 522 | public *scrollToBottomLeft( 523 | duration: number, 524 | timingFunction?: TimingFunction, 525 | interpolationFunction?: InterpolationFunction, 526 | ) { 527 | yield* this.scrollToScaled( 528 | 0, 529 | 1, 530 | duration, 531 | timingFunction, 532 | interpolationFunction, 533 | ); 534 | } 535 | 536 | public *scrollToBottomRight( 537 | duration: number, 538 | timingFunction?: TimingFunction, 539 | interpolationFunction?: InterpolationFunction, 540 | ) { 541 | yield* this.scrollToScaled( 542 | 1, 543 | 1, 544 | duration, 545 | timingFunction, 546 | interpolationFunction, 547 | ); 548 | } 549 | 550 | public *scrollDown( 551 | amount: number, 552 | duration: number, 553 | timingFunction?: TimingFunction, 554 | interpolationFunction?: InterpolationFunction, 555 | ) { 556 | yield* this.scrollBy( 557 | [0, amount], 558 | duration, 559 | timingFunction, 560 | interpolationFunction, 561 | ); 562 | } 563 | 564 | public *scrollUp( 565 | amount: number, 566 | duration: number, 567 | timingFunction?: TimingFunction, 568 | interpolationFunction?: InterpolationFunction, 569 | ) { 570 | yield* this.scrollBy( 571 | [0, -amount], 572 | duration, 573 | timingFunction, 574 | interpolationFunction, 575 | ); 576 | } 577 | 578 | public *scrollRight( 579 | amount: number, 580 | duration: number, 581 | timingFunction?: TimingFunction, 582 | interpolationFunction?: InterpolationFunction, 583 | ) { 584 | yield* this.scrollBy( 585 | [amount, 0], 586 | duration, 587 | timingFunction, 588 | interpolationFunction, 589 | ); 590 | } 591 | 592 | public *scrollLeft( 593 | amount: number, 594 | duration: number, 595 | timingFunction?: TimingFunction, 596 | interpolationFunction?: InterpolationFunction, 597 | ) { 598 | yield* this.scrollBy( 599 | [-amount, 0], 600 | duration, 601 | timingFunction, 602 | interpolationFunction, 603 | ); 604 | } 605 | 606 | public *tweenToAndFollowCurve( 607 | curve: SignalValue, 608 | navigationDuration: number, 609 | duration: number, 610 | timingFunction?: TimingFunction, 611 | interpolationFunction?: InterpolationFunction, 612 | ) { 613 | const c = unwrap(curve); 614 | yield* this.scrollOffset( 615 | () => c.getPointAtPercentage(0).position, 616 | navigationDuration, 617 | ); 618 | yield* this.followCurve( 619 | curve, 620 | duration, 621 | timingFunction, 622 | interpolationFunction, 623 | ); 624 | } 625 | 626 | public *followCurve( 627 | curve: SignalValue, 628 | duration: number, 629 | timingFunction?: TimingFunction, 630 | interpolationFunction?: InterpolationFunction, 631 | ) { 632 | yield this.scrollOpacity(this.activeOpacity, 0.1); 633 | 634 | this.path(curve); 635 | this.scrollOffset(() => { 636 | const progress = this.pathProgress(); 637 | const p = this.path().getPointAtPercentage(progress).position; 638 | return p; 639 | }); 640 | yield* all( 641 | delay( 642 | duration + this.scrollHandleDelay(), 643 | this.scrollOpacity(this.inactiveOpacity, this.scrollHandleDuration()), 644 | ), 645 | this.pathProgress(1, duration, timingFunction, interpolationFunction), 646 | ); 647 | } 648 | } 649 | -------------------------------------------------------------------------------- /src/components/Table.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Layout, 3 | LayoutProps, 4 | computed, 5 | Node, 6 | RectProps, 7 | Rect, 8 | PossibleCanvasStyle, 9 | CanvasStyleSignal, 10 | canvasStyleSignal, 11 | initial, 12 | signal, 13 | } from '@motion-canvas/2d'; 14 | import { 15 | SignalValue, 16 | SimpleSignal, 17 | createEffect, 18 | useLogger, 19 | } from '@motion-canvas/core'; 20 | 21 | export interface TableProps extends LayoutProps { 22 | stroke?: SignalValue; 23 | lineWidth?: SignalValue; 24 | } 25 | 26 | export class Table extends Layout { 27 | @initial('white') 28 | @canvasStyleSignal() 29 | public declare readonly stroke: CanvasStyleSignal; 30 | 31 | @initial(1) 32 | @signal() 33 | public declare readonly lineWidth: SimpleSignal; 34 | 35 | public constructor(props: TableProps) { 36 | super({...props, layout: true, direction: 'column'}); 37 | 38 | createEffect(() => { 39 | this.rowChildren().forEach(row => { 40 | row.dataChildren().forEach((data, idx) => { 41 | data.width(this.columnSizes()[idx] ?? 0); 42 | data.stroke(this.stroke()); 43 | data.lineWidth(this.lineWidth()); 44 | }); 45 | }); 46 | }); 47 | } 48 | 49 | @computed() 50 | public rowChildren() { 51 | return this.children().filter( 52 | (child): child is TableRow => child instanceof TableRow, 53 | ); 54 | } 55 | 56 | @computed() 57 | public columnSizes() { 58 | return this.children() 59 | .filter((child): child is TableRow => child instanceof TableRow) 60 | .reduce((sizes: number[], row: TableRow) => { 61 | row 62 | .children() 63 | .filter((child): child is TableData => child instanceof TableData) 64 | .forEach((data, i) => { 65 | if (sizes[i] === undefined) { 66 | sizes[i] = 0; 67 | } 68 | sizes[i] = Math.max(data.size().x, sizes[i]); 69 | }); 70 | return sizes; 71 | }, []); 72 | } 73 | } 74 | 75 | export class TableRow extends Layout { 76 | public constructor(props: LayoutProps) { 77 | super({...props, layout: true, direction: 'row'}); 78 | 79 | this.children().forEach(child => { 80 | child.parent(this); 81 | }); 82 | 83 | if (this.children().some(child => !(child instanceof TableData))) { 84 | useLogger().warn( 85 | 'Table rows must only contain TableData; other nodes are undefined behavior', 86 | ); 87 | } 88 | } 89 | 90 | @computed() 91 | public dataChildren() { 92 | return this.children().filter( 93 | (child): child is TableData => child instanceof TableData, 94 | ); 95 | } 96 | } 97 | 98 | export interface TableDataProps extends RectProps { 99 | children?: Node[] | Node; 100 | } 101 | 102 | export class TableData extends Rect { 103 | public constructor(props: TableDataProps) { 104 | super({ 105 | padding: 8, 106 | lineWidth: 2, 107 | ...props, 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/components/Terminal.tsx: -------------------------------------------------------------------------------- 1 | import {Colors} from '@Colors'; 2 | import { 3 | Layout, 4 | Rect, 5 | Txt, 6 | TxtProps, 7 | computed, 8 | initial, 9 | signal, 10 | Node, 11 | LayoutProps, 12 | } from '@motion-canvas/2d'; 13 | import { 14 | SignalValue, 15 | SimpleSignal, 16 | TimingFunction, 17 | createSignal, 18 | linear, 19 | unwrap, 20 | } from '@motion-canvas/core'; 21 | 22 | export interface TerminalProps extends LayoutProps { 23 | prefix?: SignalValue; 24 | defaultTxtProps?: TxtProps; 25 | wpm?: SignalValue; 26 | } 27 | 28 | export class Terminal extends Layout { 29 | private internalCanvas: CanvasRenderingContext2D = document 30 | .createElement('canvas') 31 | .getContext('2d'); 32 | 33 | @initial('❯ ') 34 | @signal() 35 | public declare readonly prefix: SimpleSignal; 36 | 37 | @initial({ 38 | fill: Colors.Catppuccin.Mocha.Text, 39 | fontFamily: 'monospace', 40 | fontSize: 40, 41 | }) 42 | @signal() 43 | public declare readonly defaultTxtProps: SimpleSignal; 44 | 45 | @initial(100) 46 | @signal() 47 | public declare readonly wpm: SimpleSignal; 48 | 49 | private lines: SimpleSignal; 50 | private cachedLines: SimpleSignal; 51 | 52 | @computed() 53 | private getLines(): Node[] { 54 | return this.lines() 55 | .slice(this.cachedLines().length) 56 | .map(fragments => { 57 | return ( 58 | 59 | {fragments.length ? ( 60 | fragments.map(fragment => { 61 | const parentedDefaults = { 62 | ...this.defaultTxtProps(), 63 | ...fragment, 64 | }; 65 | this.internalCanvas.font = `${unwrap(parentedDefaults.fontWeight) || 400} ${unwrap(parentedDefaults.fontSize)}px ${unwrap(parentedDefaults.fontFamily)}`; 66 | const spc = this.internalCanvas.measureText(' '); 67 | return unwrap(fragment.text) 68 | .split(/( )/g) 69 | .filter(Boolean) 70 | .map(spaceOrText => { 71 | if (spaceOrText == ' ') { 72 | return ( 73 | 80 | ); 81 | } 82 | return ( 83 | 88 | ); 89 | }); 90 | }) 91 | ) : ( 92 | 93 | )} 94 | 95 | ); 96 | }); 97 | } 98 | 99 | public constructor(props: TerminalProps) { 100 | super({ 101 | ...props, 102 | }); 103 | this.cachedLines = createSignal([]); 104 | this.lines = createSignal([]); 105 | this.layout(true); 106 | this.direction('column'); 107 | this.children(() => [...this.cachedLines(), ...this.getLines()]); 108 | } 109 | 110 | public lineAppear(line: string | TxtProps | TxtProps[]) { 111 | this.cachedLines([...this.cachedLines(), ...this.getLines()]); 112 | this.lines([...this.lines(), !line ? [] : this.makeProps(line)]); 113 | } 114 | 115 | public *typeLine( 116 | line: string | TxtProps, 117 | duration: number, 118 | timingFunction?: TimingFunction, 119 | ) { 120 | this.cachedLines([...this.cachedLines(), ...this.getLines()]); 121 | const l = createSignal(''); 122 | const t = typeof line == 'string' ? line : line.text; 123 | const p = this.prefix(); 124 | const props: TxtProps[] = [ 125 | typeof p == 'string' ? {text: p} : p, 126 | typeof line == 'string' 127 | ? { 128 | text: l, 129 | } 130 | : { 131 | text: l, 132 | ...line, 133 | }, 134 | ]; 135 | const fixedProps = [ 136 | typeof p == 'string' ? {text: p} : p, 137 | { 138 | ...props[0], 139 | text: t, 140 | }, 141 | ]; 142 | this.lines([...this.lines(), props]); 143 | yield* l(l() + t, duration, timingFunction); 144 | this.lines([...this.lines().slice(0, -1), fixedProps]); 145 | } 146 | 147 | public *typeAfterLine( 148 | line: string | TxtProps, 149 | duration?: number, 150 | timingFunction: TimingFunction = linear, 151 | ) { 152 | this.cachedLines(this.cachedLines().slice(0, -1)); 153 | const t = typeof line == 'string' ? line : line.text; 154 | const calcDuration = duration ?? (t.length / (this.wpm() * 5)) * 60; 155 | const l = createSignal(''); 156 | const lastLine = this.lines()[this.lines().length - 1]; 157 | 158 | const props: TxtProps = 159 | typeof line == 'string' 160 | ? { 161 | text: l, 162 | } 163 | : { 164 | text: l, 165 | ...line, 166 | }; 167 | const fixedProps = { 168 | ...props, 169 | text: t, 170 | }; 171 | lastLine.push(props); 172 | this.lines([...this.lines().slice(0, -1), lastLine]); 173 | yield* l(t, calcDuration, timingFunction); 174 | lastLine.pop(); 175 | lastLine.push(fixedProps); 176 | this.lines([...this.lines().slice(0, -1), lastLine]); 177 | } 178 | 179 | public appearAfterLine(line: string | TxtProps) { 180 | this.cachedLines(this.cachedLines().slice(0, -1)); 181 | const lastLine = this.lines()[this.lines().length - 1]; 182 | 183 | lastLine.push(...this.makeProps(line)); 184 | this.lines([...this.lines().slice(0, -1), lastLine]); 185 | } 186 | 187 | public replaceLine(newLine: string | TxtProps | TxtProps[]) { 188 | this.cachedLines(this.cachedLines().slice(0, -1)); 189 | this.lines([...this.lines().slice(0, -1), this.makeProps(newLine)]); 190 | } 191 | 192 | public deleteLine() { 193 | this.cachedLines(this.cachedLines().slice(0, -1)); 194 | this.lines([...this.lines().slice(0, -1)]); 195 | } 196 | 197 | private makeProps(line: string | TxtProps | TxtProps[]) { 198 | return Array.isArray(line) 199 | ? line 200 | : [typeof line == 'string' ? {text: line} : line]; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/components/Window.tsx: -------------------------------------------------------------------------------- 1 | import {Colors} from '@Colors'; 2 | import {belowScreenPosition} from '@Util'; 3 | import { 4 | Rect, 5 | Circle, 6 | Txt, 7 | TxtProps, 8 | Icon, 9 | Gradient, 10 | initial, 11 | PossibleCanvasStyle, 12 | CanvasStyleSignal, 13 | canvasStyleSignal, 14 | nodeName, 15 | View2D, 16 | signal, 17 | withDefaults, 18 | colorSignal, 19 | IconProps, 20 | LayoutProps, 21 | DesiredLength, 22 | } from '@motion-canvas/2d'; 23 | import { 24 | Color, 25 | ColorSignal, 26 | PossibleColor, 27 | PossibleVector2, 28 | Reference, 29 | SerializedVector2, 30 | SignalValue, 31 | SimpleSignal, 32 | Vector2, 33 | } from '@motion-canvas/core'; 34 | import {Scrollable} from './Scrollable'; 35 | import {Windows98Button} from './WindowsButton'; 36 | 37 | export enum WindowStyle { 38 | MacOS, 39 | Windows98, 40 | } 41 | 42 | export interface WindowProps extends LayoutProps { 43 | title?: SignalValue; 44 | icon?: SignalValue; 45 | iconColor?: SignalValue; 46 | titleProps?: TxtProps; 47 | headerColor?: SignalValue; 48 | bodyColor?: SignalValue; 49 | windowStyle?: WindowStyle; 50 | scrollOffset?: SignalValue; 51 | buttonColors?: SignalValue< 52 | [PossibleCanvasStyle, PossibleCanvasStyle, PossibleCanvasStyle] 53 | >; 54 | buttonIconColors?: SignalValue< 55 | [PossibleCanvasStyle, PossibleCanvasStyle, PossibleCanvasStyle] 56 | >; 57 | buttonLightColor?: SignalValue; 58 | buttonDarkColor?: SignalValue; 59 | } 60 | 61 | /** 62 | * Like an Icon, but doesn't explode if the icon is null or empty. 63 | */ 64 | class ShortCircuitIcon extends Icon { 65 | public constructor(props: IconProps) { 66 | super(props); 67 | } 68 | 69 | protected desiredSize(): SerializedVector2 { 70 | if (this.icon()) { 71 | return super.desiredSize(); 72 | } 73 | return new Vector2(0, 0); 74 | } 75 | 76 | protected getSrc(): string { 77 | if (this.icon()) { 78 | return super.getSrc(); 79 | } 80 | return null; 81 | } 82 | } 83 | 84 | @nodeName('Window') 85 | export class Window extends Rect { 86 | @signal() 87 | public declare readonly title: SimpleSignal; 88 | 89 | @signal() 90 | public declare readonly icon: SimpleSignal; 91 | 92 | @initial(Colors.Tailwind.Slate['50']) 93 | @colorSignal() 94 | public declare readonly iconColor: ColorSignal; 95 | 96 | @signal() 97 | public declare readonly titleProps: SimpleSignal; 98 | 99 | @initial(Colors.Tailwind.Slate['700']) 100 | @canvasStyleSignal() 101 | public declare readonly headerColor: CanvasStyleSignal; 102 | 103 | @initial(Colors.Tailwind.Slate['800']) 104 | @canvasStyleSignal() 105 | public declare readonly bodyColor: CanvasStyleSignal; 106 | 107 | @signal() 108 | public declare readonly buttonColors: SimpleSignal< 109 | [PossibleCanvasStyle, PossibleCanvasStyle, PossibleCanvasStyle], 110 | this 111 | >; 112 | 113 | @initial(['black', 'black', 'black']) 114 | @signal() 115 | public declare readonly buttonIconColors: SimpleSignal< 116 | [PossibleColor, PossibleColor, PossibleColor], 117 | this 118 | >; 119 | 120 | public declare readonly windowStyle: WindowStyle; 121 | 122 | @initial('white') 123 | @canvasStyleSignal() 124 | public declare readonly buttonLightColor: CanvasStyleSignal; 125 | 126 | @initial(Colors.Tailwind.Slate['950']) 127 | @canvasStyleSignal() 128 | public declare readonly buttonDarkColor: CanvasStyleSignal; 129 | 130 | public readonly scrollable: Reference; 131 | 132 | public constructor(props: WindowProps) { 133 | super({ 134 | size: 400, 135 | stroke: 'white', 136 | ...props, 137 | }); 138 | this.windowStyle = props.windowStyle ?? WindowStyle.MacOS; 139 | if (!props.buttonColors) { 140 | this.buttonColors( 141 | this.windowStyle == WindowStyle.MacOS 142 | ? [ 143 | Colors.Tailwind.Red['500'], 144 | Colors.Tailwind.Yellow['500'], 145 | Colors.Tailwind.Green['500'], 146 | ] 147 | : [ 148 | Colors.Tailwind.Slate['400'], 149 | Colors.Tailwind.Slate['400'], 150 | Colors.Tailwind.Slate['400'], 151 | ], 152 | ); 153 | } 154 | if (!props.headerColor && this.windowStyle == WindowStyle.Windows98) { 155 | this.headerColor( 156 | () => 157 | new Gradient({ 158 | stops: [ 159 | {color: '#111179', offset: 0}, 160 | {color: '#0481CF', offset: 1}, 161 | ], 162 | type: 'linear', 163 | from: {x: 0, y: 0}, 164 | to: { 165 | x: this.size.x(), 166 | y: 0, 167 | }, 168 | }), 169 | ); 170 | } 171 | 172 | this.add( 173 | 187 | {props.children} 188 | 202 | 203 | 208 | 214 | 215 | {this.windowStyle == WindowStyle.MacOS ? ( 216 | 217 | this.buttonColors()[0]}> 218 | this.buttonColors()[1]}> 219 | this.buttonColors()[2]}> 220 | 221 | ) : null} 222 | {this.windowStyle == WindowStyle.Windows98 ? ( 223 | 224 | this.buttonColors()[0]} 229 | > 230 | this.buttonIconColors()[0]} 233 | icon={'material-symbols:minimize'} 234 | /> 235 | 236 | this.buttonColors()[1]} 242 | > 243 | this.buttonIconColors()[1]} 246 | icon={'material-symbols:chrome-maximize-outline-sharp'} 247 | /> 248 | 249 | this.buttonColors()[2]} 254 | > 255 | this.buttonIconColors()[2]} 258 | icon={'material-symbols:close'} 259 | /> 260 | 261 | 262 | ) : null} 263 | 264 | , 265 | ); 266 | } 267 | 268 | public *close(view: View2D, duration: number) { 269 | yield this.scale(0, duration); 270 | yield* this.position(belowScreenPosition(view, this), duration); 271 | } 272 | 273 | public *open(view: View2D, duration: number) { 274 | const oldPosition = this.position(); 275 | const oldScale = this.scale(); 276 | this.position(belowScreenPosition(view, this)); 277 | this.scale(0); 278 | yield this.scale(oldScale, duration); 279 | yield* this.position(oldPosition, duration); 280 | } 281 | } 282 | 283 | export const Windows98Window = withDefaults(Window, { 284 | windowStyle: WindowStyle.Windows98, 285 | }); 286 | 287 | export const MacOSWindow = withDefaults(Window, { 288 | windowStyle: WindowStyle.MacOS, 289 | }); 290 | -------------------------------------------------------------------------------- /src/components/WindowsButton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Rect, 3 | ComponentChildren, 4 | RectProps, 5 | Line, 6 | PossibleCanvasStyle, 7 | } from '@motion-canvas/2d'; 8 | import {Colors} from '../Colors'; 9 | import { 10 | SignalValue, 11 | createComputed, 12 | createRef, 13 | createSignal, 14 | unwrap, 15 | } from '@motion-canvas/core'; 16 | 17 | export const Windows98Button = ({ 18 | lightColor = Colors.Tailwind.Slate['950'], 19 | darkColor = Colors.Tailwind.Slate['400'], 20 | ...props 21 | }: RectProps & { 22 | children?: SignalValue; 23 | borderSize?: SignalValue; 24 | lightColor?: SignalValue; 25 | darkColor?: SignalValue; 26 | }) => { 27 | const borderSize = createComputed(() => unwrap(props.borderSize) ?? 4); 28 | const content = createRef(); 29 | const container = createSignal(); 30 | const nonChildProps = {...props}; 31 | delete nonChildProps.children; 32 | return ( 33 | 44 | content()?.size().add(borderSize()) ?? 0} 48 | x={() => borderSize() / 2} 49 | y={() => borderSize() / 2} 50 | /> 51 | { 54 | const tr = content()?.topRight(); 55 | return tr 56 | ? [tr, tr.addX(borderSize()), tr.add([borderSize(), -borderSize()])] 57 | : []; 58 | }} 59 | fill={darkColor} 60 | > 61 | { 64 | const bl = content()?.bottomLeft(); 65 | return bl 66 | ? [bl, bl.add([-borderSize(), borderSize()]), bl.addY(borderSize())] 67 | : []; 68 | }} 69 | fill={darkColor} 70 | > 71 | 80 | {props.children} 81 | 82 | 83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Body'; 2 | export * from './CodeLineNumbers'; 3 | export * from './ErrorBox'; 4 | export * from './FileTree'; 5 | export * from './Glow'; 6 | export * from './ImgWindow'; 7 | export * from './Layout'; 8 | export * from './LinePlot'; 9 | export * from './MotionCanvasLogo'; 10 | export * from './Plot'; 11 | export * from './ScatterPlot'; 12 | export * from './Scrollable'; 13 | export * from './Table'; 14 | export * from './Terminal'; 15 | export * from './Window'; 16 | export * from './WindowsButton'; 17 | -------------------------------------------------------------------------------- /src/highlightstyle/Catppuccin.ts: -------------------------------------------------------------------------------- 1 | import {Colors} from '@Colors'; 2 | import {HighlightStyle} from '@codemirror/language'; 3 | import {tags as t} from '@lezer/highlight'; 4 | 5 | export const CatppuccinMochaHighlightStyle: HighlightStyle = 6 | HighlightStyle.define([ 7 | {tag: t.keyword, color: Colors.Catppuccin.Mocha.Mauve}, 8 | {tag: t.operator, color: Colors.Catppuccin.Mocha.Sky}, 9 | {tag: t.special(t.variableName), color: Colors.Catppuccin.Mocha.Red}, 10 | {tag: t.typeName, color: Colors.Catppuccin.Mocha.Yellow}, 11 | {tag: t.atom, color: Colors.Catppuccin.Mocha.Red}, 12 | {tag: t.number, color: Colors.Catppuccin.Mocha.Peach}, 13 | {tag: t.definition(t.variableName), color: Colors.Catppuccin.Mocha.Text}, 14 | {tag: t.string, color: Colors.Catppuccin.Mocha.Green}, 15 | {tag: t.special(t.string), color: Colors.Catppuccin.Mocha.Green}, 16 | {tag: t.comment, color: Colors.Catppuccin.Mocha.Overlay2}, 17 | {tag: t.variableName, color: Colors.Catppuccin.Mocha.Text}, 18 | {tag: t.tagName, color: Colors.Catppuccin.Mocha.Red}, 19 | {tag: t.bracket, color: Colors.Catppuccin.Mocha.Overlay2}, 20 | {tag: t.meta, color: Colors.Catppuccin.Mocha.Overlay2}, 21 | {tag: t.punctuation, color: Colors.Catppuccin.Mocha.Overlay2}, 22 | {tag: t.attributeName, color: Colors.Catppuccin.Mocha.Red}, 23 | {tag: t.propertyName, color: Colors.Catppuccin.Mocha.Blue}, 24 | {tag: t.className, color: Colors.Catppuccin.Mocha.Yellow}, 25 | {tag: t.invalid, color: Colors.Catppuccin.Mocha.Red}, 26 | { 27 | tag: t.function(t.variableName), 28 | color: Colors.Catppuccin.Mocha.Blue, 29 | }, 30 | { 31 | tag: t.function(t.propertyName), 32 | color: Colors.Catppuccin.Mocha.Blue, 33 | }, 34 | { 35 | tag: t.definition(t.function(t.variableName)), 36 | color: Colors.Catppuccin.Mocha.Blue, 37 | }, 38 | ]); 39 | -------------------------------------------------------------------------------- /src/highlightstyle/MaterialPaleNight.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Credits for color palette: 3 | 4 | Author: Mattia Astorino (http://github.com/equinusocio) 5 | Website: https://material-theme.site/ 6 | */ 7 | 8 | import {HighlightStyle} from '@codemirror/language'; 9 | import {tags as t} from '@lezer/highlight'; 10 | 11 | const stone = '#7d8799', // Brightened compared to original to increase contrast 12 | invalid = '#ffffff'; 13 | 14 | /// The highlighting style for code in the Material Palenight theme. 15 | export const MaterialPalenightHighlightStyle = HighlightStyle.define([ 16 | {tag: t.keyword, color: '#c792ea'}, 17 | {tag: t.operator, color: '#89ddff'}, 18 | {tag: t.special(t.variableName), color: '#eeffff'}, 19 | {tag: t.typeName, color: '#f07178'}, 20 | {tag: t.atom, color: '#f78c6c'}, 21 | {tag: t.number, color: '#ff5370'}, 22 | {tag: t.definition(t.variableName), color: '#82aaff'}, 23 | {tag: t.string, color: '#c3e88d'}, 24 | {tag: t.special(t.string), color: '#f07178'}, 25 | {tag: t.comment, color: stone}, 26 | {tag: t.variableName, color: '#f07178'}, 27 | {tag: t.tagName, color: '#ff5370'}, 28 | {tag: t.bracket, color: '#a2a1a4'}, 29 | {tag: t.meta, color: '#ffcb6b'}, 30 | {tag: t.attributeName, color: '#c792ea'}, 31 | {tag: t.propertyName, color: '#c792ea'}, 32 | {tag: t.className, color: '#decb6b'}, 33 | {tag: t.invalid, color: invalid}, 34 | ]); 35 | -------------------------------------------------------------------------------- /src/highlightstyle/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MaterialPaleNight'; 2 | export * from './Catppuccin'; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Colors'; 2 | export * from './Slide'; 3 | export * from './Util'; 4 | export * from './highlightstyle'; 5 | export * from './components'; 6 | -------------------------------------------------------------------------------- /test/motion-canvas.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /test/project.meta: -------------------------------------------------------------------------------- 1 | { 2 | "version": 0, 3 | "shared": { 4 | "background": "rgb(0,0,0)", 5 | "range": [ 6 | 0, 7 | null 8 | ], 9 | "size": { 10 | "x": 1920, 11 | "y": 1080 12 | }, 13 | "audioOffset": 0 14 | }, 15 | "preview": { 16 | "fps": 30, 17 | "resolutionScale": 1 18 | }, 19 | "rendering": { 20 | "fps": 60, 21 | "resolutionScale": 1, 22 | "colorSpace": "srgb", 23 | "exporter": { 24 | "name": "@motion-canvas/ffmpeg", 25 | "options": { 26 | "fastStart": true, 27 | "includeAudio": true 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /test/project.ts: -------------------------------------------------------------------------------- 1 | import {makeProject} from '@motion-canvas/core'; 2 | 3 | import example from './scenes/example?scene'; 4 | 5 | export default makeProject({ 6 | scenes: [example], 7 | }); 8 | -------------------------------------------------------------------------------- /test/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhenrichsen/canvas-commons/0412d52c5aab166846e553dd53da0b1e4fd54303/test/public/.gitkeep -------------------------------------------------------------------------------- /test/scenes/example.meta: -------------------------------------------------------------------------------- 1 | { 2 | "version": 0, 3 | "timeEvents": [ 4 | { 5 | "name": "spline follow", 6 | "targetTime": 0 7 | }, 8 | { 9 | "name": "zoomed out", 10 | "targetTime": 6 11 | }, 12 | { 13 | "name": "zoomed in", 14 | "targetTime": 19 15 | } 16 | ], 17 | "seed": 2637074891 18 | } -------------------------------------------------------------------------------- /test/scenes/example.tsx: -------------------------------------------------------------------------------- 1 | import {makeScene2D} from '@motion-canvas/2d/lib/scenes'; 2 | import {waitFor, waitUntil} from '@motion-canvas/core/lib/flow'; 3 | import { 4 | Circle, 5 | Code, 6 | Gradient, 7 | Knot, 8 | Layout, 9 | LezerHighlighter, 10 | Rect, 11 | Spline, 12 | Txt, 13 | } from '@motion-canvas/2d'; 14 | import {createRef, linear, range, useRandom} from '@motion-canvas/core'; 15 | import {Scrollable} from '@components/Scrollable'; 16 | import {WindowStyle, Window} from '@components/Window'; 17 | import {Colors} from '@Colors'; 18 | import {DistortedCurve} from '@components/DistortedCurve'; 19 | import {drawIn} from '@Util'; 20 | import {parser as javascript} from '@lezer/javascript'; 21 | import {CatppuccinMochaHighlightStyle} from '@highlightstyle/Catppuccin'; 22 | import {CodeLineNumbers} from '@components/CodeLineNumbers'; 23 | import {Terminal} from '@components/Terminal'; 24 | import {Table, TableData, TableRow} from '@components/Table'; 25 | import {Plot, LinePlot, ScatterPlot} from '@index'; 26 | 27 | export default makeScene2D(function* (view) { 28 | const code = createRef(); 29 | const codeContainer = createRef(); 30 | view.add( 31 | 32 | 38 | { 52 | if (count < 10) { 53 | count++; 54 | render(); 55 | } 56 | }); 57 | 58 | class Cat { 59 | constructor(name) { 60 | this.name = name ?? 'Mochi'; 61 | } 62 | 63 | meow() { 64 | console.log(\`Meow! I'm \${this.name}\`); 65 | } 66 | }`} 67 | fontSize={30} 68 | /> 69 | , 70 | ); 71 | yield* codeContainer().opacity(1, 1); 72 | yield* waitFor(1); 73 | yield* code().code.append('\n// This is a comment', 1); 74 | yield* waitFor(1); 75 | yield* codeContainer().opacity(0, 1); 76 | code().remove(); 77 | 78 | const draw = createRef(); 79 | view.add( 80 | , 87 | ); 88 | 89 | yield* drawIn(draw, 'white', 'white', 1, true); 90 | 91 | yield* waitFor(1); 92 | yield* draw().opacity(0, 1); 93 | yield* waitFor(1); 94 | 95 | const scrollable = createRef(); 96 | const r = createRef(); 97 | const spl = createRef(); 98 | const win = createRef(); 99 | view.add( 100 | 101 | 102 | 103 | 142 | 148 | 154 | 160 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | , 175 | ); 176 | yield* win().open(view, 1); 177 | 178 | yield* waitUntil('spline follow'); 179 | yield* scrollable().scrollTo(spl().getPointAtPercentage(0).position, 1); 180 | yield* scrollable().followCurve(spl(), 5); 181 | 182 | yield* waitUntil('zoomed out'); 183 | yield* waitFor(1); 184 | yield* scrollable().zoom(0.5, 1); 185 | yield* waitFor(1); 186 | yield* scrollable().scrollToRightCenter(1); 187 | yield* waitFor(1); 188 | yield* scrollable().scrollToBottomCenter(1); 189 | yield* waitFor(1); 190 | yield* scrollable().scrollToLeftCenter(1); 191 | yield* waitFor(1); 192 | yield* scrollable().scrollToTopCenter(1); 193 | yield* waitFor(1); 194 | yield* scrollable().scrollToCenter(1); 195 | yield* waitFor(1); 196 | 197 | yield* waitUntil('zoomed in'); 198 | 199 | yield* scrollable().zoom(2, 1); 200 | yield* waitFor(1); 201 | yield* scrollable().scrollToRightCenter(1); 202 | yield* waitFor(1); 203 | yield* scrollable().scrollToBottomCenter(1); 204 | yield* waitFor(1); 205 | yield* scrollable().scrollToLeftCenter(1); 206 | yield* waitFor(1); 207 | yield* scrollable().scrollToTopCenter(1); 208 | yield* waitFor(1); 209 | yield* scrollable().scrollToCenter(1); 210 | yield* waitFor(1); 211 | 212 | yield* win().close(view, 1); 213 | 214 | const terminal = createRef(); 215 | const terminalWindow = createRef(); 216 | yield view.add( 217 | 230 | 235 | , 236 | , 237 | ); 238 | scrollable().fill(Colors.Catppuccin.Mocha.Mantle); 239 | yield* terminalWindow().open(view, 1); 240 | 241 | yield* terminal().typeLine('npm init @motion-canvas@latest', 2); 242 | yield* waitFor(1); 243 | terminal().lineAppear(''); 244 | terminal().lineAppear('Need to install the following packages:'); 245 | terminal().lineAppear(' @motion-canvas/create'); 246 | terminal().lineAppear('Ok to proceed? (y)'); 247 | yield* waitFor(1); 248 | yield* terminal().typeAfterLine(' y', 1); 249 | terminal().lineAppear([ 250 | {text: '? Project name '}, 251 | {text: '»', fill: Colors.Catppuccin.Mocha.Surface2}, 252 | ]); 253 | yield* waitFor(1); 254 | yield* terminal().typeAfterLine(' my-animation'); 255 | yield* waitFor(1); 256 | terminal().replaceLine([ 257 | {text: '√', fill: Colors.Catppuccin.Mocha.Green}, 258 | {text: ' Project name '}, 259 | {text: '...', fill: Colors.Catppuccin.Mocha.Surface2}, 260 | {text: ' my-animation'}, 261 | ]); 262 | terminal().lineAppear([ 263 | {text: '? Project path '}, 264 | {text: '»', fill: Colors.Catppuccin.Mocha.Surface2}, 265 | ]); 266 | yield* terminal().typeAfterLine(' my-animation'); 267 | yield* waitFor(1); 268 | terminal().replaceLine([ 269 | {text: '√', fill: Colors.Catppuccin.Mocha.Green}, 270 | {text: ' Project path '}, 271 | {text: '...', fill: Colors.Catppuccin.Mocha.Surface2}, 272 | {text: ' my-animation'}, 273 | ]); 274 | terminal().lineAppear('? Language'); 275 | terminal().appearAfterLine({ 276 | text: ' » - Use arrow-keys. Return to submit.', 277 | fill: Colors.Catppuccin.Mocha.Surface2, 278 | }); 279 | terminal().lineAppear({ 280 | text: '> TypeScript (Recommended)', 281 | fill: Colors.Catppuccin.Mocha.Sky, 282 | }); 283 | terminal().lineAppear(' JavaScript'); 284 | yield* waitFor(3); 285 | 286 | terminal().deleteLine(); 287 | terminal().deleteLine(); 288 | terminal().replaceLine([ 289 | {text: '√', fill: Colors.Catppuccin.Mocha.Green}, 290 | {text: ' Language '}, 291 | {text: '...', fill: Colors.Catppuccin.Mocha.Surface2}, 292 | {text: 'TypeScript (Recommended)'}, 293 | ]); 294 | terminal().lineAppear(''); 295 | 296 | terminal().lineAppear({ 297 | text: '√ Scaffolding complete. You can now run:', 298 | fill: Colors.Catppuccin.Mocha.Green, 299 | }); 300 | terminal().lineAppear({ 301 | text: ' cd my-animation', 302 | }); 303 | terminal().lineAppear({ 304 | text: ' npm install', 305 | }); 306 | terminal().lineAppear({ 307 | text: ' npm start', 308 | }); 309 | 310 | yield* waitFor(2); 311 | yield* terminalWindow().close(view, 1); 312 | 313 | const table = createRef(); 314 | view.add( 315 |
316 | 317 | 318 | 319 | 1 320 | 321 | 322 | 323 | 324 | 2 325 | 326 | 327 | 328 | 329 | 3 330 | 331 | 332 | 333 | 334 | 335 | 336 | asdfghjkl; 337 | 338 | 339 | 340 | 341 | 2 342 | 343 | 344 | 345 | 346 | 347 | 348 |
, 349 | ); 350 | 351 | yield* table().opacity(1, 1); 352 | yield* waitFor(1); 353 | yield* table().opacity(0, 1); 354 | 355 | const random = useRandom(); 356 | 357 | const plot = createRef(); 358 | view.add( 359 | 367 | [i * 4, random.nextInt(0, 100)])} 371 | /> 372 | , 373 | , 374 | ); 375 | 376 | yield* plot().opacity(1, 2); 377 | yield* waitFor(2); 378 | 379 | yield* plot().ticks(20, 3); 380 | yield* plot().tickLabelSize(20, 2); 381 | yield* plot().size(800, 2); 382 | yield* plot().labelSize(30, 2); 383 | yield* plot().min(-100, 2); 384 | yield* plot().opacity(0, 2); 385 | plot().remove(); 386 | 387 | const plot2 = createRef(); 388 | const line2 = createRef(); 389 | view.add( 390 | `${Math.round(x / Math.PI)}π`} 398 | ticks={[4, 4]} 399 | opacity={0} 400 | > 401 | 402 | , 403 | ); 404 | 405 | line2().data(plot2().makeGraphData(0.1, x => Math.sin(x))); 406 | 407 | yield* plot2().opacity(1, 2); 408 | yield* waitFor(2); 409 | yield* line2().end(1, 1); 410 | yield* waitFor(3); 411 | 412 | yield* plot2().opacity(0, 2); 413 | 414 | const plot3 = createRef(); 415 | const scatter3 = createRef(); 416 | view.add( 417 | 425 | [i * 4, random.nextInt(0, 100)])} 432 | /> 433 | , 434 | ); 435 | 436 | yield* plot3().opacity(1, 2); 437 | yield* waitFor(2); 438 | yield scatter3().end(1, 3, linear); 439 | yield* waitFor(0.1); 440 | yield* scatter3().start(0, 3, linear); 441 | yield* waitFor(2); 442 | yield* plot3().opacity(0, 2); 443 | 444 | const plot4 = createRef(); 445 | const line4 = createRef(); 446 | view.add( 447 | 460 | 461 | , 462 | ); 463 | 464 | line4().data(plot4().makeGraphData(0.1, x => Math.pow(x, 2))); 465 | yield* plot4().opacity(1, 2); 466 | yield* waitFor(2); 467 | yield* line4().end(1, 1); 468 | 469 | yield* waitFor(5); 470 | }); 471 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@motion-canvas/2d/tsconfig.project.json", 3 | "compilerOptions": { 4 | "experimentalDecorators": true, 5 | "noImplicitAny": true, 6 | "module": "esnext", 7 | "target": "esnext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "noEmit": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": false, 14 | "useDefineForClassFields": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "allowSyntheticDefaultImports": true, 17 | "jsx": "react-jsx", 18 | "jsxImportSource": "@motion-canvas/2d/lib", 19 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 20 | "paths": { 21 | "@components/*": ["./src/components/*"], 22 | "@*": ["./src/*"] 23 | }, 24 | "outDir": "lib/tsc/" 25 | }, 26 | "include": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts", "test/**/*.tsx"] 27 | } 28 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite'; 2 | import motionCanvas from '@motion-canvas/vite-plugin'; 3 | import tsConfigPaths from 'vite-tsconfig-paths'; 4 | import ffmpeg from '@motion-canvas/ffmpeg'; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | tsConfigPaths(), 9 | motionCanvas({ 10 | project: './test/project.ts', 11 | }), 12 | ffmpeg(), 13 | ], 14 | }); 15 | --------------------------------------------------------------------------------