From a08344779bb8ee1c69050ccddd73a5112cbde526 Mon Sep 17 00:00:00 2001 From: 5ma Date: Wed, 19 Feb 2025 20:03:48 +0900 Subject: [PATCH] initial commit --- astro.config.ts | 6 +- package-lock.json | 53 ++- package.json | 7 +- public/images/0.png | Bin 0 -> 343 bytes public/images/1.png | Bin 0 -> 201 bytes public/images/2.png | Bin 0 -> 312 bytes public/images/3.png | Bin 0 -> 341 bytes public/images/4.png | Bin 0 -> 286 bytes public/images/5.png | Bin 0 -> 332 bytes public/images/6.png | Bin 0 -> 306 bytes public/images/7.png | Bin 0 -> 254 bytes public/images/8.png | Bin 0 -> 330 bytes public/images/9.png | Bin 0 -> 320 bytes public/images/colon.png | Bin 0 -> 185 bytes src/meta.json | 12 + src/pages/_alpine-example.astro | 22 -- src/pages/_react-example.astro | 18 - src/pages/index.astro | 7 +- .../alpinejs/particle-clock.component.ts | 319 ++++++++++++++++++ src/scripts/webGL/setupStage.ts | 134 ++++++++ src/scripts/webGL/shaders/includes/remap.glsl | 4 + .../shaders/includes/simplexNoise3d.glsl | 71 ++++ .../webGL/shaders/particleClock/fragment.glsl | 24 ++ .../webGL/shaders/particleClock/vertex.glsl | 58 ++++ tsconfig.json | 6 +- 25 files changed, 690 insertions(+), 51 deletions(-) create mode 100644 public/images/0.png create mode 100644 public/images/1.png create mode 100644 public/images/2.png create mode 100644 public/images/3.png create mode 100644 public/images/4.png create mode 100644 public/images/5.png create mode 100644 public/images/6.png create mode 100644 public/images/7.png create mode 100644 public/images/8.png create mode 100644 public/images/9.png create mode 100644 public/images/colon.png create mode 100644 src/meta.json delete mode 100644 src/pages/_alpine-example.astro delete mode 100644 src/pages/_react-example.astro create mode 100644 src/scripts/alpinejs/particle-clock.component.ts create mode 100644 src/scripts/webGL/setupStage.ts create mode 100644 src/scripts/webGL/shaders/includes/remap.glsl create mode 100644 src/scripts/webGL/shaders/includes/simplexNoise3d.glsl create mode 100644 src/scripts/webGL/shaders/particleClock/fragment.glsl create mode 100644 src/scripts/webGL/shaders/particleClock/vertex.glsl diff --git a/astro.config.ts b/astro.config.ts index 324511a..8c1b59c 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -3,13 +3,14 @@ import react from '@astrojs/react' import tailwind from '@astrojs/tailwind' import icon from 'astro-icon' import { defineConfig } from 'astro/config' +import glsl from 'vite-plugin-glsl' // https://astro.build/config export default defineConfig({ site: 'https://playground.shiftbrain.com/', - base: '/post/template', + base: '/post/particle-clock', server: { - open: '/post/template', + open: '/post/particle-clock/', }, prefetch: true, integrations: [ @@ -21,6 +22,7 @@ export default defineConfig({ react(), ], vite: { + plugins: [glsl()], define: { 'import.meta.vitest': 'undefined', }, diff --git a/package-lock.json b/package-lock.json index 14711dc..653308e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "playground-template", + "name": "playground-particle-clock", "lockfileVersion": 3, "requires": true, "packages": { @@ -24,7 +24,9 @@ "eslint-plugin-astro": "1.1.1", "eslint-plugin-jsx-a11y": "6.8.0", "eslint-plugin-tailwindcss": "3.15.1", + "gsap": "3.12.7", "leva": "0.9.35", + "lil-gui": "0.20.0", "postcss-fluid-sizing-function": "0.0.2", "prettier": "3.2.5", "prettier-plugin-astro": "0.14.1", @@ -34,6 +36,8 @@ "react-device-detect": "2.2.3", "react-dom": "18.3.1", "rollup-plugin-visualizer": "5.12.0", + "stats-gl": "3.6.0", + "three": "0.173.0", "tiny-invariant": "1.3.3", "vitest": "1.5.3" }, @@ -45,7 +49,8 @@ "inquirer": "9.3.2", "postcss-preset-env": "10.0.5", "tsx": "4.7.2", - "typescript-eslint": "8.8.0" + "typescript-eslint": "8.8.0", + "vite-plugin-glsl": "1.3.1" }, "engines": { "node": "20" @@ -11809,6 +11814,11 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, + "node_modules/gsap": { + "version": "3.12.7", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.7.tgz", + "integrity": "sha512-V4GsyVamhmKefvcAKaoy0h6si0xX7ogwBoBSs2CTJwt7luW0oZzC0LhdkyuKV8PJAXr7Yaj8pMjCKD4GJ+eEMg==" + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -13297,6 +13307,11 @@ "node": ">= 0.8.0" } }, + "node_modules/lil-gui": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.20.0.tgz", + "integrity": "sha512-k7Ipr0ztqslMA2XvM5z5ZaWhxQtnEOwJBfI/hmSuRh6q4iMG9L0boqqrnZSzBR1jzyJ28OMl47l65ILzRe1TdA==" + }, "node_modules/lilconfig": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", @@ -18029,6 +18044,19 @@ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==" }, + "node_modules/stats-gl": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-3.6.0.tgz", + "integrity": "sha512-Bpi5nwvC2pls9lcI5sGqAcUqB/2kMYfsR4KoAglXFtY3YUdm7lJFg8FqS7eKKNgg3ruDMNCuwU+uRwPOwJiGnw==", + "peerDependencies": { + "three": "*" + }, + "peerDependenciesMeta": { + "three": { + "optional": true + } + } + }, "node_modules/std-env": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", @@ -18501,6 +18529,11 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.173.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.173.0.tgz", + "integrity": "sha512-AUwVmViIEUgBwxJJ7stnF0NkPpZxx1aZ6WiAbQ/Qq61h6I9UR4grXtZDmO8mnlaNORhHnIBlXJ1uBxILEKuVyw==" + }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -19276,6 +19309,22 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-plugin-glsl": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vite-plugin-glsl/-/vite-plugin-glsl-1.3.1.tgz", + "integrity": "sha512-iClII8Idb9X0m6nS0YI2cWWXbBuT5EKKw5kXSAuRu4RJsNe4oypxKXE7jx0XMoyqij2s8WL0ZLfou801mpkREg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">= 16.15.1", + "npm": ">= 8.11.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", diff --git a/package.json b/package.json index c0f13a8..419028d 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,9 @@ "eslint-plugin-astro": "1.1.1", "eslint-plugin-jsx-a11y": "6.8.0", "eslint-plugin-tailwindcss": "3.15.1", + "gsap": "3.12.7", "leva": "0.9.35", + "lil-gui": "0.20.0", "postcss-fluid-sizing-function": "0.0.2", "prettier": "3.2.5", "prettier-plugin-astro": "0.14.1", @@ -47,6 +49,8 @@ "react-device-detect": "2.2.3", "react-dom": "18.3.1", "rollup-plugin-visualizer": "5.12.0", + "stats-gl": "3.6.0", + "three": "0.173.0", "tiny-invariant": "1.3.3", "vitest": "1.5.3" }, @@ -62,7 +66,8 @@ "inquirer": "9.3.2", "postcss-preset-env": "10.0.5", "tsx": "4.7.2", - "typescript-eslint": "8.8.0" + "typescript-eslint": "8.8.0", + "vite-plugin-glsl": "1.3.1" }, "overrides": { "leva": { diff --git a/public/images/0.png b/public/images/0.png new file mode 100644 index 0000000000000000000000000000000000000000..35d751b9f69613b31e87c8db72b1f7d9ab8a259b GIT binary patch literal 343 zcmeAS@N?(olHy`uVBq!ia0vp^JU}eO!3HGrSK5O(oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{u@sh$B+ufsX>i`E(QWN&$t7cgr+QDQGL=^E9gDxOOu!CdgU{ecw(!mDExwlvVy|zPDBh&+=c~n3kQ6zse+F}r mO@8jZsk1JBV0-`kFY8;m8=>~oZXX8vo59o7&t;ucLK6UqvW7nZ literal 0 HcmV?d00001 diff --git a/public/images/1.png b/public/images/1.png new file mode 100644 index 0000000000000000000000000000000000000000..1f6005cf851baeaf1f0d4be62b636a4b2ec9d4ee GIT binary patch literal 201 zcmeAS@N?(olHy`uVBq!ia0vp^JU}eO!3HGrSK5O(oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBew?R^V@L(#-ANZY4;XN`=r3(fnbPw}txqZM zK}KG}$^0CyMkjB%AEkFHJ6IhO8kxKIq-U&Mc&%#nQHwJ&vPxdJESGhiFtH3RDqo>> shR1)&*1f0JTxw6hdo$p}?(h2;qJHb|vn)OF9cUASr>mdKI;Vst0J_aX?EnA( literal 0 HcmV?d00001 diff --git a/public/images/2.png b/public/images/2.png new file mode 100644 index 0000000000000000000000000000000000000000..8d8d16e563688d38d33bca7cbe9c0876429c88e5 GIT binary patch literal 312 zcmeAS@N?(olHy`uVBq!ia0vp^JU}eO!3HGrSK5O(oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{#j2K$B+ufvq2YmTNHTKPM1C)D02{m?l_oV z021y6%a=A=_5A+_skGphX%J+urL5vJejBx=SNe8XDxagFdkE+;22WQ%mvv4FO#o|Q BcfSAt literal 0 HcmV?d00001 diff --git a/public/images/3.png b/public/images/3.png new file mode 100644 index 0000000000000000000000000000000000000000..5c7218f3593c06d1f77b43c82150eb089d548748 GIT binary patch literal 341 zcmeAS@N?(olHy`uVBq!ia0vp^JU}eO!3HGrSK5O(oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{wq%x$B+ufsX>9fEe1TbG4JG69x$jVFmiev zVDw;MeZU}-z$ntdTyP<}n%#U`Yl?p6wvxBM;%+lA9Awt7IAr}W*Xogl+q6`RKIYFh z$+yk4J548WEyw39$i|{;&sTPFJc;NqJ+~~r+c@yZ8bG`zVCg0aGI4(z}zH;<5k9@ iZQ1>EIQ>7wz2{$c;IQNImwKR3VDNPHb6Mw<&;$VBAAxWH literal 0 HcmV?d00001 diff --git a/public/images/4.png b/public/images/4.png new file mode 100644 index 0000000000000000000000000000000000000000..35c9739c7b65235cd11925d6c96c089fd2115a29 GIT binary patch literal 286 zcmeAS@N?(olHy`uVBq!ia0vp^JU}eO!3HGrSK5O(oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{$@`X$B+ufvq1-W4=eEO36WJ$PHE|xkTXee z5~l>KSpthhBX0upjfP_$3&K>E{;8Uzs&YGhem|pw3%lG4m-54(^$h!zpH=$hC-XRQ zsEV&|DwZ=|mRWWF>e1WVmO8z!-Q76p+AR^Mj#UeVYS%SwW_Z0TF6(zjLQi6(n9Om; z=Q8gC!W2C;Pybl2t+`Z4d*k+i8!xpY4P5sL?fqWsFWgla^}4I`$mGV9sLMN+wZ$7g dc)#yEx6su!lNy8Gx&xih;OXk;vd$@?2>==NXCwds literal 0 HcmV?d00001 diff --git a/public/images/5.png b/public/images/5.png new file mode 100644 index 0000000000000000000000000000000000000000..b6da212f0a2de646c3694aefde52627865600bc7 GIT binary patch literal 332 zcmeAS@N?(olHy`uVBq!ia0vp^JU}eO!3HGrSK5O(oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{zFd}$B+ufsX=cAogD=3rhJ+&q~XZ!b+J+G zMFZCh7SRigS__zWZa``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{&7zi$B+ufwL#p1EeawvaZJh*2RYmfngn|i zm@^$jlmr+(99a5l%Z=n7+%v!CVkrU}<(Ez4Xb z`}soWVFQ+A9%K9J&&AGOk_;U6T+b?>C8XTn)_Z+=)4unX2JJyz=eZ8{aR`2B435L yCtKI1FAp%}=93MbXa0;a%Q%$v{)In#<}coP|B1&pi)BDBF?hQAxvX``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{$x)V$B+uftwFbWk0|gI`xaxiIye)^%E)sCEcz*rt8NIM`Woq8x2CE~yG<|>ao;qx}`OdxH(Qnh#k7?Zd w{yQpK%Vy)@L;FI09OvWSz3Z^>z5n;@-)2l%9Ib160_Z#jPgg&ebxsLQ0EF*aGynhq literal 0 HcmV?d00001 diff --git a/public/images/8.png b/public/images/8.png new file mode 100644 index 0000000000000000000000000000000000000000..034f59d6c2f55954045bb3ffbd9a310f177c4296 GIT binary patch literal 330 zcmeAS@N?(olHy`uVBq!ia0vp^JU}eO!3HGrSK5O(oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{(Vmu$B+ufsX^Sltp+?g`u+?;6B?Kc8dy~x zFmQVu;1oK*IJ@cWLAmSGO@FthOgC{jzV-TRDW#&2M*h%6vp)C==ia_#(f6k=R$Ozb z(WK88zwb3ZwnO0cu3aDIcNE+=XYc5!y+89y$r(-$rq4E~6R(%fiJs99e^ zwai&RW}Z`glY4u|qsnC)``+;6%@O1TaS?83{1OSKcgD(I8 literal 0 HcmV?d00001 diff --git a/public/images/9.png b/public/images/9.png new file mode 100644 index 0000000000000000000000000000000000000000..c70b2e06da4cc6a6aa9514294810db2063a863d7 GIT binary patch literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^JU}eO!3HGrSK5O(oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{#8#G$B+ufvq8~|=GOl@ey-$~N7?3+TNIR*@l2W0nR7_JX2q(mUF#-)E{$C> zrOV^ZH3OcUo<#Z6n>L6h>c20~nB^vP?sH1CaO^*^3)m+NA(S5$Uo31j=V+|=)_OGIjzztSwSabF1+6NU0iP6-G8aaWDS8HWbkzL Kb6Mw<&;$T%tao_; literal 0 HcmV?d00001 diff --git a/public/images/colon.png b/public/images/colon.png new file mode 100644 index 0000000000000000000000000000000000000000..5f66f6ecc3902422b24e5a07ea9aa777dee19859 GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^JU}eO!3HGrSK5O(oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBzMrRyV@L(#)rluL84NgB%vT-|db6L?dn2p$ ziI3h6O-^=vI!y-sSJ@I29Ni~rHFY?IK4vn0eDs5p%e#FGkI8>)Eeq?Pk&t%XW@XT? b)dq}@3*}q8>gu-uO=j?P^>bP0l+XkK(u6nP literal 0 HcmV?d00001 diff --git a/src/meta.json b/src/meta.json new file mode 100644 index 0000000..adb5700 --- /dev/null +++ b/src/meta.json @@ -0,0 +1,12 @@ +{ + "slug": "particle-clock", + "title": "Particle Clock", + "author": "komakine", + "tags": [ + "three", + "animation", + "glsl", + "particle" + ], + "device": "all" +} \ No newline at end of file diff --git a/src/pages/_alpine-example.astro b/src/pages/_alpine-example.astro deleted file mode 100644 index 8039ae1..0000000 --- a/src/pages/_alpine-example.astro +++ /dev/null @@ -1,22 +0,0 @@ ---- -/** - * こちらのファイルはテンプレート用です。 - * pages/index.astroにコピペして使用してください - */ -import Layout from '../layouts/Layout.astro' -import { title, slug } from '../meta.json' ---- - - -
- {/* contents start */} -
-

- -
- {/* contents end */} -
-
diff --git a/src/pages/_react-example.astro b/src/pages/_react-example.astro deleted file mode 100644 index c1e44a5..0000000 --- a/src/pages/_react-example.astro +++ /dev/null @@ -1,18 +0,0 @@ ---- -/** - * こちらのファイルはテンプレート用です。 - * pages/index.astroにコピペして使用してください - */ -import Layout from '../layouts/Layout.astro' -import { title, slug } from '../meta.json' - -import Component from '../components/Component' ---- - - -
- {/* contents start */} - - {/* contents end */} -
-
diff --git a/src/pages/index.astro b/src/pages/index.astro index b16dc2d..9864d37 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -6,11 +6,8 @@ import { title, slug } from '../meta.json'
{/* contents start */} -
-
    -
  • Alpine example → _alpine-example.astro
  • -
  • React example → _react-example.astro
  • -
+
+
{/* contents end */}
diff --git a/src/scripts/alpinejs/particle-clock.component.ts b/src/scripts/alpinejs/particle-clock.component.ts new file mode 100644 index 0000000..5d2087d --- /dev/null +++ b/src/scripts/alpinejs/particle-clock.component.ts @@ -0,0 +1,319 @@ +import { SetupStage } from '@/scripts/webGL/setupStage' +import particleClockFragmentShader from '@/scripts/webGL/shaders/particleClock/fragment.glsl' +import particleClockVertexShader from '@/scripts/webGL/shaders/particleClock/vertex.glsl' +import Alpine from 'alpinejs' +import gsap from 'gsap' +import GUI from 'lil-gui' +import throttle from 'lodash/throttle' +import * as THREE from 'three' + +type DigitTuple = [ + hourTens: string, + hourOnes: string, + minuteTens: string, + minuteOnes: string, + secondTens: string, + secondOnes: string, +] + +type DigitItem = { + object: THREE.Points + visible: boolean + delay: number +} + +Alpine.data('particleClock', () => { + const gui = new GUI() + let webGL!: SetupStage + const timer = new THREE.Group() + let timeDigits: DigitTuple = [ + '99', //(hour tens) + '99', //(hour ones) + '99', //(minute tens) + '99', //(minute ones) + '99', //(second tens) + '99', //(second ones) + ] + const planeGeometry = new THREE.PlaneGeometry(1, 1, 32, 32 * 1.5) + let digits: DigitItem[] = [] + const colons: THREE.Points[] = + [] + const digitTextures: THREE.Texture[] = [] + let containerRef: HTMLDivElement | null + // Loaders + const textureLoader = new THREE.TextureLoader() + + return { + guiParams: { + bgColor: '#000000', + primaryColor: '#2eff3c', + secondaryColor: '#e51f1f', + uParticleSize: 0.023, + }, + async init() { + this.setDom() + if (!containerRef) return + + // webGL + webGL = new SetupStage({ + container: containerRef, + ambientLight: false, + perspectiveCamera: true, + orthographicCamera: false, + }) + webGL.camera.position.z = 1 + webGL.scene.background = new THREE.Color(this.guiParams.bgColor) + webGL.camera.position.y = 0 + webGL.camera.position.z = 5 + + webGL.scene.add(timer) + + this.addObjects() + this.setPositionCenter() + this.setupTick() + this.updateTime() + setInterval(() => { + this.updateTime() + }, 1000) + setTimeout(() => { + this.showColons() + }, 200) + this.setGui() + + // Resize + const ro = new ResizeObserver(() => { + throttle(this.onResize.bind(this), 200)() + }) + ro.observe(containerRef) + }, + addObjects() { + // load number texture + for (let i = 0; i < 10; i++) { + digitTextures.push(textureLoader.load(`/post/particle-clock/images/${i}.png`)) + } + + // fit image aspect + planeGeometry.scale(1.2 * 0.8, 1.8 * 0.8, 1) + + // create base material + const baseMaterial = new THREE.ShaderMaterial({ + vertexShader: particleClockVertexShader, + fragmentShader: particleClockFragmentShader, + transparent: true, + // blending: THREE.AdditiveBlending, + uniforms: { + uResolution: new THREE.Uniform( + new THREE.Vector2(webGL.width * webGL.pixelRatio, webGL.height * webGL.pixelRatio), + ), + uTime: new THREE.Uniform(0), + uColor: new THREE.Uniform(new THREE.Color(this.guiParams.primaryColor)), + uFinalColor: new THREE.Uniform(new THREE.Color(this.guiParams.secondaryColor)), + uShowProgress: new THREE.Uniform(0), + uFallProgress: new THREE.Uniform(0), + uParticleSize: new THREE.Uniform(this.guiParams.uParticleSize), + uTexture: new THREE.Uniform(digitTextures[0]), + }, + }) + + // 2枚ずつ重ねた数字のPointsを12個生成(6桁分) + digits = Array.from({ length: 12 }, (_, i) => { + const material = baseMaterial.clone() + const points = new THREE.Points(planeGeometry, material) + points.renderOrder = i + // インデックスが偶数なら表示、奇数なら非表示 + const isVisible = i % 2 === 0 + points.visible = isVisible + // timer グループに追加 + timer.add(points) + const indexFromLast = Math.trunc((11 - i) / 2) + return { object: points, visible: isVisible, delay: 0.015 * indexFromLast } + }) + + // colon + const colonMaterial = baseMaterial.clone() + colonMaterial.uniforms.uTexture.value = textureLoader.load( + '/post/particle-clock/images/colon.png', + ) + // colonMaterial.uniforms.uShowProgress.value = 1 + const colon = new THREE.Points(planeGeometry, colonMaterial) + colons.push(colon, colon.clone()) + timer.add(...colons) + }, + setPositionCenter() { + const digitSpacing = 0.75 // 数字グループの幅 + const colonSpacing = 0.24 // コロンの幅 + const gap = 0.06 // 各グループ間の隙間 + const groups = [ + { type: 'digit', width: digitSpacing }, + { type: 'digit', width: digitSpacing }, + { type: 'colon', width: colonSpacing }, + { type: 'digit', width: digitSpacing }, + { type: 'digit', width: digitSpacing }, + { type: 'colon', width: colonSpacing }, + { type: 'digit', width: digitSpacing }, + { type: 'digit', width: digitSpacing }, + ] as const + + let planeIndex = 0 + let colonIndex = 0 + let currentX = 0 + + groups.forEach((group, i) => { + const groupCenter = currentX + group.width / 2 + if (group.type === 'digit') { + const frontPlane = digits[planeIndex].object + const backPlane = digits[planeIndex + 1].object + frontPlane.position.x = groupCenter + backPlane.position.x = groupCenter + planeIndex += 2 + } else if (group.type === 'colon') { + const colon = colons[colonIndex] + colon.position.x = groupCenter + timer.add(colon) + colonIndex++ + } + // 次のグループの開始位置 + currentX += group.width + gap + }) + + const timerBox = new THREE.Box3().setFromObject(timer) + const center = new THREE.Vector3() + timerBox.getCenter(center) + timer.position.x = -center.x + }, + setDom() { + containerRef = this.$refs.container as HTMLDivElement | null + }, + setGui() { + console.log('setGui') + gui.addColor(this.guiParams, 'bgColor').onChange((color: string) => { + webGL.scene.background = new THREE.Color(color) + }) + gui.addColor(this.guiParams, 'primaryColor').onChange((color: string) => { + for (const digit of digits) { + digit.object.material.uniforms.uColor.value.set(color) + } + for (const colon of colons) { + colon.material.uniforms.uColor.value.set(color) + } + }) + gui.addColor(this.guiParams, 'secondaryColor').onChange((color: string) => { + for (const digit of digits) { + digit.object.material.uniforms.uFinalColor.value.set(color) + } + }) + gui + .add(this.guiParams, 'uParticleSize') + .min(0.001) + .max(0.1) + .step(0.001) + .onChange((value: number) => { + for (const digit of digits) { + digit.object.material.uniforms.uParticleSize.value = value + } + for (const colon of colons) { + colon.material.uniforms.uParticleSize.value = value + } + }) + }, + updateTime() { + const now = new Date() + const hours = now.getHours().toString().padStart(2, '0') + const minutes = now.getMinutes().toString().padStart(2, '0') + const seconds = now.getSeconds().toString().padStart(2, '0') + // 時刻を分解して、6つの数字にする(例:"12:34:56" → ['1','2','3','4','5','6']) + const oldTimeDigits = timeDigits + const newTimeDigits = [...hours, ...minutes, ...seconds] as DigitTuple + + // 各桁ごとに更新が必要かチェックして、該当する要素のみ更新する + for (let i = 0; i < newTimeDigits.length; i++) { + if (oldTimeDigits[i] === newTimeDigits[i]) continue + + const i2 = i * 2 + const digitValue = parseInt(newTimeDigits[i], 10) + const frontPoint = digits[i2] + const backPoint = digits[i2 + 1] + if (frontPoint.visible) { + this.hideObject(frontPoint) + setTimeout( + () => { + this.showObject(backPoint) + }, + 300 + backPoint.delay * 1000, + ) + backPoint.object.material.uniforms.uTexture.value = digitTextures[digitValue] + } else { + this.hideObject(backPoint) + setTimeout( + () => { + this.showObject(frontPoint) + }, + 300 + frontPoint.delay * 1000, + ) + frontPoint.object.material.uniforms.uTexture.value = digitTextures[digitValue] + } + // update + timeDigits = newTimeDigits + } + }, + hideObject(target: DigitItem) { + target.visible = false + gsap.fromTo( + target.object.material.uniforms.uFallProgress, + { + value: 0, + }, + { + value: 1, + duration: 1.25, + delay: target.delay, + overwrite: true, + ease: 'power2.inOut', + onComplete: () => { + target.object.visible = false + // reset progress + target.object.material.uniforms.uFallProgress.value = 0 + }, + }, + ) + }, + showObject(target: DigitItem) { + target.visible = true + target.object.visible = true + gsap.fromTo( + target.object.material.uniforms.uShowProgress, + { + value: 0, + }, + { + value: 1, + duration: 0.7, + overwrite: true, + }, + ) + }, + showColons() { + for (const colon of colons) { + gsap.fromTo( + colon.material.uniforms.uShowProgress, + { + value: 0, + }, + { + value: 1, + duration: 0.7, + overwrite: true, + }, + ) + } + }, + onResize() {}, + setupTick() { + webGL.tick = () => { + for (const digit of digits) { + digit.object.material.uniforms.uTime.value = webGL.elapsedTime + } + } + }, + } +}) diff --git a/src/scripts/webGL/setupStage.ts b/src/scripts/webGL/setupStage.ts new file mode 100644 index 0000000..30c1eaa --- /dev/null +++ b/src/scripts/webGL/setupStage.ts @@ -0,0 +1,134 @@ +import Stats from 'stats-gl' +import * as THREE from 'three' +import { OrbitControls } from 'three/addons/controls/OrbitControls.js' + +export class SetupStage { + container: HTMLElement + width: number = 0 + height: number = 0 + + clock: THREE.Clock + elapsedTime: number = 0 + + scene: THREE.Scene = new THREE.Scene() + camera: THREE.Camera + renderer: THREE.WebGLRenderer + ambientLight: THREE.AmbientLight | undefined + orbitControls: OrbitControls | undefined + pixelRatio: number = Math.min(2, window.devicePixelRatio) + tick: (() => void) | undefined = undefined + + stats: any + + constructor(options: { + container: HTMLElement + perspectiveCamera?: boolean + orthographicCamera?: boolean + ambientLight: boolean + }) { + const { + container, + perspectiveCamera = true, + orthographicCamera = false, + ambientLight, + } = options + + this.container = container + this.width = this.container.offsetWidth + this.height = this.container.offsetHeight + + this.clock = new THREE.Clock() + this.scene = new THREE.Scene() + + this.setCamera(perspectiveCamera, orthographicCamera) + + this.renderer = new THREE.WebGLRenderer({ + antialias: true, + alpha: true, + }) + this.container.appendChild(this.renderer.domElement) + + if (ambientLight) { + this.ambientLight = new THREE.AmbientLight(0xffffff, 6) + this.scene.add(this.ambientLight) + } + + // OrbitControls + if (this.camera instanceof THREE.PerspectiveCamera) { + this.orbitControls = new OrbitControls(this.camera, this.renderer.domElement) + this.orbitControls.enableDamping = true + } + + this.resize() + this.setupResize() + this.render() + + if (import.meta.env.DEV) { + this.stats = new Stats() + document.body.append(this.stats.domElement) + this.stats.begin() + } + } + + setCamera(perspectiveCamera: boolean, orthographicCamera: boolean) { + if (perspectiveCamera) { + this.camera = new THREE.PerspectiveCamera(70, this.width / this.height, 0.01, 100) + this.camera.position.z = 3 + } else if (orthographicCamera) { + this.camera = new THREE.OrthographicCamera( + -this.width * 0.5, + this.width * 0.5, + this.height * 0.5, + -this.height * 0.5, + 0.01, + 1000, + ) + this.camera.position.z = 1 + } + if (this.camera) { + this.scene.add(this.camera) + } + } + + setupResize() { + window.addEventListener('resize', this.resize.bind(this)) + } + + resize() { + this.width = this.container.offsetWidth + this.height = this.container.offsetHeight + this.pixelRatio = Math.min(2, window.devicePixelRatio) + + this.renderer.setSize(this.width, this.height) + this.renderer.setPixelRatio(this.pixelRatio) + + if (this.camera instanceof THREE.PerspectiveCamera) { + this.camera.aspect = this.width / this.height + this.camera.updateProjectionMatrix() + } + if (this.camera instanceof THREE.OrthographicCamera) { + this.camera.left = this.width * -0.5 + this.camera.right = this.width * 0.5 + this.camera.top = this.height * 0.5 + this.camera.bottom = this.height * -0.5 + this.camera.updateProjectionMatrix() + } + } + + protected render() { + this.elapsedTime = this.clock.getElapsedTime() + + if (this.orbitControls) { + this.orbitControls.update() + } + if (this.tick) { + this.tick() + } + if (this.stats) { + this.stats.update() + } + + this.renderer.render(this.scene, this.camera) + window.requestAnimationFrame(this.render.bind(this)) + } +} diff --git a/src/scripts/webGL/shaders/includes/remap.glsl b/src/scripts/webGL/shaders/includes/remap.glsl new file mode 100644 index 0000000..aa83226 --- /dev/null +++ b/src/scripts/webGL/shaders/includes/remap.glsl @@ -0,0 +1,4 @@ +float remap(float value, float originMin, float originMax, float destinationMin, float destinationMax) +{ + return destinationMin + (value - originMin) * (destinationMax - destinationMin) / (originMax - originMin); +} \ No newline at end of file diff --git a/src/scripts/webGL/shaders/includes/simplexNoise3d.glsl b/src/scripts/webGL/shaders/includes/simplexNoise3d.glsl new file mode 100644 index 0000000..6b983c4 --- /dev/null +++ b/src/scripts/webGL/shaders/includes/simplexNoise3d.glsl @@ -0,0 +1,71 @@ +// Simplex 3D Noise +// by Ian McEwan, Ashima Arts +// +vec4 permute(vec4 x){ return mod(((x*34.0)+1.0)*x, 289.0); } +vec4 taylorInvSqrt(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; } + +float simplexNoise3d(vec3 v) +{ + const vec2 C = vec2(1.0/6.0, 1.0/3.0) ; + const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); + + // First corner + vec3 i = floor(v + dot(v, C.yyy) ); + vec3 x0 = v - i + dot(i, C.xxx) ; + + // Other corners + vec3 g = step(x0.yzx, x0.xyz); + vec3 l = 1.0 - g; + vec3 i1 = min( g.xyz, l.zxy ); + vec3 i2 = max( g.xyz, l.zxy ); + + // x0 = x0 - 0. + 0.0 * C + vec3 x1 = x0 - i1 + 1.0 * C.xxx; + vec3 x2 = x0 - i2 + 2.0 * C.xxx; + vec3 x3 = x0 - 1. + 3.0 * C.xxx; + + // Permutations + i = mod(i, 289.0 ); + vec4 p = permute( permute( permute( i.z + vec4(0.0, i1.z, i2.z, 1.0 )) + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) + i.x + vec4(0.0, i1.x, i2.x, 1.0 )); + + // Gradients + // ( N*N points uniformly over a square, mapped onto an octahedron.) + float n_ = 1.0/7.0; // N=7 + vec3 ns = n_ * D.wyz - D.xzx; + + vec4 j = p - 49.0 * floor(p * ns.z *ns.z); // mod(p,N*N) + + vec4 x_ = floor(j * ns.z); + vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N) + + vec4 x = x_ *ns.x + ns.yyyy; + vec4 y = y_ *ns.x + ns.yyyy; + vec4 h = 1.0 - abs(x) - abs(y); + + vec4 b0 = vec4( x.xy, y.xy ); + vec4 b1 = vec4( x.zw, y.zw ); + + vec4 s0 = floor(b0)*2.0 + 1.0; + vec4 s1 = floor(b1)*2.0 + 1.0; + vec4 sh = -step(h, vec4(0.0)); + + vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ; + vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ; + + vec3 p0 = vec3(a0.xy,h.x); + vec3 p1 = vec3(a0.zw,h.y); + vec3 p2 = vec3(a1.xy,h.z); + vec3 p3 = vec3(a1.zw,h.w); + + // Normalise gradients + vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3))); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + + // Mix final noise value + vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0); + m = m * m; + return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3) ) ); +} \ No newline at end of file diff --git a/src/scripts/webGL/shaders/particleClock/fragment.glsl b/src/scripts/webGL/shaders/particleClock/fragment.glsl new file mode 100644 index 0000000..a103ab0 --- /dev/null +++ b/src/scripts/webGL/shaders/particleClock/fragment.glsl @@ -0,0 +1,24 @@ +uniform vec3 uFinalColor; + +varying vec3 vColor; +varying float vAlpha; +varying float vIntensity; + +void main() +{ + if (vIntensity < 0.08) { + discard; + } + vec2 pointUv = gl_PointCoord; + vec3 finalColor = vec3(1.0); + float distanceToCenter = length(pointUv - vec2(0.5)); + if (distanceToCenter > 0.5) + { + discard; + } + + finalColor = mix(vColor, uFinalColor, 1.0 - vAlpha); + gl_FragColor = vec4(finalColor, vAlpha); + #include + #include +} diff --git a/src/scripts/webGL/shaders/particleClock/vertex.glsl b/src/scripts/webGL/shaders/particleClock/vertex.glsl new file mode 100644 index 0000000..06cad79 --- /dev/null +++ b/src/scripts/webGL/shaders/particleClock/vertex.glsl @@ -0,0 +1,58 @@ +uniform vec2 uResolution; +uniform float uTime; +uniform vec3 uColor; +uniform sampler2D uTexture; +uniform float uShowProgress; +uniform float uParticleSize; +uniform float uFallProgress; + +varying vec3 vColor; +varying float vAlpha; +varying float vIntensity; + +#include ../includes/simplexNoise3d.glsl +#include ../includes/remap.glsl + +float easeInOutQuad(float x) { + return x < 0.5 ? 2.0 * x * x : -1.0 + (4.0 - 2.0 * x) * x; +} + +void main() +{ + // Fall animation + float fallProgress = 1.0 - uFallProgress; + float noiseValue = simplexNoise3d(position * 4.0 + uTime); + noiseValue = (noiseValue + 1.0) / 2.0; + float delay = noiseValue * 0.7; + // 各パーティクルの個別進行度を計算 + float individualFallProgress = clamp((uFallProgress - delay) / (1.0 - delay), 0.0, 1.0); + float alphaProgress = remap(individualFallProgress, 0.2, 0.5, 0.0, 1.0); + float fallDistance = 1.0; + + // Show animation + float individualShowProgress = clamp((uShowProgress - delay) / (1.0 - delay), 0.0, 1.0); + float showAlphaProgress = remap(individualShowProgress, 0.75, 0.9, 0.0, 1.0); + + vec3 newPosition = position; + newPosition.y -= individualFallProgress * fallDistance; + newPosition.y += (1.0 - individualShowProgress) * fallDistance; + + // Final position + vec4 modelPosition = modelMatrix * vec4(newPosition, 1.0); + vec4 viewPosition = viewMatrix * modelPosition; + vec4 projectedPosition = projectionMatrix * viewPosition; + gl_Position = projectedPosition; + + // Picture + float pictureIntensity = texture(uTexture, uv).r; + pictureIntensity = easeInOutQuad(pictureIntensity); + + // Point size + gl_PointSize = uParticleSize * pictureIntensity * uResolution.y; + gl_PointSize *= (1.0 / - viewPosition.z); + + // Varyings + vColor = uColor; + vAlpha = (1.0 - alphaProgress) * showAlphaProgress; + vIntensity = pictureIntensity * uShowProgress; +} diff --git a/tsconfig.json b/tsconfig.json index d85be1e..52ffe50 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,11 @@ "compilerOptions": { "types": ["vitest/importMeta"], "jsx": "react-jsx", - "jsxImportSource": "react" + "jsxImportSource": "react", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } }, "include": ["src"], "exclude": ["dist", "node_modules"]