Files
ComfyUI_frontend/apps/website/src/components/home/UseCaseSection.vue
Yourz 2322a5a497 fix: use webm video for VFX use case right asset (#12040)
*PR Created by the Glary-Bot Agent*

---

## Summary

Replaces `right1.webp` with `right1.webm` in the VFX panel of
`UseCaseSection`. `BlobMedia` already auto-detects `.webm` URLs and
mounts a `<video>` element (with the `.webp` as poster), so this single
URL swap is the only change required — matching the pattern used by the
other 4 use-case panels.

## Files changed

- `apps/website/src/components/home/UseCaseSection.vue` — swap
`right1.webp` → `right1.webm`.

## Verification

- `pnpm exec nx run website:typecheck` — clean
- `pnpm exec eslint` on changed file — clean
- `pnpm exec oxfmt --check` — clean
- pre-commit lint-staged hooks — passed

## Reviewer note (Oracle finding)

VFX is the default active panel, so the homepage's initial right-rail
asset moves from ~131 KB `.webp` to ~4.1 MB `.webm`. Behaviorally
consistent with the other 4 panels (which already use `.webm`), but
worth confirming whether `right1.webm` should be re-encoded smaller on
the CDN before promoting this PR out of draft.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12040-fix-use-webm-video-for-VFX-use-case-right-asset-3596d73d365081829976f37b733840f1)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-05-07 09:32:53 +00:00

241 lines
8.6 KiB
Vue

<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'
import { whenever } from '@vueuse/core'
import { computed, ref, useId, watch } from 'vue'
import type { Locale } from '../../i18n/translations'
import { t } from '../../i18n/translations'
import { externalLinks } from '../../config/routes'
import { useParallax } from '../../composables/useParallax'
import { usePinScrub, VH_PER_ITEM } from '../../composables/usePinScrub'
import BrandButton from '../common/BrandButton.vue'
import BlobMedia from './BlobMedia.vue'
import BlobRail from './BlobRail.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
interface Category {
label: string
leftSrc: string
rightSrc: string
rightObjectPosition?: 'top' | 'bottom' | 'center'
}
const categories: Category[] = [
{
label: t('useCase.vfx', locale),
leftSrc: 'https://media.comfy.org/website/homepage/use-case/left1.webm',
rightSrc: 'https://media.comfy.org/website/homepage/use-case/right1.webm'
},
{
label: t('useCase.advertising', locale),
leftSrc: 'https://media.comfy.org/website/homepage/use-case/left2.webm',
rightSrc: 'https://media.comfy.org/website/homepage/use-case/right2.webm'
},
{
label: t('useCase.gaming', locale),
leftSrc: 'https://media.comfy.org/website/homepage/use-case/left3.webm',
rightSrc: 'https://media.comfy.org/website/homepage/use-case/right3.webp'
},
{
label: t('useCase.ecommerce', locale),
leftSrc: 'https://media.comfy.org/website/homepage/use-case/left4.webm',
rightSrc: 'https://media.comfy.org/website/homepage/use-case/right4.webm',
rightObjectPosition: 'top'
},
{
label: t('useCase.more', locale),
leftSrc: 'https://media.comfy.org/website/homepage/use-case/left5.webm',
rightSrc: 'https://media.comfy.org/website/homepage/use-case/right5.webm'
}
]
const sectionRef = ref<HTMLElement>()
const contentRef = ref<HTMLElement>()
const navRef = ref<HTMLElement>()
const leftImgRef = ref<HTMLElement>()
const rightImgRef = ref<HTMLElement>()
const {
activeIndex: activeCategory,
isEnabled,
isPinned,
scrollToIndex
} = usePinScrub(
{ section: sectionRef, content: contentRef, nav: navRef },
{ itemCount: categories.length }
)
const activeLeft = computed(() => categories[activeCategory.value].leftSrc)
const activeRight = computed(() => categories[activeCategory.value].rightSrc)
const activeRightObjectPosition = computed(
() => categories[activeCategory.value].rightObjectPosition
)
const uid = useId()
const leftBlobId = `left-blob-${uid}`
const rightBlobId = `right-blob-${uid}`
function navButtons() {
return navRef.value?.querySelectorAll<HTMLButtonElement>(':scope > button')
}
whenever(isPinned, () => {
navButtons()?.[activeCategory.value]?.focus({ preventScroll: true })
})
watch(activeCategory, (index) => {
if (!isPinned.value) return
navButtons()?.[index]?.focus({ preventScroll: true })
})
function onNavKeydown(event: KeyboardEvent) {
const delta = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0
if (!delta) return
event.preventDefault()
const current = activeCategory.value
const next = current + delta
if (next < 0 || next >= categories.length) {
navButtons()?.[current]?.blur()
return
}
scrollToIndex(next)
navButtons()?.[next]?.focus({ preventScroll: true })
}
function onCategoryHover(index: number) {
if (isEnabled.value) return
activeCategory.value = index
}
function travelRange(el: HTMLElement) {
if (window.matchMedia('(min-width: 1024px)').matches) return 150
const rail = el.parentElement?.parentElement
if (!rail) return 0
const pb = parseFloat(getComputedStyle(rail).paddingBottom)
return Math.max(0, (rail.clientHeight - pb - el.offsetHeight) / 2)
}
const pinScrubEnd = `+=${categories.length * VH_PER_ITEM}%`
const parallaxMediaQuery = '(max-width: 1023px)'
useParallax([rightImgRef], {
trigger: sectionRef,
fromY: (el) => -travelRange(el),
y: (el) => travelRange(el),
start: 'top top',
end: pinScrubEnd,
mediaQuery: parallaxMediaQuery
})
useParallax([leftImgRef], {
trigger: sectionRef,
fromY: (el) => travelRange(el),
y: (el) => -travelRange(el),
start: 'top top',
end: pinScrubEnd,
mediaQuery: parallaxMediaQuery
})
</script>
<template>
<section
ref="sectionRef"
class="bg-primary-comfy-ink relative isolate overflow-x-clip pt-20 lg:h-[calc(100vh+60px)] lg:py-24"
>
<svg class="absolute size-0" width="0" height="0" aria-hidden="true">
<defs>
<clipPath :id="leftBlobId" clipPathUnits="objectBoundingBox">
<path
d="M0.314,0.988 C0.337,0.997 0.366,0.999 0.398,0.993 L0.600,0.949 L0.877,0.890 C0.945,0.876 1.000,0.828 1.000,0.784 L1.000,0.206 L1.000,0.195 L0.999,0.061 C0.999,0.040 0.986,0.021 0.962,0.011 C0.939,0.001 0.910,-0.001 0.879,0.007 L0.675,0.050 L0.398,0.109 C0.331,0.123 0.277,0.171 0.277,0.215 L0.277,0.314 C0.277,0.324 0.266,0.333 0.251,0.337 L0.121,0.365 C0.054,0.379 0.000,0.427 0.000,0.471 L0.000,0.504 L0.000,0.802 C0.000,0.823 0.014,0.841 0.037,0.851 C0.060,0.861 0.089,0.863 0.121,0.856 L0.229,0.833 C0.240,0.830 0.252,0.831 0.261,0.835 C0.270,0.839 0.275,0.845 0.275,0.852 L0.276,0.939 C0.276,0.960 0.289,0.978 0.314,0.988 Z"
/>
</clipPath>
<clipPath :id="rightBlobId" clipPathUnits="objectBoundingBox">
<path
d="M1,0.129 L0.187,0.005 C0.084,0 0,0.015 0,0.066 L0,0.104 L0,0.447 C0,0.472 0.022,0.500 0.058,0.523 C0.094,0.547 0.139,0.563 0.188,0.571 L0.356,0.599 C0.373,0.602 0.391,0.609 0.405,0.618 C0.419,0.627 0.427,0.637 0.427,0.645 L0.427,0.745 C0.427,0.770 0.448,0.798 0.485,0.821 C0.521,0.845 0.566,0.861 0.615,0.869 L0.734,0.890 L0.934,0.923 L1,0.934 Z"
/>
</clipPath>
</defs>
</svg>
<div
class="relative mx-auto grid w-full grid-cols-[minmax(0,5rem)_minmax(18ch,1fr)_minmax(0,5rem)] grid-rows-[auto_minmax(0,1fr)_auto] lg:h-full lg:grid-cols-[minmax(0,1fr)_minmax(24rem,42rem)_minmax(0,1fr)] lg:gap-x-0"
>
<!-- Label row -->
<div class="relative z-20 col-span-full flex justify-center py-4">
<p
class="text-primary-comfy-yellow from-primary-comfy-ink to-primary-comfy-ink/10 shrink grow-0 bg-linear-to-b text-center text-sm font-bold tracking-widest uppercase lg:text-base"
>
{{ t('useCase.label', locale) }}
</p>
</div>
<!-- Left blob rail -->
<BlobRail side="left">
<div ref="leftImgRef" class="size-full will-change-transform">
<BlobMedia :src="activeLeft" :clip-id="leftBlobId" />
</div>
</BlobRail>
<!-- Center content -->
<div class="relative z-10 min-h-0 overflow-hidden">
<div
ref="contentRef"
class="flex h-full flex-col items-center will-change-transform"
>
<nav
ref="navRef"
class="mt-16 flex w-full max-w-5/6 flex-1 flex-col items-center justify-evenly gap-12 lg:mt-[clamp(0.5rem,3vh,5rem)] lg:max-w-none lg:gap-[clamp(0.25rem,1vh,2rem)]"
:aria-label="t('useCase.navLabel', locale)"
@keydown="onNavKeydown"
>
<button
v-for="(category, index) in categories"
:key="category.label"
type="button"
:class="
cn(
'cursor-pointer text-center text-2xl font-light whitespace-pre-line transition-colors outline-none lg:text-[clamp(1rem,5vh,3rem)]',
index === activeCategory
? 'text-primary-comfy-canvas'
: 'text-primary-comfy-canvas/30 hover:text-primary-comfy-canvas/50'
)
"
:aria-current="index === activeCategory ? 'true' : undefined"
@click="scrollToIndex(index)"
@mouseenter="onCategoryHover(index)"
@focus="onCategoryHover(index)"
>
{{ category.label }}
</button>
</nav>
</div>
</div>
<!-- Right blob rail -->
<BlobRail side="right">
<div ref="rightImgRef" class="size-full will-change-transform">
<BlobMedia
:src="activeRight"
:clip-id="rightBlobId"
:object-position="activeRightObjectPosition"
/>
</div>
</BlobRail>
<div
class="col-span-full mt-8 flex flex-col items-center gap-8 px-4 lg:mt-[clamp(0.25rem,1vh,2rem)] lg:gap-[clamp(0.25rem,1vh,2rem)]"
>
<p class="text-primary-warm-gray max-w-md text-center text-base">
{{ t('useCase.body', locale) }}
</p>
<BrandButton :href="externalLinks.workflows" variant="outline">
{{ t('useCase.cta', locale) }}
</BrandButton>
</div>
</div>
</section>
</template>