feat: hero animation plays on load in two-column layout

- Replace scroll-scrubbed animation with time-based gsap tween
- Loop infinitely at constant linear rate (4s per cycle)
- Switch to side-by-side layout: logo left (60%), text right (40%)
- Stacks vertically on mobile

Amp-Thread-ID: https://ampcode.com/threads/T-019d92fc-1970-7699-928b-f03cff3ed120
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
DrJKL
2026-04-15 14:17:46 -07:00
parent b807aa95cd
commit 9014937489

View File

@@ -1,48 +1,94 @@
<script setup lang="ts">
import { ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { useFrameScrub } from '../../composables/useFrameScrub'
import { prefersReducedMotion } from '../../composables/useReducedMotion'
import type { Locale } from '../../i18n/translations'
import { t } from '../../i18n/translations'
import { gsap } from '../../scripts/gsapSetup'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const canvasRef = ref<HTMLCanvasElement>()
useFrameScrub(canvasRef, {
frameCount: 75,
frameSrc: (i) =>
`/videos/hero-logo-seq/Logo${String(i).padStart(2, '0')}.webp`,
scrollTrigger: (canvas) => ({
trigger: document.documentElement,
start: 'top top',
end: () => {
const rect = canvas.getBoundingClientRect()
return `+=${rect.bottom + window.scrollY}`
},
scrub: 0.3
const FRAME_COUNT = 75
const images: HTMLImageElement[] = []
let ctx: gsap.Context | undefined
onMounted(() => {
if (!canvasRef.value || prefersReducedMotion()) return
const canvas = canvasRef.value
const draw = canvas.getContext('2d')
if (!draw) return
let loadedCount = 0
function drawFrame(frame: number) {
const index = Math.round(frame)
const img = images[index]
if (!img || !draw) return
canvas.width = img.width
canvas.height = img.height
draw.drawImage(img, 0, 0)
}
function onFrameReady() {
loadedCount++
if (loadedCount === FRAME_COUNT) {
drawFrame(0)
const proxy = { frame: 0 }
ctx = gsap.context(() => {
gsap.to(proxy, {
frame: FRAME_COUNT - 1,
duration: 4,
ease: 'none',
repeat: -1,
onUpdate() {
drawFrame(proxy.frame)
}
})
})
}
}
for (let i = 0; i < FRAME_COUNT; i++) {
const img = new Image()
img.src = `/videos/hero-logo-seq/Logo${String(i).padStart(2, '0')}.webp`
img.onload = onFrameReady
img.onerror = onFrameReady
images.push(img)
}
})
onUnmounted(() => {
images.forEach((img) => {
img.onload = null
img.onerror = null
})
images.length = 0
ctx?.revert()
})
</script>
<template>
<section
class="flex flex-col items-center px-4 py-16 lg:px-20 lg:pt-18 lg:pb-8"
class="relative flex min-h-screen flex-col lg:flex-row lg:items-center"
>
<h1
class="text-primary-comfy-canvas text-center text-5xl font-light whitespace-pre-line lg:text-8xl"
>
{{ t('hero.title', locale) }}
</h1>
<div class="mt-12 w-full max-w-3xl overflow-hidden rounded-2xl">
<div class="relative w-full lg:w-3/5">
<canvas ref="canvasRef" class="w-full" />
</div>
<p
class="text-primary-comfy-canvas mt-10 max-w-2xl text-center text-sm/relaxed lg:text-base"
>
{{ t('hero.subtitle', locale) }}
</p>
<div class="px-6 py-12 lg:w-2/5 lg:px-16">
<h1
class="text-primary-comfy-canvas text-4xl font-light whitespace-pre-line lg:text-6xl"
>
{{ t('hero.title', locale) }}
</h1>
<p
class="text-primary-comfy-canvas mt-8 max-w-lg text-sm/relaxed lg:text-base"
>
{{ t('hero.subtitle', locale) }}
</p>
</div>
</section>
</template>