Skip to content

Commit dff8b99

Browse files
committed
fix(landing): preserve the footer liquid morph
1 parent cff92dd commit dff8b99

2 files changed

Lines changed: 54 additions & 24 deletions

File tree

apps/sim/app/(landing)/components/footer/components/footer-wordmark-loop/footer-wordmark-loop.test.tsx

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,9 @@ describe('FooterWordmarkLoop', () => {
8989
expect(html).toContain('aria-hidden="true"')
9090
expect(html).toContain('data-stage="wm" opacity="1"')
9191
expect(html).toContain('data-stage="orb" opacity="0"')
92-
expect(html).toContain('stdDeviation="0.55"')
93-
expect(html).toContain('data-goo-group="" filter="none"')
92+
expect(html).toContain('stdDeviation="0"')
93+
expect(html).toMatch(/filter="url\(#fwl-goo-/)
94+
expect(html).toContain('values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"')
9495
expect(html).not.toContain('<feGaussianBlur in="goo"')
9596
for (const shape of SHAPES) {
9697
expect(html).toContain(`data-stage="${shape}" opacity="0"`)
@@ -102,14 +103,14 @@ describe('FooterWordmarkLoop', () => {
102103

103104
it('plays the master timeline: wordmark, orb, the seven shapes, orb, wordmark', () => {
104105
expect(attr('[data-stage="wm"]', 'opacity')).toBe('1.0000')
105-
expect(attr('[data-goo]', 'stdDeviation')).toBe('0.550')
106-
expect(attr('[data-goo-group]', 'filter')).toBe('none')
106+
expect(attr('[data-goo]', 'stdDeviation')).toBe('0.000')
107+
expect(attr('[data-goo-matrix]', 'values')).toMatch(/1\.000 -?0\.000$/)
107108

108109
advanceTo(2700)
109110
expect(attr('[data-stage="wm"]', 'opacity')).toBe('0.0000')
110111
expect(attr('[data-stage="orb"]', 'opacity')).toBe('1.0000')
111112
expect(attr('[data-goo]', 'stdDeviation')).toBe('5.000')
112-
expect(attr('[data-goo-group]', 'filter')).toMatch(/^url\(#fwl-goo-/)
113+
expect(attr('[data-goo-matrix]', 'values')).toMatch(/40\.000 -19\.000$/)
113114

114115
advanceTo(3900)
115116
expect(attr('[data-stage="metaballs"]', 'opacity')).toBe('1.0000')
@@ -126,25 +127,51 @@ describe('FooterWordmarkLoop', () => {
126127
advanceTo(16000)
127128
expect(attr('[data-stage="wm"]', 'opacity')).toBe('1.0000')
128129
expect(attr('[data-stage="thinking"]', 'opacity')).toBe('0.0000')
129-
expect(attr('[data-goo]', 'stdDeviation')).toBe('0.550')
130-
expect(attr('[data-goo-group]', 'filter')).toBe('none')
130+
expect(attr('[data-goo]', 'stdDeviation')).toBe('0.000')
131+
expect(attr('[data-goo-matrix]', 'values')).toMatch(/1\.000 -?0\.000$/)
131132

132133
advanceTo(CYCLE_MS + 2700)
133134
expect(attr('[data-stage="orb"]', 'opacity')).toBe('1.0000')
134135
expect(attr('[data-stage="wm"]', 'opacity')).toBe('0.0000')
135136
})
136137

137-
it('returns to an unfiltered wordmark when reduced motion is enabled mid-morph', () => {
138+
it('returns to an identity filter when reduced motion is enabled mid-morph', () => {
138139
advanceTo(2700)
139-
expect(attr('[data-goo-group]', 'filter')).toMatch(/^url\(#fwl-goo-/)
140+
expect(attr('[data-goo-matrix]', 'values')).toMatch(/40\.000 -19\.000$/)
140141

141142
reducedMotion = true
142143
act(() => onMotionPreference?.())
143144

144145
expect(pending).toHaveLength(0)
145146
expect(attr('[data-stage="wm"]', 'opacity')).toBe('1.0000')
146147
expect(attr('[data-stage="orb"]', 'opacity')).toBe('0.0000')
147-
expect(attr('[data-goo-group]', 'filter')).toBe('none')
148+
expect(attr('[data-goo-matrix]', 'values')).toMatch(/1\.000 -?0\.000$/)
149+
expect(attr('[data-goo]', 'stdDeviation')).toBe('0.000')
150+
})
151+
152+
it('eases the same filter to identity at both wordmark boundaries', () => {
153+
advanceTo(1300)
154+
expect(attr('[data-goo]', 'stdDeviation')).toBe('0.000')
155+
expect(attr('[data-goo-matrix]', 'values')).toMatch(/1\.000 -?0\.000$/)
156+
157+
advanceTo(1301)
158+
expect(Number(attr('[data-goo]', 'stdDeviation'))).toBeLessThan(0.001)
159+
expect(attr('[data-goo-matrix]', 'values')).toMatch(/1\.000 -?0\.000$/)
160+
161+
advanceTo(1800)
162+
expect(attr('[data-goo-matrix]', 'values')).toMatch(/40\.000 -19\.000$/)
163+
164+
advanceTo(2500)
165+
expect(attr('[data-goo]', 'stdDeviation')).toBe('5.000')
166+
167+
advanceTo(15199)
168+
expect(Number(attr('[data-goo]', 'stdDeviation'))).toBeLessThan(0.001)
169+
expect(attr('[data-goo-matrix]', 'values')).toMatch(/1\.000 -?0\.000$/)
170+
171+
advanceTo(15200)
172+
expect(attr('[data-goo]', 'stdDeviation')).toBe('0.000')
173+
expect(attr('[data-goo-matrix]', 'values')).toMatch(/1\.000 -?0\.000$/)
174+
expect(host?.querySelector('feComposite')).toBeNull()
148175
})
149176

150177
it('stops requesting frames on unmount', () => {

apps/sim/app/(landing)/components/footer/components/footer-wordmark-loop/footer-wordmark-loop.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,7 @@ interface StageNode {
356356

357357
interface GooFilterNodes {
358358
blur: SVGFEGaussianBlurElement
359-
group: SVGGElement
360-
url: string
359+
matrix: SVGFEColorMatrixElement
361360
}
362361

363362
/**
@@ -415,13 +414,17 @@ function paintFrame(
415414
smooth(T_LOGO_HOLD_END, T_INTRO_END, t),
416415
1 - smooth(T_OUTRO_START, T_OUTRO_END, t)
417416
)
418-
const deviation = round(GOO_LO + (GOO_HI - GOO_LO) * liquid)
417+
/** Ease the filter to identity at rest without overlaying the unfiltered shapes. */
418+
const strength = Math.min(
419+
smooth(T_LOGO_HOLD_END, T_LOGO_HOLD_END + MORPH, t),
420+
1 - smooth(T_OUTRO_END - MORPH, T_OUTRO_END, t)
421+
)
422+
const deviation = round((GOO_LO + (GOO_HI - GOO_LO) * liquid) * strength)
419423
if (goo.blur.getAttribute('stdDeviation') !== deviation) {
420424
goo.blur.setAttribute('stdDeviation', deviation)
421425
}
422-
/** Resting vectors retain native antialiasing; only the liquid morph needs raster filtering. */
423-
const filter = liquid > 0 ? goo.url : 'none'
424-
if (goo.group.getAttribute('filter') !== filter) goo.group.setAttribute('filter', filter)
426+
const matrix = `1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 ${round(1 + 39 * strength)} ${round(-19 * strength)}`
427+
if (goo.matrix.getAttribute('values') !== matrix) goo.matrix.setAttribute('values', matrix)
425428
}
426429

427430
interface FooterWordmarkLoopProps {
@@ -463,9 +466,9 @@ export function FooterWordmarkLoop({ className }: FooterWordmarkLoopProps) {
463466
const svg = svgRef.current
464467
if (!svg) return
465468
const blur = svg.querySelector<SVGFEGaussianBlurElement>('[data-goo]')
466-
const group = svg.querySelector<SVGGElement>('[data-goo-group]')
467-
if (!blur || !group) return
468-
const goo: GooFilterNodes = { blur, group, url: `url(#${gooId})` }
469+
const matrix = svg.querySelector<SVGFEColorMatrixElement>('[data-goo-matrix]')
470+
if (!blur || !matrix) return
471+
const goo: GooFilterNodes = { blur, matrix }
469472

470473
const stages: StageNode[] = Array.from(
471474
svg.querySelectorAll<SVGGElement>('[data-stage]'),
@@ -528,7 +531,7 @@ export function FooterWordmarkLoop({ className }: FooterWordmarkLoopProps) {
528531
observer?.disconnect()
529532
reducedMotion?.removeEventListener('change', onMotionPreference)
530533
}
531-
}, [gooId])
534+
}, [])
532535

533536
return (
534537
<div className={cn('relative mx-auto aspect-[5/3] w-[clamp(180px,17vw,320px)]', className)}>
@@ -547,13 +550,14 @@ export function FooterWordmarkLoop({ className }: FooterWordmarkLoopProps) {
547550
height='160%'
548551
colorInterpolationFilters='sRGB'
549552
>
550-
<feGaussianBlur data-goo='' in='SourceGraphic' stdDeviation={GOO_LO} result='blur' />
553+
<feGaussianBlur data-goo='' in='SourceGraphic' stdDeviation={0} result='blur' />
551554
{/* A steep threshold: the melt between shapes keeps its liquid
552555
merges, but every edge resolves within a pixel, so the mark
553556
stays crisp at the cycle's full blur. */}
554557
<feColorMatrix
558+
data-goo-matrix=''
555559
in='blur'
556-
values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 40 -19'
560+
values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0'
557561
result='goo'
558562
/>
559563
</filter>
@@ -581,8 +585,7 @@ export function FooterWordmarkLoop({ className }: FooterWordmarkLoopProps) {
581585
</defs>
582586

583587
<g
584-
data-goo-group=''
585-
filter='none'
588+
filter={`url(#${gooId})`}
586589
fill={`url(#${inkId})`}
587590
stroke={`url(#${inkId})`}
588591
strokeWidth={0}

0 commit comments

Comments
 (0)