From 1f90dacbdd881eb3cea1a647e1ce0572ff934227 Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Fri, 19 Aug 2022 18:29:57 +0200 Subject: [PATCH] Update frontend --- _frontend/package.json | 6 +- _frontend/pnpm-lock.yaml | 308 +++++++++++++++++++++++++++ _frontend/rollup.config.js | 17 ++ _frontend/src/base.js | 41 ++++ _frontend/src/homepage-art.ts | 378 ++++++++++++++++++++++++++++++++++ _frontend/tsconfig.json | 9 + _views/base.html | 40 +--- _views/home.html | 4 +- 8 files changed, 762 insertions(+), 41 deletions(-) create mode 100644 _frontend/src/base.js create mode 100644 _frontend/src/homepage-art.ts create mode 100644 _frontend/tsconfig.json diff --git a/_frontend/package.json b/_frontend/package.json index da46c0b..a92abee 100644 --- a/_frontend/package.json +++ b/_frontend/package.json @@ -12,10 +12,14 @@ "license": "MIT", "devDependencies": { "@rollup/plugin-node-resolve": "^13.3.0", + "@rollup/plugin-typescript": "^8.3.4", + "esbuild": "^0.15.5", "npm-run-all": "^4.1.5", "rollup": "^2.75.3", + "rollup-plugin-esbuild": "^4.9.3", "rollup-plugin-terser": "^7.0.2", - "sass": "^1.52.3" + "sass": "^1.52.3", + "typescript": "^4.7.4" }, "dependencies": { "alpinejs": "^3.10.2", diff --git a/_frontend/pnpm-lock.yaml b/_frontend/pnpm-lock.yaml index 5f5bb96..ea1dcff 100644 --- a/_frontend/pnpm-lock.yaml +++ b/_frontend/pnpm-lock.yaml @@ -2,12 +2,16 @@ lockfileVersion: 5.4 specifiers: '@rollup/plugin-node-resolve': ^13.3.0 + '@rollup/plugin-typescript': ^8.3.4 alpinejs: ^3.10.2 + esbuild: ^0.15.5 fuse.js: ^6.6.2 npm-run-all: ^4.1.5 rollup: ^2.75.3 + rollup-plugin-esbuild: ^4.9.3 rollup-plugin-terser: ^7.0.2 sass: ^1.52.3 + typescript: ^4.7.4 dependencies: alpinejs: 3.10.3 @@ -15,10 +19,14 @@ dependencies: devDependencies: '@rollup/plugin-node-resolve': 13.3.0_rollup@2.78.0 + '@rollup/plugin-typescript': 8.3.4_nm5mlcuxlwr6samvke7b2fz27i + esbuild: 0.15.5 npm-run-all: 4.1.5 rollup: 2.78.0 + rollup-plugin-esbuild: 4.9.3_g2b53jqudjimruv6spqg4ieafm rollup-plugin-terser: 7.0.2_rollup@2.78.0 sass: 1.54.4 + typescript: 4.7.4 packages: @@ -43,6 +51,15 @@ packages: js-tokens: 4.0.0 dev: true + /@esbuild/linux-loong64/0.15.5: + resolution: {integrity: sha512-UHkDFCfSGTuXq08oQltXxSZmH1TXyWsL+4QhZDWvvLl6mEJQqk3u7/wq1LjhrrAXYIllaTtRSzUXl4Olkf2J8A==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@jridgewell/gen-mapping/0.3.2: resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} engines: {node: '>=6.0.0'} @@ -95,6 +112,23 @@ packages: rollup: 2.78.0 dev: true + /@rollup/plugin-typescript/8.3.4_nm5mlcuxlwr6samvke7b2fz27i: + resolution: {integrity: sha512-wt7JnYE9antX6BOXtsxGoeVSu4dZfw0dU3xykfOQ4hC3EddxRbVG/K0xiY1Wup7QOHJcjLYXWAn0Kx9Z1SBHHg==} + engines: {node: '>=8.0.0'} + peerDependencies: + rollup: ^2.14.0 + tslib: '*' + typescript: '>=3.7.0' + peerDependenciesMeta: + tslib: + optional: true + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.78.0 + resolve: 1.22.1 + rollup: 2.78.0 + typescript: 4.7.4 + dev: true + /@rollup/pluginutils/3.1.0_rollup@2.78.0: resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} engines: {node: '>= 8.0.0'} @@ -107,6 +141,14 @@ packages: rollup: 2.78.0 dev: true + /@rollup/pluginutils/4.2.1: + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + /@types/estree/0.0.39: resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} dev: true @@ -250,6 +292,18 @@ packages: which: 1.3.1 dev: true + /debug/4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + /deepmerge/4.2.2: resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} engines: {node: '>=0.10.0'} @@ -298,6 +352,10 @@ packages: unbox-primitive: 1.0.2 dev: true + /es-module-lexer/0.9.3: + resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==} + dev: true + /es-to-primitive/1.2.1: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} @@ -307,6 +365,215 @@ packages: is-symbol: 1.0.4 dev: true + /esbuild-android-64/0.15.5: + resolution: {integrity: sha512-dYPPkiGNskvZqmIK29OPxolyY3tp+c47+Fsc2WYSOVjEPWNCHNyqhtFqQadcXMJDQt8eN0NMDukbyQgFcHquXg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-android-arm64/0.15.5: + resolution: {integrity: sha512-YyEkaQl08ze3cBzI/4Cm1S+rVh8HMOpCdq8B78JLbNFHhzi4NixVN93xDrHZLztlocEYqi45rHHCgA8kZFidFg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64/0.15.5: + resolution: {integrity: sha512-Cr0iIqnWKx3ZTvDUAzG0H/u9dWjLE4c2gTtRLz4pqOBGjfjqdcZSfAObFzKTInLLSmD0ZV1I/mshhPoYSBMMCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64/0.15.5: + resolution: {integrity: sha512-WIfQkocGtFrz7vCu44ypY5YmiFXpsxvz2xqwe688jFfSVCnUsCn2qkEVDo7gT8EpsLOz1J/OmqjExePL1dr1Kg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64/0.15.5: + resolution: {integrity: sha512-M5/EfzV2RsMd/wqwR18CELcenZ8+fFxQAAEO7TJKDmP3knhWSbD72ILzrXFMMwshlPAS1ShCZ90jsxkm+8FlaA==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64/0.15.5: + resolution: {integrity: sha512-2JQQ5Qs9J0440F/n/aUBNvY6lTo4XP/4lt1TwDfHuo0DY3w5++anw+jTjfouLzbJmFFiwmX7SmUhMnysocx96w==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32/0.15.5: + resolution: {integrity: sha512-gO9vNnIN0FTUGjvTFucIXtBSr1Woymmx/aHQtuU+2OllGU6YFLs99960UD4Dib1kFovVgs59MTXwpFdVoSMZoQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64/0.15.5: + resolution: {integrity: sha512-ne0GFdNLsm4veXbTnYAWjbx3shpNKZJUd6XpNbKNUZaNllDZfYQt0/zRqOg0sc7O8GQ+PjSMv9IpIEULXVTVmg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm/0.15.5: + resolution: {integrity: sha512-wvAoHEN+gJ/22gnvhZnS/+2H14HyAxM07m59RSLn3iXrQsdS518jnEWRBnJz3fR6BJa+VUTo0NxYjGaNt7RA7Q==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64/0.15.5: + resolution: {integrity: sha512-7EgFyP2zjO065XTfdCxiXVEk+f83RQ1JsryN1X/VSX2li9rnHAt2swRbpoz5Vlrl6qjHrCmq5b6yxD13z6RheA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le/0.15.5: + resolution: {integrity: sha512-KdnSkHxWrJ6Y40ABu+ipTZeRhFtc8dowGyFsZY5prsmMSr1ZTG9zQawguN4/tunJ0wy3+kD54GaGwdcpwWAvZQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le/0.15.5: + resolution: {integrity: sha512-QdRHGeZ2ykl5P0KRmfGBZIHmqcwIsUKWmmpZTOq573jRWwmpfRmS7xOhmDHBj9pxv+6qRMH8tLr2fe+ZKQvCYw==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-riscv64/0.15.5: + resolution: {integrity: sha512-p+WE6RX+jNILsf+exR29DwgV6B73khEQV0qWUbzxaycxawZ8NE0wA6HnnTxbiw5f4Gx9sJDUBemh9v49lKOORA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-s390x/0.15.5: + resolution: {integrity: sha512-J2ngOB4cNzmqLHh6TYMM/ips8aoZIuzxJnDdWutBw5482jGXiOzsPoEF4j2WJ2mGnm7FBCO4StGcwzOgic70JQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64/0.15.5: + resolution: {integrity: sha512-MmKUYGDizYjFia0Rwt8oOgmiFH7zaYlsoQ3tIOfPxOqLssAsEgG0MUdRDm5lliqjiuoog8LyDu9srQk5YwWF3w==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64/0.15.5: + resolution: {integrity: sha512-2mMFfkLk3oPWfopA9Plj4hyhqHNuGyp5KQyTT9Rc8hFd8wAn5ZrbJg+gNcLMo2yzf8Uiu0RT6G9B15YN9WQyMA==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64/0.15.5: + resolution: {integrity: sha512-2sIzhMUfLNoD+rdmV6AacilCHSxZIoGAU2oT7XmJ0lXcZWnCvCtObvO6D4puxX9YRE97GodciRGDLBaiC6x1SA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32/0.15.5: + resolution: {integrity: sha512-e+duNED9UBop7Vnlap6XKedA/53lIi12xv2ebeNS4gFmu7aKyTrok7DPIZyU5w/ftHD4MUDs5PJUkQPP9xJRzg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64/0.15.5: + resolution: {integrity: sha512-v+PjvNtSASHOjPDMIai9Yi+aP+Vwox+3WVdg2JB8N9aivJ7lyhp4NVU+J0MV2OkWFPnVO8AE/7xH+72ibUUEnw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64/0.15.5: + resolution: {integrity: sha512-Yz8w/D8CUPYstvVQujByu6mlf48lKmXkq6bkeSZZxTA626efQOJb26aDGLzmFWx6eg/FwrXgt6SZs9V8Pwy/aA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild/0.15.5: + resolution: {integrity: sha512-VSf6S1QVqvxfIsSKb3UKr3VhUCis7wgDbtF4Vd9z84UJr05/Sp2fRKmzC+CSPG/dNAPPJZ0BTBLTT1Fhd6N9Gg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/linux-loong64': 0.15.5 + esbuild-android-64: 0.15.5 + esbuild-android-arm64: 0.15.5 + esbuild-darwin-64: 0.15.5 + esbuild-darwin-arm64: 0.15.5 + esbuild-freebsd-64: 0.15.5 + esbuild-freebsd-arm64: 0.15.5 + esbuild-linux-32: 0.15.5 + esbuild-linux-64: 0.15.5 + esbuild-linux-arm: 0.15.5 + esbuild-linux-arm64: 0.15.5 + esbuild-linux-mips64le: 0.15.5 + esbuild-linux-ppc64le: 0.15.5 + esbuild-linux-riscv64: 0.15.5 + esbuild-linux-s390x: 0.15.5 + esbuild-netbsd-64: 0.15.5 + esbuild-openbsd-64: 0.15.5 + esbuild-sunos-64: 0.15.5 + esbuild-windows-32: 0.15.5 + esbuild-windows-64: 0.15.5 + esbuild-windows-arm64: 0.15.5 + dev: true + /escape-string-regexp/1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -316,6 +583,10 @@ packages: resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} dev: true + /estree-walker/2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + /fill-range/7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -567,6 +838,11 @@ packages: supports-color: 7.2.0 dev: true + /joycon/3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + dev: true + /js-tokens/4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true @@ -575,6 +851,10 @@ packages: resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} dev: true + /jsonc-parser/3.1.0: + resolution: {integrity: sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==} + dev: true + /load-json-file/4.0.0: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} @@ -600,6 +880,10 @@ packages: brace-expansion: 1.1.11 dev: true + /ms/2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + /nice-try/1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} dev: true @@ -733,6 +1017,24 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /rollup-plugin-esbuild/4.9.3_g2b53jqudjimruv6spqg4ieafm: + resolution: {integrity: sha512-bxfUNYTa9Tw/4kdFfT9gtidDtqXyRdCW11ctZM7D8houCCVqp5qHzQF7hhIr31rqMA0APbG47fgVbbCGXgM49Q==} + engines: {node: '>=12'} + peerDependencies: + esbuild: '>=0.10.1' + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': 4.2.1 + debug: 4.3.4 + es-module-lexer: 0.9.3 + esbuild: 0.15.5 + joycon: 3.1.1 + jsonc-parser: 3.1.0 + rollup: 2.78.0 + transitivePeerDependencies: + - supports-color + dev: true + /rollup-plugin-terser/7.0.2_rollup@2.78.0: resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} peerDependencies: @@ -908,6 +1210,12 @@ packages: is-number: 7.0.0 dev: true + /typescript/4.7.4: + resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + /unbox-primitive/1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: diff --git a/_frontend/rollup.config.js b/_frontend/rollup.config.js index 7f1e15f..287a697 100644 --- a/_frontend/rollup.config.js +++ b/_frontend/rollup.config.js @@ -1,7 +1,16 @@ import { defineConfig } from 'rollup' import { terser } from 'rollup-plugin-terser' +import esbuild from 'rollup-plugin-esbuild' export default defineConfig([ + { + input: 'src/base.js', + output: { + file: 'out/base.min.js', + format: 'iife', + }, + plugins: [terser()], + }, { input: 'src/utenti.js', external: ['alpinejs', 'fuse.js'], // libraries to not bundle @@ -28,4 +37,12 @@ export default defineConfig([ }, plugins: [terser()], }, + { + input: 'src/homepage-art.ts', + output: { + file: 'out/homepage-art.min.js', + format: 'iife', + }, + plugins: [esbuild({ minify: true })], + }, ]) diff --git a/_frontend/src/base.js b/_frontend/src/base.js new file mode 100644 index 0000000..4abe71f --- /dev/null +++ b/_frontend/src/base.js @@ -0,0 +1,41 @@ +document.addEventListener('DOMContentLoaded', function () { + renderMathInElement(document.body, { + delimiters: [ + { left: '$', right: '$', display: false }, + { left: '$$', right: '$$', display: true }, + { left: '\\(', right: '\\)', display: false }, + { left: '\\[', right: '\\]', display: true }, + ], + }) +}) + +document.addEventListener('DOMContentLoaded', () => { + const $toggle = document.querySelector('#toggle-dark-mode') + const $toggleIcon = document.querySelector('#toggle-dark-mode i') + + // Loads preferred dark from from localStorage or defaults to media query. + let prefersDarkMode = + localStorage.getItem('theme') !== undefined + ? localStorage.getItem('theme') === 'dark' + : window.matchMedia('(prefers-color-scheme: dark)').matches + + function storePrefersDarkMode(mode) { + prefersDarkMode = mode + localStorage.setItem('theme', mode ? 'dark' : 'light') + } + + function displayToggle() { + document.body.classList.toggle('dark-mode', prefersDarkMode) + $toggleIcon.classList.toggle('fa-moon', prefersDarkMode) + $toggleIcon.classList.toggle('fa-sun', !prefersDarkMode) + + document.dispatchEvent(new CustomEvent('theme:change')) + } + + $toggle.addEventListener('click', () => { + storePrefersDarkMode(!prefersDarkMode) + displayToggle() + }) + + displayToggle() +}) diff --git a/_frontend/src/homepage-art.ts b/_frontend/src/homepage-art.ts new file mode 100644 index 0000000..61366f4 --- /dev/null +++ b/_frontend/src/homepage-art.ts @@ -0,0 +1,378 @@ +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() * 10) + 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.325) { + // 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 +} + +function getTheme() { + if (document.body.classList.contains('dark-mode')) { + return { + backgroundColor: '#282828', + circuitColor: '#38302e', + } + } else { + return { + backgroundColor: '#eaeaea', + circuitColor: '#d4d4d4', + } + } +} + +class Art { + static CELL_SIZE = 28 + static TIP_RADIUS = 4 + static WIRE_LERP_SPEED = 25 // 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) + }) + + document.addEventListener('theme:change', () => { + this.dirty = true + }) + + 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 > 400) { + 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' + : false, + } + + 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) + + const { backgroundColor, circuitColor } = getTheme() + + // 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 = circuitColor + 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 = backgroundColor + g.beginPath() + drawTip() + g.fill() + + g.beginPath() + drawTip() + g.stroke() + } + } + } +} + +const $canvas = document.querySelector('#wires-animation') as HTMLCanvasElement +new Art($canvas) diff --git a/_frontend/tsconfig.json b/_frontend/tsconfig.json new file mode 100644 index 0000000..1d53dc8 --- /dev/null +++ b/_frontend/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "esnext", + "noImplicitAny": true, + "sourceMap": true, + "noEmitOnError": true + }, + "filesGlob": ["./src/**/*.ts"] +} diff --git a/_views/base.html b/_views/base.html index 9d3dd4c..21fe288 100644 --- a/_views/base.html +++ b/_views/base.html @@ -17,44 +17,8 @@ - + + diff --git a/_views/home.html b/_views/home.html index 4758ff8..5e08e5f 100644 --- a/_views/home.html +++ b/_views/home.html @@ -3,7 +3,7 @@ {{define "title"}}Home • PHC{{end}} {{define "body"}} - +
@@ -103,5 +103,5 @@
- + {{end}}