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