diff --git a/package-lock.json b/package-lock.json index 66da983..65498e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,6 +126,7 @@ "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.1.0.tgz", "integrity": "sha512-Z9IYjuXSArkAUx3N6xj6+Bnvx8OdUSHA8YoOgyepp3+zJmtVYJIl/I18GozdJVW1p5u/CNpl3Km7/gwTJK85cw==", "license": "MIT", + "peer": true, "dependencies": { "prismjs": "^1.29.0" }, @@ -770,7 +771,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.2.tgz", "integrity": "sha512-jwtMmJa1BXXDCiDx1vC6SFN/+HfYG53UkfJa6qeN5ogvOunzbFDO3wISZy5n9xgYFUrEP6M7e8EG++riHNTv9w==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/component": "0.6.18", "@firebase/logger": "0.4.4", @@ -837,7 +837,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.4.2.tgz", "integrity": "sha512-LssbyKHlwLeiV8GBATyOyjmHcMpX/tFjzRUCS1jnwGAew1VsBB4fJowyS5Ud5LdFbYpJeS+IQoC+RQxpK7eH3Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/app": "0.13.2", "@firebase/component": "0.6.18", @@ -853,8 +852,7 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@firebase/auth": { "version": "1.10.8", @@ -1305,7 +1303,6 @@ "integrity": "sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -2246,6 +2243,7 @@ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz", "integrity": "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==", "license": "MIT", + "peer": true, "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", @@ -2260,6 +2258,7 @@ "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz", "integrity": "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==", "license": "MIT", + "peer": true, "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", @@ -2271,6 +2270,7 @@ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", "license": "MIT", + "peer": true, "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1" @@ -2281,6 +2281,7 @@ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz", "integrity": "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==", "license": "MIT", + "peer": true, "dependencies": { "@shikijs/types": "1.29.2" } @@ -2290,6 +2291,7 @@ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz", "integrity": "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==", "license": "MIT", + "peer": true, "dependencies": { "@shikijs/types": "1.29.2" } @@ -2299,6 +2301,7 @@ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", "license": "MIT", + "peer": true, "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" @@ -2504,7 +2507,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3515,7 +3517,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/entities": { "version": "6.0.1", @@ -5547,6 +5550,7 @@ "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", "integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==", "license": "MIT", + "peer": true, "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", @@ -5704,7 +5708,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5759,7 +5762,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5775,7 +5777,6 @@ "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.14.1.tgz", "integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==", "license": "MIT", - "peer": true, "dependencies": { "@astrojs/compiler": "^2.9.1", "prettier": "^3.0.0", @@ -5894,6 +5895,7 @@ "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==", "license": "MIT", + "peer": true, "dependencies": { "regex-utilities": "^2.3.0" } @@ -5903,6 +5905,7 @@ "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz", "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==", "license": "MIT", + "peer": true, "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" @@ -6202,7 +6205,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -6382,6 +6384,7 @@ "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.29.2.tgz", "integrity": "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==", "license": "MIT", + "peer": true, "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", @@ -6650,7 +6653,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7015,7 +7017,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -7641,7 +7642,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index a93b43a..5298821 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -174,6 +174,125 @@ const organizerTwitter = '@sunnyTech_MTP' ctx.restore() } + // Ink Gravity Shader — makes text vanish like ink washed away by rain. + // Uses SVG feTurbulence + feDisplacementMap as a fragment-shader-style + // displacement field, animated per element via requestAnimationFrame. + function applyInkGravityShader() { + if (document.getElementById('ink-gravity-styles')) return + + const styleEl = document.createElement('style') + styleEl.id = 'ink-gravity-styles' + styleEl.textContent = `.ink-dissolving { will-change: filter, transform, opacity; }` + document.head.appendChild(styleEl) + + let filterIdx = 0 + + function dissolveTextElement(el: HTMLElement) { + if (el.dataset.inkDissolving) return + el.dataset.inkDissolving = '1' + + // Inline elements need a block-level wrapper for filter to render correctly + let target: HTMLElement = el + if (window.getComputedStyle(el).display === 'inline') { + const wrapper = document.createElement('span') + wrapper.style.display = 'inline-block' + el.parentNode?.insertBefore(wrapper, el) + wrapper.appendChild(el) + target = wrapper + } + + filterIdx++ + const fid = `ink-g-${filterIdx}` + const seed = Math.floor(Math.random() * 999) + + // Each element gets its own SVG filter (unique seed & id) so + // animations are fully independent — just like per-pixel shader uniforms. + const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + svgEl.setAttribute('aria-hidden', 'true') + svgEl.style.cssText = 'position:fixed;width:0;height:0;overflow:hidden;pointer-events:none' + svgEl.innerHTML = ` + + + + + + + ` + document.body.appendChild(svgEl) + + target.style.filter = `url(#${fid})` + target.classList.add('ink-dissolving') + + const dispMap = svgEl.querySelector(`#${fid}-d`) as SVGElement | null + const start = performance.now() + const dur = 20000 + Math.random() * 15000 // 20 – 35 s: slow ink bleed + + // Use CSS transitions for transform/opacity so the browser compositor + // handles interpolation — gives hardware-accelerated smooth easing + // instead of 1-px-per-frame jumps driven by JS. + const durSec = (dur / 1000).toFixed(1) + target.style.transition = [ + `transform ${durSec}s cubic-bezier(0.2, 0, 0.6, 1)`, + `opacity ${durSec}s cubic-bezier(0.1, 0, 0.5, 1)`, + ].join(', ') + + // One rAF gap so the browser registers the transition before we + // set the target values that trigger it. + requestAnimationFrame(() => { + target.style.transform = 'translateY(120px)' + target.style.opacity = '0' + }) + + // Clean up once the opacity CSS transition finishes (fires once per property; + // checking propertyName avoids acting on the transform transitionend too). + target.addEventListener('transitionend', (e: TransitionEvent) => { + if (e.propertyName !== 'opacity') return + target.style.visibility = 'hidden' + svgEl.remove() + }) + + // SVG presentation attributes are not CSS-transitionable, so the + // displacement scale still needs a rAF loop with ease-in quadratic. + function frame(now: number) { + const p = Math.min((now - start) / dur, 1) + // Ease-in: displacement builds slowly then accelerates + dispMap?.setAttribute('scale', String(p * p * 40)) + if (p < 1) requestAnimationFrame(frame) + } + + requestAnimationFrame(frame) + } + + function scheduleDissolve() { + const candidates = Array.from( + document.querySelectorAll( + 'h1, h2, h3, h4, h5, h6, p, li, img, picture, figure, button, blockquote, pre' + ) + ).filter( + (el) => + !el.dataset.inkDissolving && + !el.closest('#rainy-tech-overlay') && + !el.closest('nav') && + el.getBoundingClientRect().height > 0 + ) + + if (candidates.length === 0) return + + // Dissolve a wave of 4–7 elements simultaneously + const waveSize = 4 + Math.floor(Math.random() * 4) + const shuffled = candidates.sort(() => Math.random() - 0.5).slice(0, waveSize) + shuffled.forEach(dissolveTextElement) + setTimeout(scheduleDissolve, 3000 + Math.random() * 4000) + } + + // Delay start: let the rain fall for a few seconds before text starts dissolving + setTimeout(scheduleDissolve, 4000) + } + function applyRainyTheme() { const now = new Date() @@ -315,6 +434,9 @@ const organizerTwitter = '@sunnyTech_MTP' document.body.appendChild(overlay) } + + // Activate ink-gravity shader: text dissolves like real ink in rain + applyInkGravityShader() } // Run on initial load and on Astro View Transitions navigation