├── .devcontainer └── devcontainer.json ├── .eslintrc.json ├── .github └── workflows │ └── deploy-pages.yaml ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── config-overrides.js ├── dev-guide.md ├── package.json ├── pnpm-lock.yaml ├── public ├── CCapture.all.min.js ├── images │ ├── butterfly-text.png │ └── cat.jpg └── index.html ├── src ├── animations │ ├── _ballspin.tsx │ ├── anaglyph.tsx │ ├── dither.tsx │ ├── hypocycloids.tsx │ ├── minimal2D.tsx │ ├── minimalShader.tsx │ ├── noiseDodecahedron.tsx │ ├── piArcs.tsx │ └── shaders │ │ ├── anaglyph.glsl │ │ ├── dither.glsl │ │ ├── minimalShader.glsl │ │ └── noiseDodecahedron.glsl ├── global.d.ts ├── index.css ├── index.tsx ├── lib │ ├── Animation.tsx │ ├── graphics.ts │ └── utils.ts └── react-app-env.d.ts ├── tailwind.config.js └── tsconfig.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye", 7 | // Features to add to the dev container. More info: https://containers.dev/features. 8 | "features": { 9 | "ghcr.io/devcontainers-contrib/features/pnpm:2": {}, 10 | "ghcr.io/devcontainers-contrib/features/tmux-apt-get": {} 11 | }, 12 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 13 | "forwardPorts": [ 14 | 3000 15 | ], 16 | // Use 'postCreateCommand' to run commands after the container is created. 17 | "postCreateCommand": "pnpm install", 18 | "postAttachCommand": { 19 | "server": "pnpm start" 20 | }, 21 | // Configure tool-specific properties. 22 | "customizations": { 23 | "vscode": { 24 | "extensions": [ 25 | "esbenp.prettier-vscode", 26 | "bradlc.vscode-tailwindcss", 27 | "MarkosTh09.color-picker", 28 | "raczzalan.webgl-glsl-editor" 29 | ] 30 | } 31 | }, 32 | "remoteEnv": { 33 | "WDS_SOCKET_PORT": "0" 34 | } 35 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 36 | // "remoteUser": "root" 37 | } 38 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "react-hooks/exhaustive-deps": "off" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pages.yaml: -------------------------------------------------------------------------------- 1 | name: deploy-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | # Only run one at a time. 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | # Build job 15 | build: 16 | # actions/upload-pages-artifact 17 | runs-on: ubuntu-latest 18 | steps: 19 | # checkout the repository content to github runner 20 | 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | 24 | # setup nodejs environment 25 | - name: Setup Node.js environment 26 | uses: actions/setup-node@v2.1.5 27 | with: 28 | node-version: '18.16.1' 29 | 30 | # cache the dependencies to speed up the build 31 | - name: Cache dependencies 32 | uses: actions/cache@v2 33 | with: 34 | path: ~/.npm 35 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 36 | restore-keys: | 37 | ${{ runner.os }}-node- 38 | 39 | # install dependencies 40 | - name: Install dependencies 41 | run: npm i 42 | 43 | - name: Build 44 | run: npm run build 45 | 46 | - name: Upload Pages artifact 47 | uses: actions/upload-pages-artifact@v2 48 | with: 49 | path: build 50 | 51 | # Deploy job 52 | deploy: 53 | # Add a dependency to the build job 54 | needs: build 55 | 56 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 57 | permissions: 58 | pages: write # to deploy to Pages 59 | id-token: write # to verify the deployment originates from an appropriate source 60 | 61 | # Deploy to the github-pages environment 62 | environment: 63 | name: github-pages 64 | url: ${{ steps.deployment.outputs.page_url }} 65 | 66 | # Specify runner + deployment step 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: Deploy to GitHub Pages 70 | id: deployment 71 | uses: actions/deploy-pages@v2 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "trailingComma": "all", 5 | "singleQuote": true, 6 | "semi": true, 7 | "printWidth": 120, 8 | "plugins": [ 9 | "@trivago/prettier-plugin-sort-imports", 10 | "prettier-plugin-tailwindcss" 11 | ], 12 | "importOrder": [ 13 | "^@", 14 | "^lib", 15 | "^[./]" 16 | ], 17 | "importOrderSeparation": true, 18 | "importOrderSortSpecifiers": true 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "esbenp.prettier-vscode", 7 | "bradlc.vscode-tailwindcss", 8 | "MarkosTh09.color-picker", 9 | "raczzalan.webgl-glsl-editor" 10 | ], 11 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 12 | "unwantedRecommendations": [] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "command": "pnpm run start", 5 | "name": "launch", 6 | "request": "launch", 7 | "type": "node-terminal", 8 | "cwd": "${workspaceRoot}" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vscode-color-picker.languages": [ 3 | "python", 4 | "javascript", 5 | "typescript", 6 | "typescriptreact", 7 | ], 8 | "webgl-glsl-editor.format.placeSpaceAfterKeywords": true, 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Matthew Henderson 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 | # matthen/animations 2 | 3 | [![deploy-pages](https://github.com/matthen/animations/actions/workflows/deploy-pages.yaml/badge.svg?branch=main)](https://github.com/matthen/animations/actions/workflows/deploy-pages.yaml) 4 | 5 | view live animations at: [matthen.github.io/animations](https://matthen.github.io/animations/). 6 | 7 | This is a collection of mathematical animations, written as small React components. 8 | 9 | - [animations](src/animations) - code for each animation 10 | - [lib/Animation.tsx](src/lib/Animation.tsx) - react component for rendering and exporting animations 11 | - [lib/graphics.ts](src/lib/graphics.ts) - library to simplify drawing to 2d canvas 12 | 13 | ## Creating an animation 14 | 15 | Todo 16 | 17 | ## Using `Graphics` 18 | 19 | Todo 20 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | // Rule to import glsl contents as strings 2 | module.exports = function override(config, env) { 3 | const newRule = { 4 | test: /\.glsl$/i, 5 | loader: 'raw-loader', 6 | options: { 7 | esModule: false, 8 | }, 9 | }; 10 | config.module.rules.find((r) => r.oneOf).oneOf.unshift(newRule); 11 | return config; 12 | }; 13 | -------------------------------------------------------------------------------- /dev-guide.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "animations", 3 | "homepage": "/animations", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.17.0", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.18.59", 12 | "@types/react": "^18.2.30", 13 | "@types/react-dom": "^18.2.14", 14 | "chroma-js": "^2.4.2", 15 | "glsl-pipeline": "^1.0.6", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-icons": "^4.11.0", 19 | "react-router-dom": "^6.17.0", 20 | "react-scripts": "5.0.1", 21 | "three": "^0.158.0", 22 | "typescript": "^4.9.5", 23 | "use-debounce": "^9.0.4", 24 | "web-vitals": "^2.1.4" 25 | }, 26 | "scripts": { 27 | "start": "react-app-rewired start", 28 | "build": "react-app-rewired build" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@trivago/prettier-plugin-sort-imports": "^4.2.0", 50 | "@types/chroma-js": "^2.4.2", 51 | "@types/matter-js": "^0.19.2", 52 | "@types/three": "^0.158.0", 53 | "autoprefixer": "^10.4.16", 54 | "postcss": "^8.4.31", 55 | "prettier-plugin-tailwindcss": "^0.5.6", 56 | "raw-loader": "^4.0.2", 57 | "react-app-rewired": "^2.2.1", 58 | "tailwind-dracula": "^1.1.0", 59 | "tailwindcss": "^3.3.3" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /public/CCapture.all.min.js: -------------------------------------------------------------------------------- 1 | "use strict";function download(t,e,i){function n(t){var e=t.split(/[:;,]/),i=e[1],n="base64"==e[2]?atob:decodeURIComponent,r=n(e.pop()),o=r.length,a=0,s=new Uint8Array(o);for(a;a>8,this.data[this.pos++]=t},t.prototype.writeDoubleBE=function(t){for(var e=new Uint8Array(new Float64Array([t]).buffer),i=e.length-1;i>=0;i--)this.writeByte(e[i])},t.prototype.writeFloatBE=function(t){for(var e=new Uint8Array(new Float32Array([t]).buffer),i=e.length-1;i>=0;i--)this.writeByte(e[i])},t.prototype.writeString=function(t){for(var e=0;e>8),this.writeU8(t);break;case 3:this.writeU8(32|t>>16),this.writeU8(t>>8),this.writeU8(t);break;case 4:this.writeU8(16|t>>24),this.writeU8(t>>16),this.writeU8(t>>8),this.writeU8(t);break;case 5:this.writeU8(8|t/4294967296&7),this.writeU8(t>>24),this.writeU8(t>>16),this.writeU8(t>>8),this.writeU8(t);break;default:throw new RuntimeException("Bad EBML VINT size "+e)}},t.prototype.measureEBMLVarInt=function(t){if(t<127)return 1;if(t<16383)return 2;if(t<2097151)return 3;if(t<268435455)return 4;if(t<34359738367)return 5;throw new RuntimeException("EBML VINT size not supported "+t)},t.prototype.writeEBMLVarInt=function(t){this.writeEBMLVarIntWidth(t,this.measureEBMLVarInt(t))},t.prototype.writeUnsignedIntBE=function(t,e){switch(void 0===e&&(e=this.measureUnsignedInt(t)),e){case 5:this.writeU8(Math.floor(t/4294967296));case 4:this.writeU8(t>>24);case 3:this.writeU8(t>>16);case 2:this.writeU8(t>>8);case 1:this.writeU8(t);break;default:throw new RuntimeException("Bad UINT size "+e)}},t.prototype.measureUnsignedInt=function(t){return t<256?1:t<65536?2:t<1<<24?3:t<4294967296?4:5},t.prototype.getAsDataArray=function(){if(this.posthis.length)throw"Seeking beyond the end of file is not allowed";this.pos=t},this.write=function(e){var i={offset:this.pos,data:e,length:r(e)},f=i.offset>=this.length;this.pos+=i.length,this.length=Math.max(this.length,this.pos),a=a.then(function(){if(h)return new Promise(function(e,r){n(i.data).then(function(n){var r=0,o=Buffer.from(n.buffer),a=function(n,o,s){r+=o,r>=s.length?e():t.write(h,s,r,s.length-r,i.offset+r,a)};t.write(h,o,0,o.length,i.offset,a)})});if(s)return new Promise(function(t,e){s.onwriteend=t,s.seek(i.offset),s.write(new Blob([i.data]))});if(!f)for(var e=0;e=r.offset+r.length)){if(i.offsetr.offset+r.length)throw new Error("Overwrite crosses blob boundaries");return i.offset==r.offset&&i.length==r.length?void(r.data=i.data):n(r.data).then(function(t){return r.data=t,n(i.data)}).then(function(t){i.data=t,r.data.set(i.data,i.offset-r.offset)})}}o.push(i)})},this.complete=function(t){return a=h||s?a.then(function(){return null}):a.then(function(){for(var e=[],i=0;i0&&e.trackNumber<127))throw"TrackNumber must be > 0 and < 127";return i.writeEBMLVarInt(e.trackNumber),i.writeU16BE(e.timecode),i.writeByte(128),{id:163,data:[i.getAsDataArray(),e.frame]}}function l(t){return{id:524531317,data:[{id:231,data:Math.round(t.timecode)}]}}function c(t,e,i){_.push({id:187,data:[{id:179,data:e},{id:183,data:[{id:247,data:t},{id:241,data:a(i)}]}]})}function p(){var e={id:475249515,data:_},i=new t(16+32*_.length);h(i,S.pos,e),S.write(i.getAsDataArray()),D.Cues.positionEBML.data=a(e.offset)}function m(){if(0!=T.length){for(var e=0,i=0;i=E&&m()}function y(){var e=new t(x.size),i=S.pos;h(e,x.dataOffset,x.data),S.seek(x.dataOffset),S.write(e.getAsDataArray()),S.seek(i)}function v(){var e=new t(8),i=S.pos;e.writeDoubleBE(U),S.seek(M.dataOffset),S.write(e.getAsDataArray()),S.seek(i)}var b,k,B,x,E=5e3,A=1,L=!1,T=[],U=0,F=0,I={quality:.95,fileWriter:null,fd:null,frameDuration:null,frameRate:null},D={Cues:{id:new Uint8Array([28,83,187,107]),positionEBML:null},SegmentInfo:{id:new Uint8Array([21,73,169,102]),positionEBML:null},Tracks:{id:new Uint8Array([22,84,174,107]),positionEBML:null}},M={id:17545,data:new s(0)},_=[],S=new e(n.fileWriter||n.fd);this.addFrame=function(t){if(L){if(t.width!=b||t.height!=k)throw"Frame size differs from previous frames"}else b=t.width,k=t.height,u(),L=!0;var e=r(t,{quality:n.quality});if(!e)throw"Couldn't decode WebP frame, does the browser support WebP?";g({frame:o(e),duration:n.frameDuration})},this.complete=function(){return m(),p(),y(),v(),S.complete("video/webm")},this.getWrittenSize=function(){return S.length},n=i(I,n||{}),w()}};"undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports=t(require("./ArrayBufferDataStream"),require("./BlobBuffer")):window.WebMWriter=t(ArrayBufferDataStream,BlobBuffer)}(),function(){function t(t){var e,i=new Uint8Array(t);for(e=0;e>18&63]+o[t>>12&63]+o[t>>6&63]+o[63&t]}var i,n,r,a=t.length%3,s="";for(i=0,r=t.length-a;in&&(e.push({blocks:o,length:i}),o=[],i=0),o.push(t),i+=t.headerLength+t.inputLength}),e.push({blocks:o,length:i}),e.forEach(function(e){var i=new Uint8Array(e.length),n=0;e.blocks.forEach(function(t){i.set(t.header,n),n+=t.headerLength,i.set(t.input,n),n+=t.inputLength}),t.push(i)}),t.push(new Uint8Array(2*r)),new Blob(t,{type:"octet/stream"})},t.prototype.clear=function(){this.written=0,this.out=n.clean(e)},window.Tar=t}(),function(t){function e(t,i){if({}.hasOwnProperty.call(e.cache,t))return e.cache[t];var n=e.resolve(t);if(!n)throw new Error("Failed to resolve module "+t);var r={id:t,require:e,filename:t,exports:{},loaded:!1,parent:i,children:[]};i&&i.children.push(r);var o=t.slice(0,t.lastIndexOf("/")+1);return e.cache[t]=r.exports,n.call(r.exports,r,r.exports,o,t),r.loaded=!0,e.cache[t]=r.exports}e.modules={},e.cache={},e.resolve=function(t){return{}.hasOwnProperty.call(e.modules,t)?e.modules[t]:void 0},e.define=function(t,i){e.modules[t]=i};var i=function(e){return e="/",{title:"browser",version:"v0.10.26",browser:!0,env:{},argv:[],nextTick:t.setImmediate||function(t){setTimeout(t,0)},cwd:function(){return e},chdir:function(t){e=t}}}();e.define("/gif.coffee",function(t,i,n,r){function o(t,e){return{}.hasOwnProperty.call(t,e)}function a(t,e){for(var i=0,n=e.length;ithis.frames.length;0<=this.frames.length?++e:--e)t.push(e);return t}.apply(this,arguments),n=0,r=i.length;ne;0<=e?++i:--i)t.push(i);return t}.apply(this,arguments),n=0,r=i.length;nt;this.freeWorkers.length<=t?++i:--i)e.push(i);return e}.apply(this,arguments).forEach(function(t){return function(e){var i;return console.log("spawning worker "+e),i=new Worker(t.options.workerScript),i.onmessage=function(t){return function(e){return t.activeWorkers.splice(t.activeWorkers.indexOf(i),1),t.freeWorkers.push(i),t.frameFinished(e.data)}}(t),t.freeWorkers.push(i)}}(this)),t},e.prototype.frameFinished=function(t){return console.log("frame "+t.index+" finished - "+this.activeWorkers.length+" active"),this.finishedFrames++,this.emit("progress",this.finishedFrames/this.frames.length),this.imageParts[t.index]=t,a(null,this.imageParts)?this.renderNextFrame():this.finishRendering()},e.prototype.finishRendering=function(){var t,e,i,n,r,o,a;r=0;for(var s=0,h=this.imageParts.length;s=this.frames.length?void 0:(t=this.frames[this.nextFrame++],i=this.freeWorkers.shift(),e=this.getTask(t),console.log("starting frame "+(e.index+1)+" of "+this.frames.length),this.activeWorkers.push(i),i.postMessage(e))},e.prototype.getContextData=function(t){return t.getImageData(0,0,this.options.width,this.options.height).data},e.prototype.getImageData=function(t){var e;return null!=this._canvas||(this._canvas=document.createElement("canvas"),this._canvas.width=this.options.width,this._canvas.height=this.options.height),e=this._canvas.getContext("2d"),e.setFill=this.options.background,e.fillRect(0,0,this.options.width,this.options.height),e.drawImage(t,0,0),this.getContextData(e)},e.prototype.getTask=function(t){var e,i;if(e=this.frames.indexOf(t),i={index:e,last:e===this.frames.length-1,delay:t.delay,transparent:t.transparent,width:this.options.width,height:this.options.height,quality:this.options.quality,repeat:this.options.repeat,canTransfer:"chrome"===h.name},null!=t.data)i.data=t.data;else if(null!=t.context)i.data=this.getContextData(t.context);else{if(null==t.image)throw new Error("Invalid frame");i.data=this.getImageData(t.image)}return i},e}(u),t.exports=l}),e.define("/browser.coffee",function(t,e,i,n){var r,o,a,s,h;s=navigator.userAgent.toLowerCase(),a=navigator.platform.toLowerCase(),h=s.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/)||[null,"unknown",0],o="ie"===h[1]&&document.documentMode,r={name:"version"===h[1]?h[3]:h[1],version:o||parseFloat("opera"===h[1]&&h[4]?h[4]:h[2]),platform:{name:s.match(/ip(?:ad|od|hone)/)?"ios":(s.match(/(?:webos|android)/)||a.match(/mac|win|linux/)||["other"])[0]}},r[r.name]=!0,r[r.name+parseInt(r.version,10)]=!0,r.platform[r.platform.name]=!0,t.exports=r}),e.define("events",function(t,e,n,r){i.EventEmitter||(i.EventEmitter=function(){});var o=e.EventEmitter=i.EventEmitter,a="function"==typeof Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},s=10;o.prototype.setMaxListeners=function(t){this._events||(this._events={}),this._events.maxListeners=t},o.prototype.emit=function(t){if("error"===t&&(!this._events||!this._events.error||a(this._events.error)&&!this._events.error.length))throw arguments[1]instanceof Error?arguments[1]:new Error("Uncaught, unspecified 'error' event.");if(!this._events)return!1;var e=this._events[t];if(!e)return!1;if("function"!=typeof e){if(a(e)){for(var i=Array.prototype.slice.call(arguments,1),n=e.slice(),r=0,o=n.length;r0&&this._events[t].length>i&&(this._events[t].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[t].length),console.trace())}this._events[t].push(e)}else this._events[t]=[this._events[t],e];else this._events[t]=e;return this},o.prototype.on=o.prototype.addListener,o.prototype.once=function(t,e){var i=this;return i.on(t,function n(){i.removeListener(t,n),e.apply(this,arguments)}),this},o.prototype.removeListener=function(t,e){if("function"!=typeof e)throw new Error("removeListener only takes instances of Function");if(!this._events||!this._events[t])return this;var i=this._events[t];if(a(i)){var n=i.indexOf(e);if(n<0)return this;i.splice(n,1),0==i.length&&delete this._events[t]}else this._events[t]===e&&delete this._events[t];return this},o.prototype.removeAllListeners=function(t){return t&&this._events&&this._events[t]&&(this._events[t]=null),this},o.prototype.listeners=function(t){return this._events||(this._events={}),this._events[t]||(this._events[t]=[]),a(this._events[t])||(this._events[t]=[this._events[t]]),this._events[t]}}),t.GIF=e("/gif.coffee")}.call(this,this),function(){function t(t){return t&&t.Object===Object?t:null}function e(t){return String("0000000"+t).slice(-7)}function i(){function t(){return Math.floor(65536*(1+Math.random())).toString(16).substring(1)}return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()}function n(t){var e={};this.settings=t,this.on=function(t,i){e[t]=i},this.emit=function(t){var i=e[t];i&&i.apply(null,Array.prototype.slice.call(arguments,1))},this.filename=t.name||i(),this.extension="",this.mimeType=""}function r(t){n.call(this,t),this.extension=".tar",this.mimeType="application/x-tar",this.fileExtension="",this.baseFilename=this.filename,this.tape=null,this.count=0,this.part=1,this.frames=0}function o(t){r.call(this,t),this.type="image/png",this.fileExtension=".png"}function a(t){r.call(this,t),this.type="image/jpeg",this.fileExtension=".jpg",this.quality=t.quality/100||.8}function s(t){var e=document.createElement("canvas");"image/webp"!==e.toDataURL("image/webp").substr(5,10)&&console.log("WebP not supported - try another export format"),n.call(this,t),this.quality=t.quality/100||.8,this.extension=".webm",this.mimeType="video/webm",this.baseFilename=this.filename,this.framerate=t.framerate,this.frames=0,this.part=1,this.videoWriter=new WebMWriter({quality:this.quality,fileWriter:null,fd:null,frameRate:this.framerate})}function h(t){n.call(this,t),t.quality=t.quality/100||.8,this.encoder=new FFMpegServer.Video(t),this.encoder.on("process",function(){this.emit("process")}.bind(this)),this.encoder.on("finished",function(t,e){var i=this.callback;i&&(this.callback=void 0,i(t,e))}.bind(this)),this.encoder.on("progress",function(t){this.settings.onProgress&&this.settings.onProgress(t)}.bind(this)),this.encoder.on("error",function(t){alert(JSON.stringify(t,null,2))}.bind(this))}function f(t){n.call(this,t),this.framerate=this.settings.framerate,this.type="video/webm",this.extension=".webm",this.stream=null,this.mediaRecorder=null,this.chunks=[]}function u(t){n.call(this,t),t.quality=31-(30*t.quality/100||10),t.workers=t.workers||4,this.extension=".gif",this.mimeType="image/gif",this.canvas=document.createElement("canvas"),this.ctx=this.canvas.getContext("2d"),this.sizeSet=!1,this.encoder=new GIF({workers:t.workers,quality:t.quality,workerScript:t.workersPath+"gif.worker.js"}),this.encoder.on("progress",function(t){this.settings.onProgress&&this.settings.onProgress(t)}.bind(this)),this.encoder.on("finished",function(t){var e=this.callback;e&&(this.callback=void 0,e(t))}.bind(this))}function d(t){function e(){function t(){return this._hooked||(this._hooked=!0,this._hookedTime=this.currentTime||0,this.pause(),it.push(this)),this._hookedTime+M.startTime}b("Capturer start"),U=window.Date.now(),T=U+M.startTime,I=window.performance.now(),F=I+M.startTime,window.Date.prototype.getTime=function(){return T},window.Date.now=function(){return T},window.setTimeout=function(t,e){var i={callback:t,time:e,triggerTime:T+e};return _.push(i),b("Timeout set to "+i.time),i},window.clearTimeout=function(t){for(var e=0;e<_.length;e++)_[e]!=t||(_.splice(e,1),b("Timeout cleared"))},window.setInterval=function(t,e){var i={callback:t,time:e,triggerTime:T+e};return S.push(i),b("Interval set to "+i.time),i},window.clearInterval=function(t){return b("clear Interval"),null},window.requestAnimationFrame=function(t){W.push(t)},window.performance.now=function(){return F};try{Object.defineProperty(HTMLVideoElement.prototype,"currentTime",{get:t}),Object.defineProperty(HTMLAudioElement.prototype,"currentTime",{get:t})}catch(t){b(t)}}function i(){e(),D.start(),R=!0}function n(){R=!1,D.stop(),l()}function r(t,e){Z(t,0,e)}function d(){r(y)}function l(){b("Capturer stop"),window.setTimeout=Z,window.setInterval=J,window.clearInterval=Y,window.clearTimeout=$,window.requestAnimationFrame=Q,window.Date.prototype.getTime=et,window.Date.now=X,window.performance.now=tt}function c(){var t=C/M.framerate;(M.frameLimit&&C>=M.frameLimit||M.timeLimit&&t>=M.timeLimit)&&(n(),v());var e=new Date(null);e.setSeconds(t),M.motionBlurFrames>2?j.textContent="CCapture "+M.format+" | "+C+" frames ("+O+" inter) | "+e.toISOString().substr(11,8):j.textContent="CCapture "+M.format+" | "+C+" frames | "+e.toISOString().substr(11,8)}function p(t){N.width===t.width&&N.height===t.height||(N.width=t.width,N.height=t.height,z=new Uint16Array(N.height*N.width*4),V.fillStyle="#0",V.fillRect(0,0,N.width,N.height))}function m(t){V.drawImage(t,0,0),q=V.getImageData(0,0,N.width,N.height);for(var e=0;e2?(p(t),m(t),O>=.5*M.motionBlurFrames?w():d()):(D.add(t),C++,b("Full Frame! "+C)))}function y(){var t=1e3/M.framerate,e=(C+O/M.motionBlurFrames)*t;T=U+e,F=I+e,it.forEach(function(t){t._hookedTime=e/1e3}),c(),b("Frame: "+C+" "+O);for(var i=0;i<_.length;i++)T>=_[i].triggerTime&&(r(_[i].callback),_.splice(i,1));for(var i=0;i=S[i].triggerTime&&(r(S[i].callback),S[i].triggerTime+=S[i].time);W.forEach(function(t){r(t,T-k)}),W=[]}function v(t){t||(t=function(t){return download(t,D.filename+D.extension,D.mimeType),!1}),D.save(t)}function b(t){A&&console.log(t)}function B(t,e){P[t]=e}function x(t){var e=P[t];e&&e.apply(null,Array.prototype.slice.call(arguments,1))}function E(t){x("progress",t)}var A,L,T,U,F,I,d,D,M=t||{},_=(new Date,[]),S=[],C=0,O=0,W=[],R=!1,P={};M.framerate=M.framerate||60,M.motionBlurFrames=2*(M.motionBlurFrames||1),A=M.verbose||!1,L=M.display||!1,M.step=1e3/M.framerate,M.timeLimit=M.timeLimit||0,M.frameLimit=M.frameLimit||0,M.startTime=M.startTime||0;var j=document.createElement("div");j.style.position="absolute",j.style.left=j.style.top=0,j.style.backgroundColor="black",j.style.fontFamily="monospace",j.style.fontSize="11px",j.style.padding="5px",j.style.color="red",j.style.zIndex=1e5,M.display&&document.body.appendChild(j);var z,q,N=document.createElement("canvas"),V=N.getContext("2d");b("Step is set to "+M.step+"ms");var G={gif:u,webm:s,ffmpegserver:h,png:o,jpg:a,"webm-mediarecorder":f},H=G[M.format];if(!H)throw"Error: Incorrect or missing format: Valid formats are "+Object.keys(G).join(", ");if(D=new H(M),D.step=d,D.on("process",y),D.on("progress",E),"performance"in window==0&&(window.performance={}),Date.now=Date.now||function(){return(new Date).getTime()},"now"in window.performance==0){var K=Date.now();performance.timing&&performance.timing.navigationStart&&(K=performance.timing.navigationStart),window.performance.now=function(){return Date.now()-K}}var Z=window.setTimeout,J=window.setInterval,Y=window.clearInterval,$=window.clearTimeout,Q=window.requestAnimationFrame,X=window.Date.now,tt=window.performance.now,et=window.Date.prototype.getTime,it=[];return{start:i,capture:g,stop:n,save:v,on:B}}var l={function:!0,object:!0},c=(parseFloat,parseInt,l[typeof exports]&&exports&&!exports.nodeType?exports:void 0),p=l[typeof module]&&module&&!module.nodeType?module:void 0,m=p&&p.exports===c?c:void 0,w=t(c&&p&&"object"==typeof global&&global),g=t(l[typeof self]&&self),y=t(l[typeof window]&&window),v=t(l[typeof this]&&this),b=w||y!==(v&&v.window)&&y||g||v||Function("return this")();"gc"in window||(window.gc=function(){}),HTMLCanvasElement.prototype.toBlob||Object.defineProperty(HTMLCanvasElement.prototype,"toBlob",{value:function(t,e,i){for(var n=atob(this.toDataURL(e,i).split(",")[1]),r=n.length,o=new Uint8Array(r),a=0;a0&&this.frames/this.settings.framerate>=this.settings.autoSaveTime?this.save(function(t){this.filename=this.baseFilename+"-part-"+e(this.part),download(t,this.filename+this.extension,this.mimeType);var i=this.count;this.dispose(),this.count=i+1,this.part++,this.filename=this.baseFilename+"-part-"+e(this.part),this.frames=0,this.step()}.bind(this)):(this.count++,this.frames++,this.step())}.bind(this),i.readAsArrayBuffer(t)},r.prototype.save=function(t){t(this.tape.save())},r.prototype.dispose=function(){this.tape=new Tar,this.count=0},o.prototype=Object.create(r.prototype),o.prototype.add=function(t){t.toBlob(function(t){r.prototype.add.call(this,t)}.bind(this),this.type)},a.prototype=Object.create(r.prototype),a.prototype.add=function(t){t.toBlob(function(t){r.prototype.add.call(this,t)}.bind(this),this.type,this.quality)},s.prototype=Object.create(n.prototype),s.prototype.start=function(t){this.dispose()},s.prototype.add=function(t){this.videoWriter.addFrame(t),this.settings.autoSaveTime>0&&this.frames/this.settings.framerate>=this.settings.autoSaveTime?this.save(function(t){this.filename=this.baseFilename+"-part-"+e(this.part),download(t,this.filename+this.extension,this.mimeType),this.dispose(),this.part++,this.filename=this.baseFilename+"-part-"+e(this.part),this.step()}.bind(this)):(this.frames++,this.step())},s.prototype.save=function(t){this.videoWriter.complete().then(t)},s.prototype.dispose=function(t){this.frames=0,this.videoWriter=new WebMWriter({quality:this.quality,fileWriter:null,fd:null,frameRate:this.framerate})},h.prototype=Object.create(n.prototype),h.prototype.start=function(){this.encoder.start(this.settings)},h.prototype.add=function(t){this.encoder.add(t)},h.prototype.save=function(t){this.callback=t,this.encoder.end()},h.prototype.safeToProceed=function(){return this.encoder.safeToProceed()},f.prototype=Object.create(n.prototype),f.prototype.add=function(t){this.stream||(this.stream=t.captureStream(this.framerate),this.mediaRecorder=new MediaRecorder(this.stream),this.mediaRecorder.start(),this.mediaRecorder.ondataavailable=function(t){this.chunks.push(t.data)}.bind(this)),this.step()},f.prototype.save=function(t){this.mediaRecorder.onstop=function(e){var i=new Blob(this.chunks,{type:"video/webm"});this.chunks=[],t(i)}.bind(this),this.mediaRecorder.stop()},u.prototype=Object.create(n.prototype),u.prototype.add=function(t){this.sizeSet||(this.encoder.setOption("width",t.width),this.encoder.setOption("height",t.height),this.sizeSet=!0),this.canvas.width=t.width,this.canvas.height=t.height,this.ctx.drawImage(t,0,0),this.encoder.addFrame(this.ctx,{copy:!0,delay:this.settings.step}),this.step()},u.prototype.save=function(t){this.callback=t,this.encoder.render()},(y||g||{}).CCapture=d,"function"==typeof define&&"object"==typeof define.amd&&define.amd?define(function(){return d}):c&&p?(m&&((p.exports=d).CCapture=d),c.CCapture=d):b.CCapture=d}(); -------------------------------------------------------------------------------- /public/images/butterfly-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthen/animations/31bb62005430a4e233a43434ed9fb9b4c0eb17c4/public/images/butterfly-text.png -------------------------------------------------------------------------------- /public/images/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthen/animations/31bb62005430a4e233a43434ed9fb9b4c0eb17c4/public/images/cat.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | animations 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/animations/_ballspin.tsx: -------------------------------------------------------------------------------- 1 | // Kept for reference. For now matter-js is not installed as a dependency. 2 | import Matter from 'matter-js'; 3 | 4 | import { Animation, DrawArgs, DrawFn, MakeDrawFn, Parameter } from 'lib/Animation'; 5 | import Graphics from 'lib/graphics'; 6 | import Utils from 'lib/utils'; 7 | 8 | const Ballspin = () => { 9 | const duration = 24; 10 | const canvasWidth = 1024; 11 | const canvasHeight = 1024; 12 | const bgColor = '#020115'; 13 | 14 | const parameters: Parameter[] = [{ name: 'ground', minValue: 0, maxValue: 1, defaultValue: 0, step: 1.0 }]; 15 | 16 | const makeDrawFn: MakeDrawFn = (canvas) => { 17 | const ctx = canvas.getContext('2d')!; 18 | const plotRange = 512; 19 | 20 | const engine = Matter.Engine.create(); 21 | engine.gravity = { x: 0, y: -1, scale: 300.0 }; 22 | const ballInitParams = { 23 | x: plotRange / 2, 24 | y: 0.75 * plotRange, 25 | vx: 1200, 26 | vy: 1000, 27 | angle: 0, 28 | angularVelocity: 0.0, 29 | }; 30 | const ball1 = Matter.Bodies.circle(ballInitParams.x, ballInitParams.y, 20, { 31 | restitution: 0.99, 32 | collisionFilter: { mask: 2, category: 1 }, 33 | }); 34 | const ball2 = Matter.Bodies.circle(ballInitParams.x, ballInitParams.y, 20, { 35 | restitution: 0.99, 36 | collisionFilter: { mask: 2, category: 1 }, 37 | }); 38 | const numPieces = 64; 39 | const radius = (0.9 * plotRange) / 2; 40 | const overhang = 1.0; 41 | const groundPieces = Utils.range(-overhang, overhang + Math.PI, (Math.PI + 2 * overhang) / (numPieces - 1)).map( 42 | (th) => 43 | Matter.Bodies.rectangle( 44 | plotRange / 2 - radius * Math.cos(th), 45 | plotRange / 2 - radius * Math.sin(th), 46 | 6, 47 | (2 * (Math.PI * radius)) / numPieces, 48 | { 49 | isStatic: true, 50 | angle: th, 51 | collisionFilter: { category: 2, mask: 1 }, 52 | }, 53 | ), 54 | ); 55 | 56 | Matter.World.add(engine.world, [ball1, ball2, ...groundPieces]); 57 | let lastT = 0.0; 58 | 59 | const ballGraphics = (ball: Matter.Body) => [ 60 | Graphics.Translate({ offset: [ball.position.x, ball.position.y] }), 61 | Graphics.Rotate({ angle: -ball.angle, center: [0, 0] }), 62 | Graphics.Disk({ 63 | center: [0, 0], 64 | radius: ball.circleRadius!, 65 | fill: true, 66 | edge: false, 67 | }), 68 | Graphics.Line({ 69 | pts: [ 70 | [-ball.circleRadius!, 0], 71 | [ball.circleRadius!, 0], 72 | ], 73 | }), 74 | ]; 75 | 76 | const initBall = (ball: Matter.Body, nudge: number) => { 77 | Matter.Body.setPosition(ball, { x: ballInitParams.x, y: ballInitParams.y }); 78 | Matter.Body.setVelocity(ball, { x: ballInitParams.vx + nudge, y: ballInitParams.vy }); 79 | Matter.Body.setAngle(ball, ballInitParams.angle); 80 | Matter.Body.setAngularVelocity(ball, ballInitParams.angularVelocity); 81 | }; 82 | 83 | let trace1: number[][] = []; 84 | let trace2: number[][] = []; 85 | 86 | const drawFn: DrawFn = ({ t, ground }: DrawArgs) => { 87 | if (t == 0.0) { 88 | initBall(ball1, 0); 89 | initBall(ball2, 0.1); 90 | trace1 = []; 91 | trace2 = []; 92 | } 93 | const deltaT = t - lastT; 94 | lastT = t; 95 | if (deltaT > 0 && deltaT < 0.1) { 96 | const deltaT2 = deltaT / 12; 97 | for (let i = 0; i < 12; i++) { 98 | Matter.Engine.update(engine, deltaT2); 99 | if (i % 4 == 0) { 100 | trace1.push([ball2.position.x, ball2.position.y]); 101 | trace2.push([ball1.position.x, ball1.position.y]); 102 | } 103 | } 104 | } 105 | 106 | ctx.clearRect(0, 0, canvasWidth, canvasHeight); 107 | ctx.fillStyle = bgColor; 108 | ctx.fillRect(0, 0, canvasWidth, canvasHeight); 109 | 110 | Graphics.draw( 111 | [ 112 | Graphics.AbsoluteLineWidth(4), 113 | [Graphics.Set({ strokeStyle: '#985e00ff' }), Graphics.Line({ pts: trace1 })], 114 | [Graphics.Set({ strokeStyle: '#5290a5ff' }), Graphics.Line({ pts: trace2 })], 115 | [Graphics.Set({ fillStyle: '#7dddfca0', strokeStyle: bgColor }), ballGraphics(ball1)], 116 | [Graphics.Set({ fillStyle: '#ff9d00b5', strokeStyle: bgColor }), ballGraphics(ball2)], 117 | // ground 118 | Graphics.Set({ fillStyle: '#ff000062', strokeStyle: '#ffffffff', lineWidth: 6 }), 119 | Graphics.Disk({ center: [plotRange / 2, plotRange / 2], radius, fill: false, edge: true }), 120 | ground > 0.5 121 | ? groundPieces.map((ground) => 122 | Graphics.Polygon({ 123 | pts: ground.vertices.map((pt) => [pt.x, pt.y]), 124 | edge: false, 125 | fill: true, 126 | }), 127 | ) 128 | : [], 129 | ], 130 | 131 | { 132 | xmin: 0, 133 | xmax: plotRange, 134 | ymin: 0, 135 | ymax: plotRange, 136 | }, 137 | ctx, 138 | ); 139 | }; 140 | 141 | return drawFn; 142 | }; 143 | 144 | return ( 145 | 153 | ); 154 | }; 155 | 156 | export default Ballspin; 157 | -------------------------------------------------------------------------------- /src/animations/anaglyph.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { GlslPipeline } from 'glsl-pipeline'; 3 | import { RepeatWrapping, TextureLoader, WebGLRenderer } from 'three'; 4 | 5 | import { Animation, DrawArgs, DrawFn, MakeDrawFn, Parameter } from 'lib/Animation'; 6 | import Utils from 'lib/utils'; 7 | 8 | import shader from './shaders/anaglyph.glsl'; 9 | 10 | const Anaglyph = () => { 11 | const duration = 6; 12 | const canvasWidth = 768; 13 | const canvasHeight = 768; 14 | 15 | const parameters: Parameter[] = [ 16 | { 17 | name: 'redness', 18 | minValue: 0, 19 | maxValue: 1, 20 | defaultValue: 0, 21 | compute: Utils.makeTransitionFunction([ 22 | { 23 | easing: 'smoothstep', 24 | startT: 0.5, 25 | endT: duration - 0.5, 26 | startValue: 0, 27 | endValue: 1, 28 | }, 29 | ]), 30 | }, 31 | ]; 32 | 33 | const makeDrawFn: MakeDrawFn = (canvas) => { 34 | const texLoader = new TextureLoader(); 35 | let u_tex0 = texLoader.load('/animations/images/butterfly-text.png', () => { 36 | drawFn({ t: 0, redness: 0 }); 37 | }); 38 | u_tex0.generateMipmaps = false; 39 | u_tex0.wrapS = RepeatWrapping; 40 | u_tex0.wrapT = RepeatWrapping; 41 | const renderer = new WebGLRenderer({ 42 | canvas, 43 | }); 44 | let pipeline = new GlslPipeline(renderer, { u_redness: { value: 0 }, u_tex0: { type: 't', value: u_tex0 } }); 45 | pipeline.load(shader); 46 | Utils.resetGlslPipeline(pipeline); 47 | 48 | const drawFn: DrawFn = ({ t, redness }: DrawArgs) => { 49 | if (t == 0) { 50 | Utils.resetGlslPipeline(pipeline); 51 | } 52 | pipeline.uniforms.u_redness.value = redness; 53 | pipeline.renderMain(); 54 | }; 55 | 56 | return drawFn; 57 | }; 58 | 59 | return ( 60 | 67 | ); 68 | }; 69 | 70 | export default Anaglyph; 71 | -------------------------------------------------------------------------------- /src/animations/dither.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { GlslPipeline } from 'glsl-pipeline'; 3 | import { RepeatWrapping, TextureLoader, WebGLRenderer } from 'three'; 4 | 5 | import { Animation, DrawArgs, DrawFn, MakeDrawFn, Parameter } from 'lib/Animation'; 6 | import Utils from 'lib/utils'; 7 | 8 | import shader from './shaders/dither.glsl'; 9 | 10 | const Dither = () => { 11 | const duration = 6; 12 | const canvasWidth = 768; 13 | const canvasHeight = 768; 14 | 15 | const parameters: Parameter[] = []; 16 | 17 | const makeDrawFn: MakeDrawFn = (canvas) => { 18 | const texLoader = new TextureLoader(); 19 | let u_tex0 = texLoader.load('/animations/images/cat.jpg', () => { 20 | drawFn({ t: 0 }); 21 | }); 22 | u_tex0.generateMipmaps = false; 23 | u_tex0.wrapS = RepeatWrapping; 24 | u_tex0.wrapT = RepeatWrapping; 25 | const renderer = new WebGLRenderer({ 26 | canvas, 27 | }); 28 | let pipeline = new GlslPipeline(renderer, { 29 | u_t: { value: 0 }, 30 | u_tt: { value: 0 }, 31 | u_tex0: { type: 't', value: u_tex0 }, 32 | }); 33 | pipeline.load(shader); 34 | Utils.resetGlslPipeline(pipeline); 35 | 36 | const drawFn: DrawFn = ({ t }: DrawArgs) => { 37 | if (t == 0) { 38 | Utils.resetGlslPipeline(pipeline); 39 | } 40 | pipeline.uniforms.u_t.value = t; 41 | if (t > 0.5) { 42 | pipeline.uniforms.u_tt.value = t; 43 | } 44 | 45 | pipeline.renderMain(); 46 | }; 47 | 48 | return drawFn; 49 | }; 50 | 51 | return ( 52 | 59 | ); 60 | }; 61 | 62 | export default Dither; 63 | -------------------------------------------------------------------------------- /src/animations/hypocycloids.tsx: -------------------------------------------------------------------------------- 1 | import { Animation, DrawArgs, DrawFn, MakeDrawFn, Parameter } from 'lib/Animation'; 2 | import Graphics from 'lib/graphics'; 3 | import Utils from 'lib/utils'; 4 | 5 | const Hypocycloids = () => { 6 | const duration = 12; 7 | const canvasWidth = 1024; 8 | const canvasHeight = 1024; 9 | 10 | const parameters: Parameter[] = [ 11 | { 12 | name: 'theta', 13 | minValue: 0, 14 | maxValue: 4 * Math.PI, 15 | // Start and end at 4 * Math.PI, linear between 4 and duration - 4 seconds. 16 | compute: (t) => 17 | (4 * Math.PI * (Utils.smoothstepI(t, 0, 4) - Utils.smoothstepI(t, duration - 4, duration))) / 18 | (duration - 4), 19 | step: 0.01, 20 | }, 21 | { 22 | name: 'n', 23 | minValue: 2, 24 | maxValue: 16, 25 | defaultValue: 3, 26 | step: 1, 27 | }, 28 | ]; 29 | 30 | const makeDrawFn: MakeDrawFn = (canvas) => { 31 | const ctx = canvas.getContext('2d')!; 32 | 33 | const drawFn: DrawFn = ({ theta, n }: DrawArgs) => { 34 | theta = Math.min(theta, 4 * Math.PI - 1e-5); 35 | const r = 1 / n; 36 | ctx.clearRect(0, 0, canvas.width, canvas.height); 37 | ctx.fillStyle = '#020115'; 38 | ctx.fillRect(0, 0, canvas.width, canvas.height); 39 | const inFirstHalf = theta < 2 * Math.PI; 40 | const traceInFn = (th: number) => [ 41 | Math.sin(th) * (1 - r) - Math.sin((th * (1 - r)) / r) * r, 42 | Math.cos(th) * (1 - r) + Math.cos((th * (1 - r)) / r) * r, 43 | ]; 44 | const traceOutFn = (th: number) => [ 45 | Math.sin(th) * (1 + r) - Math.sin((th * (1 + r)) / r) * r, 46 | Math.cos(th) * (1 + r) - Math.cos((th * (1 + r)) / r) * r, 47 | ]; 48 | const plotRange = 1 + 2 * r + 0.1; 49 | Graphics.draw( 50 | [ 51 | Graphics.AbsoluteLineWidth(4), 52 | Graphics.Set({ strokeStyle: '#ffffff' }), 53 | Graphics.Disk({ center: [0, 0], radius: 1, fill: false, edge: true }), 54 | [ 55 | Graphics.Set({ strokeStyle: '#bbeafe' }), 56 | // Trace inside. 57 | Graphics.Line({ 58 | pts: Utils.range(inFirstHalf ? 0 : 4 * Math.PI, theta, inFirstHalf ? 0.01 : -0.01).map( 59 | traceInFn, 60 | ), 61 | }), 62 | // Trace outside. 63 | Graphics.Line({ 64 | pts: Utils.range(inFirstHalf ? 0 : 4 * Math.PI, theta, inFirstHalf ? 0.01 : -0.01).map( 65 | traceOutFn, 66 | ), 67 | }), 68 | ], 69 | [ 70 | Graphics.Rotate({ angle: theta, center: [0, 0] }), 71 | // Rolling circles. 72 | Graphics.Set({ fillStyle: '#ffffff16' }), 73 | [ 74 | // inside 75 | Graphics.Disk({ center: [0, 1 - r], radius: r, fill: true, edge: true }), 76 | Graphics.Rotate({ angle: -theta / r, center: [0, 1 - r] }), 77 | // Point on rolling circle. 78 | Graphics.Set({ fillStyle: '#f39034', strokeStyle: 'black' }), 79 | Graphics.Disk({ 80 | center: [0, 1], 81 | radius: 16, 82 | fill: true, 83 | edge: true, 84 | radiusInPixels: true, 85 | }), 86 | ], 87 | [ 88 | // outside 89 | Graphics.Disk({ center: [0, 1 + r], radius: r, fill: true, edge: true }), 90 | Graphics.Rotate({ angle: theta / r, center: [0, 1 + r] }), 91 | // Point on rolling circle. 92 | Graphics.Set({ fillStyle: '#f39034', strokeStyle: 'black' }), 93 | Graphics.Disk({ 94 | center: [0, 1], 95 | radius: 16, 96 | fill: true, 97 | edge: true, 98 | radiusInPixels: true, 99 | }), 100 | ], 101 | ], 102 | ], 103 | 104 | { 105 | xmin: -plotRange, 106 | xmax: plotRange, 107 | ymin: -plotRange, 108 | ymax: plotRange, 109 | }, 110 | ctx, 111 | ); 112 | }; 113 | 114 | return drawFn; 115 | }; 116 | 117 | return ( 118 | 125 | ); 126 | }; 127 | 128 | export default Hypocycloids; 129 | -------------------------------------------------------------------------------- /src/animations/minimal2D.tsx: -------------------------------------------------------------------------------- 1 | import { Animation, DrawArgs, DrawFn, MakeDrawFn, Parameter } from 'lib/Animation'; 2 | import Graphics from 'lib/graphics'; 3 | import Utils from 'lib/utils'; 4 | 5 | const Minimal2D = () => { 6 | const duration = 6; 7 | const canvasWidth = 768; 8 | const canvasHeight = 768; 9 | 10 | const parameters: Parameter[] = [ 11 | { 12 | name: 'r', 13 | minValue: 0.005, 14 | maxValue: 1, 15 | compute: Utils.makeTransitionFunction([ 16 | { 17 | easing: 'smoothstep', 18 | startT: 1, 19 | endT: 3, 20 | startValue: 0.1, 21 | endValue: 1.0, 22 | }, 23 | { 24 | easing: 'smoothstep', 25 | startT: 4, 26 | endT: 6, 27 | endValue: 0.1, 28 | }, 29 | ]), 30 | step: 0.005, 31 | }, 32 | ]; 33 | 34 | const makeDrawFn: MakeDrawFn = (canvas) => { 35 | const ctx = canvas.getContext('2d')!; 36 | 37 | const drawFn: DrawFn = ({ r }: DrawArgs) => { 38 | ctx.clearRect(0, 0, canvas.width, canvas.height); 39 | ctx.fillStyle = '#020115'; 40 | ctx.fillRect(0, 0, canvas.width, canvas.height); 41 | 42 | Graphics.draw( 43 | [ 44 | Graphics.AbsoluteLineWidth(4), 45 | Graphics.Set({ strokeStyle: '#ffffff', fillStyle: '#005f5f' }), 46 | Graphics.Disk({ center: [0, 0], radius: r, fill: true, edge: true }), 47 | ], 48 | 49 | { 50 | xmin: -1.1, 51 | xmax: 1.1, 52 | ymin: -1.1, 53 | ymax: 1.1, 54 | }, 55 | ctx, 56 | ); 57 | }; 58 | 59 | return drawFn; 60 | }; 61 | 62 | return ( 63 | 70 | ); 71 | }; 72 | 73 | export default Minimal2D; 74 | -------------------------------------------------------------------------------- /src/animations/minimalShader.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { GlslPipeline } from 'glsl-pipeline'; 3 | import { WebGLRenderer } from 'three'; 4 | 5 | import { Animation, DrawArgs, DrawFn, MakeDrawFn, Parameter } from 'lib/Animation'; 6 | import Utils from 'lib/utils'; 7 | 8 | import shader from './shaders/minimalShader.glsl'; 9 | 10 | const MinimalShader = () => { 11 | const duration = 2; 12 | const canvasWidth = 768; 13 | const canvasHeight = 768; 14 | 15 | const parameters: Parameter[] = [{ name: 'speed', minValue: 0, maxValue: 10, defaultValue: 1 }]; 16 | 17 | const makeDrawFn: MakeDrawFn = (canvas) => { 18 | const renderer = new WebGLRenderer({ 19 | canvas, 20 | }); 21 | let pipeline = new GlslPipeline(renderer, { u_t: { value: 0 }, u_speed: { value: 0 } }); 22 | pipeline.load(shader); 23 | pipeline.renderMain(); 24 | 25 | const drawFn: DrawFn = ({ t, speed }: DrawArgs) => { 26 | if (t == 0) { 27 | Utils.resetGlslPipeline(pipeline); 28 | } 29 | pipeline.uniforms.u_t.value = t; 30 | pipeline.uniforms.u_speed.value = speed; 31 | pipeline.renderMain(); 32 | }; 33 | 34 | return drawFn; 35 | }; 36 | 37 | return ( 38 | 45 | ); 46 | }; 47 | 48 | export default MinimalShader; 49 | -------------------------------------------------------------------------------- /src/animations/noiseDodecahedron.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { GlslPipeline } from 'glsl-pipeline'; 3 | import { WebGLRenderer } from 'three'; 4 | 5 | import { Animation, DrawArgs, DrawFn, MakeDrawFn, Parameter } from 'lib/Animation'; 6 | import Utils from 'lib/utils'; 7 | 8 | import shader from './shaders/noiseDodecahedron.glsl'; 9 | 10 | const NoiseDodecahedron = () => { 11 | const duration = 20; 12 | 13 | const canvasWidth = 768; 14 | const canvasHeight = 768; 15 | 16 | const parameters: Parameter[] = [ 17 | { 18 | name: 'theta_1', 19 | minValue: 0, 20 | maxValue: 2 * Math.PI, 21 | defaultValue: 0, 22 | compute: Utils.makeTransitionFunction([ 23 | { 24 | easing: 'linear', 25 | startT: 0, 26 | endT: 20, 27 | startValue: 0.0, 28 | endValue: 0.1, 29 | }, 30 | ]), 31 | }, 32 | { 33 | name: 'theta_2', 34 | minValue: 0, 35 | maxValue: 2 * Math.PI, 36 | defaultValue: 0, 37 | compute: Utils.makeTransitionFunction([ 38 | { 39 | easing: 'linear', 40 | startT: 0, 41 | endT: 20, 42 | startValue: 1.0, 43 | endValue: 3.0, 44 | }, 45 | ]), 46 | }, 47 | ]; 48 | 49 | const makeDrawFn: MakeDrawFn = (canvas) => { 50 | const renderer = new WebGLRenderer({ 51 | canvas, 52 | }); 53 | let pipeline = new GlslPipeline(renderer, { 54 | u_t: { value: 0 }, 55 | u_theta_1: { value: 0 }, 56 | u_theta_2: { value: 0 }, 57 | u_update: { value: 0 }, 58 | }); 59 | pipeline.load(shader); 60 | pipeline.renderMain(); 61 | let lastUpdate = -1; 62 | 63 | const drawFn: DrawFn = ({ t, theta_1, theta_2 }: DrawArgs) => { 64 | if (t == 0) { 65 | Utils.resetGlslPipeline(pipeline); 66 | lastUpdate = -1; 67 | } 68 | pipeline.uniforms.u_t.value = t; 69 | pipeline.uniforms.u_theta_1.value = theta_1; 70 | pipeline.uniforms.u_theta_2.value = theta_2; 71 | let update: number = 0; 72 | if (t - lastUpdate > 0.04) { 73 | update = 1; 74 | lastUpdate = t; 75 | } 76 | pipeline.uniforms.u_update.value = update; 77 | pipeline.renderMain(); 78 | }; 79 | 80 | return drawFn; 81 | }; 82 | 83 | return ( 84 | 91 | ); 92 | }; 93 | 94 | export default NoiseDodecahedron; 95 | -------------------------------------------------------------------------------- /src/animations/piArcs.tsx: -------------------------------------------------------------------------------- 1 | import chroma from 'chroma-js'; 2 | 3 | import { Animation, DrawArgs, DrawFn, MakeDrawFn, Parameter } from 'lib/Animation'; 4 | import Graphics from 'lib/graphics'; 5 | import Utils from 'lib/utils'; 6 | 7 | const PiArcs = () => { 8 | const duration = 56; 9 | const canvasWidth = 1024; 10 | const canvasHeight = 1024; 11 | const bgColor = '#020115'; 12 | 13 | const tt = (t: number) => 14 | 0.25 * t + 15 | 0.25 * Utils.smoothstepI(t, 2, 3) + 16 | Utils.smoothstepI(t, 10, 15) + 17 | 1.5 * Utils.smoothstepI(t, 15, 20) + 18 | 10 * Utils.smoothstepI(t, 20, 40) - 19 | 13 * Utils.smoothstepI(t, duration - 3.5, duration - 0.5); 20 | 21 | const parameters: Parameter[] = [ 22 | { 23 | name: 'arc', 24 | minValue: 0, 25 | maxValue: 1, 26 | step: 0.01, 27 | compute: (t) => Utils.smoothstep(Utils.frac(tt(t)), 0, 0.5), 28 | }, 29 | { 30 | name: 'next', 31 | minValue: 0, 32 | maxValue: 1, 33 | step: 0.01, 34 | compute: (t) => Utils.smoothstep(Utils.frac(tt(t)), 0.5, 1.0), 35 | }, 36 | { name: 'index', minValue: 0, maxValue: Utils.piDigits.length - 1, step: 1, compute: (t) => Math.floor(tt(t)) }, 37 | 38 | { 39 | name: 'zoom', 40 | minValue: 1, 41 | maxValue: 100, 42 | step: 0.01, 43 | compute: (t) => 2.4 + 8 * Utils.smoothstep(tt(t), 1, 14) + tt(t) * 0.1, 44 | }, 45 | 46 | { name: 'centreX', minValue: -20, maxValue: 20, compute: (t) => 0.7 + 4 * Utils.smoothstep(tt(t), 1, 14) }, 47 | { 48 | name: 'centreY', 49 | minValue: -20, 50 | maxValue: 20, 51 | compute: (t) => -0.8 + 7 * Utils.smoothstep(tt(t), 1, 14) + 0.015 * Utils.smoothstepI(tt(t), 14, 30), 52 | }, 53 | ]; 54 | const pyByFive = Math.PI / 5; 55 | 56 | const makeDrawFn: MakeDrawFn = (canvas) => { 57 | const ctx = canvas.getContext('2d')!; 58 | 59 | const drawFn: DrawFn = ({ t, arc, next, index, zoom, centreX, centreY }: DrawArgs) => { 60 | ctx.clearRect(0, 0, canvas.width, canvas.height); 61 | ctx.fillStyle = bgColor; 62 | ctx.fillRect(0, 0, canvas.width, canvas.height); 63 | 64 | const digit = Number(Utils.piDigits[index]); 65 | const even = index % 2 == 0; 66 | const angle = pyByFive * (digit > 0 ? digit : 10); 67 | let oddOpacity = 0.0; 68 | let evenOpacity = 0.0; 69 | oddOpacity = Math.max(0, 1.0 - 2 * next); 70 | evenOpacity = Math.max(0, Math.min(1, -1 + 2 * next)); 71 | if (!even) { 72 | [oddOpacity, evenOpacity] = [evenOpacity, oddOpacity]; 73 | } 74 | 75 | let arcStyles = [ 76 | Graphics.Set({ strokeStyle: '#ff36e8af' }), 77 | Graphics.Set({ lineWidth: 0.06 * Math.pow(zoom / 2.4, 0.6) }), 78 | ]; 79 | let transformsAndPreviousArcs: Graphics.DrawCommand[] = []; 80 | for (let i = 0; i < index; i++) { 81 | const digitI = Number(Utils.piDigits[i]); 82 | const angleI = pyByFive * (digitI > 0 ? digitI : 9.999); 83 | transformsAndPreviousArcs.push( 84 | Graphics.Disk({ 85 | center: [0, 0], 86 | radius: 1, 87 | fill: false, 88 | edge: true, 89 | startAngle: i % 2 == 0 ? -0.5 * Math.PI : -0.5 * Math.PI - angleI, 90 | endAngle: i % 2 == 0 ? -0.5 * Math.PI + angleI : -0.5 * Math.PI, 91 | sector: false, 92 | }), 93 | ); 94 | if (i % 2 == 0) { 95 | transformsAndPreviousArcs.push( 96 | Graphics.Translate({ offset: [2 * Math.sin(angleI), 2 * Math.cos(angleI)] }), 97 | ); 98 | transformsAndPreviousArcs.push(Graphics.Rotate({ center: [0, 0], angle: angleI - Math.PI })); 99 | } else { 100 | transformsAndPreviousArcs.push( 101 | Graphics.Translate({ offset: [2 * Math.sin(-angleI), 2 * Math.cos(-angleI)] }), 102 | ); 103 | transformsAndPreviousArcs.push(Graphics.Rotate({ center: [0, 0], angle: Math.PI - angleI })); 104 | } 105 | } 106 | 107 | let piStrings = ['3.']; 108 | for (let i = 1; i <= index; i++) { 109 | piStrings[piStrings.length - 1] += Utils.piDigits[i]; 110 | if (i % 54 == 0) { 111 | piStrings.push(' '); 112 | } 113 | } 114 | 115 | Graphics.draw( 116 | [ 117 | Graphics.Set({ 118 | font: '28px Courier', 119 | fillStyle: '#a3a3ae', 120 | textAlign: 'left', 121 | textBaseline: 'top', 122 | }), 123 | piStrings.map((str, i) => Graphics.Text({ at: [24, 230 - i * 28], text: str })), 124 | ], 125 | { xmin: 0, ymin: 0, xmax: canvasWidth, ymax: canvasHeight }, 126 | ctx, 127 | ); 128 | 129 | Graphics.draw( 130 | [ 131 | [ 132 | // Move it using translates and rotates. 133 | ...arcStyles, 134 | ...transformsAndPreviousArcs, 135 | Graphics.Set({ lineWidth: 0.02 }), 136 | 137 | [ 138 | // Filled in arc 139 | Graphics.Set({ 140 | fillStyle: chroma('#178585') 141 | .alpha(1 - next) 142 | .css(), 143 | }), 144 | Graphics.Disk({ 145 | center: [0, 0], 146 | radius: 1, 147 | fill: true, 148 | edge: false, 149 | startAngle: even ? -0.5 * Math.PI : -0.5 * Math.PI - angle * arc, 150 | endAngle: even ? -0.5 * Math.PI + angle * arc : -0.5 * Math.PI, 151 | sector: true, 152 | }), 153 | ], 154 | [ 155 | // The 'clock' 156 | even 157 | ? Graphics.Translate({ 158 | offset: [next * 2 * Math.sin(angle), next * 2 * Math.cos(angle)], 159 | }) 160 | : Graphics.Translate({ 161 | offset: [next * 2 * Math.sin(-angle), next * 2 * Math.cos(-angle)], 162 | }), 163 | even 164 | ? Graphics.Rotate({ 165 | center: [0, 0], 166 | angle: next > 0.5 ? angle - Math.PI : 0, 167 | }) 168 | : Graphics.Rotate({ 169 | center: [0, 0], 170 | angle: next > 0.5 ? Math.PI - angle : 0, 171 | }), 172 | Graphics.Set({ 173 | strokeStyle: 'white', 174 | fillStyle: 'white', 175 | font: '0.2px serif', 176 | textAlign: 'center', 177 | textBaseline: 'middle', 178 | }), 179 | 180 | Utils.range(-0.5 * Math.PI, 1.5 * Math.PI, pyByFive).map((th, i) => [ 181 | Graphics.Disk({ 182 | center: [0, 0], 183 | radius: 1, 184 | fill: false, 185 | edge: true, 186 | startAngle: th, 187 | endAngle: th + pyByFive, 188 | sector: true, 189 | }), 190 | [ 191 | [ 192 | Graphics.Set({ 193 | fillStyle: chroma('white').alpha(oddOpacity).css(), 194 | }), 195 | Graphics.Text({ 196 | at: [0.7 * Math.cos(th + pyByFive / 2), -0.7 * Math.sin(th + pyByFive / 2)], 197 | text: `${(i + 1) % 10}`, 198 | }), 199 | ], 200 | [ 201 | Graphics.Set({ 202 | fillStyle: chroma('white').alpha(evenOpacity).css(), 203 | }), 204 | Graphics.Text({ 205 | at: [0.7 * Math.cos(th + pyByFive / 2), -0.7 * Math.sin(th + pyByFive / 2)], 206 | text: `${(10 - i) % 10}`, 207 | }), 208 | ], 209 | ], 210 | ]), 211 | ], 212 | [ 213 | // New arc 214 | ...arcStyles, 215 | Graphics.Disk({ 216 | center: [0, 0], 217 | radius: 1, 218 | fill: false, 219 | edge: true, 220 | startAngle: even ? -0.5 * Math.PI : -0.5 * Math.PI - angle * arc, 221 | endAngle: even ? -0.5 * Math.PI + angle * arc : -0.5 * Math.PI, 222 | sector: false, 223 | }), 224 | ], 225 | ], 226 | ], 227 | { 228 | xmin: centreX - zoom, 229 | xmax: centreX + zoom, 230 | ymin: centreY - zoom, 231 | ymax: centreY + zoom, 232 | }, 233 | ctx, 234 | ); 235 | }; 236 | 237 | return drawFn; 238 | }; 239 | 240 | return ( 241 | 249 | ); 250 | }; 251 | 252 | export default PiArcs; 253 | -------------------------------------------------------------------------------- /src/animations/shaders/anaglyph.glsl: -------------------------------------------------------------------------------- 1 | uniform vec2 u_resolution; 2 | uniform float u_redness; 3 | uniform sampler2D u_tex0; 4 | 5 | mat2 rotationMatrix(float angle) { 6 | float sine = sin(angle), cosine = cos(angle); 7 | return mat2(cosine, -sine, sine, cosine); 8 | } 9 | 10 | vec3 hash32(vec2 p) { 11 | vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973)); 12 | p3 += dot(p3, p3.yxz + 33.33); 13 | return fract((p3.xxy + p3.yzz) * p3.zyx); 14 | } 15 | 16 | float hash12(vec2 p) { 17 | vec3 p3 = fract(vec3(p.xyx) * .1031); 18 | p3 += dot(p3, p3.yzx + 33.33); 19 | return fract((p3.x + p3.y) * p3.z); 20 | } 21 | 22 | float dist(vec2 uv) { 23 | uv -= 0.5; 24 | uv *= rotationMatrix(0.3); 25 | uv *= 2.; 26 | uv += vec2(0.2, 0.3); 27 | uv.x = max(uv.x, -uv.x); 28 | vec2 p = uv * 20.0; 29 | float r = length(p); 30 | float t = atan(p.y, p.x); 31 | float butterfly = 7. - 0.5 * sin(1. * t) + 2.5 * sin(3. * t) + 2.0 * sin(5. * t) - 1.7 * sin(7. * t) + 3.0 * cos(2. * t) - 2.0 * cos(4. * t) - 0.4 * cos(16. * t) - r; 32 | return butterfly; 33 | } 34 | 35 | void main() { 36 | vec2 pixel = 1.0 / u_resolution.xy; 37 | 38 | vec2 uv = gl_FragCoord.xy * pixel; 39 | 40 | float res = 100.0; 41 | 42 | // butterfly text texture is 42 by 7 43 | int y = int(floor(uv.y * res)) % 7; 44 | float rowIndex = floor(uv.y * res / 7.0); 45 | int x = int(floor(uv.x * res) + 20. * sin(rowIndex * 2.) + 100.0) % 42; 46 | 47 | float butterflyText = texelFetch(u_tex0, ivec2(x, y), 0).r; 48 | 49 | vec2 uvI = floor(uv * res) / res; 50 | 51 | float message = 0.; 52 | 53 | // Add the text 54 | message = mix(message, 0.9, smoothstep(0., pixel.x, 1. - butterflyText)); 55 | 56 | // Add outside of butterfly 57 | message = mix(message, 0.7, smoothstep(0., pixel.x, dist(uvI))); 58 | 59 | // Subtract inside of butterfly 60 | message = mix(0.1, message, smoothstep(0., pixel.x, 2. - dist(uvI))); 61 | 62 | // Add noise 63 | message = mix(message, 0.8, smoothstep(0.0, 1.5, hash12(uvI * u_resolution.xy))); 64 | 65 | message = 1. - 0.6 * message; 66 | 67 | // Start as noise 68 | vec3 color = hash32(uvI * u_resolution.xy); 69 | // combine random color with the message. 70 | color.r = mix(clamp(color.r, 0.7, 1.0), 0.2 * color.r, message); 71 | color = mix(color, vec3(1.0, 0, 0) * color, u_redness); 72 | color.g *= 0.5; // Reduce green. 73 | 74 | // color = vec3(message); 75 | gl_FragColor = vec4(color, 1.0); 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/animations/shaders/dither.glsl: -------------------------------------------------------------------------------- 1 | uniform vec2 u_resolution; 2 | uniform sampler2D u_tex0; 3 | uniform float u_tt; 4 | 5 | float hash13(vec2 uv, float t) { 6 | vec3 p3 = vec3(uv.x, uv.y, t); 7 | p3 = fract(p3 * .1031); 8 | p3 += dot(p3, p3.zyx + 31.32); 9 | return fract((p3.x + p3.y) * p3.z); 10 | } 11 | 12 | void main() { 13 | vec2 pixel = 1.0 / u_resolution.xy; 14 | vec2 uv = gl_FragCoord.xy * pixel; 15 | float res = 256.0; 16 | uv = floor(uv * res) / res; 17 | 18 | uv *= 2.0; 19 | 20 | // shift the top 21 | uv.x -= 0.5 * floor(uv.y); 22 | 23 | float color = texture(u_tex0, uv).r; 24 | 25 | // add noise on right 26 | float tres = 0.01; 27 | float t = floor(u_tt / tres) * tres; 28 | float noise = hash13(vec2(uv.x, mod(uv.y, 1.0)) * u_resolution.xy, t); 29 | color = mix(color, color + (noise - 0.5), step(1., uv.x)); 30 | // binarize on bottom 31 | color = mix(color, step(0.5, color), 1. - step(1., uv.y)); 32 | 33 | // mask out on the top 34 | color = mix(color, 0., step(1.0, uv.y) * step(1.0, uv.x)); 35 | color = mix(color, 0., step(1.0, uv.y) * step(uv.x, 0.)); 36 | 37 | gl_FragColor = vec4(vec3(color), 1.0); 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/animations/shaders/minimalShader.glsl: -------------------------------------------------------------------------------- 1 | uniform vec2 u_resolution; 2 | uniform float u_t; 3 | uniform float u_speed; 4 | 5 | void main() { 6 | vec2 pixel = 1.0 / u_resolution.xy; 7 | vec2 uv = gl_FragCoord.xy * pixel; 8 | float t = u_t * u_speed; 9 | vec3 color = 0.5 + 0.5 * cos(t + uv.xyx + vec3(0, 2, 4)); 10 | gl_FragColor = vec4(color, 1.0); 11 | } 12 | -------------------------------------------------------------------------------- /src/animations/shaders/noiseDodecahedron.glsl: -------------------------------------------------------------------------------- 1 | uniform vec2 u_resolution; 2 | uniform float u_t; 3 | uniform float u_theta_1; 4 | uniform float u_theta_2; 5 | uniform float u_update; 6 | uniform sampler2D u_doubleBuffer0; 7 | 8 | // Tells GlslPipeline that we are using the double buffer: 9 | #ifdef DOUBLE_BUFFER_0 10 | #endif 11 | 12 | #define res 128.0 13 | #define rate 2 14 | #define TWOPI 6.28318530718 15 | 16 | vec3 vertices[20] = vec3[](vec3(0, 0, 1), vec3(.58, .33, .75), vec3(0, -.67, .75), vec3(-.58, .33, .75), vec3(.36, .87, .33), vec3(.93, -.13, .33), vec3(.58, -.75, .33), vec3(-.58, -.75, .33), vec3(-.93, -.13, .33), vec3(-.36, .87, .33), vec3(.58, .75, -.33), vec3(.93, .13, -.33), vec3(.36, -.87, -.33), vec3(-.36, -.87, -.33), vec3(-.93, .13, -.33), vec3(-.58, .75, -.33), vec3(0, .67, -.75), vec3(.58, -.33, -.75), vec3(-.58, -.33, -.75), vec3(0, 0, -1)); 17 | 18 | int seg[] = int[](0, 2, 6, 5, 1 // 3 bottom 19 | , 0, 3, 8, 7, 2, 0, 1, 4, 9, 3, 2, 7, 13, 12, 6 // 6 crown 20 | , 8, 14, 18, 13, 7, 6, 12, 17, 11, 5, 3, 9, 15, 14, 8, 1, 5, 11, 10, 4, 4, 10, 16, 15, 9, 19, 18, 14, 15, 16 // 3 top 21 | , 19, 17, 12, 13, 18, 19, 16, 10, 11, 17); 22 | 23 | float hash12(vec2 p) { 24 | vec3 p3 = fract(vec3(p.xyx) * .1031); 25 | p3 += dot(p3, p3.yzx + 33.33); 26 | return fract((p3.x + p3.y) * p3.z); 27 | } 28 | 29 | float sdSegment(in vec2 p, in vec2 a, in vec2 b) { 30 | vec2 pa = p - a, ba = b - a; 31 | float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); 32 | return length(pa - ba * h); 33 | } 34 | 35 | #define rot(a) mat2(cos(a+vec4(0,33,11,0))) 36 | 37 | vec3 T(vec3 p) { 38 | p.yz *= rot(TWOPI * u_theta_1 + 0.21); 39 | p.zx *= rot(TWOPI * u_theta_2 + 0.77); 40 | return p; 41 | } 42 | 43 | void main() { 44 | // out vec4 fragColor, in vec2 fragCoord 45 | 46 | vec2 uv = gl_FragCoord.xy / u_resolution.xy; 47 | vec2 uvI = floor(uv * res) / res; 48 | 49 | vec3 col = texture(u_doubleBuffer0, uv).rgb; 50 | 51 | if (u_t < 0.1) { 52 | col = vec3(step(0.5, hash12(u_resolution.xy * uvI))); 53 | } 54 | 55 | vec3 _P, P, P0; 56 | float dodecCol = 0.; 57 | uvI -= 0.5; 58 | uvI *= 2.; 59 | for (int i; i <= seg.length(); i++) { 60 | _P = P; 61 | P = T(vertices[seg[i % 60]]); 62 | // P *= exp(iFloat2); 63 | P /= P.z - 1.7; 64 | if (i > 0) { 65 | dodecCol += .5 * smoothstep(0.01, 0., sdSegment(uvI, _P.xy, (i % 5 > 0 ? P : P0).xy) - 0.0001); 66 | } 67 | if (i % 5 < 1) { 68 | P0 = P; 69 | } 70 | } 71 | dodecCol = step(0.5, dodecCol); 72 | 73 | col = mix(col, 1. - col, dodecCol * u_update); 74 | 75 | gl_FragColor = vec4(step(0.5, col), 1.0); 76 | } 77 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.glsl' {} 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | color-scheme: dark; 7 | } 8 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import Minimal2D from 'animations/minimal2D'; 2 | import MinimalShader from 'animations/minimalShader'; 3 | import NoiseDodecahedron from 'animations/noiseDodecahedron'; 4 | import PiArcs from 'animations/piArcs'; 5 | import React, { ReactNode } from 'react'; 6 | import ReactDOM from 'react-dom/client'; 7 | import { FaArrowLeft } from 'react-icons/fa'; 8 | import { Link, RouterProvider, createHashRouter } from 'react-router-dom'; 9 | 10 | import Hypocycloids from './animations/hypocycloids'; 11 | import './index.css'; 12 | import './index.css'; 13 | import Anaglyph from 'animations/anaglyph'; 14 | import Dither from 'animations/dither'; 15 | 16 | const animations = [ 17 | { 18 | name: 'hypocycloids', 19 | component: Hypocycloids, 20 | }, 21 | { 22 | name: 'piArcs', 23 | component: PiArcs, 24 | }, 25 | { 26 | name: 'noiseDodecahedron', 27 | component: NoiseDodecahedron, 28 | }, 29 | { 30 | name: 'anaglyph', 31 | component: Anaglyph, 32 | }, 33 | { 34 | name: 'dither', 35 | component: Dither, 36 | }, 37 | { 38 | name: 'minimal2d', 39 | component: Minimal2D, 40 | }, 41 | { 42 | name: 'minimalShader', 43 | component: MinimalShader, 44 | }, 45 | ]; 46 | 47 | const AnimationList = () => { 48 | return ( 49 |
50 |

Animations

51 |
    52 | {animations.map((animation) => ( 53 |
  • 54 | 55 | {animation.name} 56 | 57 |
  • 58 | ))} 59 |
60 |
61 | ); 62 | }; 63 | 64 | const ViewAnimation = ({ children }: { children: ReactNode }) => { 65 | return ( 66 |
67 |

68 | 69 | all animations 70 | 71 |

72 | {children} 73 |
74 | ); 75 | }; 76 | 77 | const router = createHashRouter([ 78 | { 79 | path: '/', 80 | element: , 81 | }, 82 | ...animations.map((animation) => { 83 | return { 84 | path: `/${animation.name}`, 85 | element: ( 86 | 87 | 88 | 89 | ), 90 | }; 91 | }), 92 | ]); 93 | 94 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 95 | root.render( 96 |
97 | 98 | 99 | 100 |
, 101 | ); 102 | -------------------------------------------------------------------------------- /src/lib/Animation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react'; 3 | import { IconContext } from 'react-icons'; 4 | import { FaLock, FaLockOpen, FaPause, FaPlay, FaStepBackward } from 'react-icons/fa'; 5 | import { useDebouncedCallback } from 'use-debounce'; 6 | 7 | export type DrawArgs = Record & { t: number }; 8 | export type DrawFn = (args: DrawArgs) => void; 9 | export type MakeDrawFn = (canvas: HTMLCanvasElement) => DrawFn; 10 | export interface Parameter { 11 | name: string; 12 | compute?: (t: number) => number; 13 | defaultValue?: number; 14 | minValue: number; 15 | maxValue: number; 16 | step?: number; 17 | } 18 | 19 | interface AnimationOptions { 20 | duration: number; 21 | initialCanvasWidth: number; 22 | initialCanvasHeight: number; 23 | pixelRatio?: number; 24 | makeDrawFn: MakeDrawFn; 25 | parameters: Parameter[]; 26 | enableTimeControl?: boolean; 27 | } 28 | 29 | interface CanvasDims { 30 | width: number; 31 | height: number; 32 | arLocked: boolean; // Whether the aspect ratio is locked. 33 | } 34 | 35 | export const Animation = (props: AnimationOptions) => { 36 | const enableTimeControl = props.enableTimeControl === undefined ? true : props.enableTimeControl; 37 | const [canvasDims, setCanvasDims] = useState({ 38 | width: props.initialCanvasWidth, 39 | height: props.initialCanvasHeight, 40 | arLocked: true, 41 | }); 42 | const [drawFn, setDrawFn] = useState(null); 43 | const [controlMode, setControlMode] = useState('user' as 'playing' | 'user' | 'recording'); 44 | const computeParamValues = (t: number): Record => 45 | Object.fromEntries( 46 | props.parameters 47 | .filter((param) => param.compute !== undefined) 48 | .map((param) => [param.name, param.compute!(t)]), 49 | ); 50 | const initialDrawArgs: DrawArgs = { 51 | t: 0, 52 | ...computeParamValues(0), 53 | ...Object.fromEntries( 54 | props.parameters 55 | .filter((param) => param.compute === undefined) 56 | .map((param) => [param.name, param.defaultValue !== undefined ? param.defaultValue : param.minValue]), 57 | ), 58 | }; 59 | const drawArgs = useRef(initialDrawArgs); 60 | const lastDrawArgs = useRef(null); 61 | const requestAnimationRef = useRef(0); 62 | const prevWindowTimeRef = useRef(null); 63 | const frameTimes = useRef([]); // Record how long frames are taking to draw for fps computations. 64 | const [fps, setFps] = useState(0.0); 65 | const [drawArgsUI, setDrawArgsUI] = useState(initialDrawArgs); 66 | const canvasElement = useRef(); 67 | const CCaptureObj = useRef(); 68 | 69 | const setupCanvas = useCallback((canvas: HTMLCanvasElement | null) => { 70 | if (!canvas) { 71 | return; 72 | } 73 | canvasElement.current = canvas; 74 | setDrawFn(() => props.makeDrawFn(canvas)); 75 | }, []); 76 | 77 | const updateUI = useCallback(() => { 78 | const sumFrameTime = frameTimes.current.reduce((acc, time) => acc + time, 0); 79 | if (sumFrameTime < 30) { 80 | return; 81 | } 82 | const averageFrameTime = sumFrameTime / frameTimes.current.length; 83 | const fps = 1000 / averageFrameTime; 84 | while (frameTimes.current.length > 0) { 85 | frameTimes.current.pop(); 86 | } 87 | setDrawArgsUI({ ...drawArgs.current }); 88 | setFps(fps); 89 | }, []); 90 | 91 | useEffect(() => { 92 | if (controlMode == 'user') { 93 | drawArgs.current = { ...drawArgsUI }; 94 | } 95 | }, [drawArgsUI, controlMode]); 96 | 97 | useEffect(() => { 98 | if (!drawFn) { 99 | return; 100 | } 101 | 102 | if (controlMode == 'recording' && canvasElement.current) { 103 | // @ts-ignore 104 | CCaptureObj.current = new CCapture({ format: 'webm', framerate: 60, name: 'export' }); 105 | CCaptureObj.current.start(); 106 | drawArgs.current.t = 0.0; 107 | prevWindowTimeRef.current = null; 108 | } 109 | 110 | const animationFrame = (windowTime: number) => { 111 | if (controlMode == 'playing' || controlMode == 'recording') { 112 | if (prevWindowTimeRef.current) { 113 | const deltaTime = windowTime - prevWindowTimeRef.current; 114 | frameTimes.current.push(deltaTime); 115 | updateUI(); 116 | drawArgs.current.t += deltaTime / 1000; 117 | if (drawArgs.current.t > props.duration) { 118 | if (controlMode == 'recording' && CCaptureObj.current) { 119 | CCaptureObj.current.stop(); 120 | CCaptureObj.current.save(); 121 | CCaptureObj.current = null; 122 | setControlMode('user'); 123 | } 124 | drawArgs.current.t = 0; 125 | } 126 | } 127 | const t = drawArgs.current.t; 128 | drawArgs.current = { 129 | ...drawArgs.current, 130 | t, 131 | ...computeParamValues(t), 132 | }; 133 | } 134 | prevWindowTimeRef.current = windowTime; 135 | if ( 136 | controlMode == 'recording' || 137 | lastDrawArgs.current === null || 138 | !areDrawArgsEqual(lastDrawArgs.current, drawArgs.current) 139 | ) { 140 | drawFn(drawArgs.current); 141 | 142 | if (controlMode == 'recording' && CCaptureObj.current) { 143 | CCaptureObj.current.capture(canvasElement.current); 144 | } 145 | } 146 | lastDrawArgs.current = { ...drawArgs.current }; 147 | requestAnimationRef.current = requestAnimationFrame(animationFrame); 148 | }; 149 | 150 | prevWindowTimeRef.current = null; 151 | animationFrame(0); 152 | 153 | return () => { 154 | cancelAnimationFrame(requestAnimationRef.current); 155 | }; 156 | }, [drawFn, controlMode]); 157 | 158 | const onClickPlayPause = useCallback(() => { 159 | if (controlMode == 'playing') { 160 | setControlMode('user'); 161 | } else { 162 | setControlMode('playing'); 163 | } 164 | }, [controlMode]); 165 | 166 | const onClickReset = useCallback(() => { 167 | if (controlMode == 'playing') { 168 | setControlMode('user'); 169 | } 170 | setDrawArgsUI((old) => ({ ...old, t: 0.0, ...computeParamValues(0) })); 171 | }, [controlMode]); 172 | 173 | const onClickRecord = useCallback(() => { 174 | setControlMode('recording'); 175 | }, []); 176 | 177 | const onClickCancelRecord = useCallback(() => { 178 | drawArgs.current.t = props.duration; 179 | }, []); 180 | 181 | const resizeCanvas = useDebouncedCallback(() => { 182 | if (canvasElement.current === undefined) { 183 | return; 184 | } 185 | if (canvasElement.current.width == canvasDims.width && canvasElement.current.height == canvasDims.height) { 186 | return; 187 | } 188 | canvasElement.current.width = canvasDims.width; 189 | canvasElement.current.height = canvasDims.height; 190 | lastDrawArgs.current = null; 191 | setDrawFn(() => props.makeDrawFn(canvasElement.current!)); 192 | }, 500); 193 | 194 | useEffect(() => { 195 | resizeCanvas(); 196 | }, [canvasDims]); 197 | 198 | const pixelRatio = props.pixelRatio || window.devicePixelRatio; 199 | 200 | const setParam = (value: number, param: Parameter): void => { 201 | if (param.compute || controlMode == 'user') { 202 | setDrawArgsUI((old) => { 203 | return { 204 | ...old, 205 | ...Object.fromEntries([[param.name, value]]), 206 | }; 207 | }); 208 | } else { 209 | drawArgs.current[param.name] = value; 210 | } 211 | }; 212 | 213 | const timeParameter: Parameter = { 214 | name: 't', 215 | compute: (t) => t, 216 | defaultValue: 0.0, 217 | minValue: 0.0, 218 | maxValue: props.duration, 219 | step: 0.01, 220 | }; 221 | 222 | return ( 223 | 224 |
225 | {/* canvas */} 226 |
227 |
228 |
229 | 239 |
240 |
241 | {/* controls */} 242 |
243 |
244 |
245 | 246 |
247 |
248 | {controlMode != 'recording' ? ( 249 | 255 | ) : ( 256 | 262 | )} 263 |

264 | {fps.toFixed(1)} fps 265 |

266 |
267 |
268 | 272 | setDrawArgsUI((old) => { 273 | return { 274 | ...old, 275 | t: value, 276 | ...computeParamValues(value), 277 | }; 278 | }) 279 | } 280 | disabled={controlMode != 'user' || !enableTimeControl} 281 | > 282 |
283 | 290 | 297 |
298 |
299 | {props.parameters.map((param) => ( 300 | setParam(value, param)} 305 | key={param.name} 306 | > 307 |
308 |

{param.name}

309 |
310 |
311 | ))} 312 |
313 |
314 |
315 |
316 | ); 317 | }; 318 | 319 | const CanvasDimControls = ({ 320 | canvasDims, 321 | setCanvasDims, 322 | }: { 323 | canvasDims: CanvasDims; 324 | setCanvasDims: React.Dispatch>; 325 | }) => { 326 | const updateCanvasHeight = useCallback( 327 | (value: string) => { 328 | const newHeight = Number(value); 329 | let newWidth = canvasDims.width; 330 | if (canvasDims.arLocked) { 331 | const ar = canvasDims.width / canvasDims.height; 332 | newWidth = Math.round(newHeight * ar); 333 | } 334 | setCanvasDims((old) => ({ 335 | ...old, 336 | width: newWidth, 337 | height: newHeight, 338 | })); 339 | }, 340 | [canvasDims], 341 | ); 342 | 343 | const updateCanvasWidth = useCallback( 344 | (value: string) => { 345 | const newWidth = Number(value); 346 | let newHeight = canvasDims.height; 347 | if (canvasDims.arLocked) { 348 | const arInv = canvasDims.height / canvasDims.width; 349 | newHeight = Math.round(newWidth * arInv); 350 | } 351 | setCanvasDims((old) => ({ 352 | ...old, 353 | width: newWidth, 354 | height: newHeight, 355 | })); 356 | }, 357 | [canvasDims], 358 | ); 359 | 360 | const toggleArLocked = useCallback(() => { 361 | setCanvasDims((old) => ({ 362 | ...old, 363 | arLocked: !canvasDims.arLocked, 364 | })); 365 | }, [canvasDims]); 366 | 367 | return ( 368 | 369 | updateCanvasWidth(e.target.value)} 375 | /> 376 |

×

377 | updateCanvasHeight(e.target.value)} 383 | /> 384 | 387 |
388 | ); 389 | }; 390 | 391 | const ParamController = ({ 392 | children, 393 | param, 394 | value, 395 | disabled, 396 | onChange, 397 | }: { 398 | children: React.ReactNode; 399 | param: Parameter; 400 | value: number; 401 | disabled: boolean; 402 | onChange: (value: number) => void; 403 | }) => { 404 | return ( 405 | 406 | {children} 407 |
408 | onChange(Number(e.target.value))} 417 | /> 418 |
419 |
420 | onChange(Number(e.target.value))} 429 | /> 430 |
431 |
432 | ); 433 | }; 434 | 435 | const areDrawArgsEqual = (args1: DrawArgs, args2: DrawArgs): boolean => { 436 | const keys = Object.keys(args1); 437 | for (const key of keys) { 438 | if (Math.abs(args1[key] - args2[key]) > 1e-5) { 439 | return false; 440 | } 441 | } 442 | return true; 443 | }; 444 | -------------------------------------------------------------------------------- /src/lib/graphics.ts: -------------------------------------------------------------------------------- 1 | namespace Graphics { 2 | export type DrawCommand = (ctx: CanvasRenderingContext2D) => void; 3 | 4 | type CanvasState = { 5 | strokeStyle: string; 6 | fillStyle: string; 7 | globalAlpha: number; 8 | lineWidth: number; 9 | lineCap: 'butt' | 'round' | 'square'; 10 | lineJoin: 'round' | 'bevel' | 'miter'; 11 | miterLimit: number; 12 | lineDashOffset: number; 13 | shadowOffsetX: number; 14 | shadowOffsetY: number; 15 | shadowBlur: number; 16 | shadowColor: string; 17 | font: string; 18 | textAlign: 'left' | 'right' | 'center' | 'start' | 'end'; 19 | textBaseline: 'top' | 'hanging' | 'middle' | 'alphabetic' | 'ideographic' | 'bottom'; 20 | direction: 'ltr' | 'rtl' | 'inherit'; 21 | imageSmoothingEnabled: boolean; 22 | }; 23 | 24 | export const Set = (values: Partial): DrawCommand => { 25 | return (ctx) => { 26 | for (const key in values) { 27 | if (values.hasOwnProperty(key)) { 28 | // Check if the property exists in CanvasState before setting 29 | if (key in ctx) { 30 | // @ts-ignore 31 | ctx[key] = values[key]; 32 | } 33 | } 34 | } 35 | }; 36 | }; 37 | 38 | export const AbsoluteLineWidth = (pixels: number): DrawCommand => { 39 | return (ctx) => { 40 | const transform = ctx.getTransform(); 41 | ctx.lineWidth = pixels / Math.sqrt(transform.a * transform.a + transform.b * transform.b); 42 | }; 43 | }; 44 | 45 | export const Disk = ({ 46 | center, 47 | radius, 48 | fill, 49 | edge, 50 | startAngle = 0, 51 | endAngle = 2 * Math.PI, 52 | sector = false, 53 | radiusInPixels = false, 54 | }: { 55 | center: number[]; 56 | radius: number; 57 | fill: boolean; 58 | edge: boolean; 59 | startAngle?: number; 60 | endAngle?: number; 61 | sector?: boolean; 62 | radiusInPixels?: boolean; 63 | }): DrawCommand => { 64 | return (ctx) => { 65 | if (radiusInPixels) { 66 | const transform = ctx.getTransform(); 67 | radius = radius / Math.sqrt(transform.a * transform.a + transform.b * transform.b); 68 | } 69 | if (!fill && !edge) { 70 | return; 71 | } 72 | const oldFillStyle = ctx.fillStyle; 73 | if (!fill) { 74 | ctx.fillStyle = 'rgba(0, 0, 0, 0)'; 75 | } 76 | ctx.beginPath(); 77 | if (sector) { 78 | ctx.moveTo(center[0], center[1]); 79 | } 80 | ctx.arc(center[0], -center[1], radius, startAngle, endAngle); 81 | if (sector) { 82 | ctx.moveTo(center[0], center[1]); 83 | } 84 | ctx.fill(); 85 | if (edge) { 86 | ctx.stroke(); 87 | } 88 | ctx.fillStyle = oldFillStyle; 89 | }; 90 | }; 91 | 92 | export const Polygon = ({ pts, edge, fill }: { pts: number[][]; edge: boolean; fill: boolean }): DrawCommand => { 93 | return (ctx) => { 94 | if (!fill && !edge) { 95 | return; 96 | } 97 | const oldFillStyle = ctx.fillStyle; 98 | if (!fill) { 99 | ctx.fillStyle = 'rgba(0, 0, 0, 0)'; 100 | } 101 | ctx.beginPath(); 102 | const [x0, y0] = pts[0]; 103 | ctx.moveTo(x0, -y0); 104 | pts.forEach(([x, y]) => ctx.lineTo(x, -y)); 105 | ctx.lineTo(x0, -y0); 106 | ctx.fill(); 107 | if (edge) { 108 | ctx.stroke(); 109 | } 110 | ctx.fillStyle = oldFillStyle; 111 | }; 112 | }; 113 | 114 | export const Line = ({ pts }: { pts: number[][] }): DrawCommand => { 115 | return (ctx) => { 116 | if (pts.length == 0) { 117 | return; 118 | } 119 | ctx.beginPath(); 120 | const [x0, y0] = pts[0]; 121 | ctx.moveTo(x0, -y0); 122 | pts.forEach(([x, y]) => ctx.lineTo(x, -y)); 123 | ctx.stroke(); 124 | }; 125 | }; 126 | 127 | export const Text = ({ at, text }: { at: number[]; text: string }): DrawCommand => { 128 | return (ctx) => { 129 | const [x, y] = at; 130 | ctx.fillText(text, x, -y); 131 | }; 132 | }; 133 | 134 | export const Rotate = ({ angle, center }: { angle: number; center: number[] }): DrawCommand => { 135 | // Rotate clockwise by angle radians around center. 136 | return (ctx) => { 137 | const [cx, cy] = center; 138 | ctx.translate(cx, -cy); 139 | ctx.rotate(angle); 140 | ctx.translate(-cx, cy); 141 | }; 142 | }; 143 | 144 | export const Translate = ({ offset }: { offset: number[] }): DrawCommand => { 145 | return (ctx) => { 146 | const [x, y] = offset; 147 | ctx.translate(x, -y); 148 | }; 149 | }; 150 | 151 | type DrawCommands = DrawCommand | DrawCommands[]; 152 | 153 | type DrawOptions = { 154 | xmin: number; 155 | xmax: number; 156 | ymin: number; 157 | ymax: number; 158 | }; 159 | 160 | export const draw = ( 161 | commands: DrawCommands, 162 | options: DrawOptions, 163 | ctx: CanvasRenderingContext2D, 164 | depth?: number, 165 | ): void => { 166 | if (Array.isArray(commands)) { 167 | if (commands.length == 0) { 168 | return; 169 | } 170 | ctx.save(); 171 | // compute scale and translation 172 | if (depth === undefined) { 173 | ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2); 174 | ctx.scale( 175 | ctx.canvas.width / (options.xmax - options.xmin), 176 | ctx.canvas.height / (options.ymax - options.ymin), 177 | ); 178 | // Default line width to 1 pixel. 179 | ctx.translate(-(options.xmin + options.xmax) / 2, (options.ymin + options.ymax) / 2); 180 | AbsoluteLineWidth(1)(ctx); 181 | } 182 | // then make sure all commands use negative y 183 | 184 | commands.forEach((command) => draw(command, options, ctx, depth === undefined ? 1 : depth + 1)); 185 | ctx.restore(); 186 | } else { 187 | commands(ctx); 188 | } 189 | }; 190 | } 191 | 192 | export default Graphics; 193 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { Clock } from 'three'; 2 | 3 | namespace Utils { 4 | export const range = (start: number, end: number, step: number = 1) => { 5 | const result: number[] = []; 6 | if (step === 0) { 7 | throw new Error('Step cannot be zero'); 8 | } 9 | if (start < end && step < 0) { 10 | throw new Error('Step must be positive when start is less than end'); 11 | } 12 | if (start > end && step > 0) { 13 | throw new Error('Step must be negative when start is greater than end'); 14 | } 15 | 16 | for (let i = start; step > 0 ? i < end : i > end; i += step) { 17 | result.push(i); 18 | } 19 | 20 | return result; 21 | }; 22 | 23 | export const circlePoints = ({ 24 | num, 25 | center = [0, 0], 26 | radius = 1, 27 | offset = 0, 28 | }: { 29 | num: number; 30 | center?: number[]; 31 | radius?: number; 32 | offset?: number; 33 | }) => { 34 | const a = (2 * Math.PI) / num; 35 | return range(0, num).map((i) => [ 36 | center[0] + radius * Math.cos(offset + a * i), 37 | center[1] + radius * Math.sin(offset + a * i), 38 | ]); 39 | }; 40 | 41 | export const smoothstep = (t: number, startT: number = 0, endT: number = 1): number => { 42 | const tt = (t - startT) / (endT - startT); 43 | if (tt <= 0) { 44 | return 0; 45 | } 46 | if (tt >= 1) { 47 | return 1; 48 | } 49 | return 6 * Math.pow(tt, 5) - 15 * Math.pow(tt, 4) + 10 * Math.pow(tt, 3); 50 | }; 51 | 52 | export const smoothstepI = (t: number, startT: number = 0, endT: number = 1): number => { 53 | // Integral of smoothstep. 54 | if ((t - startT) * (startT - endT) > 0) { 55 | return 0.0; 56 | } 57 | if ((t - endT) * (endT - startT) > 0) { 58 | return 0.5 * (2 * (t - endT) + endT - startT); 59 | } 60 | return -( 61 | (Math.pow(t - startT, 4) * 62 | (2 * t * t + startT * startT + 2 * t * (startT - 3 * endT) - 4 * startT * endT + 5 * endT * endT)) / 63 | (2 * Math.pow(startT - endT, 5)) 64 | ); 65 | }; 66 | 67 | export interface Transition { 68 | easing: 'smoothstep' | 'linear' | 'step'; 69 | startT: number; 70 | endT: number; 71 | startValue?: number; // May be inferred from context. 72 | endValue: number; 73 | } 74 | 75 | const computeEasing = (easing: Transition['easing'], t: number, startT: number, endT: number): number => { 76 | if (easing == 'smoothstep') { 77 | return smoothstep(t, startT, endT); 78 | } else if (easing == 'linear') { 79 | return (t - startT) / (endT - startT); 80 | } else if (easing == 'step') { 81 | return t > (startT + endT) / 2 ? 1 : 0; 82 | } 83 | return 0.0; // unreachable 84 | }; 85 | 86 | export const makeTransitionFunction = (transitions: Transition[]): ((t: number) => number) => { 87 | if (transitions.length == 0) { 88 | return (_t) => 0; 89 | } 90 | transitions.sort((tr1, tr2) => tr1.startT - tr2.startT); 91 | 92 | // Populate the undefined start values. 93 | const initialValue = transitions[0].startValue || 0.0; 94 | const finalValue = transitions[transitions.length - 1].endValue; 95 | let value = initialValue; 96 | transitions.forEach((tr) => { 97 | tr.startValue ??= value; 98 | value = tr.endValue; 99 | }); 100 | 101 | return (t: number) => { 102 | let i = 0; 103 | while (i < transitions.length && transitions[i].endT < t) { 104 | // Could be a binary search but whatever. 105 | i += 1; 106 | } 107 | if (i == transitions.length) { 108 | return finalValue; 109 | } 110 | const tr = transitions[i]; 111 | if (tr.startT < t && t < tr.endT) { 112 | // compute transition 113 | const easing = computeEasing(tr.easing, t, tr.startT, tr.endT); 114 | return tr.startValue! * (1 - easing) + tr.endValue * easing; 115 | } else if (i == 0) { 116 | return initialValue; 117 | } else { 118 | return transitions[i - 1].endValue; 119 | } 120 | }; 121 | }; 122 | 123 | export const frac = (t: number): number => { 124 | // Fractional part of t. 125 | return t - Math.floor(t); 126 | }; 127 | 128 | export const piDigits = 129 | '3141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117067982148086513282306647093844609550582231725359408128481117450284102701938521105559644622948954930381964428810975665933446128475648233786783165271201909145648566923460348610454326648213393607260249141273724587006606315588174881520920962829254091715364367892590360011330530548820466521384146951941511609433057270365759591953092186117381932611793105118548074462379962749567351885752724891227938183011949129833673362440656643086021394946395224737190702179860943702770539217176293176752384674818467669405132000568127145263560827785771342757789609173637178721468440901224953430146549585371050792279689258923542019956112129021960864034418159813629774771309960518707211349999998372978049951059731732816096318595024459455346908302642522308253344685035261931188171010003137838752886587533208381420617177669147303598253490428755468731159562863882353787593751957781857780532171226806613001927876611195909216420198938095257201065485863278865936153381827968230301952035301852968995773622599413891249721775283479131515574857242454150695950829533116861727855889075098381754637464939319255060400927701671139009848824012858361603563707660104710181942955596198946767837449448255379774726847104047534646208046684259069491293313677028989152104752162056966024058038150193511253382430035587640247496473263914199272604269922796782354781636009341721641219924586315030286182974555706749838505494588586926995690927210797509302955321165344987202755960236480665499119881834797753566369807426542527862551818417574672890977772793800081647060016145249192173217214772350141441973568548161361157352552133475741849468438523323907394143334547762416862518983569485562099219222184272550254256887671790494601653466804988627232791786085784383827967976681454100953883786360950680064225125205117392984896084128488626945604241965285022210661186306744278622039194945047123713786960956364371917287467764657573962413890865832645995813390478027590099465764078951269468398352595709825822620522489407726719478268482601476990902640136394437455305068203496252451749399651431429809190659250937221696461515709858387410597885959772975498930161753928468138268683868942774155991855925245953959431049972524680845987273644695848653836736222626099124608051243884390451244136549762780797715691435997700129616089441694868555848406353422072225828488648158456028506016842739452267467678895252138522549954666727823986456596116354886230577456498035593634568174324112515076069479451096596094025228879710893145669136867228748940560101503308617928680920874760917824938589009714909675985261365549781893129784821682998948722658804857564014270477555132379641451523746234364542858444795265867821051141354735739523113427166102135969536231442952484937187110145765403590279934403742007310578539062198387447808478489683321445713868751943506430218453191048481005370614680674919278191197939952061419663428754440643745123718192179998391015919561814675142691239748940907186494231961567945208095146550225231603881930142093762137855956638937787083039069792077346722182562599661501421503068038447734549202605414665925201497442850732518666002132434088190710486331734649651453905796268561005508106658796998163574736384052571459102897064140110971206280439039759515677157700420337869936007230558763176359421873125147120532928191826186125867321579198414848829164470609575270695722091756711672291098169091528017350671274858322287183520935396572512108357915136988209144421006751033467110314126711136990865851639831501970165151168517143765761835155650884909989859982387345528331635507647918535893226185489632132933089857064204675259070915481416549859461637180270981994309924488957571282890592323326097299712084433573265489382391193259746366730583604142813883032038249037589852437441702913276561809377344403070746921120191302033038019762110110044929321516084244485963766983895228684783123552658213144957685726243344189303968642624341077322697802807318'; 130 | 131 | export const resetGlslPipeline = (pipeline: any): void => { 132 | // Reset pipeline. 133 | // Not needed once https://github.com/patriciogonzalezvivo/glsl-pipeline/pull/2 is deployed. 134 | pipeline.clock = new Clock(); 135 | pipeline.frame = 0; 136 | pipeline.lastTime = 0.0; 137 | pipeline.time = 0.0; 138 | pipeline.doubleBuffers.forEach((buffer: any) => { 139 | buffer.renderTargets.forEach((renderTarget: any) => { 140 | pipeline.renderer.setRenderTarget(renderTarget); 141 | pipeline.renderer.clear(); 142 | }); 143 | }); 144 | }; 145 | } 146 | 147 | export default Utils; 148 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | 8 | plugins: [ 9 | // https://draculatheme.com/tailwind 10 | require("tailwind-dracula")(), 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": "src" 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | --------------------------------------------------------------------------------