commit 21c0fca8b28d17033fdac4be0409ce86f16bba25 Author: Antonio De Lucreziis Date: Fri Jul 29 14:18:50 2022 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04c01ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..19c6b97 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# PHC Wires Art + +This is the second version of the _wire art_ for the landing page of the new PHC website. + +The first version stored wires as polylines so computing intersections when adding a new wire was _O(|number of segments| * |number of segments in whole canvas|)_ and it got pretty slow after not that much time. + +This version instead uses the fact that all segments lie on a grid and can only be of three kinds (going down, down left or down right) and stores them in a "2d integer hashmap" (in js this is just an object indexed by `,` for example `0,0` or `-15,7`). + +Another improvement is that the screen is redrawn only when considered `dirty` (the update functions sets this to true after modifying the world state). \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..e937b20 --- /dev/null +++ b/index.html @@ -0,0 +1,30 @@ + + + + + + + PHC • Wires Art + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..422bebc --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "art-phc-website-landing", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "sass": "^1.54.0", + "typescript": "^4.7.4", + "vite": "^3.0.3" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..53e9ec3 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,441 @@ +lockfileVersion: 5.4 + +specifiers: + sass: ^1.54.0 + typescript: ^4.7.4 + vite: ^3.0.3 + +devDependencies: + sass: 1.54.0 + typescript: 4.7.4 + vite: 3.0.3_sass@1.54.0 + +packages: + + /anymatch/3.1.2: + resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /binary-extensions/2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /braces/3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /chokidar/3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.2 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /esbuild-android-64/0.14.51: + resolution: {integrity: sha512-6FOuKTHnC86dtrKDmdSj2CkcKF8PnqkaIXqvgydqfJmqBazCPdw+relrMlhGjkvVdiiGV70rpdnyFmA65ekBCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-android-arm64/0.14.51: + resolution: {integrity: sha512-vBtp//5VVkZWmYYvHsqBRCMMi1MzKuMIn5XDScmnykMTu9+TD9v0NMEDqQxvtFToeYmojdo5UCV2vzMQWJcJ4A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64/0.14.51: + resolution: {integrity: sha512-YFmXPIOvuagDcwCejMRtCDjgPfnDu+bNeh5FU2Ryi68ADDVlWEpbtpAbrtf/lvFTWPexbgyKgzppNgsmLPr8PA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64/0.14.51: + resolution: {integrity: sha512-juYD0QnSKwAMfzwKdIF6YbueXzS6N7y4GXPDeDkApz/1RzlT42mvX9jgNmyOlWKN7YzQAYbcUEJmZJYQGdf2ow==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64/0.14.51: + resolution: {integrity: sha512-cLEI/aXjb6vo5O2Y8rvVSQ7smgLldwYY5xMxqh/dQGfWO+R1NJOFsiax3IS4Ng300SVp7Gz3czxT6d6qf2cw0g==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64/0.14.51: + resolution: {integrity: sha512-TcWVw/rCL2F+jUgRkgLa3qltd5gzKjIMGhkVybkjk6PJadYInPtgtUBp1/hG+mxyigaT7ib+od1Xb84b+L+1Mg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32/0.14.51: + resolution: {integrity: sha512-RFqpyC5ChyWrjx8Xj2K0EC1aN0A37H6OJfmUXIASEqJoHcntuV3j2Efr9RNmUhMfNE6yEj2VpYuDteZLGDMr0w==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64/0.14.51: + resolution: {integrity: sha512-dxjhrqo5i7Rq6DXwz5v+MEHVs9VNFItJmHBe1CxROWNf4miOGoQhqSG8StStbDkQ1Mtobg6ng+4fwByOhoQoeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm/0.14.51: + resolution: {integrity: sha512-LsJynDxYF6Neg7ZC7748yweCDD+N8ByCv22/7IAZglIEniEkqdF4HCaa49JNDLw1UQGlYuhOB8ZT/MmcSWzcWg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64/0.14.51: + resolution: {integrity: sha512-D9rFxGutoqQX3xJPxqd6o+kvYKeIbM0ifW2y0bgKk5HPgQQOo2k9/2Vpto3ybGYaFPCE5qTGtqQta9PoP6ZEzw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le/0.14.51: + resolution: {integrity: sha512-vS54wQjy4IinLSlb5EIlLoln8buh1yDgliP4CuEHumrPk4PvvP4kTRIG4SzMXm6t19N0rIfT4bNdAxzJLg2k6A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le/0.14.51: + resolution: {integrity: sha512-xcdd62Y3VfGoyphNP/aIV9LP+RzFw5M5Z7ja+zdpQHHvokJM7d0rlDRMN+iSSwvUymQkqZO+G/xjb4/75du8BQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-riscv64/0.14.51: + resolution: {integrity: sha512-syXHGak9wkAnFz0gMmRBoy44JV0rp4kVCEA36P5MCeZcxFq8+fllBC2t6sKI23w3qd8Vwo9pTADCgjTSf3L3rA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-s390x/0.14.51: + resolution: {integrity: sha512-kFAJY3dv+Wq8o28K/C7xkZk/X34rgTwhknSsElIqoEo8armCOjMJ6NsMxm48KaWY2h2RUYGtQmr+RGuUPKBhyw==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64/0.14.51: + resolution: {integrity: sha512-ZZBI7qrR1FevdPBVHz/1GSk1x5GDL/iy42Zy8+neEm/HA7ma+hH/bwPEjeHXKWUDvM36CZpSL/fn1/y9/Hb+1A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64/0.14.51: + resolution: {integrity: sha512-7R1/p39M+LSVQVgDVlcY1KKm6kFKjERSX1lipMG51NPcspJD1tmiZSmmBXoY5jhHIu6JL1QkFDTx94gMYK6vfA==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64/0.14.51: + resolution: {integrity: sha512-HoHaCswHxLEYN8eBTtyO0bFEWvA3Kdb++hSQ/lLG7TyKF69TeSG0RNoBRAs45x/oCeWaTDntEZlYwAfQlhEtJA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32/0.14.51: + resolution: {integrity: sha512-4rtwSAM35A07CBt1/X8RWieDj3ZUHQqUOaEo5ZBs69rt5WAFjP4aqCIobdqOy4FdhYw1yF8Z0xFBTyc9lgPtEg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64/0.14.51: + resolution: {integrity: sha512-HoN/5HGRXJpWODprGCgKbdMvrC3A2gqvzewu2eECRw2sYxOUoh2TV1tS+G7bHNapPGI79woQJGV6pFH7GH7qnA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64/0.14.51: + resolution: {integrity: sha512-JQDqPjuOH7o+BsKMSddMfmVJXrnYZxXDHsoLHc0xgmAZkOOCflRmC43q31pk79F9xuyWY45jDBPolb5ZgGOf9g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild/0.14.51: + resolution: {integrity: sha512-+CvnDitD7Q5sT7F+FM65sWkF8wJRf+j9fPcprxYV4j+ohmzVj2W7caUqH2s5kCaCJAfcAICjSlKhDCcvDpU7nw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + esbuild-android-64: 0.14.51 + esbuild-android-arm64: 0.14.51 + esbuild-darwin-64: 0.14.51 + esbuild-darwin-arm64: 0.14.51 + esbuild-freebsd-64: 0.14.51 + esbuild-freebsd-arm64: 0.14.51 + esbuild-linux-32: 0.14.51 + esbuild-linux-64: 0.14.51 + esbuild-linux-arm: 0.14.51 + esbuild-linux-arm64: 0.14.51 + esbuild-linux-mips64le: 0.14.51 + esbuild-linux-ppc64le: 0.14.51 + esbuild-linux-riscv64: 0.14.51 + esbuild-linux-s390x: 0.14.51 + esbuild-netbsd-64: 0.14.51 + esbuild-openbsd-64: 0.14.51 + esbuild-sunos-64: 0.14.51 + esbuild-windows-32: 0.14.51 + esbuild-windows-64: 0.14.51 + esbuild-windows-arm64: 0.14.51 + dev: true + + /fill-range/7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind/1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /has/1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /immutable/4.1.0: + resolution: {integrity: sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==} + dev: true + + /is-binary-path/2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-core-module/2.9.0: + resolution: {integrity: sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==} + dependencies: + has: 1.0.3 + dev: true + + /is-extglob/2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob/4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /nanoid/3.3.4: + resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /path-parse/1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /picocolors/1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch/2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /postcss/8.4.14: + resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /readdirp/3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /resolve/1.22.1: + resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + hasBin: true + dependencies: + is-core-module: 2.9.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /rollup/2.77.2: + resolution: {integrity: sha512-m/4YzYgLcpMQbxX3NmAqDvwLATZzxt8bIegO78FZLl+lAgKJBd1DRAOeEiZcKOIOPjxE6ewHWHNgGEalFXuz1g==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /sass/1.54.0: + resolution: {integrity: sha512-C4zp79GCXZfK0yoHZg+GxF818/aclhp9F48XBu/+bm9vXEVAYov9iU3FBVRMq3Hx3OA4jfKL+p2K9180mEh0xQ==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + immutable: 4.1.0 + source-map-js: 1.0.2 + dev: true + + /source-map-js/1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /supports-preserve-symlinks-flag/1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /typescript/4.7.4: + resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /vite/3.0.3_sass@1.54.0: + resolution: {integrity: sha512-sDIpIcl3mv1NUaSzZwiXGEy1ZoWwwC2vkxUHY6yiDacR6zf//ZFuBJrozO62gedpE43pmxnLATNR5IYUdAEkMQ==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + less: '*' + sass: '*' + stylus: '*' + terser: ^5.4.0 + peerDependenciesMeta: + less: + optional: true + sass: + optional: true + stylus: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.14.51 + postcss: 8.4.14 + resolve: 1.22.1 + rollup: 2.77.2 + sass: 1.54.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true diff --git a/src/art.ts b/src/art.ts new file mode 100644 index 0000000..8874408 --- /dev/null +++ b/src/art.ts @@ -0,0 +1,356 @@ +type Point2i = [number, number] + +type WireDirection = 'down-left' | 'down' | 'down-right' + +type TipPosition = false | 'begin' | 'end' | 'begin-end' + +type WirePiece = { + direction: WireDirection + lerp: number + tipPosition: TipPosition +} + +type LatticePoint = string + +function toLatticePoint(x: number, y: number): LatticePoint { + return `${x | 0},${y | 0}` +} + +function fromLatticePoint(p: LatticePoint): Point2i { + const [x, y] = p.split(',').map(s => parseInt(s)) + return [x, y] +} + +type WireNode = { point: Point2i; direction: WireDirection } + +type Wire = WireNode[] + +type World = { + dimensions: Point2i + + wirePieces: { [point: LatticePoint]: WirePiece } + wiresQueue: { wire: Wire; cursor: number }[] +} + +function randomChoice(array: T[]): T { + return array[Math.floor(Math.random() * array.length)] +} + +const randomDirection = (): WireDirection => randomChoice(['down', 'down-left', 'down-right']) + +const nextPoint = ([x, y]: [number, number], direction: WireDirection): [number, number] => { + if (direction === 'down') return [x, y + 1] + if (direction === 'down-left') return [x - 1, y + 1] + if (direction === 'down-right') return [x + 1, y + 1] + throw 'invalid' +} + +function checkPoint(world: World, [x, y]: [number, number]): boolean { + return !!world.wirePieces[toLatticePoint(x, y)] +} + +function wireIntersects(world: World, wire: Wire): boolean { + return wire.some(({ point: [x, y], direction }) => { + // TODO: The point check actually "doubly" depends on direction + if (direction === 'down') { + return checkPoint(world, [x, y]) || checkPoint(world, [x, y + 1]) + } + if (direction === 'down-left') { + return checkPoint(world, [x, y]) || checkPoint(world, [x - 1, y]) + } + if (direction === 'down-right') { + return checkPoint(world, [x, y]) || checkPoint(world, [x + 1, y]) + } + return false + }) +} + +function generateWire(world: World): Wire { + const [w, h] = world.dimensions + + const randomPoint = (): [number, number] => [ + Math.floor(Math.random() * w), + Math.floor(Math.pow(Math.random(), 2) * h * 0.5), + ] + + const wireLength = 3 + Math.floor(Math.random() * 12) + const wire: Wire = [ + { + point: randomPoint(), + direction: randomDirection(), + }, + ] + + let prev = wire[0] + let dir = prev.direction + + for (let i = 0; i < wireLength; i++) { + const p = nextPoint(prev.point, dir) + + if (Math.random() < 0.35) { + // change direction + if (dir === 'down') { + dir = randomChoice(['down-left', 'down-right']) + } else { + dir = 'down' + } + } + + wire.push({ + point: p, + direction: dir, + }) + prev = wire[wire.length - 1] + } + + return wire +} + +class Art { + static CELL_SIZE = 24 + static TIP_RADIUS = 4 + static WIRE_LERP_SPEED = 50 // units / seconds + + renewGraphicsContext: boolean = true + dirty: boolean + + world: World + + constructor($canvas: HTMLCanvasElement) { + let g: CanvasRenderingContext2D + + let unMount = this.setup($canvas) + + window.addEventListener('resize', () => { + this.renewGraphicsContext = true + unMount() + unMount = this.setup($canvas) + }) + + const renderFn = () => { + if (this.renewGraphicsContext) { + $canvas.width = $canvas.offsetWidth * devicePixelRatio + $canvas.height = $canvas.offsetHeight * devicePixelRatio + + g = $canvas.getContext('2d')! + g.scale(devicePixelRatio, devicePixelRatio) + } + + if (this.dirty || this.renewGraphicsContext) { + console.log('Rendering') + this.render(g, $canvas.offsetWidth, $canvas.offsetHeight) + } + + this.dirty = false + this.renewGraphicsContext = false + requestAnimationFrame(renderFn) + } + + renderFn() + } + + setup($canvas: HTMLCanvasElement) { + this.world = { + dimensions: [ + Math.ceil($canvas.offsetWidth / Art.CELL_SIZE), + Math.ceil($canvas.offsetHeight / Art.CELL_SIZE), + ], + wirePieces: {}, + wiresQueue: [], + } + + let failedTries = 0 + + const wireGeneratorTimer = setInterval(() => { + if (this.world.wiresQueue.length > 0) { + return + } + + console.log('Trying to generate wire') + if (failedTries > 200) { + console.log('Stopped generating wires') + clearInterval(wireGeneratorTimer) + return + } + + const wire = generateWire(this.world) + if (!wireIntersects(this.world, wire)) { + failedTries = 0 + this.world.wiresQueue.push({ wire, cursor: 0 }) + } else { + failedTries++ + } + }, 10) + + let pieceLerpBeginTime = new Date().getTime() + const wireQueueTimer = setInterval(() => { + if (this.world.wiresQueue.length > 0) { + console.log('Interpolating queued wire') + + // get top wire to add + const wireInterp = this.world.wiresQueue[0] + if (wireInterp.cursor < wireInterp.wire.length) { + const currentNode = wireInterp.wire[wireInterp.cursor] + const pieceLerpEndTime = pieceLerpBeginTime + 1000 / Art.WIRE_LERP_SPEED + + const now = new Date().getTime() + if (now > pieceLerpEndTime) { + wireInterp.cursor++ + pieceLerpBeginTime = new Date().getTime() + + this.world.wirePieces[toLatticePoint(...currentNode.point)] = { + direction: currentNode.direction, + lerp: 1, + tipPosition: + wireInterp.cursor === 1 + ? 'begin' + : wireInterp.cursor === wireInterp.wire.length && 'end', + } + + this.dirty = true + return + } + + const lerp = ((now - pieceLerpBeginTime) / 1000) * Art.WIRE_LERP_SPEED + this.world.wirePieces[toLatticePoint(...currentNode.point)] = { + ...currentNode, + tipPosition: wireInterp.cursor === 0 ? 'begin-end' : 'end', + lerp, + } + + this.dirty = true + } else { + this.world.wiresQueue.splice(0, 1) + } + } + }, 1000 / 60) + + const unMount = () => { + clearInterval(wireGeneratorTimer) + clearInterval(wireQueueTimer) + } + + document.addEventListener('keypress', e => { + if (e.key === 'r') { + unMount() + this.setup($canvas) + } + }) + + return unMount + } + + render(g: CanvasRenderingContext2D, width: number, height: number) { + g.clearRect(0, 0, width, height) + + // Grid + // g.lineWidth = 1 + // g.strokeStyle = '#ddd' + // g.beginPath() + // for (let i = 0; i < height / Art.CELL_SIZE; i++) { + // g.moveTo(0, i * Art.CELL_SIZE) + // g.lineTo(width, i * Art.CELL_SIZE) + // } + // for (let j = 0; j < width / Art.CELL_SIZE; j++) { + // g.moveTo(j * Art.CELL_SIZE, 0) + // g.lineTo(j * Art.CELL_SIZE, height) + // } + // g.stroke() + + g.lineWidth = 3 + g.strokeStyle = '#c8c8c8' + g.lineCap = 'round' + g.lineJoin = 'round' + + for (const [lp, piece] of Object.entries(this.world.wirePieces)) { + const [x, y] = fromLatticePoint(lp) + g.beginPath() + g.moveTo(x * Art.CELL_SIZE, y * Art.CELL_SIZE) + switch (piece.direction) { + case 'down-left': + g.lineTo((x - piece.lerp) * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE) + break + case 'down': + g.lineTo(x * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE) + break + case 'down-right': + g.lineTo((x + piece.lerp) * Art.CELL_SIZE, (y + piece.lerp) * Art.CELL_SIZE) + break + } + g.stroke() + } + + for (const [lp, piece] of Object.entries(this.world.wirePieces)) { + const [x, y] = fromLatticePoint(lp) + const drawTip = () => { + if ( + y !== 0 && + (piece.tipPosition === 'begin' || piece.tipPosition === 'begin-end') + ) { + switch (piece.direction) { + case 'down-left': + { + const cx = x * Art.CELL_SIZE + const cy = y * Art.CELL_SIZE + g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI) + } + break + case 'down': + { + const cx = x * Art.CELL_SIZE + const cy = y * Art.CELL_SIZE + g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI) + } + break + case 'down-right': + { + const cx = x * Art.CELL_SIZE + const cy = y * Art.CELL_SIZE + g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI) + } + break + } + } + if (piece.tipPosition === 'end' || piece.tipPosition === 'begin-end') { + switch (piece.direction) { + case 'down-left': + { + const cx = (x - piece.lerp) * Art.CELL_SIZE + const cy = (y + piece.lerp) * Art.CELL_SIZE + g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI) + } + break + case 'down': + { + const cx = x * Art.CELL_SIZE + const cy = (y + piece.lerp) * Art.CELL_SIZE + g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI) + } + break + case 'down-right': + { + const cx = (x + piece.lerp) * Art.CELL_SIZE + const cy = (y + piece.lerp) * Art.CELL_SIZE + g.ellipse(cx, cy, Art.TIP_RADIUS, Art.TIP_RADIUS, 0, 0, 2 * Math.PI) + } + break + } + } + } + + if (piece.tipPosition) { + g.fillStyle = '#ededed' + g.beginPath() + drawTip() + g.fill() + + g.beginPath() + drawTip() + g.stroke() + } + } + } +} + +const $canvas = document.querySelector('#wires-animation') as HTMLCanvasElement +new Art($canvas)