Skip to content

feat(hero-anim): Add mobile hero animation logic. Misc cleanup relate… #1025

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 64 additions & 85 deletions assets/javascripts/new-javascripts/hero.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const heroAnimation = async () => {
const heroAnimation = async (animContainer) => {
const isReduceMotionEnabled = window.matchMedia(
'(prefers-reduced-motion: reduce)',
).matches
Expand All @@ -14,6 +14,10 @@ const heroAnimation = async () => {
})
}

// Skip to visible portion of animation when cropped on small screens
const { left, width } = animContainer.getClientRects()[0]
const offScreenDelta = Math.abs(left) / width

const heroSwoops = [
{
canvas: document.querySelector('#purple-swoop'),
Expand All @@ -25,7 +29,7 @@ const heroAnimation = async () => {
lineWidth: 210,
debugColor: 'purple',
image: null,
state: { progress: 0 },
state: { progress: offScreenDelta },
},
{
canvas: document.querySelector('#white-swoop-1'),
Expand All @@ -37,7 +41,7 @@ const heroAnimation = async () => {
lineWidth: 140,
debugColor: 'red',
image: null,
state: { progress: 0 },
state: { progress: offScreenDelta },
},
{
canvas: document.querySelector('#white-swoop-2'),
Expand All @@ -49,7 +53,7 @@ const heroAnimation = async () => {
lineWidth: 73.6,
debugColor: 'cyan',
image: null,
state: { progress: 0 },
state: { progress: offScreenDelta },
},
{
canvas: document.querySelector('#orange-swoop-bottom'),
Expand All @@ -61,7 +65,7 @@ const heroAnimation = async () => {
lineWidth: 202.2,
debugColor: 'yellow',
image: null,
state: { progress: 0 },
state: { progress: offScreenDelta },
},
{
canvas: document.querySelector('#orange-swoop-top'),
Expand All @@ -73,7 +77,7 @@ const heroAnimation = async () => {
lineWidth: 163.4,
debugColor: 'green',
image: null,
state: { progress: 0 },
state: { progress: offScreenDelta },
},
]
const logo = {
Expand All @@ -84,8 +88,9 @@ const heroAnimation = async () => {
position: [610, 672.5],
imagePath: '/assets/images/landing-page/hero/bird.png',
image: null,
state: { progress: 0 },
state: { progress: offScreenDelta },
}

const initSwoops = ({
path,
pathLength,
Expand All @@ -96,13 +101,12 @@ const heroAnimation = async () => {
image,
}) => {
const ctx = canvas.getContext('2d')
// Convert position value to account for the center anchor point in AE
// The reference animation's transform origin is in the center of the canvas
// We're not going to reset this as it will make pulling values directly from AE easier
ctx.translate(posX - image.naturalWidth / 2, posY - image.naturalHeight / 2)
// Set mask styles
ctx.lineWidth = lineWidth
ctx.lineCap = 'round'
// Convert SVG path pulled from AE masks
let pathInstance = new Path2D(path)

if (!isReduceMotionEnabled) {
Expand All @@ -127,13 +131,10 @@ const heroAnimation = async () => {
positionEnd: [endX, endY],
}) => {
const ctx = canvas.getContext('2d')
// Same reason for conversion as initSwoops
// Applying this conversion for the same purpose as init swoops
ctx.translate(posX - image.naturalWidth / 2, posY - image.naturalHeight / 2)

if (!isReduceMotionEnabled) {
ctx.globalAlpha = 0
ctx.drawImage(image, 0, 0)
} else {
if (isReduceMotionEnabled) {
ctx.globalAlpha = 1
const deltaX = endX - posX
const deltaY = endY - posY
Expand All @@ -144,144 +145,119 @@ const heroAnimation = async () => {
}

try {
// load swoop image
// Load swoop images
const swoopImages = await Promise.all(
heroSwoops.map((swoop) => loadImage(swoop.imagePath)),
)
// load logo
// Load logo
const logoImage = await loadImage(logo.imagePath)

logo.image = logoImage
// init canvas for each swoop layer
// Init canvas for each swoop layer
heroSwoops.forEach((swoop, i) => {
swoop.image = swoopImages[i]
const canvasData = initSwoops(swoop)
swoop.ctx = canvasData.ctx
swoop.pathInstance = canvasData.pathInstance
})
// init logo canvas
// Init logo canvas
logo.ctx = initLogo(logo)
} catch (error) {
console.error('Error loading images:', error)
throw error
}

// Skip animation if reduced motion is enabled
if (isReduceMotionEnabled) {
return
}

const DURATION = 1000
const DURATION = 1000 - 1000 * offScreenDelta

const tl = anime.createTimeline({
defaults: { duration: DURATION, ease: 'inOut(.8)' },
})

tl.label('start', 0)

// white swoop 1
const swoopUpdate = ({
state,
ctx,
pathLength,
pathInstance,
image,
canvas,
}) => {
// Clear canvas before next draw
ctx.clearRect(0, 0, canvas.width, canvas.height)
// Progress line dash offset
ctx.lineDashOffset = pathLength * (1 - state.progress)
// Draw stroke
ctx.stroke(pathInstance)
// Source-in will allow us to only draw as far as the stroke
ctx.globalCompositeOperation = 'source-in'
ctx.drawImage(image, 0, 0)
// Reset to default for our next stroke paint
ctx.globalCompositeOperation = 'source-out'
}

// White swoop 1
tl.add(
heroSwoops[1].state,
{
progress: 1,
duration: 950,
onUpdate: () => {
const { state, ctx, pathLength, pathInstance, image, canvas } =
heroSwoops[1]
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.lineDashOffset = pathLength * (1 - state.progress)
ctx.stroke(pathInstance)
ctx.globalCompositeOperation = 'source-in'
ctx.drawImage(image, 0, 0)
ctx.globalCompositeOperation = 'source-out'
},
duration: 950 - 950 * offScreenDelta,
onUpdate: () => swoopUpdate(heroSwoops[1]),
},
'start',
)
// // purple swoop
// Purple swoop
tl.add(
heroSwoops[0].state,
{
progress: 1,
duration: 950,
onUpdate: () => {
const { state, ctx, pathLength, pathInstance, image, canvas } =
heroSwoops[0]
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.lineDashOffset = pathLength * (1 - state.progress)
ctx.stroke(pathInstance)
ctx.globalCompositeOperation = 'source-in'
ctx.drawImage(image, 0, 0)
ctx.globalCompositeOperation = 'source-out'
},
duration: 950 - 950 * offScreenDelta,
onUpdate: () => swoopUpdate(heroSwoops[0]),
},
'start',
)
// // white swoop 2 swoop
// White swoop 2
tl.add(
heroSwoops[2].state,
{
progress: 1,
onUpdate: () => {
const { state, ctx, pathLength, pathInstance, image, canvas } =
heroSwoops[2]
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.lineDashOffset = pathLength * (1 - state.progress)
ctx.stroke(pathInstance)
ctx.globalCompositeOperation = 'source-in'
ctx.drawImage(image, 0, 0)
ctx.globalCompositeOperation = 'source-out'
},
onUpdate: () => swoopUpdate(heroSwoops[2]),
},
'start',
)
// // orange swoop bottom
// Orange swoop bottom
tl.add(
heroSwoops[3].state,
{
progress: 1,
onUpdate: () => {
const { state, ctx, pathLength, pathInstance, image, canvas } =
heroSwoops[3]
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.lineDashOffset = pathLength * (1 - state.progress)
ctx.stroke(pathInstance)
ctx.globalCompositeOperation = 'source-in'
ctx.drawImage(image, 0, 0)
ctx.globalCompositeOperation = 'source-out'
},
onUpdate: () => swoopUpdate(heroSwoops[3]),
},
'start',
)
// orange top
// Orange top
tl.add(
heroSwoops[4].state,
{
progress: 1,
// ease: 'inOutQuad',
duration: 480,
delay: 520,
onUpdate: () => {
const { state, ctx, pathLength, pathInstance, image, canvas } =
heroSwoops[4]
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.lineDashOffset = pathLength * (1 - state.progress)
ctx.stroke(pathInstance)
ctx.globalCompositeOperation = 'source-in'
ctx.drawImage(image, 0, 0)
ctx.globalCompositeOperation = 'source-out'
},
duration: 480 - 480 * offScreenDelta,
delay: 520 - 520 * offScreenDelta,
onUpdate: () => swoopUpdate(heroSwoops[4]),
},
'start',
)
// logo
// Logo
tl.add(
logo.state,
{
ease: 'out(1.1)',
duration: 200,
delay: 750,
duration: 200 - 200 * offScreenDelta,
delay: 750 - 750 * offScreenDelta,
progress: 1,
// ease: 'inOutQuad',
onUpdate: () => {
const {
state: { progress },
Expand All @@ -292,6 +268,7 @@ const heroAnimation = async () => {
positionEnd: [endX, endY],
} = logo
ctx.clearRect(0, 0, canvas.width, canvas.height)
// Progresses logo opacity from 0 to 1
ctx.globalAlpha = progress
const deltaX = (endX - startX) * progress
const deltaY = (endY - startY) * progress
Expand All @@ -302,10 +279,12 @@ const heroAnimation = async () => {
)
}

// Start animation when container is mounted
const observer = new MutationObserver(() => {
if (document.querySelector('.animation-container')) {
const animContainer = document.querySelector('.animation-container')
if (animContainer) {
observer.disconnect()
heroAnimation()
heroAnimation(animContainer)
}
})

Expand Down
2 changes: 1 addition & 1 deletion assets/stylesheets/new-stylesheets/pages/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ $icons: (
}

.animation-container {
@include noise();
aspect-ratio: 1248 / 1116;
top: calc(66px - 9.5vw);
left: 0;
width: 57vw;
Expand Down