mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-23 22:25:05 +00:00
*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>
241 lines
8.6 KiB
Vue
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>
|