throw in the file viewer
BIN
meow.txt
Normal file
514
package-lock.json
generated
|
@ -8,6 +8,7 @@
|
|||
"@hono/node-server": "^1.14.3",
|
||||
"@mdx-js/mdx": "^3.1.0",
|
||||
"@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
|
||||
"blurhash": "^2.0.5",
|
||||
"codemirror": "^6.0.1",
|
||||
"devalue": "^5.1.1",
|
||||
"esbuild": "^0.25.5",
|
||||
|
@ -15,6 +16,7 @@
|
|||
"hono": "^4.7.11",
|
||||
"marko": "^6.0.20",
|
||||
"puppeteer": "^24.10.1",
|
||||
"sharp": "^0.34.2",
|
||||
"unique-names-generator": "^4.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -487,6 +489,16 @@
|
|||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
|
||||
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
||||
|
@ -899,6 +911,402 @@
|
|||
"hono": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz",
|
||||
"integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz",
|
||||
"integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz",
|
||||
"integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz",
|
||||
"integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz",
|
||||
"integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz",
|
||||
"integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz",
|
||||
"integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz",
|
||||
"integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz",
|
||||
"integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz",
|
||||
"integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz",
|
||||
"integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz",
|
||||
"integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz",
|
||||
"integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz",
|
||||
"integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz",
|
||||
"integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
|
@ -1385,6 +1793,12 @@
|
|||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/blurhash": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
|
||||
"integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.25.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
|
||||
|
@ -1596,6 +2010,19 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1",
|
||||
"color-string": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
@ -1614,6 +2041,16 @@
|
|||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||
|
@ -1739,6 +2176,15 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
|
||||
|
@ -3723,6 +4169,74 @@
|
|||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz",
|
||||
"integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
"detect-libc": "^2.0.4",
|
||||
"semver": "^7.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.2",
|
||||
"@img/sharp-darwin-x64": "0.34.2",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.1.0",
|
||||
"@img/sharp-libvips-darwin-x64": "1.1.0",
|
||||
"@img/sharp-libvips-linux-arm": "1.1.0",
|
||||
"@img/sharp-libvips-linux-arm64": "1.1.0",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.1.0",
|
||||
"@img/sharp-libvips-linux-s390x": "1.1.0",
|
||||
"@img/sharp-libvips-linux-x64": "1.1.0",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.1.0",
|
||||
"@img/sharp-linux-arm": "0.34.2",
|
||||
"@img/sharp-linux-arm64": "0.34.2",
|
||||
"@img/sharp-linux-s390x": "0.34.2",
|
||||
"@img/sharp-linux-x64": "0.34.2",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.2",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.2",
|
||||
"@img/sharp-wasm32": "0.34.2",
|
||||
"@img/sharp-win32-arm64": "0.34.2",
|
||||
"@img/sharp-win32-ia32": "0.34.2",
|
||||
"@img/sharp-win32-x64": "0.34.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle/node_modules/is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"@hono/node-server": "^1.14.3",
|
||||
"@mdx-js/mdx": "^3.1.0",
|
||||
"@paperclover/console": "git+https://git.paperclover.net/clo/console.git",
|
||||
"blurhash": "^2.0.5",
|
||||
"codemirror": "^6.0.1",
|
||||
"devalue": "^5.1.1",
|
||||
"esbuild": "^0.25.5",
|
||||
|
@ -11,6 +12,7 @@
|
|||
"hono": "^4.7.11",
|
||||
"marko": "^6.0.20",
|
||||
"puppeteer": "^24.10.1",
|
||||
"sharp": "^0.34.2",
|
||||
"unique-names-generator": "^4.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -17,7 +17,7 @@ JavaScript into HTML. Attributes are JavaScript expressions.
|
|||
|
||||
```marko
|
||||
<div>
|
||||
// `input` is like props, but in global scope
|
||||
// `input` is like props, but given in the top-level scope
|
||||
<time datetime=input.date.toISOString()>
|
||||
// Interpolation with JS template string syntax
|
||||
${formatTimeNicely(input.date)}
|
||||
|
@ -28,6 +28,12 @@ JavaScript into HTML. Attributes are JavaScript expressions.
|
|||
|
||||
// Capital letter variables for imported components
|
||||
<MarkdownContent message=input.message />
|
||||
|
||||
// Components also can be auto-imported by lowercase.
|
||||
// This will look upwards for a `tags/` folder containing
|
||||
// "custom-footer.marko", similar to how Node.js finds
|
||||
// package names in all upwards `node_modules` folders.
|
||||
<custom-footer />
|
||||
</div>
|
||||
|
||||
// ESM `import` / `export` just work as expected.
|
||||
|
|
425
src/file-viewer/backend.tsx
Normal file
|
@ -0,0 +1,425 @@
|
|||
import { type Context, Hono } from "hono";
|
||||
import * as path from "node:path";
|
||||
import { etagMatches, serveAsset } from "../assets.ts";
|
||||
import { FilePermissions, MediaFile } from "../db.ts";
|
||||
import { renderDynamicPage } from "../framework/dynamic-pages.ts";
|
||||
import { renderToStringSync } from "../framework/render-to-string.ts";
|
||||
import { MediaPanel } from "../pages-dynamic/file_viewer.tsx";
|
||||
import mimeTypeDb from "./mime.json" with { type: "json" };
|
||||
import { Speedbump } from "./cotyledon.tsx";
|
||||
import { hasAsset } from "../assets.ts";
|
||||
import { CompressionFormat, fetchFile, prefetchFile } from "./cache.ts";
|
||||
import { requireFriendAuth } from "../journal/backend.ts";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
interface APIDirectoryList {
|
||||
path: string;
|
||||
readme: string | null;
|
||||
files: APIFile[];
|
||||
}
|
||||
|
||||
interface APIFile {
|
||||
basename: string;
|
||||
dir: boolean;
|
||||
time: number;
|
||||
size: number;
|
||||
duration: number | null;
|
||||
}
|
||||
|
||||
function checkCotyledonCookie(c: Context) {
|
||||
const cookie = c.req.header("Cookie");
|
||||
if (!cookie) return false;
|
||||
const cookies = cookie.split("; ").map((x) => x.split("="));
|
||||
return cookies.some(
|
||||
(kv) => kv[0].trim() === "cotyledon" && kv[1].trim() === "agree",
|
||||
);
|
||||
}
|
||||
|
||||
function isCotyledonPath(path: string) {
|
||||
if (path === "/cotyledon") return true;
|
||||
const year = path.match(/^\/(\d{4})($|\/)/);
|
||||
if (!year) return false;
|
||||
const yearInt = parseInt(year[1]);
|
||||
if (yearInt < 2025 && yearInt >= 2017) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
app.post("/file/cotyledon", async (c) => {
|
||||
c.res = new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Set-Cookie": "cotyledon=agree; Path=/",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/file/*", async (c, next) => {
|
||||
if (c.req.header("User-Agent")?.toLowerCase()?.includes("discordbot")) {
|
||||
return next();
|
||||
}
|
||||
let rawFilePath = c.req.path.slice(5) || "/";
|
||||
if (rawFilePath.endsWith("$partial")) {
|
||||
return getPartialPage(c, rawFilePath.slice(0, -"$partial".length));
|
||||
}
|
||||
let hasCotyledonCookie = checkCotyledonCookie(c);
|
||||
if (isCotyledonPath(rawFilePath)) {
|
||||
if (!hasCotyledonCookie) {
|
||||
return serveAsset(c, "/file/cotyledon_speedbump", 403);
|
||||
} else if (rawFilePath === "/cotyledon") {
|
||||
return serveAsset(c, "/file/cotyledon_enterance", 200);
|
||||
}
|
||||
}
|
||||
while (rawFilePath.length > 1 && rawFilePath.endsWith("/")) {
|
||||
rawFilePath = rawFilePath.slice(0, -1);
|
||||
}
|
||||
const file = MediaFile.getByPath(rawFilePath);
|
||||
if (!file) {
|
||||
// perhaps a specific 404 page for media files?
|
||||
return next();
|
||||
}
|
||||
|
||||
const permissions = FilePermissions.getByPrefix(rawFilePath);
|
||||
if (permissions !== 0) {
|
||||
const friendAuthChallenge = requireFriendAuth(c);
|
||||
if (friendAuthChallenge) return friendAuthChallenge;
|
||||
}
|
||||
|
||||
// File listings
|
||||
if (file.kind === MediaFile.Kind.directory) {
|
||||
if (c.req.header("Accept")?.includes("application/json")) {
|
||||
const json = {
|
||||
path: file.path,
|
||||
files: file.getPublicChildren().map((f) => ({
|
||||
basename: f.basename,
|
||||
dir: f.kind === MediaFile.Kind.directory,
|
||||
time: f.date.getTime(),
|
||||
size: f.size,
|
||||
duration: f.duration ? f.duration : null,
|
||||
})),
|
||||
readme: file.contents ? file.contents : null,
|
||||
} satisfies APIDirectoryList;
|
||||
return c.json(json);
|
||||
}
|
||||
c.res = await renderDynamicPage(c.req.raw, "file_viewer", {
|
||||
file,
|
||||
hasCotyledonCookie,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Redirect to directory list for regular files if client accepts HTML
|
||||
let viewMode = c.req.query("view");
|
||||
if (c.req.query("dl") !== undefined) {
|
||||
viewMode = "download";
|
||||
}
|
||||
if (viewMode == undefined && c.req.header("Accept")?.includes("text/html")) {
|
||||
prefetchFile(file.path);
|
||||
c.res = await renderDynamicPage(c.req.raw, "file_viewer", {
|
||||
file,
|
||||
hasCotyledonCookie,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const download = viewMode === "download";
|
||||
|
||||
const etag = file.hash;
|
||||
const filePath = file.path;
|
||||
const expectedSize = file.size;
|
||||
|
||||
let encoding = decideEncoding(c.req.header("Accept-Encoding"));
|
||||
|
||||
let sizeHeader = encoding === "raw"
|
||||
? expectedSize
|
||||
// Size cannot be known because of compression modes
|
||||
: undefined;
|
||||
|
||||
// Etag
|
||||
{
|
||||
const ifNoneMatch = c.req.header("If-None-Match");
|
||||
if (ifNoneMatch && etagMatches(etag, ifNoneMatch)) {
|
||||
c.res = new Response(null, {
|
||||
status: 304,
|
||||
statusText: "Not Modified",
|
||||
headers: fileHeaders(file, download, sizeHeader),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Head
|
||||
if (c.req.method === "HEAD") {
|
||||
c.res = new Response(null, {
|
||||
headers: fileHeaders(file, download, sizeHeader),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevalidate range requests
|
||||
let rangeHeader = c.req.header("Range") ?? null;
|
||||
if (rangeHeader) encoding = "raw";
|
||||
|
||||
const ifRangeHeader = c.req.header("If-Range");
|
||||
if (ifRangeHeader && ifRangeOutdated(file, ifRangeHeader)) {
|
||||
// > If the condition is not fulfilled, the full resource is
|
||||
// > sent back with a 200 OK status.
|
||||
rangeHeader = null;
|
||||
}
|
||||
|
||||
let foundFile;
|
||||
while (true) {
|
||||
let second = false;
|
||||
try {
|
||||
foundFile = await fetchFile(filePath, encoding);
|
||||
if (second) {
|
||||
console.warn(`File ${filePath} has missing compression: ${encoding}`);
|
||||
}
|
||||
break;
|
||||
} catch (error) {
|
||||
if (encoding !== "raw") {
|
||||
encoding = "raw";
|
||||
sizeHeader = file.size;
|
||||
second = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
return c.text(
|
||||
"internal server error: this file is present in the database but could not be fetched",
|
||||
);
|
||||
}
|
||||
}
|
||||
const [streamOrBuffer, actualEncoding, src] = foundFile;
|
||||
encoding = actualEncoding;
|
||||
|
||||
// Range requests
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
|
||||
// Compression is skipped because it's a confusing, but solvable problem.
|
||||
// See https://stackoverflow.com/questions/33947562/is-it-possible-to-send-http-response-using-gzip-and-byte-ranges-at-the-same-time
|
||||
if (rangeHeader) {
|
||||
const ranges = parseRange(rangeHeader, file.size);
|
||||
// TODO: multiple ranges
|
||||
if (ranges && ranges.length === 1) {
|
||||
return (c.res = handleRanges(ranges, file, streamOrBuffer, download));
|
||||
}
|
||||
}
|
||||
|
||||
// Respond in a streaming fashion
|
||||
c.res = new Response(streamOrBuffer, {
|
||||
headers: {
|
||||
...fileHeaders(file, download, sizeHeader),
|
||||
...(encoding !== "raw" && {
|
||||
"Content-Encoding": encoding,
|
||||
}),
|
||||
"X-Cache": src,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/canvas/:script", async (c, next) => {
|
||||
const script = c.req.param("script");
|
||||
if (!hasAsset(`/js/canvas/${script}.js`)) {
|
||||
return next();
|
||||
}
|
||||
return renderDynamicPage(c.req.raw, "canvas", {
|
||||
script,
|
||||
});
|
||||
});
|
||||
|
||||
function decideEncoding(encodings: string | undefined): CompressionFormat {
|
||||
if (encodings?.includes("zstd")) return "zstd";
|
||||
if (encodings?.includes("gzip")) return "gzip";
|
||||
return "raw";
|
||||
}
|
||||
|
||||
function fileHeaders(
|
||||
file: MediaFile,
|
||||
download: boolean,
|
||||
size: number | undefined = file.size,
|
||||
) {
|
||||
return {
|
||||
Vary: "Accept-Encoding, Accept",
|
||||
"Content-Type": mimeType(file.path),
|
||||
"Content-Length": size.toString(),
|
||||
ETag: file.hash,
|
||||
"Last-Modified": file.date.toUTCString(),
|
||||
...(download && {
|
||||
"Content-Disposition": `attachment; filename="${file.basename}"`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function ifRangeOutdated(file: MediaFile, ifRangeHeader: string) {
|
||||
// etag
|
||||
if (ifRangeHeader[0] === '"') {
|
||||
return ifRangeHeader.slice(1, -1) !== file.hash;
|
||||
}
|
||||
// date
|
||||
return new Date(ifRangeHeader) < file.date;
|
||||
}
|
||||
|
||||
/** The end is inclusive */
|
||||
type Ranges = Array<[start: number, end: number]>;
|
||||
|
||||
function parseRange(rangeHeader: string, fileSize: number): Ranges | null {
|
||||
const [unit, ranges] = rangeHeader.split("=");
|
||||
if (unit !== "bytes") return null;
|
||||
|
||||
const result: Array<[start: number, end: number]> = [];
|
||||
const rangeParts = ranges.split(",");
|
||||
|
||||
for (const range of rangeParts) {
|
||||
const split = range.split("-");
|
||||
if (split.length !== 2) return null;
|
||||
const [start, end] = split;
|
||||
if (start === "" && end === "") return null;
|
||||
const parsedRange: [number, number] = [
|
||||
start === "" ? fileSize - +end : +start,
|
||||
end === "" ? fileSize - 1 : +end,
|
||||
];
|
||||
result.push(parsedRange);
|
||||
}
|
||||
|
||||
// Validate that ranges do not intersect
|
||||
result.sort((a, b) => a[0] - b[0]);
|
||||
for (let i = 1; i < result.length; i++) {
|
||||
if (result[i][0] <= result[i - 1][1]) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function handleRanges(
|
||||
ranges: Ranges,
|
||||
file: MediaFile,
|
||||
streamOrBuffer: ReadableStream | Buffer,
|
||||
download: boolean,
|
||||
): Response {
|
||||
// TODO: multiple ranges
|
||||
const rangeSize = ranges.reduce((a, b) => a + (b[1] - b[0] + 1), 0);
|
||||
const rangeBody = streamOrBuffer instanceof ReadableStream
|
||||
? applySingleRangeToStream(streamOrBuffer, ranges)
|
||||
: applyRangesToBuffer(streamOrBuffer, ranges, rangeSize);
|
||||
return new Response(rangeBody, {
|
||||
status: 206,
|
||||
headers: {
|
||||
...fileHeaders(file, download, rangeSize),
|
||||
"Content-Range": `bytes ${ranges[0][0]}-${ranges[0][1]}/${file.size}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function applyRangesToBuffer(
|
||||
buffer: Buffer,
|
||||
ranges: Ranges,
|
||||
rangeSize: number,
|
||||
): Uint8Array {
|
||||
const result = new Uint8Array(rangeSize);
|
||||
let offset = 0;
|
||||
for (const [start, end] of ranges) {
|
||||
result.set(buffer.slice(start, end + 1), offset);
|
||||
offset += end - start + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function applySingleRangeToStream(
|
||||
stream: ReadableStream,
|
||||
ranges: Ranges,
|
||||
): ReadableStream {
|
||||
let reader: ReadableStreamDefaultReader<Uint8Array>;
|
||||
let position = 0;
|
||||
const [start, end] = ranges[0];
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
reader = stream.getReader();
|
||||
try {
|
||||
while (position <= end) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = new Uint8Array(value);
|
||||
|
||||
const bufferStart = position;
|
||||
const bufferEnd = position + buffer.length - 1;
|
||||
|
||||
position += buffer.length;
|
||||
|
||||
if (bufferEnd < start) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bufferStart > end) {
|
||||
break;
|
||||
}
|
||||
|
||||
const sendStart = Math.max(0, start - bufferStart);
|
||||
const sendEnd = Math.min(buffer.length - 1, end - bufferStart);
|
||||
|
||||
if (sendStart <= sendEnd) {
|
||||
controller.enqueue(buffer.slice(sendStart, sendEnd + 1));
|
||||
}
|
||||
}
|
||||
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
reader?.releaseLock();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function mimeType(file: string) {
|
||||
return (mimeTypeDb as any)[path.extname(file)] ?? "application/octet-stream";
|
||||
}
|
||||
|
||||
function getPartialPage(c: Context, rawFilePath: string) {
|
||||
if (isCotyledonPath(rawFilePath)) {
|
||||
if (!checkCotyledonCookie(c)) {
|
||||
let root = Speedbump();
|
||||
// Remove the root element, it's created client side!
|
||||
root = root.props.children;
|
||||
|
||||
const html = renderToStringSync(root);
|
||||
c.header("X-Cotyledon", "true");
|
||||
return c.html(html);
|
||||
}
|
||||
}
|
||||
|
||||
const file = MediaFile.getByPath(rawFilePath);
|
||||
const permissions = FilePermissions.getByPrefix(rawFilePath);
|
||||
if (permissions !== 0) {
|
||||
const friendAuthChallenge = requireFriendAuth(c);
|
||||
if (friendAuthChallenge) return friendAuthChallenge;
|
||||
}
|
||||
if (rawFilePath.endsWith("/")) {
|
||||
rawFilePath = rawFilePath.slice(0, -1);
|
||||
}
|
||||
if (!file) {
|
||||
return c.json({ error: "File not found" }, 404);
|
||||
}
|
||||
|
||||
let root = MediaPanel({
|
||||
file,
|
||||
isLast: true,
|
||||
activeFilename: null,
|
||||
hasCotyledonCookie: rawFilePath === "" && checkCotyledonCookie(c),
|
||||
});
|
||||
// Remove the root element, it's created client side!
|
||||
root = root.props.children;
|
||||
|
||||
const html = renderToStringSync(root);
|
||||
return c.html(html);
|
||||
}
|
||||
|
||||
export { app as mediaApp };
|
83
src/file-viewer/bin/extension-stats.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import * as path from "node:path";
|
||||
import { cache, MediaFile } from "../db";
|
||||
|
||||
// Function to get file extension statistics
|
||||
function getExtensionStats() {
|
||||
// Get all files (not directories) from the database
|
||||
const query = `
|
||||
SELECT path FROM media_files
|
||||
WHERE kind = ${MediaFile.Kind.file}
|
||||
`;
|
||||
|
||||
// Use raw query to get all file paths
|
||||
const rows = cache.query(query).all() as { path: string }[];
|
||||
|
||||
// Count extensions
|
||||
const extensionCounts: Record<string, number> = {};
|
||||
|
||||
for (const row of rows) {
|
||||
const extension = path.extname(row.path).toLowerCase();
|
||||
extensionCounts[extension] = (extensionCounts[extension] || 0) + 1;
|
||||
}
|
||||
|
||||
// Sort extensions by count (descending)
|
||||
const sortedExtensions = Object.entries(extensionCounts)
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
|
||||
return {
|
||||
totalFiles: rows.length,
|
||||
extensions: sortedExtensions,
|
||||
};
|
||||
}
|
||||
|
||||
// Function to print a visual table
|
||||
function printExtensionTable() {
|
||||
const stats = getExtensionStats();
|
||||
|
||||
// Calculate column widths
|
||||
const extensionColWidth = Math.max(
|
||||
...stats.extensions.map(([ext]) => ext.length),
|
||||
"Extension".length,
|
||||
) + 2;
|
||||
|
||||
const countColWidth = Math.max(
|
||||
...stats.extensions.map(([_, count]) => count.toString().length),
|
||||
"Count".length,
|
||||
) + 2;
|
||||
|
||||
const percentColWidth = "Percentage".length + 2;
|
||||
|
||||
// Print header
|
||||
console.log("MediaFile Extension Statistics");
|
||||
console.log(`Total files: ${stats.totalFiles}`);
|
||||
console.log();
|
||||
|
||||
// Print table header
|
||||
console.log(
|
||||
"Extension".padEnd(extensionColWidth) +
|
||||
"Count".padEnd(countColWidth) +
|
||||
"Percentage".padEnd(percentColWidth),
|
||||
);
|
||||
|
||||
// Print separator
|
||||
console.log(
|
||||
"-".repeat(extensionColWidth) +
|
||||
"-".repeat(countColWidth) +
|
||||
"-".repeat(percentColWidth),
|
||||
);
|
||||
|
||||
// Print rows
|
||||
for (const [extension, count] of stats.extensions) {
|
||||
const percentage = ((count / stats.totalFiles) * 100).toFixed(2);
|
||||
const ext = extension || "(no extension)";
|
||||
|
||||
console.log(
|
||||
ext.padEnd(extensionColWidth) +
|
||||
count.toString().padEnd(countColWidth) +
|
||||
`${percentage}%`.padEnd(percentColWidth),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the program
|
||||
printExtensionTable();
|
1015
src/file-viewer/bin/scan.ts
Normal file
418
src/file-viewer/cache.ts
Normal file
|
@ -0,0 +1,418 @@
|
|||
import { Agent, get } from "node:https";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { Buffer } from "node:buffer";
|
||||
import type { ClientRequest } from "node:http";
|
||||
import { LRUCache } from "lru-cache";
|
||||
import { open } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { scoped } from "@paperclover/console";
|
||||
import { escapeUri } from "./share.ts";
|
||||
|
||||
declare const Deno: any;
|
||||
|
||||
const sourceOfTruth = "https://nas.paperclover.net:43250";
|
||||
const caCert = fs.readFileSync(path.join(import.meta.dirname, "cert.pem"));
|
||||
|
||||
const diskCacheRoot = path.join(import.meta.dirname, "../.clover/filecache/");
|
||||
const diskCacheMaxSize = 14 * 1024 * 1024 * 1024; // 14GB
|
||||
const ramCacheMaxSize = 1 * 1024 * 1024 * 1024; // 1.5GB
|
||||
const loadInProgress = new Map<
|
||||
string,
|
||||
Promise<{ stream: ReadableStream }> | { stream: ReadableStream }
|
||||
>();
|
||||
// Disk cache serializes the access times
|
||||
const diskCacheState: Record<string, [size: number, lastAccess: number]> =
|
||||
loadDiskCacheState();
|
||||
const diskCache = new LRUCache<string, number>({
|
||||
maxSize: diskCacheMaxSize,
|
||||
ttl: 0,
|
||||
sizeCalculation: (value) => value,
|
||||
dispose: (_, key) => {
|
||||
delete diskCacheState[key];
|
||||
},
|
||||
onInsert: (size, key) => {
|
||||
diskCacheState[key] = [size, Date.now()];
|
||||
},
|
||||
});
|
||||
const ramCache = new LRUCache<string, Buffer>({
|
||||
maxSize: ramCacheMaxSize,
|
||||
ttl: 0,
|
||||
sizeCalculation: (value) => value.byteLength,
|
||||
});
|
||||
let diskCacheFlush: NodeJS.Timeout | undefined;
|
||||
|
||||
{
|
||||
// Initialize the disk cache by validating all files exist, and then
|
||||
// inserting them in last to start order. State is repaired pessimistically.
|
||||
const toDelete = new Set(Object.keys(diskCacheState));
|
||||
fs.mkdirSync(diskCacheRoot, { recursive: true });
|
||||
for (
|
||||
const file of fs.readdirSync(diskCacheRoot, {
|
||||
recursive: true,
|
||||
encoding: "utf-8",
|
||||
})
|
||||
) {
|
||||
const key = file.split("/").pop()!;
|
||||
if (key.length !== 40) continue;
|
||||
const entry = diskCacheState[key];
|
||||
if (!entry) {
|
||||
fs.rmSync(path.join(diskCacheRoot, file), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
delete diskCacheState[key];
|
||||
continue;
|
||||
}
|
||||
toDelete.delete(key);
|
||||
}
|
||||
for (const key of toDelete) {
|
||||
delete diskCacheState[key];
|
||||
}
|
||||
saveDiskCacheState();
|
||||
const sorted = Object.keys(diskCacheState).sort((a, b) =>
|
||||
diskCacheState[b][1] - diskCacheState[a][1]
|
||||
);
|
||||
for (const key of sorted) {
|
||||
diskCache.set(key, diskCacheState[key][0]);
|
||||
}
|
||||
}
|
||||
|
||||
export type CacheSource = "ram" | "disk" | "miss" | "lan" | "flight";
|
||||
export type CompressionFormat = "gzip" | "zstd" | "raw";
|
||||
const compressionFormatMap = {
|
||||
gzip: "gz",
|
||||
zstd: "zstd",
|
||||
raw: "file",
|
||||
} as const;
|
||||
|
||||
const log = scoped("file_cache");
|
||||
|
||||
const lanMount = "/Volumes/clover/Published";
|
||||
const hasLanMount = fs.existsSync(lanMount);
|
||||
|
||||
/**
|
||||
* Fetches a file with the given compression format.
|
||||
* Uncompressed files are never persisted to disk.
|
||||
*
|
||||
* Returns a promise to either:
|
||||
* - Buffer: the data is from RAM cache
|
||||
* - ReadableStream: the data is being streamed in from disk/server
|
||||
*
|
||||
* Additionally, returns a string indicating the source of the data, for debugging.
|
||||
*
|
||||
* Callers must be able to consume both output types.
|
||||
*/
|
||||
export async function fetchFile(
|
||||
pathname: string,
|
||||
format: CompressionFormat = "raw",
|
||||
): Promise<
|
||||
[Buffer | ReadableStream, encoding: CompressionFormat, src: CacheSource]
|
||||
> {
|
||||
// 1. Ram cache
|
||||
const cacheKey = hashKey(`${pathname}:${format}`);
|
||||
const ramCacheHit = ramCache.get(cacheKey);
|
||||
if (ramCacheHit) {
|
||||
log(`ram hit: ${format}${pathname}`);
|
||||
return [ramCacheHit, format, "ram"];
|
||||
}
|
||||
|
||||
// 2. Tee an existing loading stream.
|
||||
const inProgress = loadInProgress.get(cacheKey);
|
||||
if (inProgress) {
|
||||
const stream = await inProgress;
|
||||
const [stream1, stream2] = stream.stream.tee();
|
||||
loadInProgress.set(cacheKey, { stream: stream2 });
|
||||
log(`in-flight copy: ${format}${pathname}`);
|
||||
return [stream1, format, "flight"];
|
||||
}
|
||||
|
||||
// 3. Disk cache + Load into ram cache.
|
||||
if (format !== "raw") {
|
||||
const diskCacheHit = diskCache.get(cacheKey);
|
||||
if (diskCacheHit) {
|
||||
diskCacheState[cacheKey] = [diskCacheHit, Date.now()];
|
||||
saveDiskCacheStateLater();
|
||||
log(`disk hit: ${format}/${pathname}`);
|
||||
return [
|
||||
startInProgress(
|
||||
cacheKey,
|
||||
new ReadableStream({
|
||||
start: async (controller) => {
|
||||
const stream = fs.createReadStream(
|
||||
path.join(diskCacheRoot, cacheKey),
|
||||
);
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on("data", (chunk) => {
|
||||
controller.enqueue(chunk);
|
||||
chunks.push(chunk as Buffer);
|
||||
});
|
||||
stream.on("end", () => {
|
||||
controller.close();
|
||||
ramCache.set(cacheKey, Buffer.concat(chunks));
|
||||
finishInProgress(cacheKey);
|
||||
});
|
||||
stream.on("error", (error) => {
|
||||
controller.error(error);
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
format,
|
||||
"disk",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Lan Mount (access files that prod may not have)
|
||||
if (hasLanMount) {
|
||||
log(`lan hit: ${format}/${pathname}`);
|
||||
return [
|
||||
startInProgress(
|
||||
cacheKey,
|
||||
new ReadableStream({
|
||||
start: async (controller) => {
|
||||
const stream = fs.createReadStream(
|
||||
path.join(lanMount, pathname),
|
||||
);
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on("data", (chunk) => {
|
||||
controller.enqueue(chunk);
|
||||
chunks.push(chunk as Buffer);
|
||||
});
|
||||
stream.on("end", () => {
|
||||
controller.close();
|
||||
ramCache.set(cacheKey, Buffer.concat(chunks));
|
||||
finishInProgress(cacheKey);
|
||||
});
|
||||
stream.on("error", (error) => {
|
||||
controller.error(error);
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
"raw",
|
||||
"lan",
|
||||
];
|
||||
}
|
||||
|
||||
// 4. Fetch from server
|
||||
const url = `${compressionFormatMap[format]}${escapeUri(pathname)}`;
|
||||
log(`miss: ${format}${pathname}`);
|
||||
const response = await startInProgress(cacheKey, fetchFileUncached(url));
|
||||
const [stream1, stream2] = response.tee();
|
||||
handleDownload(cacheKey, format, stream2);
|
||||
return [stream1, format, "miss"];
|
||||
}
|
||||
|
||||
export async function prefetchFile(
|
||||
pathname: string,
|
||||
format: CompressionFormat = "zstd",
|
||||
) {
|
||||
const cacheKey = hashKey(`${pathname}:${format}`);
|
||||
const ramCacheHit = ramCache.get(cacheKey);
|
||||
if (ramCacheHit) {
|
||||
return;
|
||||
}
|
||||
if (hasLanMount) return;
|
||||
const url = `${compressionFormatMap[format]}${pathname}`;
|
||||
log(`prefetch: ${format}${pathname}`);
|
||||
const stream2 = await startInProgress(cacheKey, fetchFileUncached(url));
|
||||
handleDownload(cacheKey, format, stream2);
|
||||
}
|
||||
|
||||
async function handleDownload(
|
||||
cacheKey: string,
|
||||
format: CompressionFormat,
|
||||
stream2: ReadableStream,
|
||||
) {
|
||||
let chunks: Buffer[] = [];
|
||||
if (format !== "raw") {
|
||||
const file = await open(path.join(diskCacheRoot, cacheKey), "w");
|
||||
try {
|
||||
for await (const chunk of stream2) {
|
||||
await file.write(chunk);
|
||||
chunks.push(chunk);
|
||||
}
|
||||
} finally {
|
||||
file.close();
|
||||
}
|
||||
} else {
|
||||
for await (const chunk of stream2) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
const final = Buffer.concat(chunks);
|
||||
chunks.length = 0;
|
||||
ramCache.set(cacheKey, final);
|
||||
if (format !== "raw") {
|
||||
diskCache.set(cacheKey, final.byteLength);
|
||||
}
|
||||
finishInProgress(cacheKey);
|
||||
}
|
||||
|
||||
function hashKey(key: string): string {
|
||||
return createHash("sha1").update(key).digest("hex");
|
||||
}
|
||||
|
||||
function startInProgress<T extends Promise<ReadableStream> | ReadableStream>(
|
||||
cacheKey: string,
|
||||
promise: T,
|
||||
): T {
|
||||
if (promise instanceof Promise) {
|
||||
let resolve2: (stream: { stream: ReadableStream }) => void;
|
||||
let reject2: (error: Error) => void;
|
||||
const stream2Promise = new Promise<{ stream: ReadableStream }>(
|
||||
(resolve, reject) => {
|
||||
resolve2 = resolve;
|
||||
reject2 = reject;
|
||||
},
|
||||
);
|
||||
const stream1Promise = new Promise<ReadableStream>((resolve, reject) => {
|
||||
promise.then((stream) => {
|
||||
const [stream1, stream2] = stream.tee();
|
||||
const stream2Obj = { stream: stream2 };
|
||||
resolve2(stream2Obj);
|
||||
loadInProgress.set(cacheKey, stream2Obj);
|
||||
resolve(stream1);
|
||||
}, reject);
|
||||
});
|
||||
loadInProgress.set(cacheKey, stream2Promise);
|
||||
return stream1Promise as T;
|
||||
} else {
|
||||
const [stream1, stream2] = promise.tee();
|
||||
loadInProgress.set(cacheKey, { stream: stream2 });
|
||||
return stream1 as T;
|
||||
}
|
||||
}
|
||||
|
||||
function loadDiskCacheState(): Record<
|
||||
string,
|
||||
[size: number, lastAccess: number]
|
||||
> {
|
||||
try {
|
||||
const state = JSON.parse(
|
||||
fs.readFileSync(path.join(diskCacheRoot, "state.json"), "utf-8"),
|
||||
);
|
||||
return state;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveDiskCacheStateLater() {
|
||||
if (diskCacheFlush) {
|
||||
return;
|
||||
}
|
||||
diskCacheFlush = setTimeout(() => {
|
||||
saveDiskCacheState();
|
||||
}, 60_000) as NodeJS.Timeout;
|
||||
if (diskCacheFlush.unref) {
|
||||
diskCacheFlush.unref();
|
||||
}
|
||||
}
|
||||
|
||||
process.on("exit", () => {
|
||||
saveDiskCacheState();
|
||||
});
|
||||
|
||||
function saveDiskCacheState() {
|
||||
fs.writeFileSync(
|
||||
path.join(diskCacheRoot, "state.json"),
|
||||
JSON.stringify(diskCacheState),
|
||||
);
|
||||
}
|
||||
|
||||
function finishInProgress(cacheKey: string) {
|
||||
loadInProgress.delete(cacheKey);
|
||||
}
|
||||
|
||||
// Self signed certificate must be trusted to be able to request the above URL.
|
||||
//
|
||||
// Unfortunately, Bun and Deno are both not node.js compatible, so those two
|
||||
// runtimes need fallback implementations. The fallback implementations calls
|
||||
// fetch with the `agent` value as the RequestInit. Since `fetch` decompresses
|
||||
// the body for you, it must be disabled.
|
||||
const agent: any = typeof Bun !== "undefined"
|
||||
? {
|
||||
// Bun has two non-standard fetch extensions
|
||||
decompress: false,
|
||||
tls: {
|
||||
ca: caCert,
|
||||
},
|
||||
}
|
||||
// TODO: https://github.com/denoland/deno/issues/12291
|
||||
// : typeof Deno !== "undefined"
|
||||
// ? {
|
||||
// // Deno configures through the non-standard `client` extension
|
||||
// client: Deno.createHttpClient({
|
||||
// caCerts: [caCert.toString()],
|
||||
// }),
|
||||
// }
|
||||
// Node.js supports node:http
|
||||
: new Agent({
|
||||
ca: caCert,
|
||||
});
|
||||
|
||||
function fetchFileNode(pathname: string): Promise<ReadableStream> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request: ClientRequest = get(`${sourceOfTruth}/${pathname}`, {
|
||||
agent,
|
||||
});
|
||||
request.on("response", (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Failed to fetch ${pathname}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
response.on("data", (chunk) => {
|
||||
controller.enqueue(chunk);
|
||||
});
|
||||
|
||||
response.on("end", () => {
|
||||
controller.close();
|
||||
});
|
||||
|
||||
response.on("error", (error) => {
|
||||
controller.error(error);
|
||||
reject(error);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
resolve(stream);
|
||||
});
|
||||
|
||||
request.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchFileDenoBun(pathname: string): Promise<ReadableStream> {
|
||||
const req = await fetch(`${sourceOfTruth}/${pathname}`, agent);
|
||||
if (!req.ok) {
|
||||
throw new Error(`Failed to fetch ${pathname}`);
|
||||
}
|
||||
return req.body!;
|
||||
}
|
||||
|
||||
const fetchFileUncached =
|
||||
typeof Bun !== "undefined" || typeof Deno !== "undefined"
|
||||
? fetchFileDenoBun
|
||||
: fetchFileNode;
|
||||
|
||||
export async function toBuffer(
|
||||
stream: ReadableStream | Buffer,
|
||||
): Promise<Buffer> {
|
||||
if (!(stream instanceof ReadableStream)) {
|
||||
return stream;
|
||||
}
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
return Buffer.concat(chunks);
|
||||
}
|
23
src/file-viewer/cert.pem
Normal file
|
@ -0,0 +1,23 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDxTCCAq2gAwIBAgIUBaaOXVkkE+6yarNyvzofETb+WLEwDQYJKoZIhvcNAQEL
|
||||
BQAwdzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
|
||||
MRUwEwYDVQQKDAxNZWRpYSBTZXJ2ZXIxDzANBgNVBAMMBnplbml0aDEhMB8GCSqG
|
||||
SIb3DQEJARYSbWVAcGFwZXJjbG92ZXIubmV0MB4XDTI1MDQyNzIxNTU0MFoXDTM1
|
||||
MDQyNTIxNTU0MFowdzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYD
|
||||
VQQHDARDaXR5MRUwEwYDVQQKDAxNZWRpYSBTZXJ2ZXIxDzANBgNVBAMMBnplbml0
|
||||
aDEhMB8GCSqGSIb3DQEJARYSbWVAcGFwZXJjbG92ZXIubmV0MIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7lLwx8XwsuTeaIxTsHDL+Lx7eblsJ0XylVm
|
||||
0/iIJS1Mrq6Be9St6vDWK/BWqqAn+MdqzSfLMy8EKazuHKtbTm2vlUIkjw28SoWP
|
||||
6cRSCLx4hFGbF4tmRO+Bo+/4PpHPnheeolkjJ+CLO87tZ752D9JzjVND+WIj1QO+
|
||||
bm+JBIi1TFREPh22/fSZBRpaRgqHcUEhICaiXaufvxQ6eihQfGSe00I7zRzGgnMl
|
||||
51xjzkKkXd+r/FwTykd8ScJN25FMVDLsfJR59//geAZXYS25gQ4YL6R8u7ijidlS
|
||||
IoDG8N+Fzw7W4yI+y8fIN4W1x/HsjiQ665CuWY3TMYo98OaGwwIDAQABo0kwRzAm
|
||||
BgNVHREEHzAdggZ6ZW5pdGiCE25hcy5wYXBlcmNsb3Zlci5uZXQwHQYDVR0OBBYE
|
||||
FDXkgNsMYZv1Pr+95RCCk7eHACGOMA0GCSqGSIb3DQEBCwUAA4IBAQB6942odKyD
|
||||
TudifxRXbvcVe9LxSd7NimxRZzM5wTgA5KkxQT4CBM2wEPH/7e7Q/8scB9HbH2uP
|
||||
f2vixoCM+Z3BWiYHFFk+1pf2myUdiFV2BC9g80txEerRkGLc18V6CdYNJ9wNPkiO
|
||||
LW/RzXfEv+sqhaXh8dA46Ruz6SAbmscTMMYW4e9VYR+1p4Sm5UpTxrHzeg21YJKn
|
||||
ud8kO1r7RhVgUGzkAzNaIMiBuJqGGdD5yV7Ng5C/DlJ9AAeYu1diM5LkIKjf+/8M
|
||||
t/3l4eXS3Lda6+21rDvmfoK4Za6CAhcwgXIpqiRixE2MQNsxZ2XiJBVQHPrh8xYk
|
||||
L5fq8KTGFwtd
|
||||
-----END CERTIFICATE-----
|
291
src/file-viewer/cotyledon.tsx
Normal file
|
@ -0,0 +1,291 @@
|
|||
export function Speedbump() {
|
||||
return (
|
||||
<div class="panel last">
|
||||
<div className="header">
|
||||
an interlude
|
||||
</div>
|
||||
<div className="content file-view file-view-text speedbump">
|
||||
<canvas
|
||||
style="linear-gradient(45deg, #111318, #181f20)"
|
||||
data-canvas="cotyledon"
|
||||
>
|
||||
</canvas>
|
||||
<header>
|
||||
<h1>cotyledon</h1>
|
||||
</header>
|
||||
<div id="captcha" style="display: none;">
|
||||
<p style="max-width:480px">
|
||||
please prove you're not a robot by selecting all of the images with
|
||||
four-leaf clovers, until there are only regular clovers.
|
||||
<noscript>
|
||||
this will require javascript enabled on your computer to verify
|
||||
the mouse clicks.
|
||||
</noscript>
|
||||
</p>
|
||||
<div className="enter-container">
|
||||
<div className="image-grid">
|
||||
<button>
|
||||
<img src="/captcha/image/1.jpeg" alt="a four-leaf clover" />
|
||||
</button>
|
||||
<button>
|
||||
<img src="/captcha/image/2.jpeg" alt="a four-leaf clover" />
|
||||
</button>
|
||||
<button>
|
||||
<img src="/captcha/image/3.jpeg" alt="a four-leaf clover" />
|
||||
</button>
|
||||
<button>
|
||||
<img src="/captcha/image/4.jpeg" alt="a four-leaf clover" />
|
||||
</button>
|
||||
<button>
|
||||
<img src="/captcha/image/5.jpeg" alt="a four-leaf clover" />
|
||||
</button>
|
||||
<button>
|
||||
<img src="/captcha/image/6.jpeg" alt="a four-leaf clover" />
|
||||
</button>
|
||||
<button>
|
||||
<img src="/captcha/image/7.jpeg" alt="a four-leaf clover" />
|
||||
</button>
|
||||
<button>
|
||||
<img src="/captcha/image/8.jpeg" alt="a four-leaf clover" />
|
||||
</button>
|
||||
<button>
|
||||
<img src="/captcha/image/9.jpeg" alt="a four-leaf clover" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="enter-container">
|
||||
<button id="enter2">all done</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="first">
|
||||
<p>
|
||||
this place is sacred, but dangerous. i have to keep visitors to an
|
||||
absolute minimum; you'll get dust on all the artifacts.
|
||||
</p>
|
||||
<p>
|
||||
by entering our museum, you agree not to use your camera. flash off
|
||||
isn't enough; the bits and bytes are alergic even to a camera's
|
||||
sensor
|
||||
</p>
|
||||
<p style="font-size:0.9rem;">
|
||||
(in english: please do not store downloads after you're done viewing
|
||||
them)
|
||||
</p>
|
||||
<div class="enter-container">
|
||||
<button id="enter">break my boundaries</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Readme() {
|
||||
return (
|
||||
<div class="panel last">
|
||||
<div className="header">
|
||||
cotyledon
|
||||
</div>
|
||||
<div className="content file-view file-view-text">
|
||||
<div style="max-width: 71ch;padding:3rem;font-family:rmo,monospace">
|
||||
<p style="margin-top:0">
|
||||
welcome to the archive. if this is your first time here, i recommend
|
||||
starting in '<a href="/file/2017">2017</a>' and going
|
||||
chronologically from there. however, there is truly no wrong way to
|
||||
explore.
|
||||
</p>
|
||||
<p>
|
||||
note that there is a blanket trigger warning for everything in this
|
||||
archive: while there is nothing visually offensive, some portions of
|
||||
the text and emotions conveyed through this may hit extremely hard.
|
||||
you are warned.
|
||||
</p>
|
||||
<p>
|
||||
all file dates are real. at least as real as i could figure out.
|
||||
when i moved data across drives over my years, i accidentally had a
|
||||
few points where i stamped over all the dates with the day that
|
||||
moved the files. even fucked it up a final time in february 2025,
|
||||
while in the process of unfucking things.
|
||||
</p>
|
||||
<p>
|
||||
thankfully, my past self knew i'd want to assemble this kind of
|
||||
site, and because of that they were crazy about storing the dates of
|
||||
things inside of html, json/yaml files, and even in fucking
|
||||
databases. i'm glad it was all stored though, but jeez what a nerd.
|
||||
</p>
|
||||
<p>
|
||||
a few files were touched up for privacy, or otherwise re-encoded.
|
||||
some of them i added extra metadata.
|
||||
</p>
|
||||
<p>
|
||||
from the bottom of my heart: i hope you enjoy. it has been a
|
||||
nightmare putting this all together. technically and emotionally
|
||||
speaking. i'm glad we can put this all behind us, mark it as
|
||||
completed, and get started with the good shit.
|
||||
</p>
|
||||
<p>
|
||||
love,<br />clo
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
start here -> <a href="/file/2017">2017</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ForEveryone() {
|
||||
// deno-fmt-ignore
|
||||
return <><div class="for_everyone">
|
||||
<p>today is my 21st birthday. april 30th, 2025.</p>
|
||||
<p>it's been nearly six months starting hormones.</p>
|
||||
<p>sometimes i feel great,</p>
|
||||
<p>sometimes i get dysphoria.</p>
|
||||
<p>with the walls around me gone</p>
|
||||
<p>that shit hits way harder than it did before.</p>
|
||||
<p>ugh..</p>
|
||||
<p>i'm glad the pain i felt is now explained,</p>
|
||||
<p>but now rendered in high definition.</p>
|
||||
<p>the smallest strands of hair on my face and belly act</p>
|
||||
<p>as sharpened nails to pierce my soul.</p>
|
||||
<p></p>
|
||||
<p>it's all a pathway to better days; the sun had risen.</p>
|
||||
<p>one little step at a time for both of us.</p>
|
||||
<p>today i quit my job. free falling, it feels so weird.</p>
|
||||
<p>like sky diving.</p>
|
||||
<p>the only thing i feel is cold wind.</p>
|
||||
<p>the only thing i see is everything,</p>
|
||||
<p>and it's beautiful.</p>
|
||||
<p>i have a month of falling before the parachute activates,</p>
|
||||
<p>gonna spend as much time of it on art as i can.</p>
|
||||
<p>that was, after all, my life plan:</p>
|
||||
<p>i wanted to make art, all the time,</p>
|
||||
<p>for everyone.</p>
|
||||
<p></p>
|
||||
<p>then you see what happened</p>
|
||||
<p>to the world and the internet.</p>
|
||||
<p>i never really got to live through that golden age,</p>
|
||||
<p>it probably sucked back then too.</p>
|
||||
<p>but now the big sites definitely stopped being fun.</p>
|
||||
<p>they slide their cold hands up my body</p>
|
||||
<p>and feel me around. it's unwelcoming, and</p>
|
||||
<p>inconsiderate to how sensitive my skin is.</p>
|
||||
<p>i'm so fucking glad i broke up with YouTube</p>
|
||||
<p>and their devilish friends.</p>
|
||||
<p>my NAS is at 5 / 24 TB</p>
|
||||
<p>and probably wont fill for the next decade.</p>
|
||||
<p></p>
|
||||
<p>it took 2 months for me to notice my body changed.</p>
|
||||
<p>that day was really nice, but it hurt a lot.</p>
|
||||
<p>a sharp, satisfying pain in my chest gave me life.</p>
|
||||
<p>learned new instincts for my arms</p>
|
||||
<p>so they'd stop poking my new shape.</p>
|
||||
<p>when i look at my face</p>
|
||||
<p>it's like a different person.</p>
|
||||
<p>she was the same as before, but completely new.</p>
|
||||
<p>something changed</p>
|
||||
<p>or i'm now used to seeing what makes me smile.</p>
|
||||
<p>regardless, whatever i see in the mirror, i smile.</p>
|
||||
<p>and, i don't hear that old name much anymore</p>
|
||||
<p>aside from nightmares. and you'll never repeat it, ok?</p>
|
||||
<p>okay.</p>
|
||||
<p></p>
|
||||
<p>been playing 'new canaan' by 'bill wurtz' on loop</p>
|
||||
<p>in the background.</p>
|
||||
<p>it kinda just feels right.</p>
|
||||
<p>especially when that verse near the end comes on.</p>
|
||||
<p></p>
|
||||
<p>more people have been allowed to visit me.</p>
|
||||
<p>my apartment used to be just for me,</p>
|
||||
<p>but the more i felt like a person</p>
|
||||
<p>the more i felt like having others over.</p>
|
||||
<p>still have to decorate and clean it a little,</p>
|
||||
<p>but it isn't a job to do alone.</p>
|
||||
<p>we dragged a giant a rug across the city one day,</p>
|
||||
<p>and it felt was like anything was possible.</p>
|
||||
<p>sometimes i have ten people visit in a day,</p>
|
||||
<p>or sometimes i focus my little eyes on just one.</p>
|
||||
<p>i never really know what i want to do</p>
|
||||
<p>until the time actually comes.</p>
|
||||
<p></p>
|
||||
{/* FILIP */}
|
||||
<p>i think about the times i was by the water with you.</p>
|
||||
<p>the sun setting warmly, icy air fell on our shoulders.</p>
|
||||
{/* NATALIE */}
|
||||
<p>and how we walked up to the top of that hill,</p>
|
||||
<p>you picked up and disposed a nail on the ground,</p>
|
||||
<p>walking the city thru places i've never been.</p>
|
||||
{/* BEN */}
|
||||
<p>or hiking through the park talking about compilers,</p>
|
||||
<p>tiring me out until i'd fall asleep in your arms.</p>
|
||||
{/* ELENA */}
|
||||
<p>and the way you held on to my hand as i woke up,</p>
|
||||
<p>noticing how i was trying to hide nightmare's tears.</p>
|
||||
<p></p>
|
||||
{/* HIGH SCHOOL */}
|
||||
<p>i remember we were yelling lyrics loudly,</p>
|
||||
<p>out of key yet cheered on because it was fun.</p>
|
||||
{/* ADVAITH/NATALIE */}
|
||||
<p>and when we all toured the big corporate office,</p>
|
||||
{/* AYU/HARRIS */}
|
||||
<p>then snuck in to some startup's office after hours;</p>
|
||||
<p>i don't remember what movie we watched.</p>
|
||||
{/* COLLEGE, DAY 1 IN EV's ROOM */}
|
||||
<p>i remember laying on the bunk bed,</p>
|
||||
<p>while the rest played a card game.</p>
|
||||
{/* MEGHAN/MORE */}
|
||||
<p>with us all laying on the rug, staring at the TV</p>
|
||||
<p>as the ending twist to {/* SEVERANCE */'that show'} was revealed.</p>
|
||||
<p></p>
|
||||
<p>all the moments i cherish,</p>
|
||||
<p>i love because it was always me.</p>
|
||||
<p>i didn't have to pretend,</p>
|
||||
<p>even if i didn't know who i was at the time.</p>
|
||||
<p>you all were there. for me.</p>
|
||||
<p></p>
|
||||
<p>i don't want to pretend any more</p>
|
||||
<p>i want to be myself. for everyone.</p>
|
||||
<p></p>
|
||||
<p>oh, the song ended. i thought it was on loop?</p>
|
||||
<p>it's late... can hear the crickets...</p>
|
||||
<p>and i can almost see the moon... mmmm...</p>
|
||||
<p>...nah, too much light pollution.</p>
|
||||
<p></p>
|
||||
<p>one day. one day.</p>
|
||||
<p></p>
|
||||
<p class="normal">before i go, i want to show the uncensored version of "journal about a girl", because i can trust you at least. keep in mind, i think you're one of the first people to ever see this.</p>
|
||||
</div>
|
||||
<div class="for_everyone" style="max-width:80ch;">
|
||||
<blockquote>
|
||||
<p>journal - 2024-09-14</p>
|
||||
<p>been at HackMIT today on behalf of the company. it's fun. me and zack were running around looking for people that might be good hires. he had this magic arbitrary criteria to tell "oh this person is probably cracked let's talk to them" and we go to the first one. they were a nerd, perfect. they seemed to be extremely talented with some extreme software projects.<br/>
|
||||
okay.. oof... its still clouding my mind<br/>
|
||||
i cant shake that feeling away</p>
|
||||
<p>hold on...</p>
|
||||
<p>at some point they open one of their profiles to navigate to some code, and it displays for a couple of seconds: "pronouns: she/they". i don't actually know anything about this person, but it was my perception that she is trans. their appearance, physique, and age felt similar to me, which tends makes people think you are male.</p>
|
||||
<p>but... she was having fun being herself. being a legend of identity and of her skill in computer science. winning the physics major. making cool shit at the hackathon, and probably in life. my perception of her was the exact essence of who i myself wanted to be. i was jealous of her life.</p>
|
||||
<p>i tried hard to avoid a breakdown. success. but i was feeling distant. the next hour or so was disorienting, trying not to think about it too hard. i think there was one possibly interesting person we talked to. i don't remember any of the other conversations. they were not important. but i couldn't think through them regardless.</p>
|
||||
<p>later, i decided to read some of her code. i either have a huge dislike towards the Rust programming language and/or it was not high quality code. welp, so just is a person studying. my perception was just a perception, inaccurate but impacting. i know i need to become myself, whoever that is. otherwise, i'm just going to feel this shit at higher doses. i think about this every day, and the amount of time i feel being consumed by these problems only grows.</p>
|
||||
<p>getting through it all is a lonely feeling. not because no one is around, but because i am isolated emotionally. i know other people hit these feelings, but we all are too afraid to speak up, and it's all lonely.</p>
|
||||
<p>waiting on a reply from someone from healthcare. it'll be slow, but it will be okay.</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
<div class="for_everyone">
|
||||
<p class="normal">
|
||||
i've learned that even when i feel alone, it doesn't have to feel lonely. i know it's hard, dear. i know it's scary. but i promise it's possible. we're all in this together. struggling together. sacrificing together. we dedicate our lives to each you, and our art for everyone.
|
||||
</p>
|
||||
|
||||
<p class="normal" style="font-size:2rem;color:#9C91FF;font-family:times,serif;font-style:italic">
|
||||
and then we knew,<br/>
|
||||
just like paper airplanes: that we could fly...
|
||||
</p>
|
||||
<br />
|
||||
<p class="normal">
|
||||
<a href="/" style='text-decoration:underline;text-underline-offset:0.2em;'>fin.</a>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
ForEveryone.class = "text";
|
264
src/file-viewer/format.ts
Normal file
|
@ -0,0 +1,264 @@
|
|||
export function formatSize(bytes: number) {
|
||||
if (bytes < 1024) return `${bytes} bytes`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) {
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
||||
}
|
||||
export function formatDate(date: Date) {
|
||||
// YYYY-MM-DD, format in PST timezone
|
||||
return date.toLocaleDateString("sv", { timeZone: "America/Los_Angeles" });
|
||||
}
|
||||
export function formatShortDate(date: Date) {
|
||||
// YY-MM-DD, format in PST timezone
|
||||
return formatDate(date).slice(2);
|
||||
}
|
||||
export function formatDuration(seconds: number) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
|
||||
}
|
||||
export const escapeUri = (uri: string) =>
|
||||
encodeURIComponent(uri)
|
||||
.replace(/%2F/gi, "/")
|
||||
.replace(/%3A/gi, ":")
|
||||
.replace(/%2B/gi, "+")
|
||||
.replace(/%40/gi, "@")
|
||||
.replace(/%2D/gi, "-")
|
||||
.replace(/%5F/gi, "_")
|
||||
.replace(/%2E/gi, ".")
|
||||
.replace(/%2C/gi, ",");
|
||||
|
||||
import type { MediaFile } from "../db.ts";
|
||||
import { escapeHTML } from "../framework/bun-polyfill.ts";
|
||||
const findDomain = "paperclover.net";
|
||||
|
||||
// Returns escaped HTML
|
||||
// Features:
|
||||
// - autolink detection
|
||||
// - via \bpaperclover.net/[a-zA-Z0-9_\.+-]+
|
||||
// - via \b/file/[a-zA-Z0-9_\.+-]+
|
||||
// - via \bhttps://...
|
||||
// - via name of a sibling file's basename
|
||||
// - reformat (c) into ©
|
||||
//
|
||||
// This formatter was written with AI.
|
||||
export function highlightLinksInTextView(
|
||||
text: string,
|
||||
siblingFiles: MediaFile[] = [],
|
||||
) {
|
||||
const siblingLookup = Object.fromEntries(
|
||||
siblingFiles
|
||||
.filter((f) => f.basename !== "readme.txt")
|
||||
.map((f) => [f.basename, f]),
|
||||
);
|
||||
|
||||
// First escape the HTML to prevent XSS
|
||||
let processedText = escapeHTML(text);
|
||||
|
||||
// Replace (c) with ©
|
||||
processedText = processedText.replace(/\(c\)/gi, "©");
|
||||
|
||||
// Process all URL patterns in a single pass to avoid nested links
|
||||
// This regex matches:
|
||||
// 1. https:// or http:// URLs
|
||||
// 2. domain URLs without protocol (e.g., paperclover.net/path)
|
||||
// 3. /file/ URLs
|
||||
// 4. ./ relative paths
|
||||
|
||||
// We'll use a function to determine what kind of URL it is and format accordingly
|
||||
const urlRegex = new RegExp(
|
||||
"(" +
|
||||
// Group 1: https:// or http:// URLs
|
||||
"\\bhttps?:\\/\\/[a-zA-Z0-9_\\.\\-]+\\.[a-zA-Z0-9_\\.\\-]+[a-zA-Z0-9_\\.\\-\\/\\?=&%+#]*" +
|
||||
"|" +
|
||||
// Group 2: domain URLs without protocol
|
||||
findDomain +
|
||||
"\\/\\/[a-zA-Z0-9_\\.\\+\\-]+" +
|
||||
"|" +
|
||||
// Group 3: /file/ URLs
|
||||
"\\/file\\/[a-zA-Z0-9_\\.\\+\\-\\/]+" +
|
||||
")\\b" +
|
||||
"|" +
|
||||
// Group 4: ./ relative paths (not word-bounded)
|
||||
"(?<=\\s|^)\\.\\/[\\w\\-\\.]+",
|
||||
"g",
|
||||
);
|
||||
|
||||
processedText = processedText.replace(urlRegex, (match: string) => {
|
||||
// Case 1: https:// or http:// URLs
|
||||
if (match.startsWith("http")) {
|
||||
if (match.includes(findDomain)) {
|
||||
return `<a href="${
|
||||
match
|
||||
.replace(/https?:\/\/paperclover\.net\/+/, "/")
|
||||
.replace(/\/\/+/g, "/")
|
||||
}">${match}</a>`;
|
||||
}
|
||||
return `<a href="${
|
||||
match.replace(/\/\/+/g, "/")
|
||||
}" target="_blank" rel="noopener noreferrer">${match}</a>`;
|
||||
}
|
||||
|
||||
// Case 2: domain URLs without protocol
|
||||
if (match.startsWith(findDomain)) {
|
||||
return `<a href="${
|
||||
match.replace(findDomain + "/", "/").replace(/\/\/+/g, "/")
|
||||
}">${match}</a>`;
|
||||
}
|
||||
|
||||
// Case 3: /file/ URLs
|
||||
if (match.startsWith("/file/")) {
|
||||
return `<a href="${match}">${match}</a>`;
|
||||
}
|
||||
|
||||
// Case 4: ./ relative paths
|
||||
if (match.startsWith("./")) {
|
||||
const filename = match.substring(2);
|
||||
|
||||
// Check if the filename exists in sibling files
|
||||
const siblingFile = siblingFiles.find((f) => f.basename === filename);
|
||||
if (siblingFile) {
|
||||
return `<a href="/file/${siblingFile.path}">${match}</a>`;
|
||||
}
|
||||
|
||||
// If no exact match but we have sibling files, try to create a reasonable link
|
||||
if (siblingFiles.length > 0) {
|
||||
const currentDir = siblingFiles[0].path
|
||||
.split("/")
|
||||
.slice(0, -1)
|
||||
.join("/");
|
||||
return `<a href="/file/${currentDir}/${filename}">${match}</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
// Match sibling file names (only if they're not already part of a link)
|
||||
if (siblingFiles.length > 0) {
|
||||
// Create a regex pattern that matches any of the sibling file basenames
|
||||
// We need to escape special regex characters in the filenames
|
||||
const escapedBasenames = siblingFiles.map((f) =>
|
||||
f.basename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
);
|
||||
|
||||
// Join all basenames with | for the regex alternation
|
||||
const pattern = new RegExp(`\\b(${escapedBasenames.join("|")})\\b`, "g");
|
||||
|
||||
// We need to be careful not to replace text that's already in a link
|
||||
// So we'll split the text by HTML tags and only process the text parts
|
||||
const parts = processedText.split(/(<[^>]*>)/);
|
||||
|
||||
for (let i = 0; i < parts.length; i += 2) {
|
||||
// Only process text parts (even indices), not HTML tags (odd indices)
|
||||
if (i < parts.length) {
|
||||
parts[i] = parts[i].replace(pattern, (match: string) => {
|
||||
const file = siblingLookup[match];
|
||||
if (file) {
|
||||
return `<a href="/file/${
|
||||
file.path.replace(/^\//, "").replace(/\/\/+/g, "/")
|
||||
}">${match}</a>`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
processedText = parts.join("");
|
||||
}
|
||||
|
||||
return processedText;
|
||||
}
|
||||
|
||||
export function highlightConvo(text: string) {
|
||||
text = text.replace(/^#mode=convo\n/, "");
|
||||
|
||||
const lines = text.split("\n");
|
||||
const paras: { speaker: string | null; lines: string[] }[] = [];
|
||||
let currentPara: string[] = [];
|
||||
let currentSpeaker: string | null = null;
|
||||
let firstSpeaker = null;
|
||||
|
||||
const speakers: Record<string, string> = {};
|
||||
const getSpeaker = (s: string) => {
|
||||
if (s[1] === " " && speakers[s[0]]) {
|
||||
return s[0];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
let trimmed = line.trim();
|
||||
if (line.startsWith("#")) {
|
||||
// parse #X=Y
|
||||
const [_, speaker, color] = trimmed.match(/^#(.)=(.*)$/)!;
|
||||
speakers[speaker] = color;
|
||||
continue;
|
||||
}
|
||||
if (trimmed === "") {
|
||||
continue;
|
||||
}
|
||||
let speaker = getSpeaker(trimmed);
|
||||
if (speaker) {
|
||||
trimmed = trimmed.substring(speaker.length).trimStart();
|
||||
speaker = speakers[speaker];
|
||||
} else {
|
||||
speaker = "me";
|
||||
}
|
||||
|
||||
trimmed = trimmed.replace(
|
||||
/\[IMG:(\/file\/[^\]]+)\]/g,
|
||||
'<img src="$1" alt="attachment" class="convo-img" width="300" />',
|
||||
);
|
||||
|
||||
if (trimmed === "---" && speaker === "me") {
|
||||
trimmed = "<hr/>";
|
||||
}
|
||||
|
||||
if (speaker === currentSpeaker) {
|
||||
currentPara.push(trimmed);
|
||||
} else {
|
||||
if (currentPara.length > 0) {
|
||||
paras.push({
|
||||
speaker: currentSpeaker,
|
||||
lines: currentPara,
|
||||
});
|
||||
currentPara = [];
|
||||
}
|
||||
currentPara = [trimmed];
|
||||
currentSpeaker = speaker;
|
||||
firstSpeaker ??= speaker;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPara.length > 0) {
|
||||
paras.push({
|
||||
speaker: currentSpeaker,
|
||||
lines: currentPara,
|
||||
});
|
||||
}
|
||||
|
||||
return paras
|
||||
.map(({ speaker, lines }) => {
|
||||
return `<div class="s-${speaker}">${
|
||||
lines
|
||||
.map((line) => `<div class="line">${line}</div>`)
|
||||
.join("\n")
|
||||
}</div>`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function highlightHashComments(text: string) {
|
||||
const lines = text.split("\n");
|
||||
return lines
|
||||
.map((line) => {
|
||||
if (line.startsWith("#")) {
|
||||
return `<div style="color: var(--primary);">${line}</div>`;
|
||||
}
|
||||
return `<div>${line.trimEnd() || " "}</div>`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
1203
src/file-viewer/highlight-grammar/astro.plist
Normal file
1036
src/file-viewer/highlight-grammar/css.plist
Normal file
268
src/file-viewer/highlight-grammar/diff.plist
Normal file
|
@ -0,0 +1,268 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>fileTypes</key>
|
||||
<array>
|
||||
<string>patch</string>
|
||||
<string>diff</string>
|
||||
<string>rej</string>
|
||||
</array>
|
||||
<key>firstLineMatch</key>
|
||||
<string>(?x)^
|
||||
(===\ modified\ file
|
||||
|==== \s* // .+ \s - \s .+ \s+ ====
|
||||
|Index:\
|
||||
|---\ [^%\n]
|
||||
|\*\*\*.*\d{4}\s*$
|
||||
|\d+(,\d+)* (a|d|c) \d+(,\d+)* $
|
||||
|diff\ --git\
|
||||
|commit\ [0-9a-f]{40}$
|
||||
)</string>
|
||||
<key>keyEquivalent</key>
|
||||
<string>^~D</string>
|
||||
<key>name</key>
|
||||
<string>Diff</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.separator.diff</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>^((\*{15})|(={67})|(-{3}))$\n?</string>
|
||||
<key>name</key>
|
||||
<string>meta.separator.diff</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>^\d+(,\d+)*(a|d|c)\d+(,\d+)*$\n?</string>
|
||||
<key>name</key>
|
||||
<string>meta.diff.range.normal</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.range.diff</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>meta.toc-list.line-number.diff</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.range.diff</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>^(@@)\s*(.+?)\s*(@@.*)($\n?)?</string>
|
||||
<key>name</key>
|
||||
<string>meta.diff.range.unified</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.range.diff</string>
|
||||
</dict>
|
||||
<key>4</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.range.diff</string>
|
||||
</dict>
|
||||
<key>6</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.range.diff</string>
|
||||
</dict>
|
||||
<key>7</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.range.diff</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>^(((\-{3}) .+ (\-{4}))|((\*{3}) .+ (\*{4})))$\n?</string>
|
||||
<key>name</key>
|
||||
<string>meta.diff.range.context</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>^diff --git a/.*$\n?</string>
|
||||
<key>name</key>
|
||||
<string>meta.diff.header.git</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>^diff (-|\S+\s+\S+).*$\n?</string>
|
||||
<key>name</key>
|
||||
<string>meta.diff.header.command</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>4</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.from-file.diff</string>
|
||||
</dict>
|
||||
<key>6</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.from-file.diff</string>
|
||||
</dict>
|
||||
<key>7</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.from-file.diff</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>(^(((-{3}) .+)|((\*{3}) .+))$\n?|^(={4}) .+(?= - ))</string>
|
||||
<key>name</key>
|
||||
<string>meta.diff.header.from-file</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.to-file.diff</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.to-file.diff</string>
|
||||
</dict>
|
||||
<key>4</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.to-file.diff</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>(^(\+{3}) .+$\n?| (-) .* (={4})$\n?)</string>
|
||||
<key>name</key>
|
||||
<string>meta.diff.header.to-file</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.inserted.diff</string>
|
||||
</dict>
|
||||
<key>6</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.inserted.diff</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>^(((>)( .*)?)|((\+).*))$\n?</string>
|
||||
<key>name</key>
|
||||
<string>markup.inserted.diff</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.changed.diff</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>^(!).*$\n?</string>
|
||||
<key>name</key>
|
||||
<string>markup.changed.diff</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.deleted.diff</string>
|
||||
</dict>
|
||||
<key>6</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.deleted.diff</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>^(((<)( .*)?)|((-).*))$\n?</string>
|
||||
<key>name</key>
|
||||
<string>markup.deleted.diff</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>^(#)</string>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.comment.diff</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>comment</key>
|
||||
<string>Git produces unified diffs with embedded comments"</string>
|
||||
<key>end</key>
|
||||
<string>\n</string>
|
||||
<key>name</key>
|
||||
<string>comment.line.number-sign.diff</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>^index [0-9a-f]{7,40}\.\.[0-9a-f]{7,40}.*$\n?</string>
|
||||
<key>name</key>
|
||||
<string>meta.diff.index.git</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.key-value.diff</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>meta.toc-list.file-name.diff</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>^Index(:) (.+)$\n?</string>
|
||||
<key>name</key>
|
||||
<string>meta.diff.index</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>^Only in .*: .*$\n?</string>
|
||||
<key>name</key>
|
||||
<string>meta.diff.only-in</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>scopeName</key>
|
||||
<string>source.diff</string>
|
||||
<key>uuid</key>
|
||||
<string>7E848FF4-708E-11D9-97B4-0011242E4184</string>
|
||||
</dict>
|
||||
</plist>
|
169
src/file-viewer/highlight-grammar/dosbatch.plist
Normal file
|
@ -0,0 +1,169 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>uuid</key>
|
||||
<string>E07EC438-7B75-4437-8AA1-DA94C1E6EACC</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.command.dosbatch</string>
|
||||
<key>match</key>
|
||||
<string>\b(?i)(?:append|assoc|at|attrib|break|cacls|cd|chcp|chdir|chkdsk|chkntfs|cls|cmd|color|comp|compact|convert|copy|date|del|dir|diskcomp|diskcopy|doskey|echo|endlocal|erase|fc|find|findstr|format|ftype|graftabl|help|keyb|label|md|mkdir|mode|more|move|path|pause|popd|print|prompt|pushd|rd|recover|ren|rename|replace|restore|rmdir|set|setlocal|shift|sort|start|subst|time|title|tree|type|ver|verify|vol|xcopy)\b</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.control.statement.dosbatch</string>
|
||||
<key>match</key>
|
||||
<string>\b(?i)(?:goto|call|exit)\b</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.control.conditional.if.dosbatch</string>
|
||||
<key>match</key>
|
||||
<string>\b(?i)if\s+((not)\s+)(exist|defined|errorlevel|cmdextversion)\b</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.control.conditional.dosbatch</string>
|
||||
<key>match</key>
|
||||
<string>\b(?i)(?:if|else)\b</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.control.repeat.dosbatch</string>
|
||||
<key>match</key>
|
||||
<string>\b(?i)for\b</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.dosbatch</string>
|
||||
<key>match</key>
|
||||
<string>\b(?:EQU|NEQ|LSS|LEQ|GTR|GEQ)\b</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>comment.line.rem.dosbatch</string>
|
||||
<key>match</key>
|
||||
<string>\b(?i)rem(?:$|\s.*$)</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>comment.line.colons.dosbatch</string>
|
||||
<key>match</key>
|
||||
<string>\s*:\s*:.*$</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>variable.parameter.function.begin.shell</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>variable.parameter.function.dosbatch</string>
|
||||
<key>match</key>
|
||||
<string>(?i)(%)(~(?:f|d|p|n|x|s|a|t|z|\$[^:]*:)*)?\d</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>variable.parameter.loop.begin.shell</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>variable.parameter.loop.dosbatch</string>
|
||||
<key>match</key>
|
||||
<string>(?i)(%%)(~(?:f|d|p|n|x|s|a|t|z|\$[^:]*:)*)?[a-z]</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>variable.other.parsetime.begin.shell</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>variable.other.parsetime.end.shell</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>variable.other.parsetime.dosbatch</string>
|
||||
<key>match</key>
|
||||
<string>(%)[^%]+(%)</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>variable.other.delayed.begin.shell</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>variable.other.delayed.end.shell</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>variable.other.delayed.dosbatch</string>
|
||||
<key>match</key>
|
||||
<string>(!)[^!]+(!)</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>"</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.end.shell</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.begin.shell</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>string.quoted.double.dosbatch</string>
|
||||
<key>end</key>
|
||||
<string>"|$</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.pipe.dosbatch</string>
|
||||
<key>match</key>
|
||||
<string>[|]</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.redirect.shell</string>
|
||||
<key>match</key>
|
||||
<string>&>|\d*>&\d*|\d*(>>|>|<)|\d*<&|\d*<></string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>name</key>
|
||||
<string>Batch File</string>
|
||||
<key>scopeName</key>
|
||||
<string>source.dosbatch</string>
|
||||
<key>fileTypes</key>
|
||||
<array>
|
||||
<string>bat</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
386
src/file-viewer/highlight-grammar/json.plist
Normal file
|
@ -0,0 +1,386 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>fileTypes</key>
|
||||
<array>
|
||||
<string>json</string>
|
||||
<string>sublime-settings</string>
|
||||
<string>sublime-menu</string>
|
||||
<string>sublime-keymap</string>
|
||||
<string>sublime-mousemap</string>
|
||||
<string>sublime-theme</string>
|
||||
<string>sublime-build</string>
|
||||
<string>sublime-project</string>
|
||||
<string>sublime-completions</string>
|
||||
</array>
|
||||
<key>foldingStartMarker</key>
|
||||
<string>(?x) # turn on extended mode
|
||||
^ # a line beginning with
|
||||
\s* # some optional space
|
||||
[{\[] # the start of an object or array
|
||||
(?! # but not followed by
|
||||
.* # whatever
|
||||
[}\]] # and the close of an object or array
|
||||
,? # an optional comma
|
||||
\s* # some optional space
|
||||
$ # at the end of the line
|
||||
)
|
||||
| # ...or...
|
||||
[{\[] # the start of an object or array
|
||||
\s* # some optional space
|
||||
$ # at the end of the line</string>
|
||||
<key>foldingStopMarker</key>
|
||||
<string>(?x) # turn on extended mode
|
||||
^ # a line beginning with
|
||||
\s* # some optional space
|
||||
[}\]] # and the close of an object or array</string>
|
||||
<key>keyEquivalent</key>
|
||||
<string>^~J</string>
|
||||
<key>name</key>
|
||||
<string>JSON (Javascript Next)</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#value</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>repository</key>
|
||||
<dict>
|
||||
<key>array</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>\[</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.array.begin.json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>\]</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.array.end.json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>meta.structure.array.json</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#value</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>,</string>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.array.json</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>[^\s\]]</string>
|
||||
<key>name</key>
|
||||
<string>invalid.illegal.expected-array-separator.json</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>comments</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>/\*\*(?!/)</string>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.comment.json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>\*/</string>
|
||||
<key>name</key>
|
||||
<string>comment.block.documentation.json</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>/\*</string>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.comment.json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>\*/</string>
|
||||
<key>name</key>
|
||||
<string>comment.block.json</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.comment.json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>(//).*$\n?</string>
|
||||
<key>name</key>
|
||||
<string>comment.line.double-slash.js</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>constant</key>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(?:true|false|null)\b</string>
|
||||
<key>name</key>
|
||||
<string>constant.language.json</string>
|
||||
</dict>
|
||||
<key>number</key>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(?x) # turn on extended mode
|
||||
-? # an optional minus
|
||||
(?:
|
||||
0 # a zero
|
||||
| # ...or...
|
||||
[1-9] # a 1-9 character
|
||||
\d* # followed by zero or more digits
|
||||
)
|
||||
(?:
|
||||
(?:
|
||||
\. # a period
|
||||
\d+ # followed by one or more digits
|
||||
)?
|
||||
(?:
|
||||
[eE] # an e character
|
||||
[+-]? # followed by an option +/-
|
||||
\d+ # followed by one or more digits
|
||||
)? # make exponent optional
|
||||
)? # make decimal portion optional</string>
|
||||
<key>name</key>
|
||||
<string>constant.numeric.json</string>
|
||||
</dict>
|
||||
<key>object</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>\{</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.dictionary.begin.json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>\}</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.dictionary.end.json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>meta.structure.dictionary.json</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>comment</key>
|
||||
<string>the JSON object key</string>
|
||||
<key>include</key>
|
||||
<string>#objectkey</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#comments</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>:</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.dictionary.key-value.json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>(,)|(?=\})</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.dictionary.pair.json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>meta.structure.dictionary.value.json</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>comment</key>
|
||||
<string>the JSON object value</string>
|
||||
<key>include</key>
|
||||
<string>#value</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>[^\s,]</string>
|
||||
<key>name</key>
|
||||
<string>invalid.illegal.expected-dictionary-separator.json</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>[^\s\}]</string>
|
||||
<key>name</key>
|
||||
<string>invalid.illegal.expected-dictionary-separator.json</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>string</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>"</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.begin.json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>"</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.end.json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>string.quoted.double.json</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#stringcontent</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>objectkey</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>"</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.support.type.property-name.begin.json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>"</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.support.type.property-name.end.json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>string.json support.type.property-name.json</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#stringcontent</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>stringcontent</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(?x) # turn on extended mode
|
||||
\\ # a literal backslash
|
||||
(?: # ...followed by...
|
||||
["\\/bfnrt] # one of these characters
|
||||
| # ...or...
|
||||
u # a u
|
||||
[0-9a-fA-F]{4}) # and four hex digits</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.escape.json</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\.</string>
|
||||
<key>name</key>
|
||||
<string>invalid.illegal.unrecognized-string-escape.json</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>value</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#constant</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#number</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#string</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#array</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#object</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#comments</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>scopeName</key>
|
||||
<string>source.json</string>
|
||||
<key>uuid</key>
|
||||
<string>8f97457b-516e-48ce-83c7-08ae12fb327a</string>
|
||||
</dict>
|
||||
</plist>
|
1462
src/file-viewer/highlight-grammar/lua.plist
Normal file
9527
src/file-viewer/highlight-grammar/mdx.plist
Normal file
4019
src/file-viewer/highlight-grammar/php.plist
Normal file
1614
src/file-viewer/highlight-grammar/powershell.plist
Normal file
4009
src/file-viewer/highlight-grammar/python.plist
Normal file
1889
src/file-viewer/highlight-grammar/shell.plist
Normal file
736
src/file-viewer/highlight-grammar/toml.plist
Normal file
|
@ -0,0 +1,736 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>fileTypes</key>
|
||||
<array>
|
||||
<string>toml</string>
|
||||
</array>
|
||||
<key>keyEquivalent</key>
|
||||
<string>^~T</string>
|
||||
<key>name</key>
|
||||
<string>TOML</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#comments</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#groups</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#key_pair</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#invalid</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>repository</key>
|
||||
<dict>
|
||||
<key>comments</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>(^[ \t]+)?(?=#)</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.whitespace.comment.leading.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>(?!\G)</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>#</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.comment.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>\n</string>
|
||||
<key>name</key>
|
||||
<string>comment.line.number-sign.toml</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>groups</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.section.begin.toml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>[^\s.]+</string>
|
||||
<key>name</key>
|
||||
<string>entity.name.section.toml</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.section.begin.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>^\s*(\[)([^\[\]]*)(\])</string>
|
||||
<key>name</key>
|
||||
<string>meta.group.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.section.begin.toml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>[^\s.]+</string>
|
||||
<key>name</key>
|
||||
<string>entity.name.section.toml</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.section.begin.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>^\s*(\[\[)([^\[\]]*)(\]\])</string>
|
||||
<key>name</key>
|
||||
<string>meta.group.double.toml</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>invalid</key>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\S+(\s*(?=\S))?</string>
|
||||
<key>name</key>
|
||||
<string>invalid.illegal.not-allowed-here.toml</string>
|
||||
</dict>
|
||||
<key>key_pair</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>([A-Za-z0-9_-]+)\s*(=)\s*</string>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>variable.other.key.toml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.key-value.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>(?<=\S)(?<!=)|$</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#primatives</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>((")(.*?)("))\s*(=)\s*</string>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>variable.other.key.toml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.variable.begin.toml</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\([btnfr"\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.escape.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\[^btnfr"\\]</string>
|
||||
<key>name</key>
|
||||
<string>invalid.illegal.escape.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>"</string>
|
||||
<key>name</key>
|
||||
<string>invalid.illegal.not-allowed-here.toml</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>4</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.variable.end.toml</string>
|
||||
</dict>
|
||||
<key>5</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.key-value.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>(?<=\S)(?<!=)|$</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#primatives</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>((')([^']*)('))\s*(=)\s*</string>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>variable.other.key.toml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.variable.begin.toml</string>
|
||||
</dict>
|
||||
<key>4</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.variable.end.toml</string>
|
||||
</dict>
|
||||
<key>5</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.key-value.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>(?<=\S)(?<!=)|$</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#primatives</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>(?x)
|
||||
(
|
||||
(
|
||||
(?:
|
||||
[A-Za-z0-9_-]+ # Bare key
|
||||
| " (?:[^"\\]|\\.)* " # Double quoted key
|
||||
| ' [^']* ' # Sindle quoted key
|
||||
)
|
||||
(?:
|
||||
\s* \. \s* # Dot
|
||||
| (?= \s* =) # or look-ahead for equals
|
||||
)
|
||||
){2,} # Ensure at least one dot
|
||||
)
|
||||
\s*(=)\s*
|
||||
</string>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>variable.other.key.toml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\.</string>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.variable.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.variable.begin.toml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\([btnfr"\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.escape.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\[^btnfr"\\]</string>
|
||||
<key>name</key>
|
||||
<string>invalid.illegal.escape.toml</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.variable.end.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>(")((?:[^"\\]|\\.)*)(")</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.variable.begin.toml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.variable.end.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>(')[^']*(')</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.key-value.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>comment</key>
|
||||
<string>Dotted key</string>
|
||||
<key>end</key>
|
||||
<string>(?<=\S)(?<!=)|$</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#primatives</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>primatives</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>\G"""</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.begin.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>"{3,5}</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.end.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>string.quoted.triple.double.toml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\([btnfr"\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.escape.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\[^btnfr"\\\n]</string>
|
||||
<key>name</key>
|
||||
<string>invalid.illegal.escape.toml</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>\G"</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.begin.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>"</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.end.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>string.quoted.double.toml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\([btnfr"\\]|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.escape.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\[^btnfr"\\]</string>
|
||||
<key>name</key>
|
||||
<string>invalid.illegal.escape.toml</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>\G'''</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.begin.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>'{3,5}</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.end.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>string.quoted.triple.single.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>\G'</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.begin.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>'</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.end.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>string.quoted.single.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\G(?x)
|
||||
[0-9]{4}
|
||||
-
|
||||
(0[1-9]|1[012])
|
||||
-
|
||||
(?!00|3[2-9])[0-3][0-9]
|
||||
(
|
||||
[Tt ]
|
||||
(?!2[5-9])[0-2][0-9]
|
||||
:
|
||||
[0-5][0-9]
|
||||
:
|
||||
(?!6[1-9])[0-6][0-9]
|
||||
(\.[0-9]+)?
|
||||
(
|
||||
Z
|
||||
| [+-](?!2[5-9])[0-2][0-9]:[0-5][0-9]
|
||||
)?
|
||||
)?
|
||||
</string>
|
||||
<key>name</key>
|
||||
<string>constant.other.date.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\G(?x)
|
||||
(?!2[5-9])[0-2][0-9]
|
||||
:
|
||||
[0-5][0-9]
|
||||
:
|
||||
(?!6[1-9])[0-6][0-9]
|
||||
(\.[0-9]+)?
|
||||
</string>
|
||||
<key>name</key>
|
||||
<string>constant.other.time.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\G(true|false)</string>
|
||||
<key>name</key>
|
||||
<string>constant.language.boolean.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\G0x\h(\h|_\h)*</string>
|
||||
<key>name</key>
|
||||
<string>constant.numeric.hex.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\G0o[0-7]([0-7]|_[0-7])*</string>
|
||||
<key>name</key>
|
||||
<string>constant.numeric.octal.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\G0b[01]([01]|_[01])*</string>
|
||||
<key>name</key>
|
||||
<string>constant.numeric.binary.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\G[+-]?(inf|nan)</string>
|
||||
<key>name</key>
|
||||
<string>constant.numeric.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(?x)
|
||||
\G
|
||||
(
|
||||
[+-]?
|
||||
(
|
||||
0
|
||||
| ([1-9](([0-9]|_[0-9])+)?)
|
||||
)
|
||||
)
|
||||
(?=[.eE])
|
||||
(
|
||||
\.
|
||||
([0-9](([0-9]|_[0-9])+)?)
|
||||
)?
|
||||
(
|
||||
[eE]
|
||||
([+-]?[0-9](([0-9]|_[0-9])+)?)
|
||||
)?
|
||||
</string>
|
||||
<key>name</key>
|
||||
<string>constant.numeric.float.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(?x)
|
||||
\G
|
||||
(
|
||||
[+-]?
|
||||
(
|
||||
0
|
||||
| ([1-9](([0-9]|_[0-9])+)?)
|
||||
)
|
||||
)
|
||||
</string>
|
||||
<key>name</key>
|
||||
<string>constant.numeric.integer.toml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>\G\[</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.array.begin.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>\]</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.array.end.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>meta.array.toml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>(?=["'']|[+-]?[0-9]|[+-]?(inf|nan)|true|false|\[|\{)</string>
|
||||
<key>end</key>
|
||||
<string>,|(?=])</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.array.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#primatives</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#comments</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#invalid</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#comments</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#invalid</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>\G\{</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.inline-table.begin.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>\}</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.inline-table.end.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>meta.inline-table.toml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>(?=\S)</string>
|
||||
<key>end</key>
|
||||
<string>,|(?=})</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.inline-table.toml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#key_pair</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#comments</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>scopeName</key>
|
||||
<string>source.toml</string>
|
||||
<key>uuid</key>
|
||||
<string>7DEF2EDB-5BB7-4DD2-9E78-3541A26B7923</string>
|
||||
</dict>
|
||||
</plist>
|
9858
src/file-viewer/highlight-grammar/ts.plist
Normal file
10281
src/file-viewer/highlight-grammar/tsx.plist
Normal file
573
src/file-viewer/highlight-grammar/xml.plist
Normal file
|
@ -0,0 +1,573 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>fileTypes</key>
|
||||
<array>
|
||||
<string>xml</string>
|
||||
<string>xsd</string>
|
||||
<string>tld</string>
|
||||
<string>jsp</string>
|
||||
<string>pt</string>
|
||||
<string>cpt</string>
|
||||
<string>dtml</string>
|
||||
<string>rss</string>
|
||||
<string>opml</string>
|
||||
</array>
|
||||
<key>keyEquivalent</key>
|
||||
<string>^~X</string>
|
||||
<key>name</key>
|
||||
<string>XML</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>(<\?)\s*([-_a-zA-Z0-9]+)</string>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.tag.xml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.tag.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>(\?>)</string>
|
||||
<key>name</key>
|
||||
<string>meta.tag.metadata.processing.xml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string> ([a-zA-Z-]+)</string>
|
||||
<key>name</key>
|
||||
<string>entity.other.attribute-name.xml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#doublequotedString</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#singlequotedString</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>(<!)(DOCTYPE)\s+([:a-zA-Z_][:a-zA-Z0-9_.-]*)</string>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.tag.xml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.tag.xml</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.other.attribute-name.documentroot.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>\s*(>)</string>
|
||||
<key>name</key>
|
||||
<string>meta.tag.metadata.doctype.xml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#internalSubset</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string><[!%]--</string>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.comment.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>--%?></string>
|
||||
<key>name</key>
|
||||
<string>comment.block.xml</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>(<)((?:([-_a-zA-Z0-9]+)((:)))?([-_a-zA-Z0-9:]+))(?=(\s[^>]*)?></\2>)</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.tag.xml</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.tag.namespace.xml</string>
|
||||
</dict>
|
||||
<key>4</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.tag.xml</string>
|
||||
</dict>
|
||||
<key>5</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.namespace.xml</string>
|
||||
</dict>
|
||||
<key>6</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.tag.localname.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>(>(<))/(?:([-_a-zA-Z0-9]+)((:)))?([-_a-zA-Z0-9:]+)(>)</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.tag.xml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>meta.scope.between-tag-pair.xml</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.tag.namespace.xml</string>
|
||||
</dict>
|
||||
<key>4</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.tag.xml</string>
|
||||
</dict>
|
||||
<key>5</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.namespace.xml</string>
|
||||
</dict>
|
||||
<key>6</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.tag.localname.xml</string>
|
||||
</dict>
|
||||
<key>7</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.tag.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>meta.tag.no-content.xml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#tagStuff</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>(</?)(?:([-_a-zA-Z0-9]+)((:)))?([-_a-zA-Z0-9:]+)</string>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.tag.xml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.tag.namespace.xml</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.tag.xml</string>
|
||||
</dict>
|
||||
<key>4</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.namespace.xml</string>
|
||||
</dict>
|
||||
<key>5</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.tag.localname.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>(/?>)</string>
|
||||
<key>name</key>
|
||||
<string>meta.tag.xml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#tagStuff</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#entity</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#bare-ampersand</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string><%@</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.section.embedded.begin.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>%></string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.section.embedded.end.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>source.java-props.embedded.xml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>page|include|taglib</string>
|
||||
<key>name</key>
|
||||
<string>keyword.other.page-props.xml</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string><%[!=]?(?!--)</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.section.embedded.begin.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>(?!--)%></string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.section.embedded.end.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>source.java.embedded.xml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>source.java</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string><!\[CDATA\[</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.begin.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>]]></string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.end.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>string.unquoted.cdata.xml</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>repository</key>
|
||||
<dict>
|
||||
<key>EntityDecl</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>(<!)(ENTITY)\s+(%\s+)?([:a-zA-Z_][:a-zA-Z0-9_.-]*)(\s+(?:SYSTEM|PUBLIC)\s+)?</string>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.tag.xml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.other.entity.xml</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.entity.xml</string>
|
||||
</dict>
|
||||
<key>4</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>variable.language.entity.xml</string>
|
||||
</dict>
|
||||
<key>5</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.other.entitytype.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>(>)</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#doublequotedString</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#singlequotedString</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>bare-ampersand</key>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>&</string>
|
||||
<key>name</key>
|
||||
<string>invalid.illegal.bad-ampersand.xml</string>
|
||||
</dict>
|
||||
<key>doublequotedString</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>"</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.begin.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>"</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.end.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>string.quoted.double.xml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#entity</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#bare-ampersand</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>entity</key>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.constant.xml</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.constant.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>(&)([:a-zA-Z_][:a-zA-Z0-9_.-]*|#[0-9]+|#x[0-9a-fA-F]+)(;)</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.entity.xml</string>
|
||||
</dict>
|
||||
<key>internalSubset</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>(\[)</string>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.constant.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>(\])</string>
|
||||
<key>name</key>
|
||||
<string>meta.internalsubset.xml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#EntityDecl</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#parameterEntity</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>parameterEntity</key>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.constant.xml</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.constant.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>(%)([:a-zA-Z_][:a-zA-Z0-9_.-]*)(;)</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.parameter-entity.xml</string>
|
||||
</dict>
|
||||
<key>singlequotedString</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>'</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.begin.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>'</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>0</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.definition.string.end.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>string.quoted.single.xml</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#entity</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#bare-ampersand</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>tagStuff</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.other.attribute-name.namespace.xml</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.other.attribute-name.xml</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.namespace.xml</string>
|
||||
</dict>
|
||||
<key>4</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.other.attribute-name.localname.xml</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string> (?:([-_a-zA-Z0-9]+)((:)))?([-_a-zA-Z0-9]+)=</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#doublequotedString</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#singlequotedString</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>scopeName</key>
|
||||
<string>text.xml</string>
|
||||
<key>uuid</key>
|
||||
<string>D3C4E6DA-6B1C-11D9-8CC2-000D93589AF6</string>
|
||||
</dict>
|
||||
</plist>
|
1164
src/file-viewer/highlight-grammar/yaml.plist
Normal file
846
src/file-viewer/highlight-grammar/zig.plist
Normal file
|
@ -0,0 +1,846 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>fileTypes</key>
|
||||
<array>
|
||||
<string>zig</string>
|
||||
</array>
|
||||
<key>keyEquivalent</key>
|
||||
<string>^~Z</string>
|
||||
<key>name</key>
|
||||
<string>Zig</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#dummy_main</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>repository</key>
|
||||
<dict>
|
||||
<key>block</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>([a-zA-Z_][\w.]*|@\".+\")?\s*(\{)</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>storage.type.zig</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.section.braces.begin.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>(\})</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.section.braces.end.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#dummy_main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>character_escapes</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\n</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.escape.newline.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\r</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.escape.carrigereturn.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\t</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.escape.tabulator.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\\\</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.escape.backslash.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\'</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.escape.single-quote.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\\"</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.escape.double-quote.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\x[a-fA-F\d]{2}</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.escape.hexidecimal.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\u\{[a-fA-F\d]{1,6}\}</string>
|
||||
<key>name</key>
|
||||
<string>constant.character.escape.hexidecimal.zig</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>comments</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>///</string>
|
||||
<key>end</key>
|
||||
<string>$\n?</string>
|
||||
<key>name</key>
|
||||
<string>comment.line.documentation.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>//[^/]\s*TODO</string>
|
||||
<key>end</key>
|
||||
<string>$\n?</string>
|
||||
<key>name</key>
|
||||
<string>comment.line.todo.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>//[^/]*</string>
|
||||
<key>end</key>
|
||||
<string>$\n?</string>
|
||||
<key>name</key>
|
||||
<string>comment.line.zig</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>constants</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(null|undefined|true|false)\b</string>
|
||||
<key>name</key>
|
||||
<string>constant.language.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(?<!\.)(-?[\d_]+)(?!\.)\b</string>
|
||||
<key>name</key>
|
||||
<string>constant.numeric.integer.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(?<!\.)(0x[a-fA-F\d_]+)(?!\.)\b</string>
|
||||
<key>name</key>
|
||||
<string>constant.numeric.integer.hexadecimal.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(?<!\.)(0o[0-7_]+)(?!\.)\b</string>
|
||||
<key>name</key>
|
||||
<string>constant.numeric.integer.octal.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(?<!\.)(0b[01_]+)(?!\.)\b</string>
|
||||
<key>name</key>
|
||||
<string>constant.numeric.integer.binary.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(?<!\.)(-?\b[\d_]+(?:\.[\d_]+)?(?:[eE][+-]?[\d_]+)?)(?!\.)\b</string>
|
||||
<key>name</key>
|
||||
<string>constant.numeric.float.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(?<!\.)(-?\b0x[a-fA-F\d_]+(?:\.[a-fA-F\d_]+)?[pP]?(?:[+-]?[\d_]+)?)(?!\.)\b</string>
|
||||
<key>name</key>
|
||||
<string>constant.numeric.float.hexadecimal.zig</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>container_decl</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(?!\d)([a-zA-Z_]\w*|@\".+\")?(?=\s*=\s*(?:extern|packed)?\b\s*(?:union)\s*[(\{])</string>
|
||||
<key>name</key>
|
||||
<string>entity.name.union.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(?!\d)([a-zA-Z_]\w*|@\".+\")?(?=\s*=\s*(?:extern|packed)?\b\s*(?:struct)\s*[(\{])</string>
|
||||
<key>name</key>
|
||||
<string>entity.name.struct.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(?!\d)([a-zA-Z_]\w*|@\".+\")?(?=\s*=\s*(?:extern|packed)?\b\s*(?:enum)\s*[(\{])</string>
|
||||
<key>name</key>
|
||||
<string>entity.name.enum.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(?!\d)([a-zA-Z_]\w*|@\".+\")?(?=\s*=\s*(?:error)\s*[(\{])</string>
|
||||
<key>name</key>
|
||||
<string>entity.name.error.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>storage.type.error.zig</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.accessor.zig</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.error.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>\b(error)(\.)([a-zA-Z_]\w*|@\".+\")</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>dummy_main</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#label</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#function_type</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#punctuation</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#storage_modifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#container_decl</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#constants</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#comments</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#strings</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#storage</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#keywords</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#operators</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#support</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#field_decl</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#block</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#function_def</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#function_call</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#enum_literal</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>enum_literal</key>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(?<!\w|\)|\?|\}|\]|\*)(\.(?:[a-zA-Z_]\w*\b|@\"[^\"]*\"))(?!\(|\s*=[^=>])</string>
|
||||
<key>name</key>
|
||||
<string>constant.language.enum</string>
|
||||
</dict>
|
||||
<key>field_decl</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>([a-zA-Z_]\w*|@\".+\")\s*(:)\s*</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>variable.other.member.zig</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>([a-zA-Z_][\w.]*|@\".+\")?\s*(?:(,)|(=)|$)</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>storage.type.zig</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.zig</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.assignment.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#dummy_main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>function_call</key>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(?<!fn)\b([a-zA-Z_]\w*|@\".+\")(?=\s*\()</string>
|
||||
<key>name</key>
|
||||
<string>variable.function.zig</string>
|
||||
</dict>
|
||||
<key>function_def</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>(?<=fn)\s+([a-zA-Z_]\w*|@\".+\")(\()</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.function</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.section.parens.begin.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>(?<=\)[^\)])\s*([a-zA-Z_][\w.]*|@\".+\")?(!)?\s*(?:([a-zA-Z_][\w.]*|@\".+\")\b(?!\s*\())?</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>storage.type.zig</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.zig</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>storage.type.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#label</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#param_list</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>([a-zA-Z_][\w.]*|@\".+\")</string>
|
||||
<key>name</key>
|
||||
<string>storage.type.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#dummy_main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>function_type</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>\b(fn)\s*(\()</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>storage.type.function.zig</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.section.parens.begin.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>contentName</key>
|
||||
<string>meta.function.parameters.zig</string>
|
||||
<key>end</key>
|
||||
<string>(?<=\)|\})\s*([a-zA-Z_][\w.]*|@\".+\")?\s*(!)?\s*([a-zA-Z_][\w.]*|@\".+\")</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>storage.type.zig</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.zig</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>storage.type.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#label</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#param_list</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>([a-zA-Z_][\w.]*|@\".+\")</string>
|
||||
<key>name</key>
|
||||
<string>storage.type.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#dummy_main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>keywords</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(while|for|break|return|continue|asm|defer|errdefer|unreachable)\b</string>
|
||||
<key>name</key>
|
||||
<string>keyword.control.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(async|await|suspend|nosuspend|resume)\b</string>
|
||||
<key>name</key>
|
||||
<string>keyword.control.async.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(if|else|switch|try|catch|orelse)\b</string>
|
||||
<key>name</key>
|
||||
<string>keyword.control.conditional.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(?<!\w)(@import|@cImport|@cInclude)\b</string>
|
||||
<key>name</key>
|
||||
<string>keyword.control.import.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(usingnamespace)\b</string>
|
||||
<key>name</key>
|
||||
<string>keyword.other.usingnamespace.zig</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>label</key>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.control.zig</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.label.zig</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>entity.name.label.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>\b(break|continue)\s*:\s*([a-zA-Z_]\w*|@\".+\")\b|\b(?!\d)([a-zA-Z_]\w*|@\".+\")\b(?=\s*:\s*(?:\{|while\b))</string>
|
||||
</dict>
|
||||
<key>operators</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b!\b</string>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(==|(?:!|>|<)=?)</string>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.logical.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(and|or)\b</string>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.word.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>((?:(?:\+|-|\*)\%?|/|%|<<|>>|&|\|(?=[^\|])|\^)?=)</string>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.assignment.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>((?:\+|-|\*)\%?|/(?!/)|%)</string>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.arithmetic.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(<<|>>|&(?=[a-zA-Z_]|@\")|\|(?=[^\|])|\^|~)</string>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.bitwise.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(\+\+|\*\*|->|\.\?|\.\*|&(?=[a-zA-Z_]|@\")|\?|\|\||\.{2,3})</string>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.other.zig</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>param_list</key>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>([a-zA-Z_]\w*|@\".+\")\s*(:)\s*</string>
|
||||
<key>beginCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>variable.parameter.zig</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>end</key>
|
||||
<string>([a-zA-Z_][\w.]*|@\".+\")?\s*(?:(,)|(\)))</string>
|
||||
<key>endCaptures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>storage.type.zig</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.zig</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>punctuation.section.parens.end.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#dummy_main</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>([a-zA-Z_][\w.]*|@\".+\")</string>
|
||||
<key>name</key>
|
||||
<string>storage.type.zig</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>punctuation</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>,</string>
|
||||
<key>name</key>
|
||||
<string>punctuation.separator.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>;</string>
|
||||
<key>name</key>
|
||||
<string>punctuation.terminator.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(\()</string>
|
||||
<key>name</key>
|
||||
<string>punctuation.section.parens.begin.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(\))</string>
|
||||
<key>name</key>
|
||||
<string>punctuation.section.parens.end.zig</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>storage</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(bool|void|noreturn|type|anyerror|anytype)\b</string>
|
||||
<key>name</key>
|
||||
<string>storage.type.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(?<!\.)([iu]\d+|[iu]size|comptime_int)\b</string>
|
||||
<key>name</key>
|
||||
<string>storage.type.integer.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(f16|f32|f64|f128|comptime_float)\b</string>
|
||||
<key>name</key>
|
||||
<string>storage.type.float.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(c_short|c_ushort|c_int|c_uint|c_long|c_ulong|c_longlong|c_ulonglong|c_longdouble|c_void)\b</string>
|
||||
<key>name</key>
|
||||
<string>storage.type.c_compat.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>captures</key>
|
||||
<dict>
|
||||
<key>1</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>storage.type.zig</string>
|
||||
</dict>
|
||||
<key>2</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>keyword.operator.zig</string>
|
||||
</dict>
|
||||
<key>3</key>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>storage.type.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>match</key>
|
||||
<string>\b(anyframe)\b\s*(->)?\s*(?:([a-zA-Z_][\w.]*|@\".+\")\b(?!\s*\())?</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\bfn\b</string>
|
||||
<key>name</key>
|
||||
<string>storage.type.function.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\btest\b</string>
|
||||
<key>name</key>
|
||||
<string>storage.type.test.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\bstruct\b</string>
|
||||
<key>name</key>
|
||||
<string>storage.type.struct.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\benum\b</string>
|
||||
<key>name</key>
|
||||
<string>storage.type.enum.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\bunion\b</string>
|
||||
<key>name</key>
|
||||
<string>storage.type.union.zig</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\berror\b</string>
|
||||
<key>name</key>
|
||||
<string>storage.type.error.zig</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>storage_modifier</key>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\b(const|var|extern|packed|export|pub|noalias|inline|noinline|comptime|volatile|align|linksection|threadlocal|allowzero)\b</string>
|
||||
<key>name</key>
|
||||
<string>storage.modifier.zig</string>
|
||||
</dict>
|
||||
<key>strings</key>
|
||||
<dict>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>\'</string>
|
||||
<key>end</key>
|
||||
<string>\'</string>
|
||||
<key>name</key>
|
||||
<string>string.quoted.single.zig</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#character_escapes</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\[^\'][^\']*?</string>
|
||||
<key>name</key>
|
||||
<string>invalid.illegal.character.zig</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>c?\"</string>
|
||||
<key>end</key>
|
||||
<string>\"</string>
|
||||
<key>name</key>
|
||||
<string>string.quoted.double.zig</string>
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>include</key>
|
||||
<string>#character_escapes</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>\\[^\'][^\']*?</string>
|
||||
<key>name</key>
|
||||
<string>invalid.illegal.character.zig</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>begin</key>
|
||||
<string>c?\\\\</string>
|
||||
<key>end</key>
|
||||
<string>$\n?</string>
|
||||
<key>name</key>
|
||||
<string>string.quoted.other.zig</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>support</key>
|
||||
<dict>
|
||||
<key>match</key>
|
||||
<string>(?<!\w)@[^\"\d][a-zA-Z_]\w*\b</string>
|
||||
<key>name</key>
|
||||
<string>support.function.zig</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>scopeName</key>
|
||||
<string>source.zig</string>
|
||||
<key>uuid</key>
|
||||
<string>06C2FF99-3080-441A-9019-460C51E93116</string>
|
||||
</dict>
|
||||
</plist>
|
204
src/file-viewer/highlight.ts
Normal file
|
@ -0,0 +1,204 @@
|
|||
import { onceAsync } from "../lib.ts";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import * as oniguruma from "vscode-oniguruma";
|
||||
import * as textmate from "vscode-textmate";
|
||||
import { escapeHTML } from "../framework/bun-polyfill.ts";
|
||||
|
||||
const languages = [
|
||||
"ts",
|
||||
"tsx",
|
||||
"zig",
|
||||
"json",
|
||||
"css",
|
||||
"astro",
|
||||
"mdx",
|
||||
"lua",
|
||||
"shell",
|
||||
"dosbatch",
|
||||
"powershell",
|
||||
"yaml",
|
||||
"toml",
|
||||
"xml",
|
||||
"python",
|
||||
"php",
|
||||
"diff",
|
||||
] as const;
|
||||
const altScopes: Record<string, string> = {
|
||||
astro: "text.html.astro",
|
||||
xml: "text.xml",
|
||||
php: "text.html.php",
|
||||
};
|
||||
export type Language = (typeof languages)[number];
|
||||
|
||||
const scopes = [
|
||||
// CSS
|
||||
["punctuation.definition.keyword", "keyword", "css"],
|
||||
["entity.name.tag.css", "class", "css"],
|
||||
["meta.selector.css", "method", "css"],
|
||||
["entity.other.attribute-name.class.css", "builtin", "css"],
|
||||
["punctuation.definition.entity", "builtin", "css"],
|
||||
["variable.css", "parameter", "css"],
|
||||
|
||||
// JSON
|
||||
["support.type.property-name.json", "variable", "json"],
|
||||
["constant.numeric", "method", "json"],
|
||||
["constant", "class", "json"],
|
||||
|
||||
// Lua
|
||||
["entity.name", "class", "lua"],
|
||||
|
||||
// Diff
|
||||
["punctuation.definition.deleted", "variable", "diff"],
|
||||
["markup.deleted", "variable", "diff"],
|
||||
["punctuation.definition.inserted", "method", "diff"],
|
||||
["markup.inserted", "method", "diff"],
|
||||
["meta.diff.range", "string", "diff"],
|
||||
["punctuation.definition.range", "string", "diff"],
|
||||
["meta.toc-list-line.number", "keyword", "diff"],
|
||||
["meta.diff", "comment", "diff"],
|
||||
|
||||
// General
|
||||
["meta.object-literal.key", "property"],
|
||||
["comment", "comment"],
|
||||
["string", "string"],
|
||||
["storage", "keyword"],
|
||||
["keyword", "keyword"],
|
||||
["variable.parameter", "parameter"],
|
||||
["entity.name.function", "method"],
|
||||
["support.type.primitive", "builtin"],
|
||||
["entity.name.type", "class"],
|
||||
["support.type", "class"],
|
||||
["support.class", "class"],
|
||||
["constant.language", "builtin"],
|
||||
["constant", "constant"],
|
||||
["support.constant", "constant"],
|
||||
["meta.parameters", "parameter"],
|
||||
["support.function", "method"],
|
||||
["variable", "variable"],
|
||||
["punctuation", null],
|
||||
["meta.function-call", "method"],
|
||||
] as const;
|
||||
|
||||
interface HighlightLinesOptions {
|
||||
lines: string[];
|
||||
grammar: textmate.IGrammar;
|
||||
state: textmate.StateStack;
|
||||
language: Language;
|
||||
}
|
||||
|
||||
export function getStyle(scopesToCheck: string[], langugage: Language) {
|
||||
if (import.meta.main) console.log(scopesToCheck);
|
||||
for (const scope of scopes) {
|
||||
if (scope[2] && scope[2] !== langugage) continue;
|
||||
const find = scopesToCheck.find((s) => s.startsWith(scope[0]));
|
||||
if (find) {
|
||||
return scope[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function highlightLines({
|
||||
lines,
|
||||
grammar,
|
||||
state,
|
||||
language,
|
||||
}: HighlightLinesOptions) {
|
||||
let html = "";
|
||||
let lastHtmlStyle: string | null = null;
|
||||
|
||||
const { length } = lines;
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
const { tokens, ruleStack, stoppedEarly } = grammar.tokenizeLine(
|
||||
lines[i],
|
||||
state,
|
||||
);
|
||||
if (stoppedEarly) throw new Error("TODO: Tokenization stopped early?");
|
||||
state = ruleStack;
|
||||
|
||||
for (const token of tokens) {
|
||||
const str = lines[i].slice(token.startIndex, token.endIndex);
|
||||
if (str.trim().length === 0) {
|
||||
// Emit but do not consider scope changes
|
||||
html += escapeHTML(str);
|
||||
continue;
|
||||
}
|
||||
|
||||
const style = getStyle(token.scopes, language);
|
||||
if (style !== lastHtmlStyle) {
|
||||
if (lastHtmlStyle) html += "</span>";
|
||||
if (style) html += `<span class='${style}'>`;
|
||||
}
|
||||
html += escapeHTML(str);
|
||||
lastHtmlStyle = style;
|
||||
}
|
||||
html += "\n";
|
||||
}
|
||||
|
||||
if (lastHtmlStyle) html += "</span>";
|
||||
|
||||
return { state, html };
|
||||
}
|
||||
|
||||
export const getRegistry = onceAsync(async () => {
|
||||
const wasmBin = await fs.readFile(
|
||||
path.join(
|
||||
import.meta.dirname,
|
||||
"../node_modules/vscode-oniguruma/release/onig.wasm",
|
||||
),
|
||||
);
|
||||
await oniguruma.loadWASM(wasmBin);
|
||||
|
||||
return new textmate.Registry({
|
||||
onigLib: Promise.resolve({
|
||||
createOnigScanner: (patterns) => new oniguruma.OnigScanner(patterns),
|
||||
createOnigString: (s) => new oniguruma.OnigString(s),
|
||||
}),
|
||||
loadGrammar: async (scopeName: string) => {
|
||||
for (const lang of languages) {
|
||||
if (scopeName.endsWith(`.${lang}`)) {
|
||||
const file = await fs.readFile(
|
||||
path.join(import.meta.dirname, `highlight-grammar/${lang}.plist`),
|
||||
"utf-8",
|
||||
);
|
||||
return textmate.parseRawGrammar(file);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export async function highlightCode(code: string, language: Language) {
|
||||
const registry = await getRegistry();
|
||||
const grammar = await registry.loadGrammar(
|
||||
altScopes[language] ?? "source." + language,
|
||||
);
|
||||
if (!grammar) {
|
||||
throw new Error(`No grammar found for language: ${language}`);
|
||||
}
|
||||
let state = textmate.INITIAL;
|
||||
const { html } = highlightLines({
|
||||
lines: code.split("\n"),
|
||||
grammar,
|
||||
state,
|
||||
language,
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
if (import.meta.main) {
|
||||
// validate exts
|
||||
for (const ext of languages) {
|
||||
if (
|
||||
!existsSync(
|
||||
path.join(import.meta.dirname, `highlight-grammar/${ext}.plist`),
|
||||
)
|
||||
) {
|
||||
console.error(`Missing grammar for ${ext}`);
|
||||
}
|
||||
const html = await highlightCode("wwwwwwwwwwwaaaaaaaaaaaaaaaa", ext);
|
||||
}
|
||||
console.log(await highlightCode(`{"maps":"damn"`, "json"));
|
||||
}
|
0
src/file-viewer/models/BlobAsset.ts
Normal file
0
src/file-viewer/models/FilePermission.ts
Normal file
0
src/file-viewer/models/MediaFile.ts
Normal file
27
src/file-viewer/pages/file.cotyledon_enterance.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { MediaFile } from "../db";
|
||||
import { useInlineScript } from "../framework/page-resources";
|
||||
import { Readme } from "../media/cotyledon";
|
||||
import { MediaPanel } from "../pages-dynamic/file_viewer";
|
||||
import "../media/files.css";
|
||||
|
||||
export const theme = {
|
||||
bg: "#312652",
|
||||
fg: "#f0f0ff",
|
||||
primary: "#fabe32",
|
||||
};
|
||||
|
||||
export default function CotyledonPage() {
|
||||
useInlineScript("canvas_cotyledon");
|
||||
useInlineScript("file_viewer");
|
||||
return (
|
||||
<div class="files ctld ctld-et">
|
||||
<MediaPanel
|
||||
file={MediaFile.getByPath("/")!}
|
||||
isLast={false}
|
||||
activeFilename={null}
|
||||
hasCotyledonCookie={true}
|
||||
/>
|
||||
<Readme />
|
||||
</div>
|
||||
);
|
||||
}
|
27
src/file-viewer/pages/file.cotyledon_speedbump.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { MediaFile } from "../db";
|
||||
import { useInlineScript } from "../framework/page-resources";
|
||||
import { Speedbump } from "../media/cotyledon";
|
||||
import { MediaPanel } from "../pages-dynamic/file_viewer";
|
||||
import "../media/files.css";
|
||||
|
||||
export const theme = {
|
||||
bg: "#312652",
|
||||
fg: "#f0f0ff",
|
||||
primary: "#fabe32",
|
||||
};
|
||||
|
||||
export default function CotyledonPage() {
|
||||
useInlineScript("canvas_cotyledon");
|
||||
useInlineScript("file_viewer");
|
||||
return (
|
||||
<div class="files ctld ctld-sb">
|
||||
<MediaPanel
|
||||
file={MediaFile.getByPath("/")!}
|
||||
isLast={false}
|
||||
activeFilename={null}
|
||||
hasCotyledonCookie={false}
|
||||
/>
|
||||
<Speedbump />
|
||||
</div>
|
||||
);
|
||||
}
|
9
src/file-viewer/redirects.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export const mediaRedirects: Record<string, string> = {
|
||||
"/q+a/172533.png": "/q+a/172533.jpg",
|
||||
"/q+a/2021-12-05_smooth.mp4": "/2019/smooth.mp4",
|
||||
"/q+a/temp_2022-08-17-19-43-32.m4a":
|
||||
"/2023/g-missing/fragments/2022-08-17-19-43-32.m4a",
|
||||
"/q+a/2023-02-09_20-5814i.png":
|
||||
"/2023/g-is-missing/fragments/2023-02-09_20-5814i.png",
|
||||
"/2024/waterfalls/": "/2025/waterfalls/",
|
||||
};
|
233
src/file-viewer/scripts/canvas_2017.ts
Normal file
|
@ -0,0 +1,233 @@
|
|||
// Vibe coded with AI
|
||||
(globalThis as any).canvas_2017 = function (canvas: HTMLCanvasElement) {
|
||||
const isStandalone = canvas.getAttribute("data-standalone") === "true";
|
||||
// Configuration interface for the checkerboard effect
|
||||
interface CheckerboardConfig {
|
||||
fps: number; // frames per second
|
||||
color1: string; // first checkerboard color
|
||||
color2: string; // second checkerboard color
|
||||
opacity: number; // opacity of each checkerboard (0-1)
|
||||
speedX1: number; // horizontal speed of first checkerboard (pixels per second)
|
||||
speedY1: number; // vertical speed of first checkerboard (pixels per second)
|
||||
speedX2: number; // horizontal speed of second checkerboard (pixels per second)
|
||||
speedY2: number; // vertical speed of second checkerboard (pixels per second)
|
||||
baseTileSize: number; // base size of checkerboard tiles
|
||||
sizeVariation: number; // maximum variation in tile size (pixels)
|
||||
sineFrequency1: number; // frequency of first sine wave for size variation
|
||||
sineFrequency2: number; // frequency of second sine wave for size variation
|
||||
sineOffset: number; // offset between the two sine waves (radians)
|
||||
rotation: number; // rotation in degrees for the entire pattern
|
||||
rotation2: number; // rotation in degrees for the entire pattern
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
const config: CheckerboardConfig = {
|
||||
fps: 30,
|
||||
color1: "#1A1C17",
|
||||
color2: "#1A1C17",
|
||||
opacity: 0.3,
|
||||
speedX1: -0.02, // moving left slowly
|
||||
speedY1: -0.01, // moving up slowly
|
||||
speedX2: -0.015, // moving left (slightly slower)
|
||||
speedY2: 0.012, // moving down slowly
|
||||
baseTileSize: 200,
|
||||
sizeVariation: 1.5,
|
||||
sineFrequency1: 0.0005,
|
||||
sineFrequency2: 0.0008,
|
||||
sineOffset: Math.PI / 2, // 90 degrees offset
|
||||
rotation: 2, // 5 degree rotation
|
||||
rotation2: -2, // 5 degree rotation
|
||||
};
|
||||
|
||||
// Get the canvas context
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
console.error("Could not get canvas context");
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// Make canvas transparent
|
||||
if (isStandalone) {
|
||||
canvas.style.backgroundColor = "#737D60";
|
||||
} else {
|
||||
canvas.style.backgroundColor = "transparent";
|
||||
}
|
||||
|
||||
// Variables to track position and animation
|
||||
let width = canvas.width;
|
||||
let height = canvas.height;
|
||||
let animationFrameId: number;
|
||||
let lastFrameTime = 0;
|
||||
const frameInterval = 1000 / config.fps;
|
||||
|
||||
// Position offsets for the two checkerboards (centered)
|
||||
let offset1X = 0;
|
||||
let offset1Y = 0;
|
||||
let offset2X = 0;
|
||||
let offset2Y = 0;
|
||||
|
||||
// Time variable for sine wave calculation
|
||||
let time = 0;
|
||||
|
||||
// Convert rotation to radians
|
||||
const rotationRad = (config.rotation * Math.PI) / 180;
|
||||
const rotationRad2 = (config.rotation2 * Math.PI) / 180;
|
||||
|
||||
// Update canvas dimensions when resized
|
||||
const updateDimensions = () => {
|
||||
width = canvas.width = canvas.clientWidth;
|
||||
height = canvas.height = canvas.clientHeight;
|
||||
};
|
||||
|
||||
// Calculate the diagonal length of the canvas (to ensure rotation covers corners)
|
||||
const calculateDiagonal = () => {
|
||||
return Math.sqrt(width * width + height * height);
|
||||
};
|
||||
|
||||
// Draw a single checkerboard pattern scaled from center with rotation
|
||||
const drawCheckerboard = (
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
tileSize: number,
|
||||
color1: string,
|
||||
color2: string,
|
||||
opacity: number,
|
||||
rotationRad: number,
|
||||
) => {
|
||||
ctx.globalAlpha = opacity;
|
||||
|
||||
// Get the center of the viewport
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
// Save the current transformation state
|
||||
ctx.save();
|
||||
|
||||
// Move to the center of the canvas, rotate, then move back
|
||||
ctx.translate(centerX, centerY);
|
||||
ctx.rotate(rotationRad);
|
||||
|
||||
// Calculate the number of tiles needed to cover the rotated canvas
|
||||
// We need to use the diagonal length to ensure we cover the corners when rotated
|
||||
const diagonal = calculateDiagonal();
|
||||
const tilesX = Math.ceil(diagonal / tileSize) + 6; // Added extra tiles for rotation
|
||||
const tilesY = Math.ceil(diagonal / tileSize) + 6;
|
||||
|
||||
// Calculate how many tiles fit from center to edge (in each direction)
|
||||
const halfTilesX = Math.ceil(tilesX / 2);
|
||||
const halfTilesY = Math.ceil(tilesY / 2);
|
||||
|
||||
// Adjust the offset to be relative to the center
|
||||
// The modulo ensures the pattern repeats smoothly even with scaling
|
||||
const adjustedOffsetX = offsetX % (tileSize * 2);
|
||||
const adjustedOffsetY = offsetY % (tileSize * 2);
|
||||
|
||||
// Draw the checker pattern, centered on the viewport
|
||||
for (let y = -halfTilesY; y <= halfTilesY; y++) {
|
||||
for (let x = -halfTilesX; x <= halfTilesX; x++) {
|
||||
// Determine if this tile should be colored (creating checker pattern)
|
||||
// We add a large number to ensure (x+y) is always positive for the modulo
|
||||
if ((x + y + 1000) % 2 === 0) {
|
||||
ctx.fillStyle = color1;
|
||||
} else {
|
||||
ctx.fillStyle = color2;
|
||||
}
|
||||
|
||||
// Calculate the position of this tile relative to the center
|
||||
// The adjusted offset creates the movement effect
|
||||
const posX = (x * tileSize) + adjustedOffsetX;
|
||||
const posY = (y * tileSize) + adjustedOffsetY;
|
||||
|
||||
// Draw the tile
|
||||
ctx.fillRect(
|
||||
posX - tileSize / 2,
|
||||
posY - tileSize / 2,
|
||||
tileSize,
|
||||
tileSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the transformation state
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
// Animation loop
|
||||
const animate = (currentTime: number) => {
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
|
||||
// Control frame rate
|
||||
if (currentTime - lastFrameTime < frameInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the time elapsed since the last frame
|
||||
const dt = currentTime - lastFrameTime;
|
||||
lastFrameTime = currentTime;
|
||||
|
||||
// Increment time for sine wave calculation
|
||||
time += dt;
|
||||
|
||||
// Update the position offsets based on speed and elapsed time
|
||||
offset1X += config.speedX1 * dt;
|
||||
offset1Y += config.speedY1 * dt;
|
||||
offset2X += config.speedX2 * dt;
|
||||
offset2Y += config.speedY2 * dt;
|
||||
|
||||
// Calculate the tile sizes using sine waves
|
||||
const tileSize1 = config.baseTileSize +
|
||||
Math.sin(time * config.sineFrequency1) * config.sizeVariation;
|
||||
const tileSize2 = config.baseTileSize +
|
||||
Math.sin(time * config.sineFrequency2 + config.sineOffset) *
|
||||
config.sizeVariation;
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// Draw the two checkerboards
|
||||
drawCheckerboard(
|
||||
offset1X,
|
||||
offset1Y,
|
||||
tileSize1,
|
||||
config.color1,
|
||||
"transparent",
|
||||
config.opacity,
|
||||
rotationRad,
|
||||
);
|
||||
|
||||
drawCheckerboard(
|
||||
offset2X,
|
||||
offset2Y,
|
||||
tileSize2,
|
||||
config.color2,
|
||||
"transparent",
|
||||
config.opacity,
|
||||
rotationRad2,
|
||||
);
|
||||
|
||||
// Reset global alpha
|
||||
ctx.globalAlpha = 1.0;
|
||||
};
|
||||
|
||||
// Initialize the animation
|
||||
const init = () => {
|
||||
// Set up resize handler
|
||||
window.addEventListener("resize", updateDimensions);
|
||||
|
||||
// Initial setup
|
||||
updateDimensions();
|
||||
|
||||
// Start animation
|
||||
lastFrameTime = performance.now();
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
// Start the animation
|
||||
init();
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateDimensions);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
};
|
431
src/file-viewer/scripts/canvas_2018.ts
Normal file
|
@ -0,0 +1,431 @@
|
|||
// This canvas is based on the maze generation algo in Tanks. This was
|
||||
// originally written in C++ as a single function in 2018, and was ported to TS
|
||||
// by Chloe in 2025 for the cotyledon canvas.
|
||||
//
|
||||
// The main difference is that this version is a visualization, rather than the
|
||||
// practical function. Instead of taking a millisecond, only 5 steps are
|
||||
// performed per second, visualizing the whole ordeal. It also isn't a playable
|
||||
// game, obviously.
|
||||
//
|
||||
// Ported with love because I care about my old self
|
||||
// She deserves the world, but instead gave it to me.
|
||||
(globalThis as any).canvas_2018 = function (canvas: HTMLCanvasElement) {
|
||||
const isStandalone = canvas.getAttribute("data-standalone") === "true";
|
||||
if (isStandalone) {
|
||||
canvas.style.backgroundColor = "#27201E";
|
||||
}
|
||||
interface Cell {
|
||||
down: boolean;
|
||||
right: boolean;
|
||||
visited: boolean;
|
||||
|
||||
cell_flash: number;
|
||||
down_flash: number;
|
||||
right_flash: number;
|
||||
}
|
||||
interface Pos {
|
||||
x: number;
|
||||
y: number;
|
||||
/** Where the wall is relative to x, y. */
|
||||
dir: "left" | "right" | "up" | "down";
|
||||
}
|
||||
interface Maze {
|
||||
grid: Grid;
|
||||
cursor: { x: number; y: number };
|
||||
lastTick: number;
|
||||
/* Pixels */
|
||||
transform: number;
|
||||
newCellsToVisit: Pos[];
|
||||
randomWallBag: Cell[];
|
||||
randomWallTarget: number;
|
||||
renderOffset: { x: number; y: number };
|
||||
done: boolean;
|
||||
}
|
||||
const hex = (color: number[]) =>
|
||||
"#" + color.map((c) => c.toString(16).padStart(2, "0")).join("");
|
||||
let cellSize: number;
|
||||
let borderThickness: number;
|
||||
const cellFlashModifier = isStandalone ? 0.4 : 0.2;
|
||||
const color = isStandalone ? "#170d0b" : "#231C1A";
|
||||
const bg = [0x27, 0x20, 0x1E];
|
||||
const wallFlashColor = [0xFF, 0xA8, 0x7A];
|
||||
const cellFlashColor = "#FFA87A";
|
||||
const updateTime = 1000 / 7;
|
||||
const randomWallBreakInterval = [6, 12]; // every 10 to 18 walls.
|
||||
function randomBetween(min: number, max: number) {
|
||||
return Math.round(
|
||||
Math.random() * (max - min),
|
||||
) + min;
|
||||
}
|
||||
function randomOf<T>(array: T[]): T {
|
||||
return array[randomBetween(0, array.length - 1)];
|
||||
}
|
||||
function randomWallTarget() {
|
||||
return randomBetween(
|
||||
randomWallBreakInterval[0],
|
||||
randomWallBreakInterval[1],
|
||||
);
|
||||
}
|
||||
|
||||
// Originally, this used a 2-dimensional array. However, I wanted to make sure
|
||||
// that the grid could be infinitely sized. This grid constructs new cells on
|
||||
// demand, as needed.
|
||||
class Grid {
|
||||
cells = new Map<number, Cell>();
|
||||
cell({ x, y }: { x: number; y: number }) {
|
||||
const k = ((x | 0) << 16) + (y | 0);
|
||||
const { cells } = this;
|
||||
let existing = this.cells.get(k);
|
||||
if (!existing) {
|
||||
existing = {
|
||||
cell_flash: 0,
|
||||
down: true,
|
||||
down_flash: 0,
|
||||
right: true,
|
||||
right_flash: 0,
|
||||
visited: false,
|
||||
};
|
||||
cells.set(k, existing);
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
forAll(
|
||||
renderOffset: { x: number; y: number },
|
||||
width: number,
|
||||
height: number,
|
||||
cb: (cell: Cell, pos: { x: number; y: number }) => void,
|
||||
) {
|
||||
const { x: offsetX, y: offsetY } = renderOffset;
|
||||
const startX = Math.floor(-offsetX / cellSize);
|
||||
const startY = Math.floor(-offsetY / cellSize);
|
||||
const endX = Math.ceil((width - offsetX) / cellSize);
|
||||
const endY = Math.ceil((height - offsetY) / cellSize);
|
||||
for (let x = startX; x <= endX; x++) {
|
||||
for (let y = startY; y <= endY; y++) {
|
||||
const cellX = offsetX + x * cellSize;
|
||||
const cellY = offsetY + y * cellSize;
|
||||
cb(this.cell({ x, y }), { x: cellX, y: cellY });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
if (!ctx) {
|
||||
console.error("Could not get canvas context");
|
||||
return () => {};
|
||||
}
|
||||
|
||||
let width: number, height: number;
|
||||
const updateDimensions = () => {
|
||||
width = canvas.width = canvas.offsetWidth;
|
||||
height = canvas.height = canvas.offsetHeight;
|
||||
cellSize = 100;
|
||||
borderThickness = 8;
|
||||
};
|
||||
updateDimensions();
|
||||
|
||||
setTimeout(() => {
|
||||
updateDimensions();
|
||||
}, 10);
|
||||
|
||||
let maze = initMaze();
|
||||
let nextMaze: Maze | null = null;
|
||||
let completeFade = 0;
|
||||
function initMaze(): Maze {
|
||||
return {
|
||||
grid: new Grid(),
|
||||
transform: 0,
|
||||
cursor: {
|
||||
x: randomBetween(0, Math.ceil(width / cellSize)),
|
||||
y: randomBetween(0, Math.ceil(height / cellSize)),
|
||||
},
|
||||
lastTick: performance.now(),
|
||||
randomWallBag: [],
|
||||
randomWallTarget: randomWallTarget(),
|
||||
newCellsToVisit: [],
|
||||
renderOffset: { x: 0, y: 0 },
|
||||
done: false,
|
||||
};
|
||||
}
|
||||
|
||||
function isOnScreen(maze: Maze, x: number, y: number) {
|
||||
const { x: offsetX, y: offsetY } = maze.renderOffset;
|
||||
const cellX = offsetX + x * cellSize;
|
||||
const cellY = offsetY + y * cellSize;
|
||||
return (
|
||||
cellX + cellSize > 0 &&
|
||||
cellX < width &&
|
||||
cellY + cellSize > 0 &&
|
||||
cellY < height
|
||||
);
|
||||
}
|
||||
|
||||
function tick(maze: Maze, other?: Maze) {
|
||||
if (maze.done) return;
|
||||
|
||||
// The original maze algorithm broke down 4%-8% of random right facing
|
||||
// walls, and 4%-8% of down facing walls. It did this at the end.
|
||||
// To make this visual more interesting, two random walls will be broken
|
||||
// down every 12-25 cell visits. This way, the main trail is always running.
|
||||
if (maze.randomWallBag.length > maze.randomWallTarget) {
|
||||
const down: Cell = randomOf(maze.randomWallBag);
|
||||
const right: Cell = randomOf(maze.randomWallBag);
|
||||
maze.randomWallBag.forEach((cell) =>
|
||||
cell.cell_flash = Math.min(cell.cell_flash + 0.2, 1)
|
||||
);
|
||||
down.cell_flash = 1;
|
||||
down.down = false;
|
||||
down.down_flash = 1;
|
||||
right.cell_flash = 1;
|
||||
right.right = false;
|
||||
right.right_flash = 1;
|
||||
maze.randomWallBag = [];
|
||||
maze.randomWallTarget = randomWallTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
// The main algorithm was simple: Have a cursor position, and move it in a
|
||||
// random direction that it had not seen before. Once it had run out of
|
||||
// options, branch off of a previous location. Only visit each cell once.
|
||||
//
|
||||
// In this visualization, cells that are too far offscreen are softly
|
||||
// treated as "visited", which is how the simulation always stays in frame.
|
||||
const current = maze.grid.cell(maze.cursor);
|
||||
current.visited = true;
|
||||
current.cell_flash = 1;
|
||||
maze.randomWallBag.push(current);
|
||||
const adjacent = ([
|
||||
{ x: maze.cursor.x + 1, y: maze.cursor.y, dir: "left" },
|
||||
{ x: maze.cursor.x - 1, y: maze.cursor.y, dir: "right" },
|
||||
{ x: maze.cursor.x, y: maze.cursor.y + 1, dir: "up" },
|
||||
{ x: maze.cursor.x, y: maze.cursor.y - 1, dir: "down" },
|
||||
] as Pos[]).filter((pos) =>
|
||||
isOnScreen(maze, pos.x, pos.y) &&
|
||||
maze.grid.cell(pos).visited === false
|
||||
);
|
||||
if (adjacent.length === 0) {
|
||||
// move cursor to a random cell that has not been visited.
|
||||
const cells = maze.newCellsToVisit.filter((pos) =>
|
||||
isOnScreen(maze, pos.x, pos.y) &&
|
||||
maze.grid.cell(pos).visited === false
|
||||
);
|
||||
if (cells.length === 0) {
|
||||
maze.done = true;
|
||||
return;
|
||||
}
|
||||
const continuePos = randomOf(cells);
|
||||
breakWall(maze, continuePos, other);
|
||||
maze.cursor = { x: continuePos.x, y: continuePos.y };
|
||||
return;
|
||||
}
|
||||
|
||||
// break a random wall
|
||||
const toBreak = randomOf(adjacent);
|
||||
breakWall(maze, toBreak, other);
|
||||
maze.cursor = { x: toBreak.x, y: toBreak.y };
|
||||
|
||||
// add the other directions to the new cells to visit.
|
||||
maze.newCellsToVisit.push(
|
||||
...adjacent.filter((pos) => pos.dir !== toBreak.dir),
|
||||
);
|
||||
}
|
||||
|
||||
function breakWall(maze: Maze, pos: Pos, other?: Maze) {
|
||||
if (pos.dir === "right") {
|
||||
const cell = maze.grid.cell(pos);
|
||||
cell.right = false;
|
||||
cell.right_flash = 1;
|
||||
if (other) cell.right = false;
|
||||
} else if (pos.dir === "down") {
|
||||
const cell = maze.grid.cell(pos);
|
||||
cell.down = false;
|
||||
cell.down_flash = 1;
|
||||
if (other) cell.down = false;
|
||||
} else if (pos.dir === "left") {
|
||||
const cell = maze.grid.cell({ x: pos.x - 1, y: pos.y });
|
||||
cell.right = false;
|
||||
cell.right_flash = 1;
|
||||
if (other) cell.right = false;
|
||||
} else if (pos.dir === "up") {
|
||||
const cell = maze.grid.cell({ x: pos.x, y: pos.y - 1 });
|
||||
cell.down = false;
|
||||
cell.down_flash = 1;
|
||||
if (other) cell.down = false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderOffset(maze: Maze) {
|
||||
return { x: maze.transform, y: maze.transform };
|
||||
}
|
||||
|
||||
let animationFrameId: number;
|
||||
let last = performance.now();
|
||||
let dt: number = 0;
|
||||
|
||||
function renderMazeBorders(maze: Maze, opacity: number) {
|
||||
ctx.globalAlpha = opacity;
|
||||
maze.grid.forAll(
|
||||
maze.renderOffset,
|
||||
width,
|
||||
height,
|
||||
(cell, { x: cellX, y: cellY }) => {
|
||||
// Walls
|
||||
if (cell.right) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(
|
||||
cellX + cellSize - borderThickness / 2,
|
||||
cellY - borderThickness / 2,
|
||||
borderThickness,
|
||||
cellSize + borderThickness,
|
||||
);
|
||||
}
|
||||
if (cell.down) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(
|
||||
cellX - borderThickness / 2,
|
||||
cellY + cellSize - borderThickness / 2,
|
||||
cellSize + borderThickness,
|
||||
borderThickness,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
function renderCellFlash(maze: Maze) {
|
||||
maze.grid.forAll(
|
||||
maze.renderOffset,
|
||||
width,
|
||||
height,
|
||||
(cell, { x: cellX, y: cellY }) => {
|
||||
// Cell flash to show visiting path.
|
||||
if (cell.cell_flash > 0) {
|
||||
cell.cell_flash = Math.max(0, cell.cell_flash - dt / 1000);
|
||||
ctx.fillStyle = cellFlashColor;
|
||||
ctx.globalAlpha = cell.cell_flash * cellFlashModifier;
|
||||
ctx.fillRect(cellX, cellY, cellSize, cellSize);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function renderBorderFlash(maze: Maze) {
|
||||
maze.grid.forAll(
|
||||
maze.renderOffset,
|
||||
width,
|
||||
height,
|
||||
(cell, { x: cellX, y: cellY }) => {
|
||||
if (cell.right_flash == 0 && cell.down_flash == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Walls
|
||||
const cellFlash = cell.cell_flash * cellFlashModifier;
|
||||
if (cell.right_flash > 0) {
|
||||
cell.right_flash = Math.max(0, cell.right_flash - dt / 500);
|
||||
ctx.fillStyle = interpolateColor(
|
||||
bg,
|
||||
wallFlashColor,
|
||||
Math.max(cell.right_flash, cellFlash),
|
||||
);
|
||||
if (cellFlash > cell.right_flash) {
|
||||
ctx.globalAlpha = cell.right_flash / cellFlash;
|
||||
}
|
||||
ctx.fillRect(
|
||||
cellX + cellSize - borderThickness / 2,
|
||||
cellY + borderThickness / 2,
|
||||
borderThickness,
|
||||
cellSize - borderThickness,
|
||||
);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
if (cell.down_flash > 0) {
|
||||
if (cellFlash > cell.down_flash) {
|
||||
ctx.globalAlpha = cell.down_flash / cellFlash;
|
||||
}
|
||||
cell.down_flash = Math.max(0, cell.down_flash - dt / 500);
|
||||
ctx.fillStyle = interpolateColor(
|
||||
bg,
|
||||
wallFlashColor,
|
||||
Math.max(cell.down_flash, cellFlash),
|
||||
);
|
||||
ctx.fillRect(
|
||||
cellX + borderThickness / 2,
|
||||
cellY + cellSize - borderThickness / 2,
|
||||
cellSize - borderThickness,
|
||||
borderThickness,
|
||||
);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function render() {
|
||||
const now = performance.now();
|
||||
dt = now - last;
|
||||
maze.transform += dt * 0.005;
|
||||
maze.renderOffset = renderOffset(maze);
|
||||
if (!maze.done) {
|
||||
if (now - maze.lastTick >= updateTime) {
|
||||
tick(maze);
|
||||
maze.lastTick = now;
|
||||
|
||||
if (maze.done) {
|
||||
nextMaze = initMaze();
|
||||
nextMaze.transform = (maze.transform % cellSize) - dt * 0.005;
|
||||
nextMaze.lastTick = now;
|
||||
completeFade = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nextMaze) {
|
||||
nextMaze.transform += dt * 0.005;
|
||||
nextMaze.renderOffset = renderOffset(nextMaze);
|
||||
if (!nextMaze.done && now - nextMaze.lastTick >= updateTime) {
|
||||
tick(nextMaze, maze);
|
||||
nextMaze.lastTick = now;
|
||||
}
|
||||
}
|
||||
last = now;
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
renderCellFlash(maze);
|
||||
if (nextMaze) renderCellFlash(nextMaze);
|
||||
|
||||
renderMazeBorders(maze, 1);
|
||||
if (nextMaze) {
|
||||
renderMazeBorders(nextMaze, completeFade);
|
||||
completeFade += dt / 3000;
|
||||
if (completeFade >= 1) {
|
||||
maze = nextMaze;
|
||||
nextMaze = null;
|
||||
}
|
||||
}
|
||||
|
||||
renderBorderFlash(maze);
|
||||
if (nextMaze) {
|
||||
renderCellFlash(nextMaze);
|
||||
renderBorderFlash(nextMaze);
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
function interpolateColor(start: number[], end: number[], t: number) {
|
||||
return hex(start.map((s, i) => Math.round(s + (end[i] - s) * t)));
|
||||
}
|
||||
|
||||
window.addEventListener("resize", updateDimensions);
|
||||
animationFrameId = requestAnimationFrame(render);
|
||||
|
||||
// cleanup function
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateDimensions);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
};
|
213
src/file-viewer/scripts/canvas_2019.ts
Normal file
|
@ -0,0 +1,213 @@
|
|||
// Ported from CanvasAPI, allegedly written on 2019-08-26.
|
||||
(globalThis as any).canvas_2019 = function (canvas: HTMLCanvasElement) {
|
||||
const isStandalone = canvas.getAttribute("data-standalone") === "true";
|
||||
if (isStandalone) {
|
||||
canvas.parentElement!.style.backgroundColor = "#121013";
|
||||
}
|
||||
// Canvas.tsx
|
||||
abstract class CanvasAPI {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
width = 0;
|
||||
height = 0;
|
||||
private _disposed = false;
|
||||
private _running = false;
|
||||
private _last = 0;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
this.canvas = canvas;
|
||||
this.width = canvas.width = canvas.clientWidth;
|
||||
this.height = canvas.height = canvas.clientHeight;
|
||||
const ctx = this.canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
throw new Error("Canvas2D Not Supported!");
|
||||
}
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
stopRenderLoop() {
|
||||
this._running = false;
|
||||
}
|
||||
|
||||
startRenderLoop() {
|
||||
if (this._disposed) return;
|
||||
this._running = true;
|
||||
this._last = performance.now();
|
||||
requestAnimationFrame(this._renderLoop);
|
||||
}
|
||||
|
||||
private _renderLoop = (delta: number) => {
|
||||
if (!this._running) return;
|
||||
this.render(delta - this._last);
|
||||
this._last = delta;
|
||||
requestAnimationFrame(this._renderLoop);
|
||||
};
|
||||
|
||||
abstract render(delta: number): void;
|
||||
}
|
||||
|
||||
// VaultBackground.ts
|
||||
function addGSHelper(
|
||||
grad: CanvasGradient,
|
||||
color: string,
|
||||
dotOpacity: number,
|
||||
gradStop: number,
|
||||
gradOpacity: number,
|
||||
) {
|
||||
grad.addColorStop(gradStop, `rgba(${color},${dotOpacity * gradOpacity})`);
|
||||
}
|
||||
|
||||
function randAround(target: number, dist: number) {
|
||||
return (Math.random() - 0.5) * dist * 2 + target;
|
||||
}
|
||||
|
||||
class Dot {
|
||||
x = Math.random() * 1.1 - 0.05;
|
||||
y = Math.random() / 4 + 0.9;
|
||||
size = Math.random() * 200 + 50;
|
||||
opacity = 0;
|
||||
opacityRandom = Math.random() / 3 + 0.3;
|
||||
fadeInOpacity = 1;
|
||||
color = `${randAround(217, 30)}, ${randAround(170, 30)}, ${
|
||||
randAround(255, 20)
|
||||
}`;
|
||||
|
||||
life = 0;
|
||||
|
||||
ySpeed_1 = 0;
|
||||
ySpeed_2 = -0.0000063;
|
||||
ySpeed_3 = 0.000000016;
|
||||
ySpeed_4 = 0.000000000009;
|
||||
|
||||
seed = Math.random();
|
||||
|
||||
delete = false;
|
||||
|
||||
update(init: boolean) {
|
||||
this.life += 0.8;
|
||||
if (this.life < 115) {
|
||||
this.opacity = this.life / 230;
|
||||
} else if (this.life > 450) {
|
||||
this.delete = true;
|
||||
} else if (this.life > 300) {
|
||||
this.opacity = (150 + 300 - this.life) / 300;
|
||||
}
|
||||
|
||||
this.ySpeed_3 += this.ySpeed_4;
|
||||
this.ySpeed_2 += this.ySpeed_3;
|
||||
this.ySpeed_1 += this.ySpeed_2;
|
||||
this.y += this.ySpeed_1 * 0.5;
|
||||
|
||||
this.size -= 0.08;
|
||||
|
||||
if (this.delete) {
|
||||
Object.assign(this, new Dot());
|
||||
}
|
||||
}
|
||||
|
||||
render(scene: VaultBackground) {
|
||||
const ctx = scene.ctx;
|
||||
|
||||
if (this.fadeInOpacity < 1) {
|
||||
this.fadeInOpacity += 0.0075;
|
||||
}
|
||||
|
||||
const finalX = this.x +
|
||||
Math.sin(this.seed * Math.PI * 2 + Date.now() / 15000) * 0.2;
|
||||
|
||||
const drawX = scene.shakeX +
|
||||
finalX * Math.max(700, scene.width) -
|
||||
(Math.max(700, scene.width) - scene.width) / 2;
|
||||
const drawY = scene.shakeY + (this.y * 1.5 - 0.5) * scene.height;
|
||||
|
||||
const opacity = this.opacity * this.opacityRandom * this.fadeInOpacity;
|
||||
|
||||
const grad = ctx.createRadialGradient(
|
||||
drawX,
|
||||
drawY,
|
||||
0,
|
||||
drawX,
|
||||
drawY,
|
||||
this.size,
|
||||
);
|
||||
addGSHelper(grad, this.color, opacity, 0, 1);
|
||||
addGSHelper(grad, this.color, opacity, 0.8, 0.7);
|
||||
addGSHelper(grad, this.color, opacity, 0.87, 0.5);
|
||||
addGSHelper(grad, this.color, opacity, 0.93, 0.3);
|
||||
addGSHelper(grad, this.color, opacity, 1, 0);
|
||||
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(
|
||||
drawX - this.size,
|
||||
drawY - this.size,
|
||||
this.size * 2,
|
||||
this.size * 2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VaultBackground extends CanvasAPI {
|
||||
private items = new Set<Dot>();
|
||||
|
||||
private shakeVar = 0;
|
||||
private dom?: HTMLElement;
|
||||
shakeX = 0;
|
||||
shakeY = 0;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super(canvas);
|
||||
for (let i = 0; i < 450; i++) {
|
||||
if (i % 7 === 0) {
|
||||
this.items.add(new Dot());
|
||||
}
|
||||
this.items.forEach((x) => x.update(true));
|
||||
}
|
||||
this.items.forEach((x) => x.fadeInOpacity = 0);
|
||||
}
|
||||
|
||||
render(): void {
|
||||
this.ctx.clearRect(0, 0, this.width, this.height);
|
||||
|
||||
this.items.forEach((x) => (x.update(false), x.render(this)));
|
||||
|
||||
if (this.shakeVar >= 0.0001) {
|
||||
this.shakeVar *= 0.97 - 0.22 * this.shakeVar;
|
||||
|
||||
if (this.shakeVar >= 0.0001) {
|
||||
this.shakeX = (Math.random() * 2 - 1) * this.shakeVar * 65;
|
||||
this.shakeY = (Math.random() * 2 - 1) * this.shakeVar * 65;
|
||||
if (this.dom) {
|
||||
this.dom.style.transform =
|
||||
`translate(${this.shakeX}px,${this.shakeY}px)`;
|
||||
}
|
||||
} else {
|
||||
this.shakeX = 0;
|
||||
this.shakeY = 0;
|
||||
if (this.dom) this.dom.style.removeProperty("transform");
|
||||
this.dom = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shake(dom?: HTMLElement | null) {
|
||||
this.dom = dom || document.body;
|
||||
this.shakeVar = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Binding code
|
||||
let bg = new VaultBackground(canvas);
|
||||
bg.startRenderLoop();
|
||||
canvas.style.opacity = "0.2";
|
||||
function onResize() {
|
||||
bg.width = canvas.width = canvas.clientWidth;
|
||||
bg.height = canvas.height = canvas.clientHeight;
|
||||
}
|
||||
window.addEventListener("resize", onResize);
|
||||
onResize();
|
||||
(globalThis as any).vault = bg;
|
||||
return () => {
|
||||
bg.stopRenderLoop();
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
};
|
197
src/file-viewer/scripts/canvas_2020.ts
Normal file
|
@ -0,0 +1,197 @@
|
|||
// Vibe coded with AI
|
||||
(globalThis as any).canvas_2020 = function (canvas: HTMLCanvasElement) {
|
||||
const isStandalone = canvas.getAttribute("data-standalone") === "true";
|
||||
// Rain effect with slanted lines
|
||||
// Configuration interface for the rain effect
|
||||
interface RainConfig {
|
||||
fps: number; // frames per second
|
||||
color: string; // color of rain particles
|
||||
angle: number; // angle in degrees
|
||||
particleDensity: number; // particles per 10000 pixels of canvas area
|
||||
speed: number; // speed of particles (pixels per frame)
|
||||
lineWidth: number; // thickness of rain lines
|
||||
lineLength: number; // length of rain lines
|
||||
}
|
||||
|
||||
// Rain particle interface
|
||||
interface RainParticle {
|
||||
x: number; // x position
|
||||
y: number; // y position
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
const config: RainConfig = {
|
||||
fps: 16,
|
||||
color: isStandalone ? "#00FEFB99" : "#081F24",
|
||||
angle: -18,
|
||||
particleDensity: 1,
|
||||
speed: 400,
|
||||
lineWidth: 8,
|
||||
lineLength: 100,
|
||||
};
|
||||
|
||||
// Get the canvas context
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
console.error("Could not get canvas context");
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// Make canvas transparent
|
||||
if (isStandalone) {
|
||||
canvas.style.backgroundColor = "#0F252B";
|
||||
} else {
|
||||
canvas.style.backgroundColor = "transparent";
|
||||
}
|
||||
|
||||
// Calculate canvas dimensions and update when resized
|
||||
let width = canvas.width;
|
||||
let height = canvas.height;
|
||||
let particles: RainParticle[] = [];
|
||||
let animationFrameId: number;
|
||||
let lastFrameTime = 0;
|
||||
const frameInterval = 1000 / config.fps;
|
||||
|
||||
// Calculate angle in radians
|
||||
const angleRad = (config.angle * Math.PI) / 180;
|
||||
|
||||
// Update canvas dimensions and particle count when resized
|
||||
const updateDimensions = () => {
|
||||
width = canvas.width = canvas.offsetWidth;
|
||||
height = canvas.height = canvas.offsetHeight;
|
||||
|
||||
// Calculate the canvas area in pixels
|
||||
const canvasArea = width * height;
|
||||
|
||||
// Calculate target number of particles based on canvas area
|
||||
const targetParticleCount = Math.floor(
|
||||
(canvasArea / 10000) * config.particleDensity,
|
||||
);
|
||||
|
||||
// Calculate buffer for horizontal offset due to slanted angle
|
||||
const buffer = Math.abs(height * Math.tan(angleRad)) + config.lineLength;
|
||||
|
||||
// Adjust the particles array
|
||||
if (particles.length < targetParticleCount) {
|
||||
// Add more particles if needed
|
||||
for (let i = particles.length; i < targetParticleCount; i++) {
|
||||
particles.push(createParticle(true, buffer));
|
||||
}
|
||||
} else if (particles.length > targetParticleCount) {
|
||||
// Remove excess particles
|
||||
particles = particles.slice(0, targetParticleCount);
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new particle
|
||||
// Added initialDistribution parameter to distribute particles across the entire canvas at startup
|
||||
const createParticle = (
|
||||
initialDistribution = false,
|
||||
buffer: number,
|
||||
): RainParticle => {
|
||||
// For initial distribution, place particles throughout the canvas
|
||||
// Otherwise start them above the canvas
|
||||
let x = Math.random() * (width + buffer * 2) - buffer;
|
||||
let y;
|
||||
|
||||
if (initialDistribution) {
|
||||
// Distribute across the entire canvas height for initial setup
|
||||
y = Math.random() * (height + config.lineLength * 2) - config.lineLength;
|
||||
} else {
|
||||
// Start new particles from above the canvas with some randomization
|
||||
y = -config.lineLength - (Math.random() * config.lineLength * 20);
|
||||
}
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
};
|
||||
|
||||
// Update particle positions
|
||||
const updateParticles = () => {
|
||||
// Calculate buffer for horizontal offset due to slanted angle
|
||||
const buffer = Math.abs(height * Math.tan(angleRad)) + config.lineLength;
|
||||
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
const p = particles[i];
|
||||
|
||||
// Update position based on speed and angle
|
||||
p.x += Math.sin(angleRad) * config.speed;
|
||||
p.y += Math.cos(angleRad) * config.speed;
|
||||
|
||||
// Reset particles that go offscreen - only determined by position
|
||||
// Add extra buffer to ensure particles fully exit the visible area before resetting
|
||||
if (
|
||||
p.y > height + config.lineLength ||
|
||||
p.x < -buffer ||
|
||||
p.x > width + buffer
|
||||
) {
|
||||
particles[i] = createParticle(false, buffer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Draw particles
|
||||
const drawParticles = () => {
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// Set drawing properties
|
||||
ctx.strokeStyle = config.color;
|
||||
ctx.lineWidth = config.lineWidth;
|
||||
ctx.lineCap = "square";
|
||||
|
||||
// Draw each rain line
|
||||
ctx.beginPath();
|
||||
for (const p of particles) {
|
||||
// Only draw particles that are either on screen or within a reasonable buffer
|
||||
// This is for performance reasons - we don't need to draw particles far offscreen
|
||||
if (p.y >= -config.lineLength * 2 && p.y <= height + config.lineLength) {
|
||||
const endX = p.x + Math.sin(angleRad) * config.lineLength;
|
||||
const endY = p.y + Math.cos(angleRad) * config.lineLength;
|
||||
|
||||
ctx.moveTo(p.x, p.y);
|
||||
ctx.lineTo(endX, endY);
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
// Animation loop
|
||||
const animate = (currentTime: number) => {
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
|
||||
// Control frame rate
|
||||
if (currentTime - lastFrameTime < frameInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastFrameTime = currentTime;
|
||||
|
||||
updateParticles();
|
||||
drawParticles();
|
||||
};
|
||||
|
||||
// Initialize the animation
|
||||
const init = () => {
|
||||
// Set up resize handler
|
||||
window.addEventListener("resize", updateDimensions);
|
||||
|
||||
// Initial setup
|
||||
updateDimensions();
|
||||
|
||||
// Start animation
|
||||
lastFrameTime = performance.now();
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
// Start the animation
|
||||
init();
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateDimensions);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
};
|
783
src/file-viewer/scripts/canvas_2021.ts
Normal file
|
@ -0,0 +1,783 @@
|
|||
// Vibe coded.
|
||||
(globalThis as any).canvas_2021 = function (canvas: HTMLCanvasElement) {
|
||||
const isStandalone = canvas.getAttribute("data-standalone") === "true";
|
||||
// Constants for simulation
|
||||
const PARTICLE_RADIUS = 4.5;
|
||||
const PARTICLE_DENSITY = 0.004; // Particles per pixel
|
||||
const MIN_SPEED = 0.05;
|
||||
const MAX_SPEED = 6.0;
|
||||
const FRICTION = 0.96;
|
||||
const REPULSION_STRENGTH = 0.1;
|
||||
const REPULSION_RADIUS = 50;
|
||||
const FORCE_RADIUS = 400; // Increased radius
|
||||
const FORCE_STRENGTH = 0.25;
|
||||
const FORCE_FALLOFF_EXPONENT = 3; // Higher value = sharper falloff
|
||||
const FORCE_SPACING = 10; // Pixels between force points
|
||||
const MIN_FORCE_STRENGTH = 0.05; // Minimum force strength for very slow movements
|
||||
const MAX_FORCE_STRENGTH = 0.4; // Maximum force strength for fast movements
|
||||
const MIN_SPEED_THRESHOLD = 1; // Movement speed (px/frame) that produces minimum force
|
||||
const MAX_SPEED_THRESHOLD = 20; // Movement speed that produces maximum force
|
||||
const OVERSCAN_PIXELS = 250;
|
||||
const CELL_SIZE = REPULSION_RADIUS; // For spatial hashing
|
||||
|
||||
let globalOpacity = 0;
|
||||
|
||||
if (isStandalone) {
|
||||
canvas.style.backgroundColor = "#301D02";
|
||||
} else {
|
||||
canvas.style.backgroundColor = "transparent";
|
||||
}
|
||||
|
||||
// Interfaces
|
||||
interface Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
charge: number; // 0 to 1, affecting color
|
||||
}
|
||||
|
||||
interface Force {
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
strength: number;
|
||||
radius: number;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
interface SpatialHash {
|
||||
[key: string]: Particle[];
|
||||
}
|
||||
|
||||
// State
|
||||
let first = true;
|
||||
let particles: Particle[] = [];
|
||||
let forces: Force[] = [];
|
||||
let width = canvas.width;
|
||||
let height = canvas.height;
|
||||
let targetParticleCount = 0;
|
||||
let spatialHash: SpatialHash = {};
|
||||
let ctx: CanvasRenderingContext2D | null = null;
|
||||
let animationId: number | null = null;
|
||||
let isRunning = false;
|
||||
|
||||
// Mouse tracking
|
||||
let lastMousePosition: { x: number; y: number } | null = null;
|
||||
// Track position of the last created force
|
||||
let lastForcePosition: { x: number; y: number } | null = null;
|
||||
|
||||
// Keep track of previous canvas dimensions for resize logic
|
||||
let previousWidth = 0;
|
||||
let previousHeight = 0;
|
||||
|
||||
// Initialize and cleanup
|
||||
function init(): void {
|
||||
ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Set canvas to full size
|
||||
resizeCanvas();
|
||||
|
||||
// Event listeners
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
|
||||
// Start animation immediately
|
||||
start();
|
||||
}
|
||||
|
||||
function cleanup(): void {
|
||||
// Stop the animation
|
||||
stop();
|
||||
|
||||
// Remove event listeners
|
||||
window.removeEventListener("resize", resizeCanvas);
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
|
||||
// Clear arrays
|
||||
particles = [];
|
||||
forces = [];
|
||||
spatialHash = {};
|
||||
lastMousePosition = null;
|
||||
lastForcePosition = null;
|
||||
}
|
||||
|
||||
// Resize canvas and adjust particle count
|
||||
function resizeCanvas(): void {
|
||||
// Store previous dimensions
|
||||
previousWidth = width;
|
||||
previousHeight = height;
|
||||
|
||||
// Update to new dimensions
|
||||
width = window.innerWidth;
|
||||
height = window.innerHeight;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const oldTargetCount = targetParticleCount;
|
||||
targetParticleCount = Math.floor(width * height * PARTICLE_DENSITY);
|
||||
|
||||
// Adjust particle count
|
||||
if (targetParticleCount > oldTargetCount) {
|
||||
// Add more particles if needed, but only in newly available space
|
||||
addParticles(targetParticleCount - oldTargetCount, !first);
|
||||
first = false;
|
||||
}
|
||||
// Note: Removal of excess particles happens naturally during update
|
||||
}
|
||||
|
||||
// Handle mouse movement
|
||||
function handleMouseMove(e: MouseEvent): void {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const currentX = e.clientX - rect.left;
|
||||
const currentY = e.clientY - rect.top;
|
||||
|
||||
// Initialize positions if this is the first movement
|
||||
if (!lastMousePosition || !lastForcePosition) {
|
||||
lastMousePosition = { x: currentX, y: currentY };
|
||||
lastForcePosition = { x: currentX, y: currentY };
|
||||
return;
|
||||
}
|
||||
|
||||
// Store current mouse position
|
||||
const mouseX = currentX;
|
||||
const mouseY = currentY;
|
||||
|
||||
// Calculate vector from last mouse position to current
|
||||
const dx = mouseX - lastMousePosition.x;
|
||||
const dy = mouseY - lastMousePosition.y;
|
||||
const distMoved = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Skip if essentially no movement (avoids numerical issues)
|
||||
if (distMoved < 0.1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the vector from the last force to the current mouse position
|
||||
const forceDx = mouseX - lastForcePosition.x;
|
||||
const forceDy = mouseY - lastForcePosition.y;
|
||||
const forceDistance = Math.sqrt(forceDx * forceDx + forceDy * forceDy);
|
||||
|
||||
// Only create forces if we've moved far enough from the last force
|
||||
if (forceDistance >= FORCE_SPACING) {
|
||||
// Calculate the direction vector from last force to current mouse
|
||||
let dirX = forceDx / forceDistance;
|
||||
let dirY = forceDy / forceDistance;
|
||||
|
||||
// Calculate how many force points to create
|
||||
const numPoints = Math.floor(forceDistance / FORCE_SPACING);
|
||||
|
||||
// Calculate movement speed based on the recent movement
|
||||
const movementSpeed = distMoved; // Simple approximation of speed
|
||||
|
||||
// Scale force strength based on movement speed
|
||||
let speedFactor;
|
||||
if (movementSpeed <= MIN_SPEED_THRESHOLD) {
|
||||
speedFactor = MIN_FORCE_STRENGTH;
|
||||
} else if (movementSpeed >= MAX_SPEED_THRESHOLD) {
|
||||
speedFactor = MAX_FORCE_STRENGTH;
|
||||
} else {
|
||||
// Linear interpolation between min and max
|
||||
const t = (movementSpeed - MIN_SPEED_THRESHOLD) /
|
||||
(MAX_SPEED_THRESHOLD - MIN_SPEED_THRESHOLD);
|
||||
speedFactor = MIN_FORCE_STRENGTH +
|
||||
t * (MAX_FORCE_STRENGTH - MIN_FORCE_STRENGTH);
|
||||
}
|
||||
|
||||
// Store current force position to update incrementally
|
||||
let currentForceX = lastForcePosition.x;
|
||||
let currentForceY = lastForcePosition.y;
|
||||
|
||||
// Create evenly spaced force points along the path from last force to current mouse
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
// Calculate position for this force point
|
||||
const t = (i + 1) / numPoints;
|
||||
const fx = lastForcePosition.x + forceDx * t;
|
||||
const fy = lastForcePosition.y + forceDy * t;
|
||||
|
||||
// Create force at this position with the direction vector
|
||||
createForce(fx, fy, dirX, dirY, speedFactor);
|
||||
|
||||
// Update the last force position to this new force
|
||||
currentForceX = fx;
|
||||
currentForceY = fy;
|
||||
}
|
||||
|
||||
// Update the last force position
|
||||
lastForcePosition = { x: currentForceX, y: currentForceY };
|
||||
}
|
||||
|
||||
// Always update the last mouse position
|
||||
lastMousePosition = { x: mouseX, y: mouseY };
|
||||
}
|
||||
|
||||
// Create a new force
|
||||
function createForce(
|
||||
x: number,
|
||||
y: number,
|
||||
dx: number,
|
||||
dy: number,
|
||||
strength = FORCE_STRENGTH,
|
||||
): void {
|
||||
forces.push({
|
||||
x,
|
||||
y,
|
||||
dx,
|
||||
dy,
|
||||
strength,
|
||||
radius: 1,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Improved particle addition with fill strategy options
|
||||
function addParticles(count: number, inNewAreaOnly: boolean = false): void {
|
||||
// Determine available space
|
||||
const minX = -OVERSCAN_PIXELS;
|
||||
const maxX = width + OVERSCAN_PIXELS;
|
||||
const minY = -OVERSCAN_PIXELS;
|
||||
const maxY = height + OVERSCAN_PIXELS;
|
||||
|
||||
// Use a grid system that guarantees uniform spacing of particles
|
||||
const gridSpacing = REPULSION_RADIUS * 0.8; // Slightly less than repulsion radius
|
||||
const gridWidth = Math.ceil((maxX - minX) / gridSpacing);
|
||||
const gridHeight = Math.ceil((maxY - minY) / gridSpacing);
|
||||
|
||||
// Track which grid cells are already occupied
|
||||
const occupiedCells: Set<string> = new Set();
|
||||
|
||||
// Mark cells occupied by existing particles
|
||||
for (const particle of particles) {
|
||||
const cellX = Math.floor((particle.x - minX) / gridSpacing);
|
||||
const cellY = Math.floor((particle.y - minY) / gridSpacing);
|
||||
|
||||
// Ensure cell coordinates are within valid range
|
||||
if (cellX >= 0 && cellX < gridWidth && cellY >= 0 && cellY < gridHeight) {
|
||||
occupiedCells.add(`${cellX},${cellY}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create arrays of all cells and filter by placement strategy
|
||||
const allGridCells: { x: number; y: number }[] = [];
|
||||
|
||||
for (let cellY = 0; cellY < gridHeight; cellY++) {
|
||||
for (let cellX = 0; cellX < gridWidth; cellX++) {
|
||||
const cellKey = `${cellX},${cellY}`;
|
||||
if (!occupiedCells.has(cellKey)) {
|
||||
const posX = minX + (cellX + 0.5) * gridSpacing;
|
||||
const posY = minY + (cellY + 0.5) * gridSpacing;
|
||||
|
||||
// For new area only placement, filter to expanded areas
|
||||
if (inNewAreaOnly && previousWidth > 0 && previousHeight > 0) {
|
||||
const expandedRight = width > previousWidth;
|
||||
const expandedBottom = height > previousHeight;
|
||||
|
||||
const inNewRightArea = expandedRight && posX >= previousWidth &&
|
||||
posX <= width;
|
||||
const inNewBottomArea = expandedBottom && posY >= previousHeight &&
|
||||
posY <= height;
|
||||
|
||||
if (inNewRightArea || inNewBottomArea) {
|
||||
allGridCells.push({ x: cellX, y: cellY });
|
||||
}
|
||||
} else if (!inNewAreaOnly) {
|
||||
// Standard placement - add all valid cells
|
||||
allGridCells.push({ x: cellX, y: cellY });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allGridCells.length == 0) {
|
||||
throw new Error("No cells available to place particles");
|
||||
}
|
||||
|
||||
// We now have all grid cells that match our placement criteria
|
||||
|
||||
// If we need more particles than we have available cells, we need to adjust
|
||||
// gridSpacing to fit more cells into the same space
|
||||
if (count > allGridCells.length) {
|
||||
// Retry with a smaller grid spacing
|
||||
// Proportionally reduce the grid spacing to fit the required number of particles
|
||||
const scaleFactor = Math.sqrt(allGridCells.length / count);
|
||||
const newGridSpacing = gridSpacing * scaleFactor;
|
||||
|
||||
// Clear particles and try again with new spacing
|
||||
// This is a recursive call, but with adjusted parameters that will fit
|
||||
return addParticlesWithCustomSpacing(
|
||||
count,
|
||||
inNewAreaOnly,
|
||||
newGridSpacing,
|
||||
);
|
||||
}
|
||||
|
||||
// Shuffle the available cells for random selection
|
||||
shuffleArray(allGridCells);
|
||||
|
||||
// Take the number of cells we need
|
||||
const cellsToUse = Math.min(count, allGridCells.length);
|
||||
const selectedCells = allGridCells.slice(0, cellsToUse);
|
||||
|
||||
// Create particles in selected cells
|
||||
for (const cell of selectedCells) {
|
||||
// Add jitter within the cell for natural look
|
||||
const jitterX = (Math.random() - 0.5) * gridSpacing * 0.8;
|
||||
const jitterY = (Math.random() - 0.5) * gridSpacing * 0.8;
|
||||
|
||||
// Calculate final position
|
||||
const x = minX + (cell.x + 0.5) * gridSpacing + jitterX;
|
||||
const y = minY + (cell.y + 0.5) * gridSpacing + jitterY;
|
||||
|
||||
// Create a particle at this position
|
||||
particles.push(createParticle(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to add particles with custom grid spacing
|
||||
function addParticlesWithCustomSpacing(
|
||||
count: number,
|
||||
inNewAreaOnly: boolean,
|
||||
gridSpacing: number,
|
||||
): void {
|
||||
if (gridSpacing == 0) throw new Error("Grid spacing is 0");
|
||||
// Determine available space
|
||||
const minX = -OVERSCAN_PIXELS;
|
||||
const maxX = width + OVERSCAN_PIXELS;
|
||||
const minY = -OVERSCAN_PIXELS;
|
||||
const maxY = height + OVERSCAN_PIXELS;
|
||||
|
||||
// Create grid using the custom spacing
|
||||
const gridWidth = Math.ceil((maxX - minX) / gridSpacing);
|
||||
const gridHeight = Math.ceil((maxY - minY) / gridSpacing);
|
||||
|
||||
// Track which grid cells are already occupied
|
||||
const occupiedCells: Set<string> = new Set();
|
||||
|
||||
// Mark cells occupied by existing particles
|
||||
for (const particle of particles) {
|
||||
const cellX = Math.floor((particle.x - minX) / gridSpacing);
|
||||
const cellY = Math.floor((particle.y - minY) / gridSpacing);
|
||||
|
||||
// Ensure cell coordinates are within valid range
|
||||
if (cellX >= 0 && cellX < gridWidth && cellY >= 0 && cellY < gridHeight) {
|
||||
occupiedCells.add(`${cellX},${cellY}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create arrays of all cells and filter by placement strategy
|
||||
const allGridCells: { x: number; y: number }[] = [];
|
||||
|
||||
for (let cellY = 0; cellY < gridHeight; cellY++) {
|
||||
for (let cellX = 0; cellX < gridWidth; cellX++) {
|
||||
const cellKey = `${cellX},${cellY}`;
|
||||
if (!occupiedCells.has(cellKey)) {
|
||||
const posX = minX + (cellX + 0.5) * gridSpacing;
|
||||
const posY = minY + (cellY + 0.5) * gridSpacing;
|
||||
|
||||
// For new area only placement, filter to expanded areas
|
||||
if (inNewAreaOnly && previousWidth > 0 && previousHeight > 0) {
|
||||
const expandedRight = width > previousWidth;
|
||||
const expandedBottom = height > previousHeight;
|
||||
|
||||
const inNewRightArea = expandedRight && posX >= previousWidth &&
|
||||
posX <= width;
|
||||
const inNewBottomArea = expandedBottom && posY >= previousHeight &&
|
||||
posY <= height;
|
||||
|
||||
if (inNewRightArea || inNewBottomArea) {
|
||||
allGridCells.push({ x: cellX, y: cellY });
|
||||
}
|
||||
} else if (!inNewAreaOnly) {
|
||||
// Standard placement - add all valid cells
|
||||
allGridCells.push({ x: cellX, y: cellY });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle the available cells for random distribution
|
||||
shuffleArray(allGridCells);
|
||||
|
||||
// Take the number of cells we need (or all if we have fewer)
|
||||
const cellsToUse = Math.min(count, allGridCells.length);
|
||||
|
||||
// Create particles in selected cells
|
||||
for (let i = 0; i < cellsToUse; i++) {
|
||||
const cell = allGridCells[i];
|
||||
|
||||
// Add jitter within the cell
|
||||
const jitterX = (Math.random() - 0.5) * gridSpacing * 0.8;
|
||||
const jitterY = (Math.random() - 0.5) * gridSpacing * 0.8;
|
||||
|
||||
// Calculate final position
|
||||
const x = minX + (cell.x + 0.5) * gridSpacing + jitterX;
|
||||
const y = minY + (cell.y + 0.5) * gridSpacing + jitterY;
|
||||
|
||||
// Create a particle at this position
|
||||
particles.push(createParticle(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
// Utility to shuffle an array (Fisher-Yates algorithm)
|
||||
function shuffleArray<T>(array: T[]): void {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified createParticle function that just places at a specific position
|
||||
function createParticle(x: number, y: number): Particle {
|
||||
return {
|
||||
x: x + (Math.random() * 4 - 2),
|
||||
y: y + (Math.random() * 4 - 2),
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
charge: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Function to create a particle on one of the edges
|
||||
function createParticleOnEdge(): Particle {
|
||||
// Overscan bounds with fixed pixel size
|
||||
const minX = -OVERSCAN_PIXELS;
|
||||
const maxX = width + OVERSCAN_PIXELS;
|
||||
const minY = -OVERSCAN_PIXELS;
|
||||
const maxY = height + OVERSCAN_PIXELS;
|
||||
|
||||
let x: number, y: number;
|
||||
|
||||
// Place on one of the edges
|
||||
const edge = Math.floor(Math.random() * 4);
|
||||
switch (edge) {
|
||||
case 0: // Top
|
||||
x = minX + Math.random() * (maxX - minX);
|
||||
y = minY;
|
||||
break;
|
||||
case 1: // Right
|
||||
x = maxX;
|
||||
y = minY + Math.random() * (maxY - minY);
|
||||
break;
|
||||
case 2: // Bottom
|
||||
x = minX + Math.random() * (maxX - minX);
|
||||
y = maxY;
|
||||
break;
|
||||
case 3: // Left
|
||||
x = minX;
|
||||
y = minY + Math.random() * (maxY - minY);
|
||||
break;
|
||||
default:
|
||||
x = minX + Math.random() * (maxX - minX);
|
||||
y = minY + Math.random() * (maxY - minY);
|
||||
}
|
||||
|
||||
return createParticle(x, y);
|
||||
}
|
||||
|
||||
// Spatial hashing functions
|
||||
function getHashKey(x: number, y: number): string {
|
||||
const cellX = Math.floor(x / CELL_SIZE);
|
||||
const cellY = Math.floor(y / CELL_SIZE);
|
||||
return `${cellX},${cellY}`;
|
||||
}
|
||||
|
||||
function addToSpatialHash(particle: Particle): void {
|
||||
const key = getHashKey(particle.x, particle.y);
|
||||
if (!spatialHash[key]) {
|
||||
spatialHash[key] = [];
|
||||
}
|
||||
spatialHash[key].push(particle);
|
||||
}
|
||||
|
||||
function updateSpatialHash(): void {
|
||||
// Clear previous hash
|
||||
spatialHash = {};
|
||||
|
||||
// Add all particles to hash
|
||||
for (const particle of particles) {
|
||||
addToSpatialHash(particle);
|
||||
}
|
||||
}
|
||||
|
||||
function getNearbyParticles(
|
||||
x: number,
|
||||
y: number,
|
||||
radius: number,
|
||||
): Particle[] {
|
||||
const result: Particle[] = [];
|
||||
const cellRadius = Math.ceil(radius / CELL_SIZE);
|
||||
|
||||
const centerCellX = Math.floor(x / CELL_SIZE);
|
||||
const centerCellY = Math.floor(y / CELL_SIZE);
|
||||
|
||||
for (
|
||||
let cellX = centerCellX - cellRadius;
|
||||
cellX <= centerCellX + cellRadius;
|
||||
cellX++
|
||||
) {
|
||||
for (
|
||||
let cellY = centerCellY - cellRadius;
|
||||
cellY <= centerCellY + cellRadius;
|
||||
cellY++
|
||||
) {
|
||||
const key = `${cellX},${cellY}`;
|
||||
const cell = spatialHash[key];
|
||||
|
||||
if (cell) {
|
||||
result.push(...cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Main update function
|
||||
function update(): void {
|
||||
const now = Date.now();
|
||||
// Fixed pixel overscan
|
||||
const minX = -OVERSCAN_PIXELS;
|
||||
const maxX = width + OVERSCAN_PIXELS;
|
||||
const minY = -OVERSCAN_PIXELS;
|
||||
const maxY = height + OVERSCAN_PIXELS;
|
||||
|
||||
// Update spatial hash
|
||||
updateSpatialHash();
|
||||
|
||||
// Update forces and remove expired ones
|
||||
if (forces.length > 40) {
|
||||
forces = forces.slice(-40);
|
||||
}
|
||||
forces = forces.filter((force) => {
|
||||
force.strength *= 0.95;
|
||||
force.radius *= 0.95;
|
||||
return force.strength > 0.001;
|
||||
});
|
||||
|
||||
// Update particles
|
||||
const newParticles: Particle[] = [];
|
||||
|
||||
for (const particle of particles) {
|
||||
// Apply forces
|
||||
for (const force of forces) {
|
||||
const dx = particle.x - force.x;
|
||||
const dy = particle.y - force.y;
|
||||
const distSq = dx * dx + dy * dy;
|
||||
|
||||
const radius = force.radius * FORCE_RADIUS;
|
||||
|
||||
if (distSq < radius * radius) {
|
||||
const dist = Math.sqrt(distSq);
|
||||
|
||||
// Exponential falloff - much more concentrated at center
|
||||
// (1 - x/R)^n where n controls how sharp the falloff is
|
||||
const normalizedDist = dist / radius;
|
||||
const factor = Math.pow(1 - normalizedDist, FORCE_FALLOFF_EXPONENT);
|
||||
|
||||
// Calculate force line projection for directional effect
|
||||
// This makes particles along the force's path experience stronger effect
|
||||
const dotProduct = (dx * -force.dx) + (dy * -force.dy);
|
||||
const projectionFactor = Math.max(0, dotProduct / dist);
|
||||
|
||||
// Apply the combined factors - stronger directional bias
|
||||
const finalFactor = factor * force.strength *
|
||||
(0.1 + 0.9 * projectionFactor);
|
||||
|
||||
particle.vx += force.dx * finalFactor;
|
||||
particle.vy += force.dy * finalFactor;
|
||||
// charge for the first 100ms
|
||||
if ((now - force.createdAt) < 100) {
|
||||
particle.charge = Math.min(
|
||||
1,
|
||||
particle.charge + (finalFactor * finalFactor) * 0.2,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply repulsion from nearby particles
|
||||
const nearby = getNearbyParticles(
|
||||
particle.x,
|
||||
particle.y,
|
||||
REPULSION_RADIUS,
|
||||
);
|
||||
|
||||
for (const other of nearby) {
|
||||
if (other === particle) continue;
|
||||
|
||||
const dx = particle.x - other.x;
|
||||
const dy = particle.y - other.y;
|
||||
const distSq = dx * dx + dy * dy;
|
||||
|
||||
if (distSq < REPULSION_RADIUS * REPULSION_RADIUS && distSq > 0) {
|
||||
const dist = Math.sqrt(distSq);
|
||||
const factor = REPULSION_STRENGTH * (1 - dist / REPULSION_RADIUS);
|
||||
|
||||
const fx = dx / dist * factor;
|
||||
const fy = dy / dist * factor;
|
||||
|
||||
particle.vx += fx;
|
||||
particle.vy += fy;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply friction
|
||||
particle.vx *= FRICTION;
|
||||
particle.vy *= FRICTION;
|
||||
|
||||
// Ensure minimum speed
|
||||
const speed = Math.sqrt(
|
||||
particle.vx * particle.vx + particle.vy * particle.vy,
|
||||
);
|
||||
if (speed < MIN_SPEED && speed > 0) {
|
||||
const scale = MIN_SPEED / speed;
|
||||
particle.vx *= scale;
|
||||
particle.vy *= scale;
|
||||
}
|
||||
|
||||
// Cap at maximum speed
|
||||
if (speed > MAX_SPEED) {
|
||||
const scale = MAX_SPEED / speed;
|
||||
particle.vx *= scale;
|
||||
particle.vy *= scale;
|
||||
}
|
||||
|
||||
// Update position
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
|
||||
// Decrease charge
|
||||
particle.charge *= 0.99;
|
||||
|
||||
// Check if particle is within extended bounds
|
||||
if (
|
||||
particle.x >= minX && particle.x <= maxX &&
|
||||
particle.y >= minY && particle.y <= maxY
|
||||
) {
|
||||
// If outside screen but within overscan, keep it if we need more particles
|
||||
if (
|
||||
(particle.x < 0 || particle.x > width ||
|
||||
particle.y < 0 || particle.y > height) &&
|
||||
newParticles.length >= targetParticleCount
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newParticles.push(particle);
|
||||
} else {
|
||||
// Out of bounds, respawn if needed
|
||||
if (newParticles.length < targetParticleCount) {
|
||||
newParticles.push(createParticleOnEdge());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add more particles if needed
|
||||
while (newParticles.length < targetParticleCount) {
|
||||
newParticles.push(createParticleOnEdge());
|
||||
}
|
||||
|
||||
particles = newParticles;
|
||||
}
|
||||
|
||||
// Render function
|
||||
const mul = isStandalone ? 0.9 : 0.5;
|
||||
const add = isStandalone ? 0.1 : 0.03;
|
||||
function render(): void {
|
||||
if (!ctx) return;
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// Draw particles
|
||||
for (const particle of particles) {
|
||||
// Only draw if within canvas bounds (plus a small margin)
|
||||
if (
|
||||
particle.x >= -PARTICLE_RADIUS &&
|
||||
particle.x <= width + PARTICLE_RADIUS &&
|
||||
particle.y >= -PARTICLE_RADIUS && particle.y <= height + PARTICLE_RADIUS
|
||||
) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(particle.x, particle.y, PARTICLE_RADIUS, 0, Math.PI * 2);
|
||||
|
||||
// Color based on charge
|
||||
ctx.fillStyle = "#FFCB1F";
|
||||
ctx.globalAlpha = (particle.charge * mul + add) * globalOpacity;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// // Debug: Draw forces and falloff visualization
|
||||
// if (ctx) {
|
||||
// for (const force of forces) {
|
||||
// const R = force.radius * FORCE_RADIUS;
|
||||
|
||||
// // Draw force point
|
||||
// ctx.beginPath();
|
||||
// ctx.arc(force.x, force.y, 5, 0, Math.PI * 2);
|
||||
// ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
|
||||
// ctx.fill();
|
||||
|
||||
// // Draw force direction
|
||||
// ctx.beginPath();
|
||||
// ctx.moveTo(force.x, force.y);
|
||||
// ctx.lineTo(force.x + force.dx * 20, force.y + force.dy * 20);
|
||||
// ctx.strokeStyle = 'red';
|
||||
// ctx.stroke();
|
||||
|
||||
// // Visualize the falloff curve with rings
|
||||
// for (let i = 0; i <= 10; i++) {
|
||||
// const radius = (R * i) / 10;
|
||||
// const normalizedDist = radius / R;
|
||||
// const intensity = Math.pow(1 - normalizedDist, FORCE_FALLOFF_EXPONENT);
|
||||
|
||||
// ctx.beginPath();
|
||||
// ctx.arc(force.x, force.y, radius, 0, Math.PI * 2);
|
||||
// ctx.strokeStyle = `rgba(255, 0, 0, ${intensity * 0.2})`;
|
||||
// ctx.stroke();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// Animation loop
|
||||
let r = Math.random();
|
||||
function animate(): void {
|
||||
globalOpacity = Math.min(1, globalOpacity + 0.03);
|
||||
update();
|
||||
render();
|
||||
|
||||
if (isRunning) {
|
||||
animationId = requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
|
||||
// Start/stop functions
|
||||
function start(): void {
|
||||
if (isRunning) return;
|
||||
|
||||
// Calculate target particle count based on canvas size
|
||||
targetParticleCount = Math.floor(width * height * PARTICLE_DENSITY);
|
||||
|
||||
// Clear any existing particles and create new ones with proper spacing
|
||||
particles = [];
|
||||
addParticles(targetParticleCount);
|
||||
|
||||
isRunning = true;
|
||||
animate();
|
||||
}
|
||||
|
||||
function stop(): void {
|
||||
isRunning = false;
|
||||
|
||||
if (animationId !== null) {
|
||||
cancelAnimationFrame(animationId);
|
||||
animationId = null;
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
return cleanup;
|
||||
};
|
160
src/file-viewer/scripts/canvas_2022.ts
Normal file
|
@ -0,0 +1,160 @@
|
|||
(globalThis as any).canvas_2022 = function (canvas: HTMLCanvasElement) {
|
||||
const isStandalone = canvas.getAttribute("data-standalone") === "true";
|
||||
// Configuration for the grid of rotating squares
|
||||
const config = {
|
||||
gridRotation: 20, // Overall grid rotation in degrees
|
||||
squareSize: 20, // Size of each square
|
||||
spacing: 100, // Distance between square centers
|
||||
moveSpeedX: 0.01, // Horizontal movement speed (pixels per second)
|
||||
moveSpeedY: 0.01, // Vertical movement speed (pixels per second)
|
||||
squareColor: "#00220A", // Color of the squares
|
||||
squareOpacity: 1, // Opacity of the squares
|
||||
|
||||
// Function to determine square rotation based on its coordinates and time
|
||||
// Can be adjusted for different patterns
|
||||
rotationFunction: (x: number, y: number, time: number): number => {
|
||||
// Combination of spatial wave and time-based rotation
|
||||
return Math.sin(x * 0.05) * Math.cos(y * 0.05) * 180;
|
||||
},
|
||||
};
|
||||
|
||||
// Convert grid rotation to radians
|
||||
const gridRotationRad = (config.gridRotation * Math.PI) / 180;
|
||||
|
||||
// Get the canvas context
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
console.error("Could not get canvas context");
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// Make canvas transparent
|
||||
if (isStandalone) {
|
||||
canvas.style.backgroundColor = "#154226";
|
||||
} else {
|
||||
canvas.style.backgroundColor = "transparent";
|
||||
}
|
||||
|
||||
// Animation variables
|
||||
let width = canvas.width;
|
||||
let height = canvas.height;
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
let time = 0;
|
||||
let animationFrameId: number;
|
||||
let lastTime = 0;
|
||||
|
||||
// Update canvas dimensions when resized
|
||||
const updateDimensions = () => {
|
||||
width = canvas.width = canvas.clientWidth;
|
||||
height = canvas.height = canvas.clientHeight;
|
||||
};
|
||||
|
||||
// Calculate the diagonal length of the canvas (to ensure rotation covers corners)
|
||||
const calculateDiagonal = () => {
|
||||
return Math.sqrt(width * width + height * height);
|
||||
};
|
||||
|
||||
// Draw a single square with rotation
|
||||
const drawSquare = (x: number, y: number, size: number, rotation: number) => {
|
||||
ctx.save();
|
||||
|
||||
// Move to the center of the square position, rotate, then draw
|
||||
ctx.translate(x, y);
|
||||
ctx.rotate((rotation * Math.PI) / 180); // Convert rotation degrees to radians
|
||||
|
||||
// Draw square centered at position
|
||||
ctx.fillRect(-size / 2, -size / 2, size, size);
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
// Draw the entire grid of squares
|
||||
const drawGrid = () => {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// Set drawing properties
|
||||
ctx.fillStyle = config.squareColor;
|
||||
ctx.globalAlpha = config.squareOpacity;
|
||||
|
||||
// Save the current transformation state
|
||||
ctx.save();
|
||||
|
||||
// Move to the center of the canvas, rotate the grid, then move back
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
ctx.translate(centerX, centerY);
|
||||
ctx.rotate(gridRotationRad);
|
||||
|
||||
// Calculate how much of the grid to draw based on canvas size
|
||||
const diagonal = calculateDiagonal();
|
||||
const gridSize = Math.ceil(diagonal / config.spacing) + 2;
|
||||
|
||||
// Adjust for offset to create movement
|
||||
const adjustedOffsetX = offsetX % config.spacing;
|
||||
const adjustedOffsetY = offsetY % config.spacing;
|
||||
|
||||
// Draw grid with enough squares to cover the rotated canvas
|
||||
const halfGrid = Math.ceil(gridSize / 2);
|
||||
|
||||
for (let y = -halfGrid; y <= halfGrid; y++) {
|
||||
for (let x = -halfGrid; x <= halfGrid; x++) {
|
||||
// Calculate actual position with offset
|
||||
const posX = x * config.spacing + adjustedOffsetX;
|
||||
const posY = y * config.spacing + adjustedOffsetY;
|
||||
|
||||
// Calculate square rotation based on its position and time
|
||||
const squareRotation = config.rotationFunction(posX, posY, time);
|
||||
|
||||
// Draw the square
|
||||
drawSquare(posX, posY, config.squareSize, squareRotation);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the transformation state
|
||||
ctx.restore();
|
||||
|
||||
// Reset global alpha
|
||||
ctx.globalAlpha = 1.0;
|
||||
};
|
||||
|
||||
// Animation loop
|
||||
const animate = (currentTime: number) => {
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
|
||||
// Calculate time elapsed since last frame
|
||||
const elapsed = currentTime - lastTime;
|
||||
lastTime = currentTime;
|
||||
|
||||
// Update time variable for rotation function
|
||||
time += elapsed;
|
||||
|
||||
// Update position offsets for movement
|
||||
offsetX += config.moveSpeedX * elapsed;
|
||||
offsetY += config.moveSpeedY * elapsed;
|
||||
|
||||
// Draw the grid
|
||||
drawGrid();
|
||||
};
|
||||
|
||||
// Initialize the animation
|
||||
const init = () => {
|
||||
// Set up resize handler
|
||||
window.addEventListener("resize", updateDimensions);
|
||||
|
||||
// Initial setup
|
||||
updateDimensions();
|
||||
|
||||
// Start animation
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
// Start the animation
|
||||
init();
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateDimensions);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
};
|
197
src/file-viewer/scripts/canvas_2023.ts
Normal file
|
@ -0,0 +1,197 @@
|
|||
(globalThis as any).canvas_2023 = function (canvas: HTMLCanvasElement) {
|
||||
const isStandalone = canvas.getAttribute("data-standalone") === "true";
|
||||
const config = {
|
||||
heartBaseSize: 50,
|
||||
heartMaxSize: 100,
|
||||
spacing: 150,
|
||||
rowSpeed: 0.1,
|
||||
heartColor: "#FF90D9",
|
||||
heartOpacity: isStandalone ? 0.5 : 0.04,
|
||||
mouseInfluenceRadius: 1000,
|
||||
heartScaleFunction: (distance: number, radius: number): number => {
|
||||
if (distance > radius) return 1;
|
||||
|
||||
const normalizedDistance = distance / radius;
|
||||
const scaleFactor = 1 +
|
||||
(1 - normalizedDistance) *
|
||||
(config.heartMaxSize / config.heartBaseSize - 1);
|
||||
|
||||
return 1 + (scaleFactor - 1) * Math.pow(1 - normalizedDistance, 2);
|
||||
},
|
||||
};
|
||||
|
||||
const heart = new Path2D(
|
||||
"M23.9451 45.3973L20.8672 42.6493C16.9551 39.0174 13.7054 35.8927 11.1181 33.275C8.53056 30.6574 6.46731 28.286 4.92839 26.1608C3.38946 24.0356 2.31772 22.1028 1.71314 20.3624C1.10856 18.6219 0.806274 16.8705 0.806274 15.1081C0.806274 11.4718 2.03118 8.42016 4.481 5.95312C6.93118 3.48608 9.93831 2.25256 13.5024 2.25256C15.5649 2.25256 17.482 2.70142 19.2536 3.59912C21.0255 4.49682 22.5893 5.80674 23.9451 7.52887C25.484 5.73346 27.1059 4.40522 28.8108 3.54416C30.5161 2.6831 32.3751 2.25256 34.3877 2.25256C38.0141 2.25256 41.0551 3.48663 43.5108 5.95477C45.9661 8.42291 47.1938 11.4758 47.1938 15.1136C47.1938 16.8712 46.8823 18.6115 46.2594 20.3343C45.6365 22.0568 44.5648 23.9807 43.0442 26.1059C41.5236 28.231 39.4721 30.6136 36.8896 33.2536C34.3068 35.8936 31.0362 39.0255 27.0779 42.6493L23.9451 45.3973ZM23.9176 38.802C27.6088 35.431 30.6339 32.5547 32.9928 30.173C35.3518 27.7913 37.2091 25.7211 38.5648 23.9624C39.9205 22.2036 40.864 20.6137 41.3953 19.1928C41.9266 17.7715 42.1923 16.4101 42.1923 15.1086C42.1923 12.8768 41.4529 11.0098 39.974 9.50748C38.4952 8.0052 36.6461 7.25406 34.4268 7.25406C32.631 7.25406 30.9572 7.6811 29.4055 8.87193C27.8537 10.0628 25.5389 13.0434 25.5389 13.0434L23.9451 15.3299L22.3512 13.0434C22.3512 13.0434 20.0643 10.2311 18.4638 9.04031C16.8634 7.84948 15.2194 7.25406 13.4991 7.25406C11.2929 7.25406 9.46857 7.98816 8.02602 9.45637C6.58383 10.9246 5.86273 12.8162 5.86273 15.1311C5.86273 16.4784 6.13644 17.8679 6.68386 19.2994C7.23127 20.731 8.18394 22.3333 9.54185 24.1064C10.8998 25.879 12.7329 27.9562 15.0413 30.3379C17.3497 32.7196 20.3084 35.5409 23.9176 38.802Z",
|
||||
);
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
console.error("Could not get canvas context");
|
||||
return () => {};
|
||||
}
|
||||
|
||||
if (isStandalone) {
|
||||
canvas.style.backgroundColor = "#2F1C21";
|
||||
} else {
|
||||
canvas.style.backgroundColor = "transparent";
|
||||
}
|
||||
|
||||
let width = canvas.width;
|
||||
let height = canvas.height;
|
||||
let animationFrameId: number;
|
||||
let lastFrameTime = 0;
|
||||
|
||||
let mouseX = width / 2;
|
||||
let mouseY = height / 2;
|
||||
|
||||
let offset = config.spacing / 2;
|
||||
|
||||
const updateDimensions = () => {
|
||||
width = canvas.width = canvas.clientWidth;
|
||||
height = canvas.height = canvas.clientHeight;
|
||||
|
||||
mouseX = width / 2;
|
||||
mouseY = height / 2;
|
||||
};
|
||||
|
||||
const drawHeart = (x: number, y: number, size: number) => {
|
||||
const scale = size / 30;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
ctx.scale(scale, scale);
|
||||
|
||||
ctx.fillStyle = config.heartColor;
|
||||
ctx.fill(heart);
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
const c = 400;
|
||||
const h = 40;
|
||||
const k = solveForK(c, h);
|
||||
|
||||
const drawHeartGrid = () => {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
ctx.globalAlpha = config.heartOpacity;
|
||||
|
||||
const numRows = Math.ceil(height / config.spacing) + 1;
|
||||
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
const direction = row % 2 === 0 ? 1 : -1;
|
||||
const rowOffset = (offset * direction) % config.spacing;
|
||||
|
||||
const posYInit = row * config.spacing + config.spacing / 2;
|
||||
|
||||
for (
|
||||
let posXInit = -config.spacing + rowOffset;
|
||||
posXInit < width + config.spacing;
|
||||
posXInit += config.spacing
|
||||
) {
|
||||
const dx = (posXInit + config.heartBaseSize / 2) - mouseX;
|
||||
const dy = (posYInit + config.heartBaseSize / 2) - mouseY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
const pushIntensity = asymmetricBump(distance, h, c, k, 0.00002);
|
||||
|
||||
const pushAngle = Math.atan2(dy, dx);
|
||||
|
||||
const pushDistanceX = pushIntensity * Math.cos(pushAngle);
|
||||
const pushDistanceY = pushIntensity * Math.sin(pushAngle);
|
||||
const posX = posXInit + pushDistanceX * 1;
|
||||
const posY = posYInit + pushDistanceY * 2;
|
||||
|
||||
const scaleFactor = config.heartScaleFunction(
|
||||
distance,
|
||||
config.mouseInfluenceRadius,
|
||||
);
|
||||
const heartSize = config.heartBaseSize * scaleFactor;
|
||||
|
||||
if (
|
||||
posX > -config.heartMaxSize &&
|
||||
posX < width + config.heartMaxSize &&
|
||||
posY > -config.heartMaxSize &&
|
||||
posY < height + config.heartMaxSize
|
||||
) {
|
||||
drawHeart(posX - heartSize / 2, posY - heartSize / 2, heartSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1.0;
|
||||
};
|
||||
|
||||
function solveForK(c: number, k: number) {
|
||||
// input -> f(x)=h*e^{(-k*(x-c)^{2})}
|
||||
// desired result is (0, 0.45). (0, 0) is unsolvable but 0.45px will round down to 0.
|
||||
//
|
||||
// solution: -\frac{\ln\left(\frac{0.45}{h}\right)}{c^{2}}
|
||||
return -Math.log(0.45 / h) / (c * c);
|
||||
}
|
||||
|
||||
function asymmetricBump(
|
||||
x: number,
|
||||
h: number,
|
||||
c: number,
|
||||
leftK: number,
|
||||
rightK: number,
|
||||
) {
|
||||
const k = (x <= c) ? leftK : rightK;
|
||||
return h * Math.exp(-k * Math.pow(x - c, 2));
|
||||
}
|
||||
|
||||
const updateOffset = (elapsed: number) => {
|
||||
offset += config.rowSpeed * elapsed;
|
||||
|
||||
if (offset > 1000000) {
|
||||
offset -= 1000000;
|
||||
}
|
||||
};
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
|
||||
const elapsed = currentTime - lastFrameTime;
|
||||
lastFrameTime = currentTime;
|
||||
|
||||
updateOffset(elapsed * 0.05);
|
||||
|
||||
drawHeartGrid();
|
||||
};
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
mouseX = event.clientX - rect.left;
|
||||
mouseY = event.clientY - rect.top;
|
||||
};
|
||||
|
||||
const handleTouchMove = (event: TouchEvent) => {
|
||||
if (event.touches.length > 0) {
|
||||
event.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
mouseX = event.touches[0].clientX - rect.left;
|
||||
mouseY = event.touches[0].clientY - rect.top;
|
||||
}
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
window.addEventListener("resize", updateDimensions);
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("touchmove", handleTouchMove, { passive: false });
|
||||
|
||||
updateDimensions();
|
||||
|
||||
lastFrameTime = performance.now();
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateDimensions);
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("touchmove", handleTouchMove);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
};
|
251
src/file-viewer/scripts/canvas_2024.ts
Normal file
|
@ -0,0 +1,251 @@
|
|||
// Vibe coded with AI
|
||||
(globalThis as any).canvas_2024 = function (canvas: HTMLCanvasElement) {
|
||||
const isStandalone = canvas.getAttribute("data-standalone") === "true";
|
||||
if (isStandalone) {
|
||||
canvas.parentElement!.style.backgroundColor = "black";
|
||||
}
|
||||
|
||||
const gl = canvas.getContext("webgl", {
|
||||
alpha: true,
|
||||
premultipliedAlpha: false,
|
||||
});
|
||||
if (!gl) {
|
||||
console.error("WebGL not supported");
|
||||
return () => {};
|
||||
}
|
||||
|
||||
canvas.style.imageRendering = "pixelated";
|
||||
canvas.style.opacity = isStandalone ? "0.3" : "0.15";
|
||||
|
||||
// Resize canvas to match display size
|
||||
const resize = () => {
|
||||
const displayWidth = Math.floor(
|
||||
(canvas.clientWidth || window.innerWidth) / 3,
|
||||
);
|
||||
const displayHeight = Math.floor(
|
||||
(canvas.clientHeight || window.innerHeight) / 3,
|
||||
);
|
||||
|
||||
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
|
||||
canvas.width = displayWidth;
|
||||
canvas.height = displayHeight;
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
};
|
||||
resize();
|
||||
|
||||
// Vertex shader (just passes coordinates)
|
||||
const vertexShaderSource = `
|
||||
attribute vec2 a_position;
|
||||
void main() {
|
||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
// Fragment shader creates random noise with higher opacity to ensure visibility
|
||||
const fragmentShaderSource = `
|
||||
precision mediump float;
|
||||
uniform float u_time;
|
||||
|
||||
float noise1(float seed1,float seed2){
|
||||
return(
|
||||
fract(seed1+12.34567*
|
||||
fract(100.*(abs(seed1*0.91)+seed2+94.68)*
|
||||
fract((abs(seed2*0.41)+45.46)*
|
||||
fract((abs(seed2)+757.21)*
|
||||
fract(seed1*0.0171))))))
|
||||
* 1.0038 - 0.00185;
|
||||
}
|
||||
|
||||
float n(float seed1, float seed2, float seed3){
|
||||
float buff1 = abs(seed1+100.81) + 1000.3;
|
||||
float buff2 = abs(seed2+100.45) + 1000.2;
|
||||
float buff3 = abs(noise1(seed1, seed2)+seed3) + 1000.1;
|
||||
buff1 = (buff3*fract(buff2*fract(buff1*fract(buff2*0.146))));
|
||||
buff2 = (buff2*fract(buff2*fract(buff1+buff2*fract(buff3*0.52))));
|
||||
buff1 = noise1(buff1, buff2);
|
||||
return(buff1);
|
||||
}
|
||||
|
||||
void main() {
|
||||
float noise = n(gl_FragCoord.x, gl_FragCoord.y, u_time);
|
||||
|
||||
gl_FragColor = vec4(1.0, 0.7, 0.7, 0.8*noise);
|
||||
}
|
||||
`;
|
||||
|
||||
// Create and compile shaders
|
||||
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
|
||||
const fragmentShader = createShader(
|
||||
gl,
|
||||
gl.FRAGMENT_SHADER,
|
||||
fragmentShaderSource,
|
||||
);
|
||||
|
||||
// Check if shader creation failed
|
||||
if (!vertexShader || !fragmentShader) {
|
||||
console.error("Failed to create shaders");
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// Create program and link shaders
|
||||
const program = createProgram(gl, vertexShader, fragmentShader);
|
||||
|
||||
// Check if program creation failed
|
||||
if (!program) {
|
||||
console.error("Failed to create program");
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// Get attribute and uniform locations
|
||||
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
|
||||
const timeUniformLocation = gl.getUniformLocation(program, "u_time");
|
||||
|
||||
// Create a position buffer for a rectangle covering the entire canvas
|
||||
const positionBuffer = gl.createBuffer();
|
||||
if (!positionBuffer) {
|
||||
console.error("Failed to create position buffer");
|
||||
return () => {};
|
||||
}
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
// Rectangle that covers the entire clip space
|
||||
const positions = [
|
||||
-1.0,
|
||||
-1.0, // bottom left
|
||||
1.0,
|
||||
-1.0, // bottom right
|
||||
-1.0,
|
||||
1.0, // top left
|
||||
1.0,
|
||||
1.0, // top right
|
||||
];
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
|
||||
|
||||
// Set up blending
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// Fixed 24 FPS timing
|
||||
const FPS = 24;
|
||||
const FRAME_TIME = 1000 / FPS; // ms per frame
|
||||
|
||||
// Handle animation
|
||||
let animationTimerId: number;
|
||||
let startTime = Date.now();
|
||||
let lastFrameTime = 0;
|
||||
|
||||
const render = () => {
|
||||
// Get current time
|
||||
const currentTime = Date.now();
|
||||
const deltaTime = currentTime - lastFrameTime;
|
||||
|
||||
// Skip frame if it's too early (maintain 24 FPS)
|
||||
if (deltaTime < FRAME_TIME) {
|
||||
animationTimerId = window.setTimeout(render, 0); // Check again ASAP but yield to browser
|
||||
return;
|
||||
}
|
||||
|
||||
// Update last frame time, accounting for any drift
|
||||
lastFrameTime = currentTime - (deltaTime % FRAME_TIME);
|
||||
|
||||
// Resize canvas if needed
|
||||
resize();
|
||||
|
||||
// Calculate elapsed time in seconds for animation
|
||||
const elapsedTime = (currentTime - startTime) / 1000;
|
||||
|
||||
// Clear the canvas with transparent black
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
// Use our shader program
|
||||
gl.useProgram(program);
|
||||
|
||||
// Set up the position attribute
|
||||
gl.enableVertexAttribArray(positionAttributeLocation);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
gl.vertexAttribPointer(
|
||||
positionAttributeLocation,
|
||||
2, // 2 components per vertex
|
||||
gl.FLOAT, // data type
|
||||
false, // normalize
|
||||
0, // stride (0 = compute from size and type)
|
||||
0, // offset
|
||||
);
|
||||
|
||||
// Update time uniform for animation
|
||||
gl.uniform1f(timeUniformLocation, elapsedTime);
|
||||
|
||||
// Draw the rectangle (2 triangles)
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
// Schedule next frame (aiming for 24 FPS)
|
||||
const timeToNextFrame = Math.max(
|
||||
0,
|
||||
FRAME_TIME - (Date.now() - currentTime),
|
||||
);
|
||||
animationTimerId = window.setTimeout(render, timeToNextFrame);
|
||||
};
|
||||
|
||||
// Helper function to create shaders
|
||||
function createShader(
|
||||
gl: WebGLRenderingContext,
|
||||
type: number,
|
||||
source: string,
|
||||
): WebGLShader | null {
|
||||
const shader = gl.createShader(type);
|
||||
if (!shader) {
|
||||
console.error("Failed to create shader object");
|
||||
return null;
|
||||
}
|
||||
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
console.error("Shader compilation error:", gl.getShaderInfoLog(shader));
|
||||
gl.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
// Helper function to create program and link shaders
|
||||
function createProgram(
|
||||
gl: WebGLRenderingContext,
|
||||
vertexShader: WebGLShader,
|
||||
fragmentShader: WebGLShader,
|
||||
): WebGLProgram | null {
|
||||
const program = gl.createProgram();
|
||||
if (!program) {
|
||||
console.error("Failed to create program object");
|
||||
return null;
|
||||
}
|
||||
|
||||
gl.attachShader(program, vertexShader);
|
||||
gl.attachShader(program, fragmentShader);
|
||||
gl.linkProgram(program);
|
||||
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||
console.error("Program linking error:", gl.getProgramInfoLog(program));
|
||||
return null;
|
||||
}
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
// Start the rendering with initial timestamp
|
||||
lastFrameTime = Date.now();
|
||||
render();
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
clearTimeout(animationTimerId);
|
||||
if (program) gl.deleteProgram(program);
|
||||
if (vertexShader) gl.deleteShader(vertexShader);
|
||||
if (fragmentShader) gl.deleteShader(fragmentShader);
|
||||
if (positionBuffer) gl.deleteBuffer(positionBuffer);
|
||||
};
|
||||
};
|
362
src/file-viewer/scripts/canvas_cotyledon.ts
Normal file
|
@ -0,0 +1,362 @@
|
|||
// @ts-ignore
|
||||
globalThis.canvas_cotyledon = function (
|
||||
canvas: HTMLCanvasElement,
|
||||
panel: HTMLElement,
|
||||
) {
|
||||
let running = true;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = canvas.clientWidth;
|
||||
canvas.height = canvas.clientHeight;
|
||||
}
|
||||
|
||||
resizeCanvas();
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
|
||||
const clover = new Path2D(
|
||||
"M18.9845 34.4839C20.4004 34.5218 21.8336 34.6883 23.2644 34.9578C20.1378 31.095 18.4268 27.1546 18.0555 23.2959C17.321 15.6622 21.9022 9.36595 28.8908 5.78535C34.6355 2.84212 40.258 2.98454 44.2809 5.96879C45.6605 6.99221 46.7683 8.2886 47.5877 9.78593C48.3054 8.50307 49.134 7.26623 50.0858 6.17951C51.8368 4.18037 54.1947 2.47127 57.2294 2.15019C60.2768 1.82766 63.467 2.9608 66.7548 5.52299C70.9834 8.81811 73.084 12.8864 73.5996 17.2135C74.1044 21.4504 73.0711 25.7433 71.4155 29.6117C70.6566 31.3849 69.7488 33.1106 68.7557 34.7506C70.3664 33.9983 72.0168 33.3376 73.6816 32.8312C77.2262 31.7528 81.0258 31.3024 84.8151 32.2149C88.6451 33.1371 92.2246 35.3946 95.3823 39.3157C98.4534 43.1293 99.9219 46.6818 99.997 49.9677C100.073 53.3033 98.7051 55.9829 96.8652 57.9789C95.0586 59.9387 92.7653 61.2872 90.7505 62.1315C90.692 62.1561 90.6334 62.1802 90.5746 62.2042L90.4465 62.256C91.4852 63.7304 92.4724 65.5955 93.0127 67.6979C93.5916 69.9509 93.6669 72.5285 92.674 75.1356C91.679 77.7482 89.7006 80.1559 86.5767 82.2161C86.5556 82.23 86.5342 82.2438 86.5126 82.2571C84.1333 83.7267 81.5504 84.7197 78.6932 84.9352C75.832 85.151 72.8634 84.5742 69.7337 83.1522C64.7667 80.8953 59.274 76.4525 52.8745 69.3645C52.8789 70.1568 52.8844 70.9254 52.9004 71.6677C52.9643 74.6226 53.1868 77.4534 54.0666 80.6265C55.2259 84.503 57.2821 88.4684 60.9561 92.3161C61.644 93.0366 61.8512 94.0908 61.4872 95.018L60.9919 96.2799C60.6464 97.16 59.8435 97.778 58.9041 97.8865C57.9647 97.9952 57.042 97.5769 56.5047 96.7985C52.5406 91.0574 50.3441 86.3289 49.1491 82.0434C48.0155 78.2319 47.6244 74.4579 47.5085 71.0024C45.418 73.6873 42.8696 76.4687 40.0618 78.9101C34.3517 83.8756 26.6803 88.1931 19.142 85.9955C15.5301 84.9425 12.8635 83.2751 11.0848 81.1179C9.2952 78.9474 8.5557 76.4627 8.4981 74.0631C8.43961 71.6256 9.07998 69.225 10.075 67.1703C7.76333 66.828 5.38011 65.9682 3.47071 64.2327C-0.339092 60.7699 -1.2199 54.8876 1.86982 46.4552C3.47011 42.0878 5.90372 38.9798 8.98328 37.0179C12.0444 35.0677 15.5215 34.3912 18.9845 34.4839Z",
|
||||
);
|
||||
|
||||
// Background
|
||||
const base = [0x14, 0x1a, 0x19];
|
||||
|
||||
let blobTextureCanvas: OffscreenCanvas;
|
||||
let blobTextureCtx: OffscreenCanvasRenderingContext2D;
|
||||
const blobSize = 1000; // Size of the noise texture
|
||||
{
|
||||
const blobTexture = new ImageData(blobSize, blobSize);
|
||||
const data = blobTexture.data;
|
||||
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (x >= blobSize) {
|
||||
x = 0;
|
||||
y++;
|
||||
}
|
||||
|
||||
const noiseX = Math.sin(x * .2 + y * .1) * 0.03;
|
||||
const noiseY = Math.cos(y * .6 + x * .2) * 0.05;
|
||||
|
||||
const centerX = blobSize / 2;
|
||||
const centerY = blobSize / 2;
|
||||
const dx = (x + noiseX) - centerX;
|
||||
const dy = (y + noiseY) - centerY;
|
||||
const distanceFromCenterRaw = (dx * dx + dy * dy) ** 0.5;
|
||||
|
||||
const maxDistance = blobSize / 2;
|
||||
const distanceFromCenter = Math.min(
|
||||
1,
|
||||
distanceFromCenterRaw / maxDistance,
|
||||
);
|
||||
|
||||
const noiseValue = (1 - 0.5 * Math.sin(x * 0.02 - y * 0.04)) *
|
||||
(1 - 0.5 * Math.cos(x * 0.03 + y * 0.04)) * 0.3;
|
||||
|
||||
const gradient = (1 - distanceFromCenter) *
|
||||
(0.95 - distanceFromCenter * 0.4);
|
||||
const finalValue = Math.max(
|
||||
0,
|
||||
Math.min(1, gradient * (Math.random() * 0.3 + 0.85 + noiseValue)),
|
||||
);
|
||||
|
||||
data[i] = 121;
|
||||
data[i + 1] = 219;
|
||||
data[i + 2] = 160;
|
||||
data[i + 3] = Math.floor(finalValue * 255.99); // Alpha
|
||||
x++;
|
||||
}
|
||||
blobTextureCanvas = new OffscreenCanvas(blobSize, blobSize);
|
||||
blobTextureCtx = blobTextureCanvas.getContext("2d")!;
|
||||
blobTextureCtx.putImageData(blobTexture, 0, 0);
|
||||
}
|
||||
|
||||
class CotyledonParticle {
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
velocityX: number;
|
||||
velocityY: number;
|
||||
rotation: number;
|
||||
rotationSpeed: number;
|
||||
color: string;
|
||||
|
||||
constructor(positioning: "random" | "edge") {
|
||||
this.size = Math.random() * 0.1 + 0.6;
|
||||
|
||||
if (positioning === "edge") {
|
||||
const edge = Math.floor(Math.random() * 5);
|
||||
if (edge === 0 || edge === 1) {
|
||||
// Right edge
|
||||
this.x = 1.05;
|
||||
this.y = Math.random();
|
||||
this.velocityX = -Math.random() * 0.1 - 0.2;
|
||||
this.velocityY = 0;
|
||||
} else if (edge === 2 || edge === 3) {
|
||||
// Top edge
|
||||
this.x = Math.random();
|
||||
this.y = -0.05;
|
||||
this.velocityX = -Math.random() * 0.1 - 0.1;
|
||||
this.velocityY = -Math.random() * 0.2 - 0.05;
|
||||
} else {
|
||||
// Bottom edge
|
||||
this.x = Math.random() * 0.5 + 0.5;
|
||||
this.y = 1.05;
|
||||
this.velocityX = Math.random() * 0.1 + 0.1;
|
||||
this.velocityY = -Math.random() * 0.3 - 0.1;
|
||||
}
|
||||
} else {
|
||||
let tries = 0;
|
||||
do {
|
||||
this.x = Math.random();
|
||||
this.y = Math.random();
|
||||
this.velocityX = -Math.random() * 0.05 - 0.1;
|
||||
this.velocityY = -Math.random() * 0.2 + 0.1;
|
||||
} while (this.tooCloseToAnyOtherParticle() && (tries++ < 10));
|
||||
}
|
||||
|
||||
this.rotation = Math.random() * Math.PI * 2;
|
||||
this.rotationSpeed = (Math.random() * 0.003 - 0.0015) *
|
||||
(Math.random() > 0.5 ? 1 : -1);
|
||||
|
||||
const opacity = Math.random() * 0.4 + 0.2;
|
||||
this.color = `rgba(${
|
||||
base.map((x) => x + Math.floor(x * opacity)).join(",")
|
||||
}, 1)`;
|
||||
}
|
||||
|
||||
tooCloseToAnyOtherParticle() {
|
||||
for (let i = 0; i < cotyledonParticles.length; i++) {
|
||||
const otherParticle = cotyledonParticles[i];
|
||||
const distance = Math.sqrt(
|
||||
(this.x - otherParticle.x) ** 2 + (this.y - otherParticle.y) ** 2,
|
||||
);
|
||||
if (distance < 0.1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.velocityY < 0.01) {
|
||||
this.velocityY += 0.00025;
|
||||
}
|
||||
this.velocityX -= 0.0001;
|
||||
this.x += this.velocityX / 1300;
|
||||
this.y += this.velocityY / 1000;
|
||||
this.rotation += this.rotationSpeed;
|
||||
return this.x < -0.05 || (this.y > 1 && this.velocityY < 0);
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(this.x * canvas.width, this.y * canvas.height);
|
||||
ctx.rotate(this.rotation);
|
||||
ctx.scale(this.size, this.size);
|
||||
ctx.translate(-50, -50);
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.fill(clover);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
class BlobParticle {
|
||||
x: number;
|
||||
y: number;
|
||||
opacity: number;
|
||||
state: 0 | 1 | 2;
|
||||
stateTime: number;
|
||||
stayDuration: number;
|
||||
innerColor: string;
|
||||
rot: number = Math.random() * Math.PI * 2;
|
||||
|
||||
constructor() {
|
||||
this.x = Math.random() * 0.6 + 0.2;
|
||||
this.y = Math.random() * 0.6 + 0.2;
|
||||
|
||||
this.opacity = 0; // Start fully transparent
|
||||
|
||||
this.state = 0;
|
||||
this.stateTime = 0;
|
||||
this.stayDuration = Math.random() * 10000 + 5000; // Random stay duration between 5-15 seconds
|
||||
|
||||
const colorMultiplier = Math.random() * 0.5 + 1.5; // 0.5-1.0 multiplier
|
||||
const colorValues = base.map((x) => Math.floor(x * colorMultiplier));
|
||||
this.innerColor = `rgba(${colorValues.join(",")}, 1)`;
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
this.stateTime += deltaTime;
|
||||
|
||||
if (this.state === 0) {
|
||||
this.opacity = Math.min(1, this.stateTime / 15000);
|
||||
if (this.stateTime >= 15000) {
|
||||
this.state = 1;
|
||||
this.stateTime = 0;
|
||||
}
|
||||
} else if (this.state === 1) {
|
||||
if (this.stateTime >= this.stayDuration) {
|
||||
this.state = 2;
|
||||
this.stateTime = 0;
|
||||
}
|
||||
} else if (this.state === 2) {
|
||||
this.opacity = Math.max(0, 1 - (this.stateTime / 15000));
|
||||
if (this.stateTime >= 15000) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (!ctx) return;
|
||||
|
||||
const screenX = this.x * canvas.width;
|
||||
const screenY = this.y * canvas.height;
|
||||
const screenSize = (Math.min(canvas.width, canvas.height) * 2) / blobSize;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(screenX - screenSize * 0.5, screenY - screenSize * 0.5);
|
||||
ctx.scale(screenSize, screenSize);
|
||||
ctx.rotate(this.rot);
|
||||
ctx.globalAlpha = this.opacity * 0.2;
|
||||
ctx.globalCompositeOperation = "overlay";
|
||||
ctx.drawImage(blobTextureCanvas, 0, 0);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
const cotyledonParticles: CotyledonParticle[] = [];
|
||||
const blobParticles: BlobParticle[] = [];
|
||||
let blobParticleTop: BlobParticle = new BlobParticle();
|
||||
for (let i = 0; i < 80; i++) {
|
||||
cotyledonParticles.push(new CotyledonParticle("random"));
|
||||
}
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const blobParticle = new BlobParticle();
|
||||
if (i < 4) {
|
||||
blobParticle.state = 1;
|
||||
blobParticle.opacity = 1;
|
||||
blobParticle.stayDuration = Math.random() * 10000;
|
||||
} else {
|
||||
blobParticle.state = i < 7 ? 2 : 0;
|
||||
blobParticle.stateTime = Math.random() * 15000;
|
||||
}
|
||||
if (i > 0) {
|
||||
do {
|
||||
blobParticle.x = Math.random();
|
||||
blobParticle.y = Math.random();
|
||||
} while (
|
||||
blobParticles.some((p) =>
|
||||
Math.sqrt((p.x - blobParticle.x) ** 2 + (p.y - blobParticle.y) ** 2) <
|
||||
0.1
|
||||
)
|
||||
);
|
||||
}
|
||||
blobParticles.push(blobParticle);
|
||||
}
|
||||
|
||||
let lastTime = performance.now();
|
||||
|
||||
function animate(currentTime: number) {
|
||||
if (!running) return;
|
||||
if (!ctx) return;
|
||||
|
||||
const deltaTime = currentTime - lastTime;
|
||||
lastTime = currentTime;
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (let i = blobParticles.length - 1; i >= 0; i--) {
|
||||
const shouldRemove = blobParticles[i].update(deltaTime);
|
||||
if (shouldRemove) {
|
||||
blobParticles[i] = new BlobParticle();
|
||||
} else {
|
||||
blobParticles[i].draw();
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = cotyledonParticles.length - 1; i >= 0; i--) {
|
||||
const shouldRemove = cotyledonParticles[i].update();
|
||||
if (shouldRemove) {
|
||||
cotyledonParticles[i] = new CotyledonParticle("edge");
|
||||
} else {
|
||||
cotyledonParticles[i].draw();
|
||||
}
|
||||
}
|
||||
|
||||
if (blobParticleTop.update(deltaTime)) {
|
||||
blobParticleTop = new BlobParticle();
|
||||
}
|
||||
blobParticleTop.draw();
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
let clickedButton = false;
|
||||
const enterButton = panel.querySelector("button#enter")!;
|
||||
enterButton.addEventListener("click", () => {
|
||||
if (clickedButton) return;
|
||||
clickedButton = true;
|
||||
const first = panel.querySelector("#first")! as HTMLElement;
|
||||
const second = panel.querySelector("#captcha")! as HTMLElement;
|
||||
first.style.transition = second.style.transition = "opacity 1s ease-in-out";
|
||||
first.style.opacity = "0";
|
||||
second.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
first.style.display = "none";
|
||||
second.style.display = "block";
|
||||
setTimeout(() => {
|
||||
second.style.opacity = "1";
|
||||
|
||||
document.getElementById("enter2")?.addEventListener("click", () => {
|
||||
second.style.opacity = "0";
|
||||
let p = fetch("/file/cotyledon", {
|
||||
method: "POST",
|
||||
body: "I AGREE",
|
||||
});
|
||||
setTimeout(() => {
|
||||
p.then(() => {
|
||||
location.reload();
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
}, 10);
|
||||
}, 1000);
|
||||
});
|
||||
const imageButtons = panel.querySelectorAll(".image-grid button")!;
|
||||
imageButtons.forEach((button) => {
|
||||
let canClick = true;
|
||||
button.addEventListener("click", () => {
|
||||
if (!canClick) return;
|
||||
canClick = false;
|
||||
const image = button.querySelector("img")!;
|
||||
image.style.transition = "opacity 0.05s linear";
|
||||
image.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
image.style.transition = "opacity 2s linear";
|
||||
let newNum;
|
||||
do {
|
||||
newNum = Math.floor(Math.random() * 18); // 0-17 inclusive
|
||||
} while (
|
||||
document.querySelector(`img[src="/captcha/image/${newNum}.jpeg"]`)
|
||||
);
|
||||
image.setAttribute("src", `/captcha/image/${newNum}.jpeg`);
|
||||
setTimeout(() => {
|
||||
image.style.opacity = "0.75";
|
||||
canClick = true;
|
||||
}, 50);
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
// Start animation
|
||||
animate(performance.now());
|
||||
return () => {
|
||||
window.removeEventListener("resize", resizeCanvas);
|
||||
running = false;
|
||||
};
|
||||
};
|
BIN
src/file-viewer/static/captcha/0.jpeg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/file-viewer/static/captcha/1.jpeg
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
src/file-viewer/static/captcha/10.jpeg
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/file-viewer/static/captcha/11.jpeg
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
src/file-viewer/static/captcha/12.jpeg
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
src/file-viewer/static/captcha/13.jpeg
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
src/file-viewer/static/captcha/14.jpeg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/file-viewer/static/captcha/15.jpeg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/file-viewer/static/captcha/16.jpeg
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/file-viewer/static/captcha/17.jpeg
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
src/file-viewer/static/captcha/2.jpeg
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
src/file-viewer/static/captcha/3.jpeg
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/file-viewer/static/captcha/4.jpeg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/file-viewer/static/captcha/5.jpeg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/file-viewer/static/captcha/6.jpeg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/file-viewer/static/captcha/7.jpeg
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
src/file-viewer/static/captcha/8.jpeg
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/file-viewer/static/captcha/9.jpeg
Normal file
After Width: | Height: | Size: 8.2 KiB |
12
src/file-viewer/views/canvas.astro
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
import { useInlineScript } from "../framework/page-resources.ts";
|
||||
|
||||
const { script } = Astro.props;
|
||||
useInlineScript('canvas_' + script as any);
|
||||
useInlineScript('canvas_demo');
|
||||
---
|
||||
<canvas id="canvas"
|
||||
data-canvas={script}
|
||||
data-standalone="true"
|
||||
style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;"
|
||||
></canvas>
|
10
src/file-viewer/views/canvas.client.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
const canvas = document.querySelector("canvas");
|
||||
const id = canvas?.getAttribute("data-canvas");
|
||||
if (!id) {
|
||||
throw new Error("No canvas id found");
|
||||
}
|
||||
const func = (window as any)["canvas_" + id];
|
||||
if (!func) {
|
||||
throw new Error("No canvas function found");
|
||||
}
|
||||
func(canvas, document.body);
|
685
src/file-viewer/views/clofi.client.ts
Normal file
|
@ -0,0 +1,685 @@
|
|||
const filesContainer = document.querySelector(".files")!;
|
||||
|
||||
// push the scrollbar to the end of the view.
|
||||
let distanceFromEnd = 0;
|
||||
let fakeScrollToEnd: number | null = null;
|
||||
filesContainer.scrollLeft = filesContainer.scrollWidth -
|
||||
filesContainer.clientWidth;
|
||||
window.addEventListener("resize", () => {
|
||||
if (fakeScrollToEnd) return;
|
||||
const { scrollWidth, clientWidth } = filesContainer;
|
||||
if (scrollWidth <= clientWidth) {
|
||||
distanceFromEnd = 0;
|
||||
} else {
|
||||
filesContainer.scrollLeft = scrollWidth - clientWidth - distanceFromEnd;
|
||||
}
|
||||
}, { passive: false });
|
||||
let lastScrollLeft = filesContainer.scrollLeft;
|
||||
filesContainer.addEventListener("scroll", (ev) => {
|
||||
const { scrollWidth, scrollLeft, clientWidth } = filesContainer;
|
||||
if (scrollWidth <= clientWidth) {
|
||||
distanceFromEnd = 0;
|
||||
} else {
|
||||
distanceFromEnd = scrollWidth - scrollLeft - clientWidth;
|
||||
if (fakeScrollToEnd && scrollLeft < lastScrollLeft) {
|
||||
cancelAnimationFrame(fakeScrollToEnd);
|
||||
fakeScrollToEnd = null;
|
||||
}
|
||||
}
|
||||
lastScrollLeft = scrollLeft;
|
||||
});
|
||||
const lerp = (a: number, b: number, t: number) => a + t * (b - a);
|
||||
function snapToEnd(initScrollStart: number) {
|
||||
if (fakeScrollToEnd) {
|
||||
cancelAnimationFrame(fakeScrollToEnd);
|
||||
}
|
||||
let lastTime = performance.now();
|
||||
let scrollStart = initScrollStart ?? filesContainer.scrollLeft;
|
||||
if (scrollStart >= filesContainer.scrollWidth) {
|
||||
return;
|
||||
}
|
||||
fakeScrollToEnd = requestAnimationFrame(function tick() {
|
||||
const now = performance.now();
|
||||
const dt = now - lastTime;
|
||||
lastTime = now;
|
||||
|
||||
const f = 1 - (0.98 ** dt);
|
||||
const { scrollWidth, clientWidth } = filesContainer;
|
||||
const target = Math.floor(scrollWidth - clientWidth);
|
||||
scrollStart = lerp(scrollStart, target, f);
|
||||
filesContainer.scrollLeft = scrollStart;
|
||||
if (Math.abs(scrollStart - target) < 0.2) {
|
||||
fakeScrollToEnd = null;
|
||||
} else {
|
||||
fakeScrollToEnd = requestAnimationFrame(tick);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
interface CacheEntry {
|
||||
html: string;
|
||||
expires: number;
|
||||
}
|
||||
// It is intentional that the existing page is NOT put into the cache. This is
|
||||
// just to avoid the differences between the partials and the full page
|
||||
// (subtle differences in activeFilename & isLast)
|
||||
let currentFile: string = location.pathname.replace(/^\/file/, "");
|
||||
let navigationId = 0;
|
||||
const cache = new Map<string, CacheEntry>();
|
||||
const prefetching = new Map<string, Promise<CacheEntry>>();
|
||||
const fetchLater: string[] = [];
|
||||
let hasCotyledonSpeedbump = false;
|
||||
|
||||
function prefetchEntry(
|
||||
filePath: string,
|
||||
lazy = false,
|
||||
): void | Promise<CacheEntry> {
|
||||
console.assert(filePath[0] === "/", "filePath must start with a /");
|
||||
const existingEntry = cache.get(filePath);
|
||||
if (existingEntry) {
|
||||
if (existingEntry.expires > Date.now()) {
|
||||
return;
|
||||
}
|
||||
cache.delete(filePath);
|
||||
}
|
||||
|
||||
const existingPromise = prefetching.get(filePath);
|
||||
if (existingPromise) return existingPromise;
|
||||
|
||||
// lazy prefetches should be limited
|
||||
if (lazy && prefetching.size > 2) {
|
||||
if (!fetchLater.includes(filePath)) {
|
||||
fetchLater.push(filePath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (filePath === "/cotyledon") {
|
||||
ensureCanvasReady("cotyledon");
|
||||
}
|
||||
|
||||
const promise = fetch(`/file${filePath}$partial`)
|
||||
.then((resp) => {
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(`Failed to fetch ${filePath}`);
|
||||
}
|
||||
return resp.text();
|
||||
})
|
||||
.then((html) => {
|
||||
const entry: CacheEntry = { html, expires: Date.now() + 1000 * 60 * 20 };
|
||||
cache.set(filePath, entry);
|
||||
prefetching.delete(filePath);
|
||||
|
||||
if (fetchLater.length > 0 && prefetching.size < 2) {
|
||||
const filePath = fetchLater.shift()!;
|
||||
prefetchEntry(filePath, false);
|
||||
}
|
||||
|
||||
return entry;
|
||||
});
|
||||
prefetching.set(filePath, promise);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
function fetchEntry(filePath: string): Promise<CacheEntry> {
|
||||
const pf = prefetchEntry(filePath);
|
||||
if (pf) return pf;
|
||||
return Promise.resolve(cache.get(filePath)!);
|
||||
}
|
||||
|
||||
type CanvasFn = (canvas: HTMLCanvasElement, panel: HTMLElement) => void;
|
||||
const fetchCanvas = new Map<string, Promise<CanvasFn>>();
|
||||
function ensureCanvasReady(id: string): Promise<CanvasFn> {
|
||||
let func = (globalThis as any)["canvas_" + id];
|
||||
if (func) return Promise.resolve(func);
|
||||
let promise = fetchCanvas.get(id);
|
||||
if (promise) return promise;
|
||||
let resolve: (c: CanvasFn) => void;
|
||||
promise = new Promise<CanvasFn>((r) => resolve = r);
|
||||
fetchCanvas.set(id, promise);
|
||||
const script = document.createElement("script");
|
||||
script.src = `/js/canvas/${id}.js`;
|
||||
script.async = true;
|
||||
script.onload = () => {
|
||||
func = (globalThis as any)["canvas_" + id];
|
||||
fetchCanvas.delete(id);
|
||||
if (func) {
|
||||
resolve(func);
|
||||
} else {
|
||||
console.error(`Error loading canvas script: ${id}`);
|
||||
}
|
||||
};
|
||||
script.onerror = () => {
|
||||
console.error(`Error loading canvas script: ${id}`);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
return promise;
|
||||
}
|
||||
|
||||
interface Tooltip {
|
||||
tooltip: HTMLElement;
|
||||
top: number;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const panels: Panel[] = [];
|
||||
|
||||
class Panel {
|
||||
index: number;
|
||||
panel: HTMLElement;
|
||||
content: HTMLElement;
|
||||
width: number;
|
||||
tooltips: Tooltip[] | null;
|
||||
linkFlags: number[] | null = null;
|
||||
basenames: string[] | null = null;
|
||||
unmountCanvas: (() => void) | null = null;
|
||||
|
||||
constructor(panel: HTMLElement, index: number) {
|
||||
console.assert(panel.classList.contains("panel"));
|
||||
this.panel = panel;
|
||||
this.index = index;
|
||||
this.content = panel.querySelector(".content.primary")!;
|
||||
|
||||
if (index === 0) {
|
||||
this.panel.classList.add("first");
|
||||
}
|
||||
|
||||
const canvas = panel.querySelector(
|
||||
"canvas[data-canvas]",
|
||||
) as HTMLCanvasElement;
|
||||
if (canvas) {
|
||||
canvas.width = canvas.clientWidth;
|
||||
canvas.height = canvas.clientHeight;
|
||||
requestAnimationFrame(() => {
|
||||
canvas.width = canvas.clientWidth;
|
||||
canvas.height = canvas.clientHeight;
|
||||
});
|
||||
const id = canvas.getAttribute("data-canvas")!;
|
||||
let cancelled = false;
|
||||
this.unmountCanvas = () => (cancelled = true);
|
||||
ensureCanvasReady(id).then((func) => {
|
||||
if (cancelled) return;
|
||||
this.unmountCanvas = func(canvas, panel) as any;
|
||||
});
|
||||
if (id === "cotyledon") {
|
||||
filesContainer.classList.add("ctld-sb");
|
||||
const group = document.querySelector(
|
||||
"[data-group='cotyledon']",
|
||||
)! as HTMLElement;
|
||||
if (group) {
|
||||
group.setAttribute("inert", "true");
|
||||
group.style.opacity = "0.5";
|
||||
group.style.pointerEvents = "none";
|
||||
hasCotyledonSpeedbump = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ul = panel.querySelector("ul");
|
||||
if (!ul) {
|
||||
this.width = 0;
|
||||
this.linkFlags = null;
|
||||
this.basenames = null;
|
||||
this.tooltips = null;
|
||||
return;
|
||||
}
|
||||
this.width = ul.offsetWidth;
|
||||
const links = panel.querySelectorAll("ul > li > a.li");
|
||||
|
||||
this.content.setAttribute("data-clover", `${index}`);
|
||||
|
||||
this.tooltips = [];
|
||||
this.linkFlags = new Array(links.length).fill(0);
|
||||
const basenames = this.basenames = new Array(links.length);
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
const link = links[i] as HTMLAnchorElement;
|
||||
link.setAttribute("data-clover", `${index};${i}`);
|
||||
link.addEventListener("mouseenter", onLinkMouseEnter);
|
||||
basenames[i] = link.classList.contains("readme")
|
||||
? "readme.txt"
|
||||
: link.getAttribute("href")!.split("/").pop()!;
|
||||
}
|
||||
}
|
||||
|
||||
update(newActiveFile: string) {
|
||||
console.assert(newActiveFile);
|
||||
const p = this.panel;
|
||||
p.querySelector(".li.active")?.classList.remove("active");
|
||||
const basenames = this.basenames;
|
||||
if (!basenames) return;
|
||||
const ul = p.querySelector("ul")!;
|
||||
this.width = ul.offsetWidth;
|
||||
console.assert(!newActiveFile.includes("/"));
|
||||
if (hasCotyledonSpeedbump) {
|
||||
newActiveFile = "__";
|
||||
}
|
||||
|
||||
const linkIndex = basenames.indexOf(newActiveFile);
|
||||
if (linkIndex === -1) return;
|
||||
const link = p.querySelector(`[data-clover="${this.index};${linkIndex}"]`)!;
|
||||
link.classList.add("active");
|
||||
|
||||
const newActiveTooltip = this.tooltips!.findIndex((t) =>
|
||||
t.index === linkIndex
|
||||
);
|
||||
for (let i = 0; i < this.tooltips!.length; i++) {
|
||||
const { tooltip } = this.tooltips![i];
|
||||
tooltip.classList[i === newActiveTooltip ? "add" : "remove"]("active");
|
||||
}
|
||||
}
|
||||
|
||||
hideReadme() {
|
||||
this.panel.classList.remove("last");
|
||||
const lastHsplit = this.panel.querySelector(".hsplit")!;
|
||||
if (lastHsplit) {
|
||||
lastHsplit.className = "hsplit-hidden";
|
||||
const previousReadme = this.panel.querySelector(
|
||||
".content.readme",
|
||||
)! as HTMLElement;
|
||||
console.assert(previousReadme, "No readme found");
|
||||
previousReadme.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
showReadme() {
|
||||
this.panel.classList.add("last");
|
||||
const hsplit = this.panel.querySelector(".hsplit-hidden")!;
|
||||
if (hsplit) {
|
||||
hsplit.className = "hsplit";
|
||||
const previousReadme = this.panel.querySelector(
|
||||
".content.readme",
|
||||
)! as HTMLElement;
|
||||
console.assert(previousReadme, "No readme found");
|
||||
previousReadme.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.unmountCanvas) {
|
||||
this.unmountCanvas();
|
||||
}
|
||||
this.panel.querySelectorAll("audio,video").forEach((el) =>
|
||||
(el as HTMLVideoElement | HTMLAudioElement).pause()
|
||||
);
|
||||
this.panel.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function onContentScrollForTooltip(ev: Event) {
|
||||
const content = ev.target as HTMLElement;
|
||||
const panelIndex = parseInt(content.getAttribute("data-clover")!);
|
||||
const panel = panels[panelIndex];
|
||||
const scrollTop = content.scrollTop;
|
||||
for (const tooltip of panel.tooltips!) {
|
||||
tooltip.tooltip.style.transform = tooltipTransform(tooltip.top, scrollTop);
|
||||
}
|
||||
}
|
||||
|
||||
function tooltipTransform(offsetTop: number, scrollTop: number) {
|
||||
return `translateY(${offsetTop - scrollTop}px)`;
|
||||
}
|
||||
|
||||
let activeTooltip: HTMLElement | null = null;
|
||||
let activeTooltipCancel: (() => void) | null = null;
|
||||
|
||||
function onLinkMouseEnter(e: MouseEvent) {
|
||||
const link = e.target as HTMLAnchorElement;
|
||||
console.assert(link.classList.contains("li"));
|
||||
const attr = link.getAttribute("data-clover")!;
|
||||
console.assert(attr && attr.match(/^\d+;\d+$/));
|
||||
const [panelIndex, linkIndex] = attr.split(";").map(Number);
|
||||
const panel = panels[panelIndex];
|
||||
console.assert(panel, `panel${panelIndex}`);
|
||||
const linkWidths = panel.linkFlags;
|
||||
let filePath: string | null = null;
|
||||
if (linkWidths![linkIndex] == 0) {
|
||||
// filter only links that truncate their text
|
||||
// insane discovery: while this is recommended online, it doesn't
|
||||
// account for the `...` itself, meaning when just the file size
|
||||
// is truncated, a tooltip won't be available.
|
||||
// > if (link.scrollWidth <= link.offsetWidth) continue;
|
||||
const lastChild = link.lastElementChild! as HTMLElement;
|
||||
linkWidths![linkIndex] = lastChild?.offsetLeft !== undefined
|
||||
? (lastChild.offsetLeft + lastChild.offsetWidth) ^ 0
|
||||
: 1;
|
||||
|
||||
const href = (link as HTMLAnchorElement).getAttribute("href") ?? null;
|
||||
filePath = href?.startsWith("/file") ? href.slice(5) || "/" : null;
|
||||
}
|
||||
if (filePath) {
|
||||
prefetchEntry(filePath, true);
|
||||
}
|
||||
if (linkWidths![linkIndex] > panel.width) {
|
||||
if (activeTooltipCancel) {
|
||||
activeTooltipCancel();
|
||||
activeTooltipCancel = null;
|
||||
}
|
||||
maybeBuildTooltipUi(link, linkIndex, panel);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelOnMouseLeave() {
|
||||
activeTooltipCancel!();
|
||||
activeTooltipCancel = null;
|
||||
}
|
||||
|
||||
function maybeBuildTooltipUi(
|
||||
link: HTMLAnchorElement,
|
||||
linkIndex: number,
|
||||
panel: Panel,
|
||||
) {
|
||||
if (activeTooltip) {
|
||||
activeTooltip.remove();
|
||||
buildTooltipUi(link, linkIndex, panel, false);
|
||||
} else {
|
||||
link.addEventListener("mouseleave", cancelOnMouseLeave);
|
||||
const timer = setTimeout(() => {
|
||||
activeTooltipCancel = null;
|
||||
link.removeEventListener("mouseleave", cancelOnMouseLeave);
|
||||
buildTooltipUi(link, linkIndex, panel, true);
|
||||
}, 150);
|
||||
activeTooltipCancel = () => {
|
||||
clearTimeout(timer);
|
||||
link.removeEventListener("mouseleave", cancelOnMouseLeave);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function buildTooltipUi(
|
||||
link: HTMLAnchorElement,
|
||||
linkIndex: number,
|
||||
panel: Panel,
|
||||
animateIn: boolean,
|
||||
) {
|
||||
const tooltip = activeTooltip = document.createElement("div");
|
||||
tooltip.classList.add("tooltip");
|
||||
if (link.classList.contains("active")) {
|
||||
tooltip.classList.add("active");
|
||||
}
|
||||
if (animateIn) {
|
||||
tooltip.style.animation = "fadeIn .1s ease-out forwards";
|
||||
}
|
||||
tooltip.innerHTML = link.innerHTML;
|
||||
const top = link.parentElement!.offsetTop;
|
||||
tooltip.style.transform = tooltipTransform(top, panel.content.scrollTop);
|
||||
panel.panel.appendChild(tooltip);
|
||||
panel.tooltips!.push({ tooltip, top, index: linkIndex });
|
||||
if (panel.tooltips!.length === 1) {
|
||||
panel.content.addEventListener("scroll", onContentScrollForTooltip);
|
||||
}
|
||||
|
||||
link.addEventListener("mouseleave", (e) => {
|
||||
tooltip.style.animation = "fadeIn .3s .2s ease reverse forwards";
|
||||
const timer = setTimeout(() => {
|
||||
activeTooltipCancel = null;
|
||||
tooltip.remove();
|
||||
activeTooltip = null;
|
||||
|
||||
const tt = panel.tooltips = panel.tooltips!.filter((t) =>
|
||||
t.tooltip !== tooltip
|
||||
);
|
||||
if (tt.length === 0) {
|
||||
panel.content.removeEventListener("scroll", onContentScrollForTooltip);
|
||||
}
|
||||
}, 500);
|
||||
activeTooltipCancel = () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
function addPanel(panel: HTMLElement, activeFile?: string) {
|
||||
const p = new Panel(panel, panels.length);
|
||||
panels.push(p);
|
||||
if (activeFile) {
|
||||
p.update(activeFile);
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeInterceptClick(event: MouseEvent, element: HTMLElement) {
|
||||
const href = (element as HTMLAnchorElement).href;
|
||||
const url = new URL(href, window.location.origin);
|
||||
if (maybeNavigate(url, true)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
type URLLike = Pick<URL, "pathname" | "origin" | "search">;
|
||||
function maybeNavigate(url: URLLike, pushState: boolean) {
|
||||
if (!url.pathname.startsWith("/file")) return false;
|
||||
if (url.origin !== location.origin) return false;
|
||||
if (url.search !== "") return false;
|
||||
navigate(url.pathname, pushState);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function navigate(pathname: string, pushState: boolean) {
|
||||
const filePath = pathname.slice(5) || "/";
|
||||
|
||||
const currentNavigationId = ++navigationId;
|
||||
|
||||
if (filePath === currentFile) return;
|
||||
|
||||
const currentSplit = splitSlashes(currentFile);
|
||||
const filePathSplit = splitSlashes(filePath);
|
||||
|
||||
// Find the first index where the currentSplit and filePathSplit differ, then
|
||||
// add all the paths after that to panelsToFetch
|
||||
let deleteCount = Math.max(currentSplit.length - filePathSplit.length, 0);
|
||||
let appendPanels = [];
|
||||
for (let i = -1; i < filePathSplit.length; i++) {
|
||||
if (currentSplit[i] !== filePathSplit[i]) {
|
||||
deleteCount = currentSplit.length - i;
|
||||
appendPanels = [];
|
||||
for (let j = i; j < filePathSplit.length; j++) {
|
||||
appendPanels.push("/" + filePathSplit.slice(0, j + 1).join("/"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Before fetching, prepare to mark the panels as loading
|
||||
const loadingPanels = new Set<HTMLElement>();
|
||||
{
|
||||
let lastPanel = filesContainer.lastElementChild;
|
||||
let toDelete = deleteCount;
|
||||
while (lastPanel && toDelete > 0) {
|
||||
lastPanel.querySelectorAll(".content").forEach((content) => {
|
||||
loadingPanels.add(content as HTMLElement);
|
||||
});
|
||||
lastPanel = lastPanel.previousElementSibling;
|
||||
toDelete--;
|
||||
}
|
||||
if (deleteCount == 0) {
|
||||
const last = filesContainer.lastElementChild!;
|
||||
console.assert(last, "Last panel is not a panel");
|
||||
|
||||
const readme = last.querySelector(".content.readme")!;
|
||||
if (readme) {
|
||||
loadingPanels.add(readme as HTMLElement);
|
||||
}
|
||||
}
|
||||
const folderWithReadme = panels[panels.length - deleteCount - 1];
|
||||
const readmes = folderWithReadme.panel.querySelectorAll(".readme");
|
||||
if (readmes.length === 1) {
|
||||
deleteCount += 1;
|
||||
appendPanels.unshift(
|
||||
"/" + currentSplit.slice(0, panels.length - deleteCount).join("/"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.assert(
|
||||
deleteCount > 0 || appendPanels.length > 0,
|
||||
"No difference found",
|
||||
);
|
||||
let timer = loadingPanels.size > 0
|
||||
? setTimeout(() => {
|
||||
if (navigationId !== currentNavigationId) {
|
||||
return; // cancelled
|
||||
}
|
||||
document.querySelectorAll(".loading")
|
||||
.forEach((thing) => thing.classList.remove("loading"));
|
||||
for (const panel of loadingPanels) {
|
||||
panel.classList.add("loading");
|
||||
}
|
||||
timer = null;
|
||||
}, 100)
|
||||
: null;
|
||||
|
||||
// Fetch the data
|
||||
let appendEntries;
|
||||
try {
|
||||
appendEntries = await Promise.all(appendPanels.map(fetchEntry));
|
||||
} catch (e) {
|
||||
console.error("error", e);
|
||||
if (navigationId === currentNavigationId) {
|
||||
console.error(e);
|
||||
location.href = "/file" + (filePath.length > 1 ? filePath : "");
|
||||
}
|
||||
return; // cancelled
|
||||
}
|
||||
if (navigationId !== currentNavigationId) {
|
||||
return; // cancelled
|
||||
}
|
||||
if (timer) clearTimeout(timer);
|
||||
else {for (const panel of loadingPanels) {
|
||||
panel.classList.remove("loading");
|
||||
}}
|
||||
currentFile = filePath;
|
||||
|
||||
if (pushState) {
|
||||
history.pushState(null, "", `/file${filePath.length > 1 ? filePath : ""}`);
|
||||
}
|
||||
|
||||
const startScrollleft = filesContainer.scrollLeft;
|
||||
|
||||
if (currentSplit[0] !== filePathSplit[0]) {
|
||||
if (currentSplit[0] === "cotyledon") {
|
||||
filesContainer.classList.remove("ctld-et", "ctld-sb");
|
||||
}
|
||||
if (parseInt(currentSplit[0]) < 2025) {
|
||||
filesContainer.classList.remove("ctld", "ctld-" + currentSplit[0]);
|
||||
}
|
||||
if (parseInt(filePathSplit[0]) < 2025) {
|
||||
filesContainer.classList.add("ctld", "ctld-" + filePathSplit[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Make the last panel into a regular panel
|
||||
panels[panels.length - 1].hideReadme();
|
||||
|
||||
// Delete the panels that are no longer needed
|
||||
for (let i = 0; i < deleteCount; i++) {
|
||||
const panel = panels.pop();
|
||||
console.assert(panel, "No panel found");
|
||||
if (panel) {
|
||||
panel.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Update the last panel
|
||||
const currentFileSplit = splitSlashes(currentFile);
|
||||
const activeFile = currentFileSplit[panels.length - 1];
|
||||
panels[panels.length - 1]?.update(activeFile ?? "readme.txt");
|
||||
|
||||
// Insert the new panels
|
||||
if (appendEntries.length > 0) {
|
||||
let lastNewPanel: HTMLElement | null = null;
|
||||
for (const entry of appendEntries) {
|
||||
const panel = document.createElement("div");
|
||||
panel.classList.add("panel");
|
||||
if (panels.length >= 2) {
|
||||
panel.classList.add("fade-slide-in");
|
||||
}
|
||||
panel.innerHTML = entry.html;
|
||||
filesContainer.appendChild(panel);
|
||||
lastNewPanel = panel;
|
||||
const current = currentFileSplit[panels.length];
|
||||
addPanel(panel, current ?? "readme.txt");
|
||||
if (current) {
|
||||
panels[panels.length - 1].hideReadme();
|
||||
}
|
||||
}
|
||||
console.assert(lastNewPanel, "No last new panel found");
|
||||
lastNewPanel!.classList.add("last");
|
||||
|
||||
// Automatically play videos
|
||||
const video = lastNewPanel!.querySelector("video") ||
|
||||
lastNewPanel!.querySelector("audio");
|
||||
if (video) {
|
||||
const timer = setTimeout(() => {
|
||||
video.play();
|
||||
}, 50);
|
||||
video.play().then(() => {
|
||||
clearTimeout(timer);
|
||||
}, () => {});
|
||||
}
|
||||
} else {
|
||||
// Make the last panel the .last panel
|
||||
const lastPanel = filesContainer.lastElementChild!;
|
||||
console.assert(lastPanel, "No last panel found");
|
||||
lastPanel.classList.add("last");
|
||||
panels[panels.length - 1].showReadme();
|
||||
}
|
||||
|
||||
updateWidths();
|
||||
filesContainer.scrollLeft = startScrollleft;
|
||||
requestAnimationFrame(() => {
|
||||
updateWidths();
|
||||
filesContainer.scrollLeft = startScrollleft;
|
||||
snapToEnd(startScrollleft);
|
||||
});
|
||||
}
|
||||
|
||||
function updateWidths() {
|
||||
for (const panel of panels.slice(-2)) {
|
||||
const ul = panel.panel.querySelector("ul")!;
|
||||
if (ul) {
|
||||
panel.width = ul.offsetWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function splitSlashes(path: string) {
|
||||
if (path.length <= 1) return [];
|
||||
return path.slice(1).split("/");
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
document.querySelectorAll(".panel").forEach((panel) =>
|
||||
addPanel(panel as HTMLElement)
|
||||
);
|
||||
(document.querySelector(".files")! as HTMLElement).addEventListener(
|
||||
"click",
|
||||
(event, element = event.target as HTMLAnchorElement) => {
|
||||
if (
|
||||
!(event.button ||
|
||||
event.which != 1 ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
event.shiftKey ||
|
||||
event.altKey ||
|
||||
event.defaultPrevented)
|
||||
) {
|
||||
while (element && element !== document.body) {
|
||||
if (
|
||||
element.nodeName.toUpperCase() === "A"
|
||||
) {
|
||||
maybeInterceptClick(event, element);
|
||||
return;
|
||||
}
|
||||
element =
|
||||
(element.assignedSlot ?? element.parentNode) as HTMLAnchorElement;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", (event) => {
|
||||
if (!maybeNavigate(window.location, false)) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
798
src/file-viewer/views/clofi.css
Normal file
|
@ -0,0 +1,798 @@
|
|||
html, body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
--dark-bg: #27143f;
|
||||
--bar: #804fc9;
|
||||
}
|
||||
@font-face {
|
||||
font-family: c1;
|
||||
src: url(/cydn_header.woff2);
|
||||
font-variation-settings: "wght" 50;
|
||||
}
|
||||
body {
|
||||
display: grid;
|
||||
background-color: var(--dark-bg);
|
||||
}
|
||||
.files {
|
||||
background-color: var(--bg);
|
||||
--scroll-bg: var(--dark-bg);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
min-height: 100vh;
|
||||
border-bottom: 0;
|
||||
box-sizing: border-box;
|
||||
--tooltip: lch(from var(--bg) calc(l - 2) c h);
|
||||
--muted: lch(from var(--dark-bg) 60 20 h / 0.8);
|
||||
z-index: 1;
|
||||
}
|
||||
.ctld {
|
||||
--c: 4.5;
|
||||
--bg: lch(13 var(--c) var(--hue));
|
||||
--dark-bg: lch(10 var(--c) var(--hue));
|
||||
--bar: lch(90 96 var(--hue));
|
||||
--primary: lch(90 96 var(--hue));
|
||||
}
|
||||
.ctld-2017 {
|
||||
--hue: 120;
|
||||
}
|
||||
.ctld-2018 {
|
||||
--hue: 40;
|
||||
}
|
||||
.ctld-2019 {
|
||||
--hue: 290;
|
||||
--c: 10;
|
||||
}
|
||||
.ctld-2020 {
|
||||
--hue: 220;
|
||||
--c: 10;
|
||||
}
|
||||
.ctld-2021 {
|
||||
--hue: 70;
|
||||
--c: 20;
|
||||
}
|
||||
.ctld-2022 {
|
||||
--hue: 150;
|
||||
--c: 20;
|
||||
}
|
||||
.ctld-2023 {
|
||||
--hue: 5;
|
||||
--c: 10;
|
||||
}
|
||||
.ctld-et {
|
||||
--hue: 150;
|
||||
--c: 0;
|
||||
}
|
||||
.ctld-2024, .ctld-sb {
|
||||
--bg: #191919;
|
||||
--dark-bg: #101010;
|
||||
--fg: #a7a7a7;
|
||||
--bar: white;
|
||||
--primary: white;
|
||||
.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.ctld-2024 {
|
||||
--dark-bg: #00000080;
|
||||
--scroll-bg: #0000004d;
|
||||
.content:not(.file-view-image):not(.file-view-video):not(.file-view-audio) {
|
||||
background-color: #00000050;
|
||||
}
|
||||
}
|
||||
.ctld-2024 .convo {
|
||||
--primary: #ff6c5c;
|
||||
}
|
||||
.files::-webkit-scrollbar {
|
||||
height: 24px;
|
||||
background-color: transparent;
|
||||
}
|
||||
.files::-webkit-scrollbar-thumb {
|
||||
background-color: var(--bar);
|
||||
border: 6px solid var(--dark-bg);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.files::-webkit-scrollbar-track {
|
||||
background-color: var(--dark-bg);
|
||||
}
|
||||
.files .panel {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
min-width: 235px;
|
||||
max-width: 235px;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 59px); /* safari hack */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.files .panel div.header {
|
||||
display: none;
|
||||
}
|
||||
.files .panel .header {
|
||||
white-space: nowrap;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
background-color: var(--dark-bg);
|
||||
padding: 0.5rem 0.5rem 0.5rem 0.25rem;
|
||||
color: var(--primary);
|
||||
--color: var(--primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
height: 19px;
|
||||
margin-right: 4px;
|
||||
& > * {
|
||||
position: relative;
|
||||
width: 22px;
|
||||
display: flex;
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.files .panel .content {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
a.header {
|
||||
display: flex;
|
||||
}
|
||||
.files .panel.last {
|
||||
min-width: min(calc(100vw - 255px), calc(90% - 20rem));
|
||||
max-width: none;
|
||||
div.header {
|
||||
display: flex;
|
||||
}
|
||||
a.header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.files .panel.first:not(.last) {
|
||||
max-width: 216px;
|
||||
min-width: 216px;
|
||||
}
|
||||
.files .panel.first .content:not(.readme) {
|
||||
max-width: 200px;
|
||||
min-width: 200px;
|
||||
}
|
||||
.panel:first-child {
|
||||
border-left: 16px solid var(--dark-bg);
|
||||
}
|
||||
.panel:last-child {
|
||||
flex: 1;
|
||||
}
|
||||
.files .panel .hsplit {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
height: calc(100vh - 59px);
|
||||
.content {
|
||||
min-width: 235px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.content.readme {
|
||||
min-width: 300px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
height: calc(100vh - 59px);
|
||||
}
|
||||
.files .panel ul {
|
||||
white-space: nowrap;
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&, li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
li a {
|
||||
&:active {
|
||||
--muted: red !important;
|
||||
}
|
||||
position: relative;
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.5rem;
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
& > * {
|
||||
color: var(--fg);
|
||||
font-size: 1rem;
|
||||
}
|
||||
&:hover:not(.active) {
|
||||
background-color: rgb(from var(--dark-bg) r g b / 0.5);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark-bg) !important;
|
||||
color: red;
|
||||
date {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.li, .tooltip {
|
||||
padding: 0.5rem 0.2rem 0.5rem 0.2rem;
|
||||
color: var(--fg);
|
||||
&.active {
|
||||
background-color: var(--dark-bg);
|
||||
color: var(--primary);
|
||||
--color: var(--primary);
|
||||
--muted: var(--primary);
|
||||
}
|
||||
date {
|
||||
color: var(--muted);
|
||||
letter-spacing: -0.08em;
|
||||
padding-right: 0.3rem;
|
||||
transform: scaleX(0.95);
|
||||
display: inline-block;
|
||||
&.inline {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
pre {
|
||||
padding: 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.files .panel ul a.readme {
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
align-items: center;
|
||||
color: #91fff2;
|
||||
&.active {
|
||||
--muted: #91fff2;
|
||||
}
|
||||
.line {
|
||||
display: block;
|
||||
width: 58px;
|
||||
margin-right: 8px;
|
||||
margin-left: 2px;
|
||||
height: 2px;
|
||||
background-color: var(--muted);
|
||||
}
|
||||
}
|
||||
.panel .content::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
.panel .content::-webkit-scrollbar-thumb {
|
||||
background-color: var(--bar);
|
||||
border: 2px solid var(--dark-bg);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.panel .content::-webkit-scrollbar-track {
|
||||
background-color: var(--scroll-bg);
|
||||
}
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 35px;
|
||||
background-color: var(--tooltip);
|
||||
pointer-events: none;
|
||||
padding-right: 1rem;
|
||||
border-bottom-right-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
white-space: nowrap;
|
||||
z-index: 100;
|
||||
}
|
||||
h3 {
|
||||
padding-left: 0.25rem;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 2px;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 1px;
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.file-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.bold-slash {
|
||||
font-weight: bold;
|
||||
text-shadow: 1px 0 0 var(--color);
|
||||
}
|
||||
.size {
|
||||
display: inline-block;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
padding-left: 0.4rem;
|
||||
}
|
||||
.first .size {
|
||||
display: inline-block;
|
||||
font-size: 0.7rem;
|
||||
padding-left: 0.2rem;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
.mobile-back {
|
||||
display: none;
|
||||
}
|
||||
.actions .download::before {
|
||||
content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIiB3aWR0aD0iMjQiIGZpbGw9IiNmZmZmZmYiPjxwYXRoIGQ9Ik00ODAtMzIwIDI4MC01MjBsNTYtNTggMTA0IDEwNHYtMzI2aDgwdjMyNmwxMDQtMTA0IDU2IDU4LTIwMCAyMDBaTTI0MC0xNjBxLTMzIDAtNTYuNS0yMy41VDE2MC0yNDB2LTEyMGg4MHYxMjBoNDgwdi0xMjBoODB2MTIwcTAgMzMtMjMuNSA1Ni41VDcyMC0xNjBIMjQwWiIvPjwvc3ZnPg==);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
.actions .full-screen::before {
|
||||
content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMTIwLTEyMHYtMzIwaDgwdjE4NGw1MDQtNTA0SDUyMHYtODBoMzIwdjMyMGgtODB2LTE4NEwyNTYtMjAwaDE4NHY4MEgxMjBaIi8+PC9zdmc+);
|
||||
}
|
||||
.ico {
|
||||
display: inline-block;
|
||||
height: 19px;
|
||||
position: relative;
|
||||
width: 19px;
|
||||
margin-right: 2px;
|
||||
&::before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
transform: translate(-2px, 3px);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: var(--muted);
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMjU5LjcyLTY0cS00MC40NSAwLTY5LjA4LTI4Ljc5UTE2Mi0xMjEuNTggMTYyLTE2MnYtNjM2cTAtNDAuNDIgMjguNzktNjkuMjFRMjE5LjU3LTg5NiAyNjAtODk2aDMwNGwyMzQgMjMzdjUwMXEwIDQwLjQyLTI4LjggNjkuMjFRNzQwLjQtNjQgNjk5Ljk2LTY0SDI1OS43MlpNNTA0LTYwMmgxOTZMNTA0LTc5OHYxOTZaIi8+PC9zdmc+);
|
||||
}
|
||||
}
|
||||
.header .ico::before {
|
||||
transform: translate(-2px, 0);
|
||||
}
|
||||
/* file icons alter the mask image. 24x24 */
|
||||
.ico-dir::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMTYzLjA0LTEyMC4wOXEtNTEuMyAwLTg3LjEyLTM1LjgzLTM1LjgzLTM1LjgyLTM1LjgzLTg3LjEydi00NzMuOTJxMC01MS4zIDM1LjgzLTg3LjEyIDM1LjgyLTM1LjgzIDg3LjEyLTM1LjgzaDIxMS41M0w0ODAtNzM0LjQ4aDMxNi45NnE1MS4zIDAgODcuMTIgMzUuODMgMzUuODMgMzUuODMgMzUuODMgODcuMTN2MzY4LjQ4cTAgNTEuMy0zNS44MyA4Ny4xMi0zNS44MiAzNS44My04Ny4xMiAzNS44M0gxNjMuMDRabTAtMTIyLjk1aDYzMy45MnYtMzY4LjQ4SDQyOS4yMkwzMjMuNzgtNzE2Ljk2SDE2My4wNHY0NzMuOTJabTAgMHYtNDczLjkyIDQ3My45MloiLz48L3N2Zz4=);
|
||||
}
|
||||
.active .ico-dir::before, .ico-dir-open::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMTU3LjgzLTEyNC42NXEtNDYuOTMgMC04MC4wNi0zMy4xMi0zMy4xMi0zMy4xMy0zMy4xMi04MC4wNnYtNDg0LjM0cTAtNDcuMTcgMzMuMTItODAuMTggMzMuMTMtMzMgODAuMDYtMzNoMjE2LjQxTDQ4MC03MjkuNTloMzIyLjE3cTQ3LjE3IDAgODAuMTggMzMuMTMgMzMgMzMuMTIgMzMgODAuMDVINDMzLjEzTDMyNy4zNy03MjIuMTdIMTU3LjgzdjQ3NC40NWw4NS40Ny0yODguNjloNzExLjcybC05MS4yOCAzMDYuMTVxLTE2LjY3IDU3LjMtNTAuOTIgODEuNDYtMzQuMjUgMjQuMTUtOTkuMDggMjQuMTVIMTU3LjgzWm0xMTUuODQtMTEzLjE4aDQ3My45NGw1NC42NS0xODUuNDFIMzI4LjA5bC01NC40MiAxODUuNDFabTAgMCA1NC40Mi0xODUuNDEtNTQuNDIgMTg1LjQxWk0xNTcuODMtNjE2LjQxdi0xMDUuNzYgMTA1Ljc2WiIvPjwvc3ZnPg==);
|
||||
}
|
||||
.ico-webpage::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNDgwLTY0cS04NiAwLTE2Mi0zMi41dC0xMzIuNS04OVExMjktMjQyIDk2LjUtMzE4VDY0LTQ4MHEwLTg2IDMyLjUtMTYydDg5LTEzMi41UTI0Mi04MzEgMzE4LTg2My41VDQ4MC04OTZxODYgMCAxNjIgMzIuNXQxMzIuNSA4OVE4MzEtNzE4IDg2My41LTY0MlQ4OTYtNDgwcTAgODYtMzIuNSAxNjJ0LTg5IDEzMi41UTcxOC0xMjkgNjQyLTk2LjVUNDgwLTY0Wm0wLTEwMnExNS0xNiAzMS02M3QyNS05OUg0MjRxOSA1MiAyNSA5OXQzMSA2M1ptLTEwMS0xMnEtMTMtMjctMjIuNS02NVQzNDAtMzI4SDIwMXEyNyA1NCA3NSA5My41VDM3OS0xNzhabTIwMiAwcTU1LTE3IDEwMy01Ni41dDc1LTkzLjVINjIwcS03IDQ3LTE2LjUgODVUNTgxLTE3OFpNMTcwLTQxMGgxNTlxLTEtMTctMS41LTM1LjVUMzI3LTQ4MnEwLTE4IC41LTM1LjVUMzI5LTU1MEgxNzBxLTUgMTctNi41IDM0LjVUMTYyLTQ4MHEwIDE4IDEuNSAzNS41VDE3MC00MTBabTI0MyAwaDEzNHEyLTE4IDIuNS0zNS41dC41LTM0LjVxMC0xNy0uNS0zNXQtMi41LTM1SDQxM3EtMiAxNy0yLjUgMzV0LS41IDM1cTAgMTcgLjUgMzV0Mi41IDM1Wm0yMTggMGgxNTlxNS0xNyA2LjUtMzQuNVQ3OTgtNDgwcTAtMTgtMS41LTM2dC02LjUtMzRINjMxcTEgMTcgMS41IDM1LjV0LjUgMzYuNXEwIDE4LS41IDM1LjVUNjMxLTQxMFptLTExLTIyMmgxMzlxLTI3LTU0LTc1LTkzLjVUNTgxLTc4MnExMyAyNyAyMi41IDY1dDE2LjUgODVabS0xOTYgMGgxMTJxLTktNTItMjUtOTguNVQ0ODAtNzk5cS0xNSAyMi0zMSA2OC41VDQyNC02MzJabS0yMjMgMGgxMzlxNy00NyAxNi41LTg1dDIyLjUtNjVxLTU1IDE3LTEwMyA1Ni41VDIwMS02MzJaIi8+PC9zdmc+);
|
||||
}
|
||||
.ico-image::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMjEyLTc2cS01Ny4xMiAwLTk2LjU2LTM5LjQ0UTc2LTE1NC44OCA3Ni0yMTJ2LTUzNnEwLTU3LjEzIDM5LjQ0LTk2LjU2UTE1NC44OC04ODQgMjEyLTg4NGg1MzZxNTcuMTMgMCA5Ni41NiAzOS40NFE4ODQtODA1LjEzIDg4NC03NDh2NTM2cTAgNTcuMTItMzkuNDQgOTYuNTZRODA1LjEzLTc2IDc0OC03NkgyMTJabTQtMTg2aDUyOEw1NzEtNTAyIDQ1MC0zNDFsLTkxLTEyMS0xNDMgMjAwWm0xMjQuMjQtMjcwcTM3LjE4IDAgNjIuNDctMjUuNTNRNDI4LTU4My4wNSA0MjgtNjIwLjI0cTAtMzcuMTgtMjUuNTMtNjIuNDdRMzc2Ljk1LTcwOCAzMzkuNzYtNzA4cS0zNy4xOCAwLTYyLjQ3IDI1LjUzUTI1Mi02NTYuOTUgMjUyLTYxOS43NnEwIDM3LjE4IDI1LjUzIDYyLjQ3UTMwMy4wNS01MzIgMzQwLjI0LTUzMloiLz48L3N2Zz4=);
|
||||
}
|
||||
.ico-video::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMTk4LTE2MnEtMzkuNzMgMC02OC44Ni0yOS4xNFExMDAtMjIwLjI3IDEwMC0yNjB2LTQ0MHEwLTQwLjQyIDI5LjE0LTY5LjIxUTE1OC4yNy03OTggMTk4LTc5OGg0NDBxNDAuNDIgMCA2OS4yMSAyOC43OVE3MzYtNzQwLjQyIDczNi03MDB2MTU0bDE3Mi0xNzJ2NDc2TDczNi00MTR2MTU0cTAgMzkuNzMtMjguNzkgNjguODZRNjc4LjQyLTE2MiA2MzgtMTYySDE5OFoiLz48L3N2Zz4=);
|
||||
}
|
||||
.ico-blend::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiNmZmYiIHZpZXdCb3g9IjAgMCAyMCAyMCI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNNi4zNiAxNC4xYy0uMTItLjM5LS4yLS45LS4yLTEuMTZsLS4wMi0uMDFjMC0uMjYuMDUtLjcuMDUtLjdsLTMuOSAzLjIxYy0uNy41Ni0xLjYxLjU1LTIuMDQgMC0uNDMtLjU0LS4yMS0xLjQuNDYtMS45MmwuMDMtLjAyIDcuMTktNS42LTQuMDIuMDFjLS42OCAwLTEuMTMtLjQ2LTEtMS4wMS4xMy0uNTUuNzYtLjk4IDEuNDEtLjk4aDcuOTRzLS42OC0xLjAzLTEuMzItMS41Yy0uNDctLjM1LTEuMi0uNzgtMS4yLS43OHMtLjQ2LS4xNy0xLjI4LS42OGMtLjgyLS41MS0uMi0xLjcuNTktMS45LjUgMCAxLjM4LjQzIDIuMjEuODMgMi44NCAxLjQgNS43IDQuMiA2LjMgNC44Mi41OS42MiAxLjEzIDEuMjUgMS41NCAxLjlhNS45IDUuOSAwIDAgMS0uNyA3LjE3IDcuNyA3LjcgMCAwIDEtMi4zIDEuOTUgNy40MyA3LjQzIDAgMCAxLTYuMS4wMWMtLjktLjQtMi4yMi0xLjU4LTIuMjItMS41OHMtMS4xNS0xLjIyLTEuNDItMi4wNVptNi44MiAxLjg4YzIuMyAwIDQuMTYtMS44IDQuMTYtNCAwLTIuMjEtMS44Ni00LTQuMTYtNFM5IDkuNzcgOSAxMS45OGMwIDIuMiAxLjg3IDQgNC4xNyA0WiIgY2xpcC1ydWxlPSJldmVub2RkIi8+PGVsbGlwc2UgY3g9IjEzLjI3IiBjeT0iMTIuMDYiIHJ4PSIxLjgxIiByeT0iMS45NSIvPjwvc3ZnPg==);
|
||||
}
|
||||
.ico-fusion::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAyMCAyMCI+PGNpcmNsZSBjeD0iOS4yMiIgY3k9IjE3LjU4IiByPSIxLjUiIGZpbGw9IiNEOUQ5RDkiLz48cGF0aCBmaWxsPSIjRDlEOUQ5IiBkPSJNMTYuNDMgMi45NUEyLjk1IDIuOTUgMCAwIDEgMTEuNyA1LjNhMi45NCAyLjk0IDAgMCAwLTQuMDEgMS4xMSAyLjk1IDIuOTUgMCAxIDEtLjg1LTMuNDMgMi43NiAyLjc2IDAgMCAwIDMuOS0xLjEyIDIuOTUgMi45NSAwIDAgMSA1LjY5IDEuMDlabS0uNjkgNy40NWEyLjE4IDIuMTggMCAwIDEtMy41IDEuNzMgMi4xNyAyLjE3IDAgMCAwLTIuOTYuODIgMi4xOCAyLjE4IDAgMSAxLS42Mi0yLjU0IDIuMDQgMi4wNCAwIDAgMCAyLjg3LS44MiAyLjE4IDIuMTggMCAwIDEgNC4yMS44WiIvPjwvc3ZnPg==);
|
||||
}
|
||||
.ico-audio::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNTM2LTc4di0xMDFxMTA2LTIzIDE3My0xMDguNVQ3NzYtNDgxcTAtMTA3LTY4LTE5MC41VDUzNi03ODF2LTEwMXExNDcgMjMgMjQyLjUgMTM3VDg3NC00ODJxMCAxNTAtOTUgMjY1LjVUNTM2LTc4Wk04Ni0zNjV2LTIzMmgxNThsMjIwLTIyMnY2NzZMMjQ0LTM2NUg4NlptNDUwIDQ3di0zMjRxNDkgMjEgNzguNSA2NC41VDY0NC00ODFxMCA1NC0yOS41IDk3LjVUNTM2LTMxOFoiLz48L3N2Zz4=);
|
||||
}
|
||||
.ico-archive::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNDQ0LjQzLTU3Ljk2di00MDMuNDNMMTA0LjA5LTY1NC4yMnYzMzMuNTdxMCAzMC45NSAxNS4yNiA1Ny42OXQ0Mi4yMiA0MmwyODIuODYgMTYzWm03Mi0xIDI4MS41Ny0xNjJxMjYuOTYtMTUuMjYgNDIuMjItNDIgMTUuMjYtMjYuNzQgMTUuMjYtNTcuNjl2LTMzNS43TDUxNi40My00NjMuODN2NDA0Ljg3Wm0xNzQuMTQtNTg3Ljg3IDEzNC42NS03NS4zOS0yODguMTgtMTY1LjNxLTI2LjUyLTE1LjI2LTU3LjA0LTE1LjI2dC01Ny40OCAxNS4yNmwtNzQuNjEgNDMuMyAzNDIuNjYgMTk3LjM5Wk00NzgtNTI1LjI2bDE0MC04MC0zNDQuMjItMTk3Ljk2LTE0MC41NiA4MS41N0w0NzgtNTI1LjI2WiIvPjwvc3ZnPg==);
|
||||
}
|
||||
.ico-text::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNOTgtMjA4di0xMjhoNTI0djEyOEg5OFptMC0yMDh2LTEyOGg3NjR2MTI4SDk4Wm0wLTIwOXYtMTI4aDc2NHYxMjhIOThaIi8+PC9zdmc+);
|
||||
}
|
||||
.ico-link::before {
|
||||
transform: translate(-2px, 3px);
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNDMyLTI4OEgyODhxLTc5LjY4IDAtMTM1Ljg0LTU2LjIzUTk2LTQwMC40NSA5Ni00ODAuMjMgOTYtNTYwIDE1Mi4xNi02MTZxNTYuMTYtNTYgMTM1Ljg0LTU2aDE0NHY3MkgyODhxLTUwIDAtODUgMzV0LTM1IDg1cTAgNTAgMzUgODV0ODUgMzVoMTQ0djcyWm0tOTYtMTU2di03MmgyODh2NzJIMzM2Wm0xOTIgMTU2di03MmgxNDRxNTAgMCA4NS0zNXQzNS04NXEwLTUwLTM1LTg1dC04NS0zNUg1Mjh2LTcyaDE0NHE3OS42OCAwIDEzNS44NCA1Ni4yMyA1Ni4xNiA1Ni4yMiA1Ni4xNiAxMzZRODY0LTQwMCA4MDcuODQtMzQ0IDc1MS42OC0yODggNjcyLTI4OEg1MjhaIi8+PC9zdmc+);
|
||||
}
|
||||
.ico-readme::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNDI0LTI2N2gxMTJ2LTI2MUg0MjR2MjYxWm01NS42MS0zMTlxMjUuODkgMCA0My4xNC0xNi44NlE1NDAtNjE5LjczIDU0MC02NDUuNjFxMC0yNS44OS0xNi44Ni00My4xNFE1MDYuMjctNzA2IDQ4MC4zOS03MDZxLTI1Ljg5IDAtNDMuMTQgMTYuODZRNDIwLTY3Mi4yNyA0MjAtNjQ2LjM5cTAgMjUuODkgMTYuODYgNDMuMTRRNDUzLjczLTU4NiA0NzkuNjEtNTg2Wm0uNjcgNTM2cS04OC45MiAwLTE2Ny43NS0zMy4xLTc4LjgyLTMzLjExLTEzNy41Ny05MS44NlQ4My4xLTMxMi40OVE1MC0zOTEuMjggNTAtNDgwLjQ2cTAtODkuNDQgMzMuMTYtMTY3LjQ5IDMzLjE3LTc4LjA1IDkyLjE4LTEzNi45NCA1OS4wMS01OC44OSAxMzcuNTMtOTJRMzkxLjM5LTkxMCA0ODAuNDYtOTEwcTg5LjQzIDAgMTY3LjUyIDMzLjA5IDc4LjA5IDMzLjEgMTM2Ljk2IDkxLjk3IDU4Ljg3IDU4Ljg3IDkxLjk3IDEzNy4yMVE5MTAtNTY5LjM4IDkxMC00ODAuMTl0LTMzLjExIDE2Ny41MnEtMzMuMTEgNzguMzItOTIgMTM3LjMzUTcyNi0xMTYuMzMgNjQ3LjcyLTgzLjE2IDU2OS40NS01MCA0ODAuMjgtNTBaIi8+PC9zdmc+);
|
||||
}
|
||||
.ico-chat::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNTAtNDZ2LTc0MXEwLTUzLjgzIDM3LjA5LTkwLjkxUTEyNC4xOC05MTUgMTc4LTkxNWg2MDRxNTMuODMgMCA5MC45MSAzNy4wOVE5MTAtODQwLjgzIDkxMC03ODd2NDU5cTAgNTMuODMtMzcuMDkgOTAuOTFRODM1LjgzLTIwMCA3ODItMjAwSDIwNEw1MC00NlptMTg0LTMzOGgzNDd2LTcySDIzNHY3MlptMC0xMzhoNDkydi03MkgyMzR2NzJabTAtMTM4aDQ5MnYtNzJIMjM0djcyWiIvPjwvc3ZnPg);
|
||||
}
|
||||
.ico-snow::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNDU2LTI4MyAzMzYtMTYxcS04IDgtMTYuNSA4dC0xNy41LTdxLTgtOS04LTE4dDgtMTdsMTU0LTE1M3YtMTA4SDM1MEwxOTQtMzAycS04IDktMTcgOC41dC0xNy04LjVxLTgtOS04LTE3LjV0OS0xNy41bDEyNC0xMTlIMTI4cS03IDAtMTQuNS02LjVUMTA2LTQ3OXEwLTcgNi41LTE1dDE2LjUtOGgxNTZMMTYxLTYyM3EtOC04LTgtMTYuNXQ3LTE2LjVxOS05IDE4LTl0MTcgOWwxNTUgMTU0aDEwNnYtMTEwTDMwMi03NjRxLTgtOC04LTE3dDgtMTdxOS04IDE3LjUtOHQxNy41IDlsMTE5IDEyMHYtMTU0cTAtNyA2LjUtMTV0MTYuNS04cTcgMCAxNSA4dDggMTV2MTU0bDEyMS0xMjJxOC04IDE2LjUtOHQxNi41IDhxOSA4IDkgMTd0LTkgMTdMNTAyLTYxMnYxMTBoMTA4bDE1NC0xNTVxOC04IDE3LTh0MTcgOHE4IDkgOCAxNy41dC05IDE3LjVMNjc1LTUwMmgxNTZxNyAwIDE1IDh0OCAxNXEwIDEwLTggMTYuNXQtMTUgNi41SDY3NWwxMjMgMTIwcTkgOCA5IDE2LjV0LTggMTcuNXEtOCA4LTE3IDguNXQtMTgtNy41TDYxMC00NTZINTAydjEwOGwxNTQgMTU0cTkgOCA5IDE3LjV0LTggMTYuNXEtOSA4LTE3LjUgOHQtMTYuNS04TDUwMi0yODN2MTU1cTAgNy04IDE0LjV0LTE1IDcuNXEtMTAgMC0xNi41LTYuNVQ0NTYtMTI5di0xNTRaIi8+PC9zdmc+);
|
||||
}
|
||||
.ico-code::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMzE5LTIwMCAzOC00ODBsMjgyLTI4MiA5MSA5MC0xOTAgMTkxIDE5MCAxODktOTIgOTJabTMyMiAxLTkxLTkxIDE5MC0xOTAtMTkwLTE5MCA5MS05MCAyODEgMjgwLTI4MSAyODFaIi8+PC9zdmc+);
|
||||
}
|
||||
.ico-json::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyMHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNNjE0LjA0LTE1Ni42NXYtMTAzLjk0aDM1LjE4cTIwLjc4IDAgMzUuNDgtMTQuODMgMTQuNzEtMTQuODIgMTQuNzEtMzUuNnYtNDkuNDZxMC00Ni41MiAzMC4yNi04MC4xNiAzMC4yNi0zMy42NCA3Ni41NS00MC42NHYtMi4yNHEtNDYuNTItMy45Ni03Ni42Ny0zNi43Mi0zMC4xNC0zMi43Ni0zMC4xNC03OS4yOHYtNDkuN3EwLTIwLjc4LTE0LjcxLTM1LjQ4LTE0LjctMTQuNzEtMzUuNDgtMTQuNzFoLTM1LjE4di0xMDMuOTRINjc1cTUzLjQzIDAgOTAuODkgMzcuOTZ0MzcuNDYgOTEuNjN2NDkuNDZxMCAyMC43NyAxNC4zMyAzNS42IDE0LjMyIDE0LjgzIDMyLjgyIDE0LjgzaDQ4Ljg1djE4Ny43NEg4NTAuNXEtMTguNSAwLTMyLjgyIDE0LjcxLTE0LjMzIDE0LjcxLTE0LjMzIDM1LjQ5djQ5LjY5cTAgNTMuNjctMzcuNDYgOTEuNjMtMzcuNDYgMzcuOTYtOTAuODkgMzcuOTZoLTYwLjk2Wm0tMzI5LjA0IDBxLTUzLjQzIDAtOTAuODktMzcuOTZ0LTM3LjQ2LTkxLjYzdi00OS42OXEwLTIwLjc4LTE0LjcxLTM1LjQ5dC0zNi40OC0xNC43MUg2MC42NXYtMTg3Ljc0aDQ0LjgxcTIxLjc3IDAgMzYuNDgtMTQuODMgMTQuNzEtMTQuODMgMTQuNzEtMzUuNnYtNDkuNDZxMC01My42NyAzNy40Ni05MS42MyAzNy40Ni0zNy45NiA5MC44OS0zNy45Nmg2MC45NnYxMDMuOTRoLTM0Ljk0cS0yMC43OCAwLTM1LjYgMTQuNzEtMTQuODMgMTQuNy0xNC44MyAzNS40OHY0OS43cTAgNDcuNTItMzAuMTQgODAuNzgtMzAuMTUgMzMuMjYtNzYuNDMgMzQuMjJ2Mi4zOXE0Ni4yOCA1Ljg1IDc2LjQzIDM5Ljk5IDMwLjE0IDM0LjE0IDMwLjE0IDgxLjY2djQ5LjQ2cTAgMjAuNzggMTQuODMgMzUuNiAxNC44MiAxNC44MyAzNS42IDE0LjgzaDM0Ljk0djEwMy45NEgyODVaIi8+PC9zdmc+);
|
||||
}
|
||||
.loading {
|
||||
animation: loading 2s linear infinite;
|
||||
background-color: hsl(from var(--bg) h s calc(l + 20) / 50%) !important;
|
||||
}
|
||||
@keyframes loading {
|
||||
from {
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
to {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
.mobile {
|
||||
display: none !important;
|
||||
}
|
||||
.file-view-image, .file-view-video, .file-view-audio {
|
||||
background-color: lch(from var(--dark-bg) calc(l - 3) calc(c - 15) h);
|
||||
}
|
||||
.file-view-video, .file-view-audio {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
& > * {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
.file-view-download {
|
||||
p:first-child {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 2rem;
|
||||
max-width: 60ch;
|
||||
}
|
||||
code {
|
||||
background-color: lch(from var(--dark-bg) calc(l - 5) calc(c - 5) h);
|
||||
padding: 0.3rem 0.5rem;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
}
|
||||
.speedbump {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
header {
|
||||
font-family: c1;
|
||||
height: max(min(40vh, 400px), 100px);
|
||||
color: #00ff80;
|
||||
letter-spacing: 0.8ch;
|
||||
margin-right: -0.8ch;
|
||||
font-size: 6vw;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-variation-settings: "wght" 50;
|
||||
text-shadow: 0 0 20px #00ff8022, 0 0 10px #00ff8033;
|
||||
margin-bottom: 2rem;
|
||||
h1 {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
p {
|
||||
max-width: 50ch;
|
||||
margin: 0 auto 1rem auto;
|
||||
color: #ecffea88;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.01em;
|
||||
word-spacing: 0.3em;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
button {
|
||||
background-color: #00ff80bb;
|
||||
color: #111318;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.3rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
letter-spacing: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
.enter-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 0.5rem;
|
||||
margin: 0 auto;
|
||||
background-color: #050d06;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.75rem;
|
||||
button {
|
||||
background-color: #0c1f0e;
|
||||
width: 100%;
|
||||
max-width: 100px;
|
||||
aspect-ratio: 1/1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 0.5rem;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
opacity: 0.75;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
&:hover {
|
||||
img {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.lyrics {
|
||||
align-self: flex-start;
|
||||
}
|
||||
.fullscreen-canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.cotyledon-link {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
color: #27143f;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
.convo {
|
||||
padding: 1rem;
|
||||
.line {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
/* different colors for different ppl */
|
||||
.s-other_a {
|
||||
padding-left: 4rem;
|
||||
color: var(--primary);
|
||||
}
|
||||
.s-other_b {
|
||||
padding-left: 8rem;
|
||||
color: hsl(from var(--primary) calc(h + 200) s l);
|
||||
}
|
||||
.s-other_b2 {
|
||||
padding-left: 4rem;
|
||||
color: hsl(from var(--primary) calc(h + 200) s l);
|
||||
}
|
||||
.s-other_c {
|
||||
padding-left: 12rem;
|
||||
color: hsl(from var(--primary) calc(h + 100) s l);
|
||||
}
|
||||
.s-other_d {
|
||||
padding-left: 4rem;
|
||||
color: hsl(from var(--primary) calc(h + 50) s l);
|
||||
}
|
||||
.s-other_e {
|
||||
padding-left: 4rem;
|
||||
color: hsl(from var(--primary) calc(h + 80) s l);
|
||||
}
|
||||
.s-other_f {
|
||||
padding-left: 4rem;
|
||||
color: #00ff80;
|
||||
}
|
||||
hr {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border: none;
|
||||
border-top: 2px solid var(--muted);
|
||||
}
|
||||
}
|
||||
:not(.active) .ext {
|
||||
font-size: 80%;
|
||||
color: var(--muted);
|
||||
}
|
||||
.file-view-code {
|
||||
color: lch(from var(--primary) 90 10 h / 0.88);
|
||||
|
||||
.comment {
|
||||
color: var(--muted);
|
||||
}
|
||||
.keyword {
|
||||
font-weight: bold;
|
||||
color: lch(from var(--primary) 100 20 h / 1);
|
||||
}
|
||||
.string {
|
||||
color: lch(from var(--primary) calc(l + 30) calc(c + 30) h);
|
||||
}
|
||||
.constant {
|
||||
color: lch(from var(--primary) calc(l + 30) calc(c - 30) h);
|
||||
}
|
||||
.method {
|
||||
color: lch(from var(--primary) l c calc(h - 25));
|
||||
}
|
||||
.class {
|
||||
color: lch(from var(--primary) l c calc(h + 25));
|
||||
}
|
||||
.builtin {
|
||||
color: var(--primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
.parameter {
|
||||
font-style: italic;
|
||||
}
|
||||
.variable {
|
||||
color: lch(from var(--primary) 90 30 h / 0.88);
|
||||
}
|
||||
}
|
||||
.for_everyone {
|
||||
max-width: 69ch;
|
||||
padding: 1rem 3rem;
|
||||
font-family: rmo, monospace;
|
||||
line-height: 1;
|
||||
&:last-child {
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
&:first-child {
|
||||
padding-top: 3rem;
|
||||
}
|
||||
p {
|
||||
/* line */
|
||||
margin: 0;
|
||||
min-height: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
p.normal {
|
||||
line-height: 1.5;
|
||||
}
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid #f66244;
|
||||
color: #ffbfb2;
|
||||
line-height: 1.5;
|
||||
p {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.fade-slide-in {
|
||||
animation: fadeSlideIn 0.4s cubic-bezier(0.3, 0.8, 0.3, 1);
|
||||
}
|
||||
@keyframes fadeSlideIn {
|
||||
from {
|
||||
opacity: 0.5;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
/* MOBILE */
|
||||
@media (max-width: 1000px) {
|
||||
html, body {
|
||||
overflow: unset;
|
||||
width: unset;
|
||||
height: unset;
|
||||
}
|
||||
.mobile {
|
||||
display: inline !important;
|
||||
}
|
||||
.desktop {
|
||||
display: none !important;
|
||||
}
|
||||
h3 {
|
||||
padding-left: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.files {
|
||||
overflow-x: unset;
|
||||
overflow-y: unset;
|
||||
border-bottom: 16px solid var(--dark-bg);
|
||||
min-height: 100vh;
|
||||
.panel {
|
||||
overflow-x: hidden;
|
||||
border: none !important;
|
||||
}
|
||||
.panel, .panel .hsplit {
|
||||
height: unset;
|
||||
min-height: unset;
|
||||
}
|
||||
.panel .header {
|
||||
padding: 0.75rem;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
.mobile-back {
|
||||
margin-left: -0.75rem;
|
||||
display: block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
&::before {
|
||||
display: block;
|
||||
content: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJtMzEzLTQ0MCAyMjQgMjI0LTU3IDU2LTMyMC0zMjAgMzIwLTMyMCA1NyA1Ni0yMjQgMjI0aDQ4N3Y4MEgzMTNaIi8+PC9zdmc+);
|
||||
transform: scale(2);
|
||||
transform-origin: top left;
|
||||
}
|
||||
}
|
||||
.full-screen, .download {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
&::before {
|
||||
transform: scale(2);
|
||||
}
|
||||
}
|
||||
.panel ul li a {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding: 0.5rem 1rem;
|
||||
text-overflow: unset;
|
||||
white-space: wrap;
|
||||
date {
|
||||
font-size: 0.9rem;
|
||||
padding-right: 0;
|
||||
}
|
||||
.size {
|
||||
padding-left: 0;
|
||||
}
|
||||
.size:not(:first-child) {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
.first .size {
|
||||
font-size: 0.9rem;
|
||||
&::before {
|
||||
content: "(";
|
||||
}
|
||||
&::after {
|
||||
content: ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel:not(.last) {
|
||||
display: none;
|
||||
}
|
||||
.panel {
|
||||
position: relative;
|
||||
min-width: unset !important;
|
||||
max-width: unset !important;
|
||||
border-left-width: 10px !important;
|
||||
}
|
||||
.content {
|
||||
height: unset !important;
|
||||
min-width: unset !important;
|
||||
max-width: unset !important;
|
||||
overflow: unset !important;
|
||||
}
|
||||
.hsplit {
|
||||
display: block !important;
|
||||
}
|
||||
.li.readme {
|
||||
display: none !important;
|
||||
}
|
||||
.tooltip {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
1007
src/file-viewer/views/clofi.tsx
Normal file
75
src/friend-auth.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
let friendPassword = "";
|
||||
try {
|
||||
friendPassword = require("./friends/hardcoded-password.ts").friendPassword;
|
||||
} catch {}
|
||||
|
||||
export const app = new Hono();
|
||||
|
||||
const cookieAge = 60 * 60 * 24 * 30; // 1 month
|
||||
|
||||
function checkFriendsCookie(c: Context) {
|
||||
const cookie = c.req.header("Cookie");
|
||||
if (!cookie) return false;
|
||||
const cookies = cookie.split("; ").map((x) => x.split("="));
|
||||
return cookies.some(
|
||||
(kv) =>
|
||||
kv[0].trim() === "friends_password" &&
|
||||
kv[1].trim() &&
|
||||
kv[1].trim() === friendPassword,
|
||||
);
|
||||
}
|
||||
|
||||
export function requireFriendAuth(c: Context) {
|
||||
const k = c.req.query("password") || c.req.query("k");
|
||||
if (k) {
|
||||
if (k === friendPassword) {
|
||||
return c.body(null, 303, {
|
||||
Location: "/friends",
|
||||
"Set-Cookie":
|
||||
`friends_password=${k}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${cookieAge}`,
|
||||
});
|
||||
} else {
|
||||
return c.body(null, 303, {
|
||||
Location: "/friends",
|
||||
});
|
||||
}
|
||||
}
|
||||
if (checkFriendsCookie(c)) {
|
||||
return undefined;
|
||||
} else {
|
||||
return serveAsset(c, "/friends/auth", 403);
|
||||
}
|
||||
}
|
||||
|
||||
app.get("/friends", (c) => {
|
||||
const friendAuthChallenge = requireFriendAuth(c);
|
||||
if (friendAuthChallenge) return friendAuthChallenge;
|
||||
return serveAsset(c, "/friends", 200);
|
||||
});
|
||||
|
||||
let incorrectMap: Record<string, boolean> = {};
|
||||
app.post("/friends", async (c) => {
|
||||
const ip = c.header("X-Forwarded-For") ?? getConnInfo(c).remote.address ??
|
||||
"unknown";
|
||||
if (incorrectMap[ip]) {
|
||||
return serveAsset(c, "/friends/auth/fail", 403);
|
||||
}
|
||||
const data = await c.req.formData();
|
||||
const k = data.get("password");
|
||||
if (k === friendPassword) {
|
||||
return c.body(null, 303, {
|
||||
Location: "/friends",
|
||||
"Set-Cookie":
|
||||
`friends_password=${k}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${cookieAge}`,
|
||||
});
|
||||
}
|
||||
incorrectMap[ip] = true;
|
||||
await setTimeout(2500);
|
||||
incorrectMap[ip] = false;
|
||||
return serveAsset(c, "/friends/auth/fail", 403);
|
||||
});
|
||||
|
||||
import { type Context, Hono } from "hono";
|
||||
import { serveAsset } from "#sitegen/assets";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
import { getConnInfo } from "hono/bun";
|
4
src/friends/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# this directory is private, and not checked into the git repository
|
||||
# instead, it is version-controlled via computer clover's backup system.
|
||||
*
|
||||
!.gitignore
|
9
src/pages/friends/auth.fail.marko
Normal file
|
@ -0,0 +1,9 @@
|
|||
<main>
|
||||
<p>incorrect or outdated password</p>
|
||||
<p>please contact clover</p>
|
||||
<form method="post" action="/friends">
|
||||
<h1>try again</h1>
|
||||
<input type="password" name="password"/>
|
||||
<button type="submit">enter</button>
|
||||
</form>
|
||||
</main>
|
8
src/pages/friends/auth.marko
Normal file
|
@ -0,0 +1,8 @@
|
|||
<main>
|
||||
<form method="post" action="/friends">
|
||||
<h1>what's the password</h1>
|
||||
<input type="password" name="password"/>
|
||||
<button type="submit">enter</button>
|
||||
<p>(clover will give the password to you if you're a friend)</p>
|
||||
</form>
|
||||
</main>
|
97
src/pages/subscribe.client.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
// @ts-nocheck
|
||||
// manually obfuscated to make it very difficult to reverse engineer
|
||||
// if you want to decode what the email is, visit the page!
|
||||
// stops people from automatically scraping the email address
|
||||
//
|
||||
// Unfortunately this needs a rewrite to support Chrome without
|
||||
// hardware acceleration and some Linux stuff. I will probably
|
||||
// go with a proof of work alternative.
|
||||
requestAnimationFrame(() => {
|
||||
const hash = "SHA";
|
||||
const a = [
|
||||
{ parentElement: document.getElementById("subscribe") },
|
||||
function (b) {
|
||||
let c = 0, d = 0;
|
||||
for (let i = 0; i < b.length; i++) {
|
||||
c = (c + b[i] ^ 0xF8) % 8;
|
||||
d = (c * b[i] ^ 0x82) % 193;
|
||||
}
|
||||
a[c + 1]()[c](d, b.buffer);
|
||||
},
|
||||
function () {
|
||||
const i = a[4](a[3]());
|
||||
const b = i.innerText = a.pop();
|
||||
if (a[b.indexOf("@") / 3]) {
|
||||
i.href = "mailto:" + b;
|
||||
}
|
||||
},
|
||||
function () {
|
||||
return a[a.length % 10];
|
||||
},
|
||||
function (x) {
|
||||
return x.parentElement;
|
||||
},
|
||||
function (b, c) {
|
||||
throw new Uint8Array(
|
||||
c,
|
||||
0,
|
||||
64,
|
||||
c.parentElement = this[8].call(b.call(this)).location,
|
||||
);
|
||||
},
|
||||
function (b, c) {
|
||||
this.width = 8;
|
||||
this.height = 16;
|
||||
b.clearColor(0.5, 0.7, 0.9, 1.0);
|
||||
b.clear(16408 ^ this.width ^ this.height);
|
||||
const e = new Uint8Array(4 * this.width * this.height);
|
||||
b.readPixels(0, 0, this.width, this.height, b.RGBA, b.UNSIGNED_BYTE, e);
|
||||
let parent = a[this.width / 2](this);
|
||||
while (parent.tagName !== "BODY") {
|
||||
parent = a[2 * this.height / this.width](parent);
|
||||
}
|
||||
try {
|
||||
let d = [hash, e.length].join("-");
|
||||
const b = a[0.25 * this.height](a.pop()(parent)).subtle.digest(d, e);
|
||||
[, d] = a;
|
||||
b.then(c.bind(a, a[this.width].bind(globalThis))).catch(d);
|
||||
} catch (e) {
|
||||
fetch(e).then(a[5]).catch(a[2]);
|
||||
}
|
||||
},
|
||||
function (b, c) {
|
||||
const d = a.splice(
|
||||
9,
|
||||
1,
|
||||
[
|
||||
a[3]().parentElement.id,
|
||||
c.parentElement.hostname,
|
||||
].join(String.fromCharCode(b)),
|
||||
);
|
||||
var e = new Error();
|
||||
Object.defineProperty(e, "stack", {
|
||||
get() {
|
||||
a[9] = d;
|
||||
},
|
||||
});
|
||||
a[2].call(console.log(e));
|
||||
},
|
||||
function () {
|
||||
return this;
|
||||
},
|
||||
"[failed to verify your browser]",
|
||||
function (a) {
|
||||
a = a.parentElement.ownerDocument.defaultView;
|
||||
return { parentElement: a.navigator.webdriver || a.crypto };
|
||||
},
|
||||
];
|
||||
try {
|
||||
const c = document.querySelector("canvas");
|
||||
const g = c.getContext("webgl2") || c.getContext("webgl");
|
||||
a[0].parentElement.innerText = "[...loading...]";
|
||||
g.field || requestAnimationFrame(a[6].bind(c, g, a[5]));
|
||||
} catch {
|
||||
a.pop();
|
||||
fetch(":").then(a[5]).catch(a[2]);
|
||||
}
|
||||
});
|
17
src/pages/subscribe.css
Normal file
|
@ -0,0 +1,17 @@
|
|||
#subscribe[href] {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
#subscribe:not([href]) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
}
|
39
src/pages/subscribe.marko
Normal file
|
@ -0,0 +1,39 @@
|
|||
import './subscribe.css';
|
||||
export const meta = {
|
||||
title: "paper clover mailing list",
|
||||
description: "subscribe to the mailing list",
|
||||
};
|
||||
|
||||
client import "./subscribe.client.ts";
|
||||
|
||||
<main>
|
||||
<h1>mailing list</h1>
|
||||
<canvas style="display: none;"></canvas>
|
||||
<p>
|
||||
the mailing list is used for big project updates. this is about once every
|
||||
couple of months.
|
||||
</p>
|
||||
<p>
|
||||
the list is currently managed manually. to get added, please email anything
|
||||
with the word "subscribe" in the subject or body to the following address:
|
||||
</p>
|
||||
<code class="flex">
|
||||
<a id="subscribe">
|
||||
[please enable javascript, this is an anti-spam/scraping measure]
|
||||
</a>
|
||||
</code>
|
||||
<p>
|
||||
to unsubscribe, send an email asking to unsubscribe. for those who are close
|
||||
friends with me, add your name and i'll add you to the 'friends' list, where i
|
||||
send life updates in addition to new content (1-2 times per month).
|
||||
</p>
|
||||
<p>
|
||||
alternatively, you can join <a href="/discord">the discord</a> to receive mentions in the <code>#new</code> channel.
|
||||
</p>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<p>
|
||||
<a href="/">back to home</a>
|
||||
</p>
|
||||
</main>
|
31
src/pages/waterfalls.css
Normal file
63
src/pages/waterfalls.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { Video } from "@/tags/Video.tsx";
|
||||
export default () => (
|
||||
<main class="waterfalls">
|
||||
<div class="waterfalls_bg1" />
|
||||
<main class="waterfalls">
|
||||
<div class="contain">
|
||||
<h1>waterfalls and gender identity update</h1>
|
||||
<p>
|
||||
this is a song about identity, jealousness, and the seasons. more
|
||||
specifically, it's a personal account of how it felt realizing i was a
|
||||
transgender woman. enjoy.
|
||||
</p>
|
||||
</div>
|
||||
<Video
|
||||
title={
|
||||
<>
|
||||
<strong>music video</strong>: waterfalls
|
||||
</>
|
||||
}
|
||||
width={1920}
|
||||
height={1080}
|
||||
poster="/file/2025/waterfalls/thumbnail.jpeg"
|
||||
posterHash="hMM%+pl^]eOvxCXU$$Ew4TB~LceBsAacSiOq3*F;MevmthFf+]$*WM]%E2KybarEt7NIJ3K$J8r=igxbTERP"
|
||||
sources={[
|
||||
"/file/2025/waterfalls/stream/hls.m3u8",
|
||||
"/file/2025/waterfalls/waterfalls.webm",
|
||||
]}
|
||||
downloads={[
|
||||
"/file/2025/waterfalls/waterfalls.mp3",
|
||||
"/file/2025/waterfalls/waterfalls.mp4",
|
||||
"/file/2025/waterfalls/waterfalls.webm",
|
||||
"/file/2025/waterfalls/lyrics.txt",
|
||||
"/file/2025/waterfalls/",
|
||||
]}
|
||||
/>
|
||||
<div class="contain">
|
||||
<p>
|
||||
it's nice to be <em>free</em>.
|
||||
</p>
|
||||
<p>like paper airplanes in the wind.</p>
|
||||
<p>the stars shine in the sky. it's beautiful.</p>
|
||||
<p>
|
||||
love
|
||||
<br />
|
||||
–clover caruso
|
||||
</p>
|
||||
<br />
|
||||
|
||||
<p>
|
||||
<strong>more</strong>: <br />
|
||||
<a href="/file/2025/waterfalls/fragments">fragments</a>
|
||||
<br />
|
||||
<a href="/q+a">q&a page</a>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
);
|
||||
|
||||
export const theme = {
|
||||
fg: "#fff",
|
||||
bg: "#000",
|
||||
};
|
|
@ -15,4 +15,4 @@ export const siteSections: Section[] = [
|
|||
];
|
||||
|
||||
import * as path from "node:path";
|
||||
import { Section } from "#sitegen";
|
||||
import type { Section } from "#sitegen";
|
||||
|
|
16
src/tags/PhotoGrid.css
Normal file
|
@ -0,0 +1,16 @@
|
|||
photo-grid {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
div {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 2px solid #0009;
|
||||
}
|
||||
img {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
134
src/tags/PhotoGrid.tsx
Normal file
|
@ -0,0 +1,134 @@
|
|||
import "./PhotoGrid.css";
|
||||
import { MediaFile } from "@/file-viewer/models/MediaFile.ts";
|
||||
const { isArray } = Array;
|
||||
|
||||
export interface PhotoGridProps {
|
||||
style: any;
|
||||
base: string;
|
||||
width: number;
|
||||
widths?: string[];
|
||||
heights?: number[];
|
||||
items: Item[];
|
||||
}
|
||||
|
||||
type Item = [img: string, options?: ItemOptions];
|
||||
|
||||
interface ItemOptions {
|
||||
w?: number;
|
||||
h?: number;
|
||||
align?: keyof typeof alignments;
|
||||
}
|
||||
|
||||
const alignments = {
|
||||
bottom: "50% 80%",
|
||||
"bottom-end": "50% 100%",
|
||||
top: "50% 30%",
|
||||
"top-end": "50% 0%",
|
||||
left: "30% 50%",
|
||||
"left-end": "0% 50%",
|
||||
right: "80% 50%",
|
||||
"right-end": "100% 50%",
|
||||
};
|
||||
|
||||
const typicalPageWidth = 768;
|
||||
|
||||
export function PhotoGrid({
|
||||
style,
|
||||
base,
|
||||
width,
|
||||
widths,
|
||||
heights,
|
||||
items,
|
||||
}: PhotoGridProps) {
|
||||
if (!base.endsWith("/")) base += "/";
|
||||
let rows: boolean[][] = [];
|
||||
const row = (y: number) => (rows[y] ??= new Array(width).fill(false));
|
||||
const anyCollide = (x: number, y: number, w: number, h: number) => {
|
||||
for (let yy = 0; yy < h; yy++) {
|
||||
const r = row(y + yy);
|
||||
for (let xx = 0; xx < w; xx++) if (r[x + xx]) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const firstRowHeight = heights?.[0] ?? 325;
|
||||
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let contents = [];
|
||||
for (const item of items) {
|
||||
const [img, { w = 1, h = 1, align } = {}] = isArray(item) ? item : [item];
|
||||
if (w > width) throw new Error(`Item ${img} too wide (${w} > ${width})`);
|
||||
|
||||
while (anyCollide(x, y, w, h)) {
|
||||
x += 1;
|
||||
if (x > width - w) x = 0, y += 1;
|
||||
}
|
||||
for (let yy = 0; yy < h; yy++) {
|
||||
const r = row(y + yy);
|
||||
for (let xx = 0; xx < w; xx++) r[x + xx] = true;
|
||||
}
|
||||
|
||||
const info = fileInfo(base, img);
|
||||
contents.push(
|
||||
<div
|
||||
style={{
|
||||
...x === 0 && y === 0 &&
|
||||
{
|
||||
"aspect-ratio": ((w / width) * typicalPageWidth) / firstRowHeight,
|
||||
},
|
||||
"grid-area": `${y + 1} / ${x + 1} / ${1 + y + h} / ${1 + x + w}`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
{...info}
|
||||
style={{
|
||||
"object-position": align
|
||||
? (alignments as Record<string, string>)[align] ?? align
|
||||
: "50% 50%",
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
const height = rows.length;
|
||||
return (
|
||||
<photo-grid
|
||||
style={{
|
||||
...style,
|
||||
"grid-template-columns": widths
|
||||
? mapSizes(widths).join(" ")
|
||||
: `repeat(${width}, 1fr)`,
|
||||
"grid-template-rows": heights
|
||||
? heights.map((h) => (h / firstRowHeight) + "fr")
|
||||
.concat(new Array(height - heights.length).fill("1fr"))
|
||||
.join(" ")
|
||||
: `repeat(${height}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{contents}
|
||||
</photo-grid>
|
||||
);
|
||||
}
|
||||
|
||||
function fileInfo(base: string, img: string) {
|
||||
const filePath = path.join(base, img);
|
||||
const file = MediaFile.getByPath(filePath);
|
||||
const dimensions = file?.parseDimensions();
|
||||
if (!dimensions) {
|
||||
throw new Error(`File does not exist or has no dimensions: ${filePath}`);
|
||||
}
|
||||
const src = "/file" + base + encodeURIComponent(img);
|
||||
const alt = path.basename(img, path.extname(img));
|
||||
return { src, alt, width: dimensions.width, height: dimensions.height };
|
||||
}
|
||||
|
||||
function mapSizes(sizes: Array<string | number>) {
|
||||
return sizes.map(mapSize);
|
||||
}
|
||||
function mapSize(size: string | number) {
|
||||
return typeof size === "number" ? size + "px" : size;
|
||||
}
|
||||
|
||||
import * as path from "node:path";
|
|
@ -14,7 +14,6 @@ export namespace Video {
|
|||
borderless?: boolean;
|
||||
}
|
||||
}
|
||||
function PrecomputedBlurhash({ hash }: { hash: string }) {
|
||||
export function Video(
|
||||
{ title, sources, height, poster, posterHash, width, borderless }:
|
||||
Video.Props,
|
||||
|
@ -23,7 +22,7 @@ export function Video(
|
|||
return (
|
||||
<figure class={`video ${borderless ? "borderless" : ""}`}>
|
||||
<figcaption>{title}</figcaption>
|
||||
{/* posterHash && <PrecomputedBlurhash hash={posterHash} /> */}
|
||||
{posterHash && <PrecomputedBlurhash hash={posterHash} />}
|
||||
{poster && <img src={poster} alt="waterfalls" />}
|
||||
<video
|
||||
controls
|
||||
|
|
34
src/tags/blurhash.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import * as blurhash from "blurhash";
|
||||
import sharp from "sharp";
|
||||
function decode83(str: string) {
|
||||
return [...str].reduce(
|
||||
(value, char) =>
|
||||
value * 83 +
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" //
|
||||
.indexOf(char),
|
||||
0,
|
||||
);
|
||||
}
|
||||
function getSizeFromHash(hash: string) {
|
||||
const sizeFlag = decode83(hash[0]);
|
||||
const height = ((sizeFlag / 9) | 0) + 1;
|
||||
const width = (sizeFlag % 9) + 1;
|
||||
return { width, height };
|
||||
}
|
||||
export async function encodeBlurhashPng(hash: string) {
|
||||
const size = getSizeFromHash(hash);
|
||||
const data = blurhash.decode(hash, size.width, size.height);
|
||||
return (await sharp(data, {
|
||||
raw: { width: size.width, height: size.height, channels: 4 },
|
||||
}).png({ colors: size.width * size.height }).toBuffer());
|
||||
}
|
||||
export async function PrecomputedBlurhash({ hash }: { hash: string }) {
|
||||
const data = await encodeBlurhashPng(hash);
|
||||
return (
|
||||
<span
|
||||
style={`background:url("data:image/png;base64,${
|
||||
data.toString("base64url")
|
||||
}")0px 0px/100% auto;`}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
"rootDir": ".",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"verbaitimModuleSyntax": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"target": "es2022"
|
||||
},
|
||||
"include": ["framework/**/*", "src/**/*"]
|
||||
|
|