Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

122 changes: 122 additions & 0 deletions src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<defs>
<filter id="${fid}" x="-15%" y="-5%" width="140%" height="600%"
color-interpolation-filters="sRGB">
<!-- Turbulence noise ≈ fragment shader noise function -->
<feTurbulence type="turbulence" baseFrequency="0.04 0.07"
numOctaves="4" seed="${seed}" stitchTiles="stitch" result="turb"/>
<!-- Displacement map: y-channel drives downward gravity flow -->
<feDisplacementMap in="SourceGraphic" in2="turb"
xChannelSelector="R" yChannelSelector="G"
scale="0" id="${fid}-d"/>
</filter>
</defs>`
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<HTMLElement>(
'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()

Expand Down Expand Up @@ -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
Expand Down
Loading