mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 06:35:10 +00:00
fix(website): stabilize Lottie product showcase rendering
- LottieVideoPlayer: unify asset preloading (videos + images), use timeout
instead of transitionend so poster fade works even when display:none
- ProductShowcaseSection: lock Lottie aspect to 1056/784, restore items-stretch
for equal-height columns, let active accordion item flex-grow with
justify-between layout, wrap each scene in absolute layer to avoid
relative/absolute class conflict
- scene{1,2,3}.json: drop redundant Assets/ prefix on asset URLs
- scene1.json: rename embedded font family from 'PP Formula' to
'PP Formula Lottie' to stop @font-face from overriding global font weights
- Flatten animations/scene*/scene*.json to animations/scene*.json
Amp-Thread-ID: https://ampcode.com/threads/T-019e0ce9-0296-730f-9c91-162432c4b8e6
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1075,19 +1075,19 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{ "id": "2", "e": 0, "w": 531, "h": 1422, "p": "2.png", "u": "Assets/" },
|
||||
{ "id": "3", "e": 0, "w": 134, "h": 175, "p": "3.png", "u": "Assets/" },
|
||||
{ "id": "4", "e": 0, "w": 112, "h": 140, "p": "4.png", "u": "Assets/" },
|
||||
{ "id": "5", "e": 0, "w": 112, "h": 140, "p": "5.png", "u": "Assets/" },
|
||||
{ "id": "6", "e": 0, "w": 112, "h": 140, "p": "6.png", "u": "Assets/" },
|
||||
{ "id": "7", "e": 0, "w": 112, "h": 140, "p": "7.png", "u": "Assets/" },
|
||||
{ "id": "8", "e": 0, "w": 112, "h": 140, "p": "8.png", "u": "Assets/" },
|
||||
{ "id": "9", "e": 0, "w": 112, "h": 140, "p": "9.png", "u": "Assets/" },
|
||||
{ "id": "10", "e": 0, "w": 683, "h": 754, "p": "10.png", "u": "Assets/" },
|
||||
{ "id": "11", "e": 0, "w": 683, "h": 754, "p": "11.png", "u": "Assets/" },
|
||||
{ "id": "12", "e": 0, "w": 683, "h": 754, "p": "12.png", "u": "Assets/" },
|
||||
{ "id": "13", "e": 0, "w": 683, "h": 754, "p": "13.png", "u": "Assets/" },
|
||||
{ "id": "14", "e": 0, "w": 683, "h": 754, "p": "14.png", "u": "Assets/" },
|
||||
{ "id": "15", "e": 0, "w": 683, "h": 754, "p": "15.png", "u": "Assets/" }
|
||||
{ "id": "2", "e": 0, "w": 531, "h": 1422, "p": "2.png", "u": "" },
|
||||
{ "id": "3", "e": 0, "w": 134, "h": 175, "p": "3.png", "u": "" },
|
||||
{ "id": "4", "e": 0, "w": 112, "h": 140, "p": "4.png", "u": "" },
|
||||
{ "id": "5", "e": 0, "w": 112, "h": 140, "p": "5.png", "u": "" },
|
||||
{ "id": "6", "e": 0, "w": 112, "h": 140, "p": "6.png", "u": "" },
|
||||
{ "id": "7", "e": 0, "w": 112, "h": 140, "p": "7.png", "u": "" },
|
||||
{ "id": "8", "e": 0, "w": 112, "h": 140, "p": "8.png", "u": "" },
|
||||
{ "id": "9", "e": 0, "w": 112, "h": 140, "p": "9.png", "u": "" },
|
||||
{ "id": "10", "e": 0, "w": 683, "h": 754, "p": "10.png", "u": "" },
|
||||
{ "id": "11", "e": 0, "w": 683, "h": 754, "p": "11.png", "u": "" },
|
||||
{ "id": "12", "e": 0, "w": 683, "h": 754, "p": "12.png", "u": "" },
|
||||
{ "id": "13", "e": 0, "w": 683, "h": 754, "p": "13.png", "u": "" },
|
||||
{ "id": "14", "e": 0, "w": 683, "h": 754, "p": "14.png", "u": "" },
|
||||
{ "id": "15", "e": 0, "w": 683, "h": 754, "p": "15.png", "u": "" }
|
||||
]
|
||||
}
|
||||
@@ -1141,19 +1141,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "video_1",
|
||||
"w": 1056,
|
||||
"h": 784,
|
||||
"u": "Assets/",
|
||||
"p": "eye.webm",
|
||||
"e": 0
|
||||
},
|
||||
{ "id": "video_1", "w": 1056, "h": 784, "u": "", "p": "eye.webm", "e": 0 },
|
||||
{
|
||||
"id": "video_2",
|
||||
"w": 1056,
|
||||
"h": 784,
|
||||
"u": "Assets/",
|
||||
"u": "",
|
||||
"p": "dusk_mountains.webm",
|
||||
"e": 0
|
||||
},
|
||||
@@ -1161,7 +1154,7 @@
|
||||
"id": "video_3",
|
||||
"w": 1056,
|
||||
"h": 784,
|
||||
"u": "Assets/",
|
||||
"u": "",
|
||||
"p": "clouds.webm",
|
||||
"e": 0
|
||||
},
|
||||
@@ -1169,7 +1162,7 @@
|
||||
"id": "video_4",
|
||||
"w": 1056,
|
||||
"h": 784,
|
||||
"u": "Assets/",
|
||||
"u": "",
|
||||
"p": "swings.webm",
|
||||
"e": 0
|
||||
},
|
||||
@@ -1177,7 +1170,7 @@
|
||||
"id": "video_5",
|
||||
"w": 1056,
|
||||
"h": 784,
|
||||
"u": "Assets/",
|
||||
"u": "",
|
||||
"p": "kyrie.webm",
|
||||
"e": 0
|
||||
},
|
||||
@@ -1185,7 +1178,7 @@
|
||||
"id": "video_6",
|
||||
"w": 1056,
|
||||
"h": 784,
|
||||
"u": "Assets/",
|
||||
"u": "",
|
||||
"p": "eat_it_dance.webm",
|
||||
"e": 0
|
||||
},
|
||||
@@ -1193,7 +1186,7 @@
|
||||
"id": "video_7",
|
||||
"w": 1056,
|
||||
"h": 784,
|
||||
"u": "Assets/",
|
||||
"u": "",
|
||||
"p": "flower.webm",
|
||||
"e": 0
|
||||
},
|
||||
@@ -1201,7 +1194,7 @@
|
||||
"id": "video_8",
|
||||
"w": 1056,
|
||||
"h": 784,
|
||||
"u": "Assets/",
|
||||
"u": "",
|
||||
"p": "paul_trillo.webm",
|
||||
"e": 0
|
||||
},
|
||||
@@ -1209,7 +1202,7 @@
|
||||
"id": "video_9",
|
||||
"w": 1056,
|
||||
"h": 784,
|
||||
"u": "Assets/",
|
||||
"u": "",
|
||||
"p": "dududu.webm",
|
||||
"e": 0
|
||||
}
|
||||
@@ -22,57 +22,85 @@ const emit = defineEmits<{ ready: [] }>()
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg'
|
||||
const XLINK_NS = 'http://www.w3.org/1999/xlink'
|
||||
|
||||
const POSTER_FADE_MS = 500
|
||||
|
||||
const lottieContainer = useTemplateRef<HTMLDivElement>('lottieContainer')
|
||||
const assetsReady = ref(false)
|
||||
const posterFaded = ref(!poster)
|
||||
let anim: AnimationItem | null = null
|
||||
let loadGen = 0
|
||||
let fadeTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function swapImagesForVideos(container: HTMLElement): HTMLVideoElement[] {
|
||||
watch(assetsReady, (ready) => {
|
||||
if (fadeTimer) {
|
||||
clearTimeout(fadeTimer)
|
||||
fadeTimer = null
|
||||
}
|
||||
if (!ready) {
|
||||
posterFaded.value = !poster
|
||||
return
|
||||
}
|
||||
if (!poster) {
|
||||
posterFaded.value = true
|
||||
return
|
||||
}
|
||||
fadeTimer = setTimeout(() => {
|
||||
posterFaded.value = true
|
||||
fadeTimer = null
|
||||
}, POSTER_FADE_MS)
|
||||
})
|
||||
|
||||
function swapImageForVideo(
|
||||
image: SVGImageElement,
|
||||
href: string
|
||||
): HTMLVideoElement {
|
||||
const width = image.getAttribute('width') ?? '0'
|
||||
const height = image.getAttribute('height') ?? '0'
|
||||
const fo = document.createElementNS(SVG_NS, 'foreignObject')
|
||||
fo.setAttribute('x', '0')
|
||||
fo.setAttribute('y', '0')
|
||||
fo.setAttribute('width', width)
|
||||
fo.setAttribute('height', height)
|
||||
const v = document.createElement('video')
|
||||
v.src = href
|
||||
v.autoplay = true
|
||||
v.loop = true
|
||||
v.muted = true
|
||||
v.playsInline = true
|
||||
v.setAttribute('playsinline', '')
|
||||
v.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;'
|
||||
fo.appendChild(v)
|
||||
image.replaceWith(fo)
|
||||
return v
|
||||
}
|
||||
|
||||
function whenLoaded(el: HTMLVideoElement | HTMLImageElement): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (el instanceof HTMLVideoElement && el.readyState >= 2) return resolve()
|
||||
if (el instanceof HTMLImageElement && el.complete) return resolve()
|
||||
const event = el instanceof HTMLVideoElement ? 'loadeddata' : 'load'
|
||||
el.addEventListener(event, () => resolve(), { once: true })
|
||||
el.addEventListener('error', () => resolve(), { once: true })
|
||||
})
|
||||
}
|
||||
|
||||
function prepareAssets(container: HTMLElement): Promise<void> {
|
||||
const svg = container.querySelector('svg')
|
||||
if (!svg) return []
|
||||
const videos: HTMLVideoElement[] = []
|
||||
if (!svg) return Promise.resolve()
|
||||
const pending: Promise<void>[] = []
|
||||
for (const image of Array.from(svg.querySelectorAll('image'))) {
|
||||
const href =
|
||||
image.getAttribute('href') ?? image.getAttributeNS(XLINK_NS, 'href') ?? ''
|
||||
if (!/\.(webm|mp4)$/i.test(href)) continue
|
||||
const width = image.getAttribute('width') ?? '0'
|
||||
const height = image.getAttribute('height') ?? '0'
|
||||
const fo = document.createElementNS(SVG_NS, 'foreignObject')
|
||||
fo.setAttribute('x', '0')
|
||||
fo.setAttribute('y', '0')
|
||||
fo.setAttribute('width', width)
|
||||
fo.setAttribute('height', height)
|
||||
const v = document.createElement('video')
|
||||
v.src = href
|
||||
v.autoplay = true
|
||||
v.loop = true
|
||||
v.muted = true
|
||||
v.playsInline = true
|
||||
v.setAttribute('playsinline', '')
|
||||
v.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;'
|
||||
fo.appendChild(v)
|
||||
image.replaceWith(fo)
|
||||
videos.push(v)
|
||||
if (!href) continue
|
||||
if (/\.(webm|mp4)$/i.test(href)) {
|
||||
pending.push(whenLoaded(swapImageForVideo(image, href)))
|
||||
} else {
|
||||
const img = new Image()
|
||||
img.src = href
|
||||
pending.push(whenLoaded(img))
|
||||
}
|
||||
}
|
||||
return videos
|
||||
}
|
||||
|
||||
function waitForVideosReady(videos: HTMLVideoElement[]): Promise<void> {
|
||||
if (videos.length === 0) return Promise.resolve()
|
||||
return new Promise((resolve) => {
|
||||
let pending = videos.length
|
||||
const finish = () => {
|
||||
pending--
|
||||
if (pending <= 0) resolve()
|
||||
}
|
||||
for (const v of videos) {
|
||||
if (v.readyState >= 2) finish()
|
||||
else {
|
||||
v.addEventListener('loadeddata', finish, { once: true })
|
||||
v.addEventListener('error', finish, { once: true })
|
||||
}
|
||||
}
|
||||
})
|
||||
return Promise.all(pending).then(() => undefined)
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -98,8 +126,7 @@ watch(
|
||||
created.addEventListener('DOMLoaded', () => {
|
||||
if (gen !== loadGen || anim !== created) return
|
||||
created.goToAndStop(0, true)
|
||||
const videos = swapImagesForVideos(container)
|
||||
void waitForVideosReady(videos).then(() => {
|
||||
void prepareAssets(container).then(() => {
|
||||
if (gen !== loadGen || anim !== created) return
|
||||
assetsReady.value = true
|
||||
emit('ready')
|
||||
@@ -115,7 +142,7 @@ watch(
|
||||
)
|
||||
|
||||
watch(
|
||||
() => assetsReady.value && playing,
|
||||
() => assetsReady.value && posterFaded.value && playing,
|
||||
(shouldPlay) => {
|
||||
if (!anim) return
|
||||
if (shouldPlay) anim.goToAndPlay(0, true)
|
||||
@@ -124,6 +151,7 @@ watch(
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (fadeTimer) clearTimeout(fadeTimer)
|
||||
anim?.destroy()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -15,7 +15,7 @@ const SHOWCASE_CDN = 'https://media.comfy.org/website/homepage/showcase'
|
||||
type LottieConfig = { src: string; assetsPath: string; poster: string }
|
||||
|
||||
const lottieScene = (scene: string): LottieConfig => ({
|
||||
src: `/animations/${scene}/${scene}.json`,
|
||||
src: `/animations/${scene}.json`,
|
||||
assetsPath: `${SHOWCASE_CDN}/${scene}/`,
|
||||
poster: `${SHOWCASE_CDN}/${scene}/poster.webp`
|
||||
})
|
||||
@@ -75,11 +75,11 @@ useIntersectionObserver(sectionRef, ([entry]) => {
|
||||
<!-- Content area -->
|
||||
<div class="mt-12 flex flex-col lg:mt-24 lg:flex-row lg:items-stretch">
|
||||
<!-- Lottie area (desktop only) -->
|
||||
<div class="hidden flex-1 lg:flex">
|
||||
<div class="hidden flex-1 lg:block">
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'rounded-5xl relative flex w-full items-center justify-center overflow-hidden p-0.5',
|
||||
'rounded-5xl relative aspect-1056/784 max-h-160 w-full overflow-hidden p-0.5',
|
||||
isVisible && 'animate-border-spin'
|
||||
)
|
||||
"
|
||||
@@ -87,22 +87,26 @@ useIntersectionObserver(sectionRef, ([entry]) => {
|
||||
<div
|
||||
class="bg-primary-comfy-ink relative size-full overflow-hidden rounded-[calc(2.5rem-2px)]"
|
||||
>
|
||||
<LottieVideoPlayer
|
||||
<div
|
||||
v-for="(feature, i) in features"
|
||||
v-show="isVisible"
|
||||
:key="feature.title"
|
||||
:src="feature.lottie.src"
|
||||
:assets-path="feature.lottie.assetsPath"
|
||||
:poster="feature.lottie.poster"
|
||||
:playing="activeIndex === i"
|
||||
poster-class="bg-transparency-white-t4"
|
||||
:class="
|
||||
cn(
|
||||
'bg-transparency-white-t4 absolute inset-0 size-full transition-opacity duration-300 will-change-[opacity]',
|
||||
'absolute inset-0 transition-opacity duration-300 will-change-[opacity]',
|
||||
activeIndex === i ? 'opacity-100' : 'opacity-0'
|
||||
)
|
||||
"
|
||||
/>
|
||||
>
|
||||
<LottieVideoPlayer
|
||||
:src="feature.lottie.src"
|
||||
:assets-path="feature.lottie.assetsPath"
|
||||
:poster="feature.lottie.poster"
|
||||
:playing="activeIndex === i"
|
||||
poster-class="bg-transparency-white-t4"
|
||||
class="bg-transparency-white-t4 size-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,7 +152,11 @@ useIntersectionObserver(sectionRef, ([entry]) => {
|
||||
<!-- Accordion item with connector -->
|
||||
<div
|
||||
:class="
|
||||
cn('flex items-stretch', activeIndex !== i && 'mt-4 lg:mt-0')
|
||||
cn(
|
||||
'flex items-stretch',
|
||||
activeIndex !== i && 'mt-4 lg:mt-0',
|
||||
activeIndex === i && 'lg:flex-1'
|
||||
)
|
||||
"
|
||||
>
|
||||
<img
|
||||
@@ -162,9 +170,9 @@ useIntersectionObserver(sectionRef, ([entry]) => {
|
||||
type="button"
|
||||
:class="
|
||||
cn(
|
||||
'rounded-5xl w-full cursor-pointer p-8 text-left transition-colors duration-300',
|
||||
'rounded-5xl flex w-full cursor-pointer flex-col justify-between p-8 text-left transition-colors duration-300',
|
||||
activeIndex === i
|
||||
? 'bg-primary-comfy-yellow text-primary-comfy-ink'
|
||||
? 'bg-primary-comfy-yellow text-primary-comfy-ink lg:h-full'
|
||||
: 'bg-transparency-white-t4 text-primary-comfy-canvas lg:ml-5'
|
||||
)
|
||||
"
|
||||
|
||||
Reference in New Issue
Block a user