fix: performance fixes for dtmc animation

This commit is contained in:
Christian Andolf
2025-06-04 17:11:29 +02:00
parent 89fe73c1bf
commit 21566eb5b5
2 changed files with 52 additions and 70 deletions

View File

@@ -1,11 +1,6 @@
import {
type CSSProperties,
type MouseEvent,
type TouchEvent,
useEffect,
useRef,
useState,
} from "react"
"use client"
import { useEffect, useRef, useState } from "react"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
@@ -27,6 +22,7 @@ export default function DigitalTeamMemberCardContent({
const cardRef = useRef<HTMLDivElement>(null)
const [isHovering, setIsHovering] = useState(false)
const [coords, setCoords] = useState({ x: 0, y: 0 })
const shimmerRef = useRef<HTMLDivElement>(null)
const [rect, setRect] = useState<DOMRect>({
top: 0,
left: 0,
@@ -38,8 +34,6 @@ export default function DigitalTeamMemberCardContent({
y: 0,
toJSON() {},
})
let ticking = false
let animationFrame = 0
useEffect(() => {
const observer = new ResizeObserver(
@@ -56,82 +50,67 @@ export default function DigitalTeamMemberCardContent({
}
}, [])
function requestTick(
evt: MouseEvent<HTMLDivElement> | TouchEvent<HTMLDivElement>
) {
if (!ticking) {
animationFrame = requestAnimationFrame(update(evt))
function onInteractionMove(e: React.MouseEvent | React.TouchEvent) {
let x, y
if ("touches" in e) {
x = e.touches[0].clientX - rect.left
y = e.touches[0].clientY - rect.top
} else {
x = e.clientX - rect.left
y = e.clientY - rect.top
}
ticking = true
}
function onInteractionMove(
evt: MouseEvent<HTMLDivElement> | TouchEvent<HTMLDivElement>
) {
requestTick(evt)
}
function update(
evt: MouseEvent<HTMLDivElement> | TouchEvent<HTMLDivElement>
) {
return () => {
let x, y
if ("touches" in evt) {
x = evt.touches[0].clientX - rect.left
y = evt.touches[0].clientY - rect.top
} else {
x = evt.clientX - rect.left
y = evt.clientY - rect.top
}
setCoords({ x, y })
}
}
function getShimmerStyle(): CSSProperties {
const { x, y } = coords
const oppositeX = 100 - (x / rect.width) * 100
const oppositeY = 100 - (y / rect.height) * 100
return {
background: `radial-gradient(circle at ${oppositeX}% ${oppositeY}%, rgb(233 171 163 / 40%) 0%, rgb(255 255 255 / 0%) 50%)`,
opacity: isHovering ? 1 : 0,
}
}
function getSkewStyle(): CSSProperties {
const { x, y } = coords
const centerX = rect.width / 2
const centerY = rect.height / 2
const rotateY = ((x - centerX) / centerX) * 5 // Max 5 degrees
const rotateX = ((centerY - y) / centerY) * 5 // Max 5 degrees
return {
transform: isHovering
? `rotateX(${rotateX}deg) rotateY(${rotateY}deg)`
: "rotateX(0) rotateY(0)",
const rotateY = ((x - centerX) / centerX) * 8
const rotateX = ((centerY - y) / centerY) * 8
// Update shimmer position to be in the opposite corner
if (shimmerRef.current) {
// Calculate opposite position (invert percentage within bounds)
const oppositeX = 100 - (x / rect.width) * 100
const oppositeY = 100 - (y / rect.height) * 100
shimmerRef.current.style.background = `radial-gradient(
circle at ${oppositeX}% ${oppositeY}%,
rgba(233, 171, 163, 0.4) 0%,
rgba(255, 255, 255, 0) 50%
)`
}
setCoords({ x: rotateX, y: rotateY })
}
function handleInteractionStart() {
function onInteractionStart() {
setIsHovering(true)
}
function handleInteractionEnd() {
function onInteractionEnd() {
setIsHovering(false)
cancelAnimationFrame(animationFrame)
setCoords({ x: 0, y: 0 })
}
return (
<div
className={styles.cardContainer}
onMouseEnter={handleInteractionStart}
onMouseMove={onInteractionMove}
onMouseLeave={handleInteractionEnd}
onTouchStart={handleInteractionStart}
ref={cardRef}
onTouchStart={onInteractionStart}
onTouchMove={onInteractionMove}
onTouchEnd={handleInteractionEnd}
onTouchEnd={onInteractionEnd}
onMouseEnter={onInteractionStart}
onMouseMove={onInteractionMove}
onMouseLeave={onInteractionEnd}
>
<div className={styles.card} ref={cardRef} style={getSkewStyle()}>
<div className={styles.shimmer} style={getShimmerStyle()} />
<div
className={styles.card}
style={{
transform: `rotateX(${coords.x}deg) rotateY(${coords.y}deg)`,
}}
>
<div
className={styles.shimmer}
ref={shimmerRef}
style={{ opacity: isHovering ? 1 : 0 }}
/>
<div className={styles.content}>
<Typography variant="Tag/sm">
<div className={styles.top}>

View File

@@ -28,6 +28,7 @@
.shimmer {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 3;
opacity: 0;
transition: opacity 0.3s ease;
@@ -35,16 +36,18 @@
}
.card {
touch-action: none;
overflow: hidden;
border-radius: var(--Corner-radius-lg);
box-shadow: 0 2px 1px rgb(255 255 255 / 11%) inset;
padding: var(--Space-x2);
height: 400px;
width: 327px;
max-width: 100%;
color: var(--Text-Brand-OnPrimary-3-Accent);
background-color: var(--Surface-Brand-Primary-1-OnSurface-Default);
box-shadow: 0 4px 44px rgb(0 0 0 / 25%);
box-shadow:
0 2px 1px rgb(255 255 255 / 11%) inset,
0 4px 44px rgb(0 0 0 / 25%);
transition: transform 0.3s ease-out;
will-change: transform;
user-select: none;