Skip to content

Commit 66f74b0

Browse files
committed
Style updates to computer animation
1 parent 90cfa03 commit 66f74b0

20 files changed

Lines changed: 259 additions & 542 deletions

File tree

COVERS.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@ Design an abstract visualization of a wide funnel at the top receiving hundreds
1818

1919
### Prompt 4: Signal Amplification Through Reduction
2020

21-
Create a visual metaphor showing a radar or sonar screen with a single bright, clear ping in the center, surrounded by a faded halo showing where noise used to be. The clear ping should be labeled or styled to suggest it represents a critical, actionable alert. Style: military-tech aesthetic, dark background with green/cyan glow, vintage CRT monitor texture, 16:9 aspect ratio.
22-
23-
### Prompt 5: The Alert Audit Dashboard
24-
25-
Illustrate a stylized cloud monitoring dashboard showing alert metrics transforming from chaotic (left side: crowded charts, red indicators, overwhelmed UI) to clean (right side: sparse, green indicators, clear hierarchy). The transition should flow naturally across the image. Style: UI/UX focused illustration, modern SaaS aesthetic, subtle gradients, professional color palette, 16:9 aspect ratio.
21+
Create a visual metaphor showing a radar or sonar screen with a single bright, clear ping in the center, surrounded by a faded halo showing where noise used to be. The clear ping should be labeled or styled to suggest it represents a critical, actionable cloud monitoring alert. Style: military-tech aesthetic, dark background with green/cyan glow, vintage CRT monitor texture, 16:9 aspect ratio.
2622

2723
## api-deprecation-sunset-headers-consumer-migration
2824

src/components/Animations/Computers/client/__tests__/index.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ describe('ComputersAnimationElement', () => {
303303
})
304304
})
305305

306-
it('pauses when the element is not fully visible and resumes when it returns', async () => {
306+
it('pauses when the element is not visible and resumes when it returns', async () => {
307307
await renderComputersAnimation(async ({ element, window }) => {
308308
void window
309309
element.initialize()
@@ -315,8 +315,8 @@ describe('ComputersAnimationElement', () => {
315315
intersectionObserverCallback?.(
316316
[
317317
{
318-
isIntersecting: true,
319-
intersectionRatio: 0.5,
318+
isIntersecting: false,
319+
intersectionRatio: 0,
320320
} as unknown as IntersectionObserverEntry,
321321
],
322322
{} as IntersectionObserver

src/components/Animations/Computers/client/index.ts

Lines changed: 75 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ const Anticipate = {
4747
export class ComputersAnimationElement extends LitElement {
4848
private timeline: Timeline | null = null
4949
private initialized = false
50+
private animationReady = false
51+
private deferredInitRafId: number | null = null
52+
private deferredInitFrames = 0
5053
private animationController: AnimationControllerHandle | undefined
5154
private toggleButton: HTMLButtonElement | null = null
5255
private intersectionObserver: IntersectionObserver | undefined
@@ -111,36 +114,75 @@ export class ComputersAnimationElement extends LitElement {
111114
// is below the `lg` breakpoint, because it is not shown.
112115
if (this.isHiddenOnThisViewport()) {
113116
this.setAnimationState('paused')
117+
this.scheduleDeferredInitialization()
114118
this.initialized = true
115119
return
116120
}
117121

118-
this.startAnimation()
122+
this.setupAnimationAndController()
123+
this.initialized = true
124+
} catch (error) {
125+
handleScriptError(error, context)
126+
}
127+
}
128+
129+
private scheduleDeferredInitialization(): void {
130+
if (typeof window === 'undefined') return
131+
if (this.deferredInitRafId !== null) return
119132

120-
const defaultState = this.getDefaultAnimationState()
121-
this.setAnimationState(defaultState)
122-
if (defaultState === 'paused') {
123-
this.timeline?.pause(0)
133+
const tick = () => {
134+
this.deferredInitRafId = null
135+
136+
if (this.animationReady) {
137+
return
124138
}
125139

126-
this.animationController = createAnimationController({
127-
animationId: 'computers-animation',
128-
debugLabel: SCRIPT_NAME,
129-
defaultState,
130-
onPause: () => {
131-
this.pause()
132-
},
133-
onPlay: () => {
134-
this.resume()
135-
},
136-
})
140+
if (!this.isHiddenOnThisViewport()) {
141+
this.setupAnimationAndController()
142+
return
143+
}
137144

138-
this.setupVisibilityHandlers()
139-
this.syncPlaybackWithVisibility()
140-
this.initialized = true
141-
} catch (error) {
142-
handleScriptError(error, context)
145+
this.deferredInitFrames += 1
146+
if (this.deferredInitFrames >= 60) {
147+
return
148+
}
149+
150+
this.deferredInitRafId = window.requestAnimationFrame(tick)
143151
}
152+
153+
this.deferredInitRafId = window.requestAnimationFrame(tick)
154+
}
155+
156+
private setupAnimationAndController(): void {
157+
if (this.animationReady) return
158+
if (this.isHiddenOnThisViewport()) return
159+
160+
this.startAnimation()
161+
if (!this.timeline) return
162+
163+
// Start from a known baseline and let the global lifecycle store decide what happens next.
164+
// The store may immediately pause (reduced motion / persisted preference / overlays).
165+
this.timeline.play(0)
166+
this.setAnimationState('playing')
167+
168+
this.setupVisibilityHandlers()
169+
170+
this.animationController = createAnimationController({
171+
animationId: 'computers-animation',
172+
debugLabel: SCRIPT_NAME,
173+
defaultState: 'playing',
174+
initialState: 'playing',
175+
onPause: () => {
176+
this.pause()
177+
},
178+
onPlay: () => {
179+
this.resume()
180+
},
181+
})
182+
183+
// Ensure visibility rules (viewport/document) take effect even if the store chooses 'playing'.
184+
this.syncPlaybackWithVisibility()
185+
this.animationReady = true
144186
}
145187

146188
private isHiddenOnThisViewport(): boolean {
@@ -201,13 +243,12 @@ export class ComputersAnimationElement extends LitElement {
201243
this.intersectionObserver = new IntersectionObserverCtor(
202244
entries => {
203245
const entry = entries[0]
204-
const ratio = entry?.intersectionRatio ?? 0
205-
// Pause as soon as the element is even partially out of view.
206-
// Only consider it "in viewport" when it is fully visible.
207-
this.isInViewport = Boolean(entry?.isIntersecting && ratio >= 0.999)
246+
// Auto-play as long as *any* part of the animation is visible.
247+
// This avoids the hero animation being paused on load due to minor clipping.
248+
this.isInViewport = Boolean(entry?.isIntersecting)
208249
this.syncPlaybackWithVisibility()
209250
},
210-
{ threshold: [0, 0.999] }
251+
{ threshold: [0] }
211252
)
212253

213254
this.intersectionObserver.observe(this)
@@ -267,6 +308,12 @@ export class ComputersAnimationElement extends LitElement {
267308
this.resetToggleButton()
268309
this.removeAttribute('data-animation-state')
269310
this.initialized = false
311+
this.animationReady = false
312+
this.deferredInitFrames = 0
313+
if (this.deferredInitRafId !== null && typeof window !== 'undefined') {
314+
window.cancelAnimationFrame(this.deferredInitRafId)
315+
}
316+
this.deferredInitRafId = null
270317
} catch (error) {
271318
handleScriptError(error, context)
272319
}
@@ -315,6 +362,8 @@ export class ComputersAnimationElement extends LitElement {
315362

316363
if (!this.toggleButton) return
317364

365+
this.setupAnimationAndController()
366+
318367
const state = this.getAnimationState()
319368

320369
if (state === 'playing') {
@@ -339,25 +388,6 @@ export class ComputersAnimationElement extends LitElement {
339388
return state ?? 'playing'
340389
}
341390

342-
private getDefaultAnimationState(): AnimationPlayState {
343-
if (typeof window === 'undefined') {
344-
return 'playing'
345-
}
346-
347-
try {
348-
if (typeof window.matchMedia === 'function') {
349-
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
350-
if (mediaQuery.matches) {
351-
return 'paused'
352-
}
353-
}
354-
} catch {
355-
// Best effort only; fall through to playing
356-
}
357-
358-
return 'playing'
359-
}
360-
361391
private startAnimation() {
362392
if (typeof document === 'undefined') return
363393
if (document.getElementById('heroAnimation') == undefined) return

src/components/Animations/Computers/index.astro

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,19 @@ import Icon from '@components/Icon/index.astro'
99
<div class="relative w-full max-w-lg mx-auto">
1010
<button
1111
type="button"
12-
class="absolute bottom-1 right-1 z-(--z-raised) flex items-center justify-center rounded-full border border-trim bg-content-inverse p-1 shadow-lg transition-colors hover:bg-page-base-offset"
12+
class="absolute bottom-1 right-1 z-(--z-raised) flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-inverse transition-colors hover:bg-primary-offset"
1313
data-animation-toggle
1414
aria-label="Pause animation"
1515
aria-pressed="false"
1616
>
17-
<span data-animation-icon="pause">
18-
<Icon name="pause" size={20} />
17+
<span data-animation-icon="pause" class="flex h-10 w-10 items-center justify-center bg-transparent">
18+
<Icon name="pause" size={20} variant="custom" class="text-primary-inverse fill-primary-inverse stroke-primary-inverse" />
1919
</span>
20-
<span data-animation-icon="play" class="hidden">
21-
<Icon name="play" size={20} />
20+
<span
21+
data-animation-icon="play"
22+
class="flex h-10 w-10 items-center justify-center bg-transparent hidden"
23+
>
24+
<Icon name="play" size={20} variant="custom" class="text-primary-inverse fill-primary-inverse stroke-primary-inverse" />
2225
</span>
2326
</button>
2427
<div class="w-full h-auto rounded-2xl">
@@ -35,7 +38,7 @@ import Icon from '@components/Icon/index.astro'
3538
<clipPath id="monitorEdgeMask">
3639
<path
3740
class="monitorEdgeMask"
38-
fill="var(--color-gray-700)"
41+
fill="var(--theme-color-primary)"
3942
d="M559.7 79h-520C32.5 79 27 84.6 27 91.8V403h546V91.8c0-7.2-6.1-12.8-13.3-12.8z"
4043
></path>
4144
</clipPath>
@@ -60,15 +63,15 @@ import Icon from '@components/Icon/index.astro'
6063
<g class="monitorStandGroup" clip-path="url(#monitorStandMask)">
6164
<path
6265
class="monitorStand"
63-
fill="var(--color-gray-50)"
66+
fill="var(--color-primary)"
6467
d="M386.3 512H374l-20.7-57h-106L226 512h-12.3c-2.6 0-4.7 1.9-4.7 4.5s2.1 4.5 4.7 4.5h172.7c2.6 0 4.7-1.9 4.7-4.5s-2.2-4.5-4.8-4.5z"
6568
></path>
6669
</g>
67-
<path class="monitorStandShadow" fill="var(--color-gray-200)" d="M357.4 467H242.6l4.5-12h106"></path>
70+
<path class="monitorStandShadow" fill="var(--color-content-offset)" d="M357.4 467H242.6l4.5-12h106"></path>
6871
<g class="monitorEdgeGroup" clip-path="url(#monitorEdgeMask)">
6972
<path
7073
class="monitorEdge"
71-
fill="var(--color-gray-700)"
74+
fill="var(--color-primary)"
7275
d="M559.7 79h-520C32.5 79 27 84.6 27 91.8V403h546V91.8c0-7.2-6.1-12.8-13.3-12.8z"
7376
></path>
7477
<path
@@ -105,19 +108,19 @@ import Icon from '@components/Icon/index.astro'
105108
</g>
106109
<path
107110
class="monitorBottom"
108-
fill="var(--color-gray-50)"
111+
fill="var(--color-content-offset)"
109112
d="M573 403v39c0 7.1-6.1 13-13.3 13h-520c-7.2 0-12.7-5.9-12.7-13v-39h546z"></path>
110113
<circle class="monitorLogo" fill="var(--color-gray-400)" cx="300" cy="426" r="10"></circle>
111114
</g>
112115
<g class="laptopGroup">
113-
<path class="laptopEdgeLeft" fill="var(--color-gray-50)" d="M310 401.5H131.5v-226c0-3.9 3.2-7 7-7H310"
116+
<path class="laptopEdgeLeft" fill="var(--color-primary)" d="M310 401.5H131.5v-226c0-3.9 3.2-7 7-7H310"
114117
></path>
115-
<path class="laptopEdgeRight" fill="var(--color-gray-50)" d="M290 168.5h171.5c3.8 0 7 3.1 7 7v226H290"
118+
<path class="laptopEdgeRight" fill="var(--color-primary)" d="M290 168.5h171.5c3.8 0 7 3.1 7 7v226H290"
116119
></path>
117120
<g class="laptopBaseGroup" clip-path="url(#laptopBaseMask)">
118121
<path
119122
class="laptopBase"
120-
fill="var(--color-gray-50)"
123+
fill="var(--color-primary)"
121124
d="M487 409v15.5c0 3.8-3.2 7.5-7 7.5H120c-3.8 0-7-3.7-7-7.5V409h374z"></path>
122125
</g>
123126
<path class="laptopTrackpad" fill="var(--color-gray-400)" d="M326.7 421.8h-53.4l-5-7.6h63.4"></path>
@@ -156,7 +159,7 @@ import Icon from '@components/Icon/index.astro'
156159
<g class="phoneGroup" opacity="0">
157160
<path
158161
class="phoneEdge"
159-
fill="var(--color-gray-50)"
162+
fill="var(--color-primary)"
160163
d="M255.2 371.4V198.6c0-3.2 2.7-5.9 5.9-5.9H339c3.2 0 5.9 2.7 5.9 5.9v172.9c0 3.2-2.7 5.9-5.9 5.9h-77.9c-3.3-.1-5.9-2.7-5.9-6z"
161164
></path>
162165
<circle class="phoneButton" fill="var(--color-gray-400)" cx="300" cy="363.1" r="6"></circle>
@@ -201,7 +204,7 @@ import Icon from '@components/Icon/index.astro'
201204
<g class="tabletGroup">
202205
<path
203206
class="tabletEdge"
204-
fill="var(--color-gray-50)"
207+
fill="var(--color-primary)"
205208
d="M198.5 404.5v-239c0-7.7 6.3-14 14-14h175c7.7 0 14 6.3 14 14v239c0 7.7-6.3 14-14 14h-175c-7.7 0-14-6.3-14-14z"
206209
></path>
207210
<path

src/components/Hero/Home/__tests__/index.spec.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@ describe('Home Hero (Astro)', () => {
99
container = await AstroContainer.create()
1010
})
1111

12-
test('labels the hero section and hides decorative CTA arrow svgs', async () => {
12+
test('labels the hero section and renders the hardcoded CTAs', async () => {
1313
const HomeHero = (await import('@components/Hero/Home/index.astro')).default
1414

15-
const renderedHtml = await container.renderToString(HomeHero)
15+
const renderedHtml = await container.renderToString(HomeHero, {
16+
props: {
17+
pretitle:
18+
'I turn deployment nightmares into one-click operations. Legacy migrations, CI/CD pipelines, observability stacks—built to survive production.',
19+
benefits: ['Zero-downtime migrations', 'Self-healing infrastructure', 'Developer-friendly golden paths'],
20+
},
21+
})
1622

1723
await withJsdomEnvironment(async ({ window }) => {
1824
window.document.body.innerHTML = renderedHtml
@@ -24,17 +30,17 @@ describe('Home Hero (Astro)', () => {
2430
const title = window.document.getElementById('home-hero-title')
2531
expect(title?.tagName).toBe('H1')
2632

27-
const primaryLink = window.document.querySelector('a[href="/services/web-development"]')
33+
const primaryLink = window.document.querySelector('a[href="/about"]')
2834
expect(primaryLink).toBeTruthy()
29-
const primaryArrow = primaryLink?.querySelector('svg')
30-
expect(primaryArrow?.getAttribute('aria-hidden')).toBe('true')
31-
expect(primaryArrow?.getAttribute('focusable')).toBe('false')
35+
expect(primaryLink?.getAttribute('class')).toContain('bg-success')
3236

33-
const secondaryLink = window.document.querySelector('a[href="/services/consulting"]')
37+
const secondaryLink = window.document.querySelector('a[href="/contact"]')
3438
expect(secondaryLink).toBeTruthy()
35-
const secondaryArrow = secondaryLink?.querySelector('svg')
36-
expect(secondaryArrow?.getAttribute('aria-hidden')).toBe('true')
37-
expect(secondaryArrow?.getAttribute('focusable')).toBe('false')
39+
const secondaryClass = secondaryLink?.getAttribute('class') || ''
40+
expect(secondaryClass).toContain('decoration-dotted')
41+
expect(secondaryClass).toContain('focus-visible:decoration-dotted')
42+
expect(secondaryClass).toContain('hover:decoration-content-offset')
43+
expect(secondaryClass).toContain('focus-visible:decoration-content-offset')
3844
})
3945
})
4046
})

0 commit comments

Comments
 (0)