Skip to content
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
61 changes: 19 additions & 42 deletions src/components/animations/NodesAnimation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { Card, Flex, Text } from "@code0-tech/pictor"
import { IconNote } from "@tabler/icons-react"
import { animate, m as motion, useInView, useMotionValue, useReducedMotion } from "motion/react"
import { useInView, useReducedMotion } from "motion/react"
import { useEffect, useRef, useState } from "react"
import { LiteralBadge } from "../badges/LiteralBadge"
import { ReferenceBadge } from "../badges/ReferenceBadge"
Expand Down Expand Up @@ -33,8 +33,8 @@ function NodeRow({
}) {
const listRef = useRef<HTMLDivElement>(null)
const [loopDistance, setLoopDistance] = useState(0)
const x = useMotionValue(direction === "left" ? 0 : -loopDistance)
const groupGap = 16
const velocity = 28
const iconColorMap: Record<NodeAccent, string> = {
brand: "var(--text-brand)",
yellow: "var(--text-yellow)",
Expand Down Expand Up @@ -64,53 +64,30 @@ function NodeRow({
const listElement = listRef.current
if (!listElement) return

let frame = 0
let previousDistance = 0
const resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0]
if (!entry) return

const updateLoopDistance = () => {
const nextDistance = Math.round(listElement.getBoundingClientRect().width + groupGap)
if (nextDistance !== previousDistance) {
previousDistance = nextDistance
setLoopDistance(nextDistance)
}
}

updateLoopDistance()

const resizeObserver = new ResizeObserver(() => {
cancelAnimationFrame(frame)
frame = requestAnimationFrame(updateLoopDistance)
setLoopDistance(Math.round(entry.contentRect.width + groupGap))
})
resizeObserver.observe(listElement)

return () => {
cancelAnimationFrame(frame)
resizeObserver.disconnect()
}
return () => resizeObserver.disconnect()
}, [nodes.length])

useEffect(() => {
if (!loopDistance || !active) {
x.set(direction === "left" ? 0 : -loopDistance)
return
}

const controls = animate(
x,
direction === "left" ? [0, -loopDistance] : [-loopDistance, 0],
{
duration: 45,
ease: "linear",
repeat: Infinity,
repeatType: "loop",
},
)

return () => controls.stop()
}, [active, direction, loopDistance, x])
const duration = loopDistance > 0 ? loopDistance / velocity : 0

return (
<motion.div className="flex w-max items-start gap-4 will-change-transform" style={{ x }}>
<div
className="flex w-max items-start gap-4 will-change-transform"
style={loopDistance > 0 ? {
animationName: direction === "left" ? "node-marquee-left" : "node-marquee-right",
animationDuration: `${duration}s`,
animationTimingFunction: "linear",
animationIterationCount: "infinite",
animationPlayState: active ? "running" : "paused",
} : undefined}
>
<div ref={listRef} className="flex items-start gap-4">
{nodes.map((node, index) => (
<Card
Expand Down Expand Up @@ -149,7 +126,7 @@ function NodeRow({
</Card>
))}
</div>
</motion.div>
</div>
)
}

Expand Down
62 changes: 18 additions & 44 deletions src/components/animations/RoleSystemAnimation.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client"

import { Badge, Card, Text } from "@code0-tech/pictor"
import { animate, m as motion, useInView, useMotionValue, useReducedMotion } from "motion/react"
import { useInView, useReducedMotion } from "motion/react"
import { useEffect, useRef, useState } from "react"

interface RoleItem {
Expand All @@ -21,60 +21,28 @@ export function RoleSystemAnimation({ roles }: RoleSystemAnimationProps) {
const isInView = useInView(containerRef, { amount: 0.2 })
const prefersReducedMotion = useReducedMotion()
const [loopDistance, setLoopDistance] = useState(0)
const y = useMotionValue(0)
const groupGap = 16
const velocity = 30

useEffect(() => {
const listElement = listRef.current
if (!listElement) return

let frame = 0
let previousDistance = 0
const resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0]
if (!entry) return

const updateLoopDistance = () => {
const nextDistance = Math.round(listElement.getBoundingClientRect().height + groupGap)
if (nextDistance !== previousDistance) {
previousDistance = nextDistance
setLoopDistance(nextDistance)
}
}

updateLoopDistance()

const resizeObserver = new ResizeObserver(() => {
cancelAnimationFrame(frame)
frame = requestAnimationFrame(updateLoopDistance)
setLoopDistance(Math.round(entry.contentRect.height + groupGap))
})
resizeObserver.observe(listElement)

return () => {
cancelAnimationFrame(frame)
resizeObserver.disconnect()
}
return () => resizeObserver.disconnect()
}, [roles.length])

useEffect(() => {
if (!loopDistance || !isInView || prefersReducedMotion) {
y.set(0)
return
}

const controls = animate(
y,
[0, -loopDistance],
{
duration: 20,
ease: "linear",
repeat: Infinity,
repeatType: "loop",
},
)

return () => controls.stop()
}, [isInView, loopDistance, prefersReducedMotion, y])

if (!roles.length) return null

const duration = loopDistance > 0 ? loopDistance / velocity : 0

const renderRoleCard = (role: RoleItem, index: number) => (
<Card
key={`${role.name}-${role.updatedAt}-${index}`}
Expand All @@ -99,17 +67,23 @@ export function RoleSystemAnimation({ roles }: RoleSystemAnimationProps) {

return (
<div ref={containerRef} className="relative flex h-full w-full cursor-default items-start justify-center overflow-hidden">
<motion.div
<div
className="flex flex-col items-center gap-4 will-change-transform"
style={{ y }}
style={loopDistance > 0 ? {
animationName: "role-marquee-up",
animationDuration: `${duration}s`,
animationTimingFunction: "linear",
animationIterationCount: "infinite",
animationPlayState: isInView && !prefersReducedMotion ? "running" : "paused",
} : undefined}
>
<div ref={listRef} className="flex flex-col items-center gap-4">
{roles.map(renderRoleCard)}
</div>
<div className="flex flex-col items-center gap-4" aria-hidden="true">
{roles.map((role, index) => renderRoleCard(role, index + roles.length))}
</div>
</motion.div>
</div>
</div>
)
}
119 changes: 85 additions & 34 deletions src/components/blog/TableOfContents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,29 +53,53 @@ export function TableOfContents({ headings }: TableOfContentsProps) {

if (!elements.length) return

const updateActiveHeading = () => {
const activationOffset = isMobile ? headingScrollOffset + 16 : desktopTopOffset + 24
let currentId = elements[0].id
const activationOffset = isMobile ? headingScrollOffset + 16 : desktopTopOffset + 24
const visibleHeadings = new Map<string, number>()

const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
const id = (entry.target as HTMLElement).id

if (entry.isIntersecting) {
visibleHeadings.set(id, entry.boundingClientRect.top)
} else {
visibleHeadings.delete(id)
}
}

let nextId = elements[0].id

if (visibleHeadings.size > 0) {
const sortedVisibleIds = Array.from(visibleHeadings.entries())
.sort((a, b) => b[1] - a[1])
.map(([id]) => id)

for (const element of elements) {
if (element.getBoundingClientRect().top <= activationOffset) {
currentId = element.id
nextId = sortedVisibleIds[0]
} else {
break
for (const element of elements) {
if (element.offsetTop <= window.scrollY + activationOffset) {
nextId = element.id
} else {
break
}
}
}
}

setActiveIds([currentId])
setActiveIds((prev) => (prev[0] === nextId ? prev : [nextId]))
},
{
root: null,
rootMargin: `-${activationOffset}px 0px -55% 0px`,
threshold: [0, 1],
},
)

for (const element of elements) {
observer.observe(element)
}

updateActiveHeading()
window.addEventListener("scroll", updateActiveHeading, { passive: true })
window.addEventListener("resize", updateActiveHeading)

return () => {
window.removeEventListener("scroll", updateActiveHeading)
window.removeEventListener("resize", updateActiveHeading)
}
return () => observer.disconnect()
}, [headings, isMobile])

useEffect(() => {
Expand Down Expand Up @@ -113,14 +137,28 @@ export function TableOfContents({ headings }: TableOfContentsProps) {
return
}

let frame = 0

const handleScrollVisibility = () => {
setShowMobileToc(window.scrollY > 32)
setIsOpen(false)
if (frame) return

frame = window.requestAnimationFrame(() => {
frame = 0

const shouldShow = window.scrollY > 32
setShowMobileToc((prev) => (prev === shouldShow ? prev : shouldShow))
setIsOpen((prev) => (prev ? false : prev))
})
}

handleScrollVisibility()
window.addEventListener("scroll", handleScrollVisibility, { passive: true })
return () => window.removeEventListener("scroll", handleScrollVisibility)
return () => {
if (frame) {
window.cancelAnimationFrame(frame)
}
window.removeEventListener("scroll", handleScrollVisibility)
}
}, [isMobile])

useEffect(() => {
Expand All @@ -129,29 +167,45 @@ export function TableOfContents({ headings }: TableOfContentsProps) {
return
}

const updateDesktopPosition = () => {
const wrapper = desktopWrapperRef.current
if (!wrapper) return
const wrapper = desktopWrapperRef.current
if (!wrapper) return

const measure = () => {
const rect = wrapper.getBoundingClientRect()
setDesktopStyle((prev) => (
prev?.left === rect.left && prev?.width === rect.width
? prev
: { left: rect.left, width: rect.width }
))
}

measure()

const resizeObserver = new ResizeObserver(measure)
resizeObserver.observe(wrapper)

const updateDesktopPosition = () => {
const rect = wrapper.getBoundingClientRect()
const nextFixed = rect.top <= desktopTopOffset

setIsDesktopFixed(nextFixed)
setDesktopStyle({
left: rect.left,
width: rect.width,
})
setIsDesktopFixed((prev) => (prev === nextFixed ? prev : nextFixed))
setDesktopStyle((prev) => (
prev?.left === rect.left && prev?.width === rect.width
? prev
: { left: rect.left, width: rect.width }
))
}

updateDesktopPosition()
window.addEventListener("scroll", updateDesktopPosition, { passive: true })
window.addEventListener("resize", updateDesktopPosition)

return () => {
resizeObserver.disconnect()
window.removeEventListener("scroll", updateDesktopPosition)
window.removeEventListener("resize", updateDesktopPosition)
}
}, [isMobile, desktopTopOffset])
}, [desktopTopOffset, isMobile])

const scrollToHeading = (id: string) => {
const element = document.getElementById(id)
Expand Down Expand Up @@ -254,12 +308,9 @@ export function TableOfContents({ headings }: TableOfContentsProps) {
)}
</AnimatePresence>

<div ref={desktopWrapperRef} className="hidden lg:block w-52">
<div ref={desktopWrapperRef} className="hidden lg:block w-52 self-start">
<div
className={cn(
"max-h-[calc(100vh-8rem)] overflow-y-auto",
isDesktopFixed && "fixed z-30",
)}
className={cn("max-h-[calc(100vh-8rem)] overflow-y-auto", isDesktopFixed && "fixed z-30")}
style={isDesktopFixed && desktopStyle ? {
top: `${desktopTopOffset}px`,
left: `${desktopStyle.left}px`,
Expand Down
8 changes: 4 additions & 4 deletions src/components/cards/FeatureCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,16 @@ export function FeatureCard({
<div
ref={cardRef}
className={cn(
"group relative h-full overflow-hidden rounded-[1.6rem] border border-white/8 bg-[linear-gradient(160deg,rgba(255,255,255,0.08),rgba(255,255,255,0.02)_28%,rgba(8,10,20,0.92)_100%)] shadow-[0_18px_60px_rgba(0,0,0,0.35)] transition-[transform,opacity] duration-700 ease-out before:pointer-events-none before:absolute before:inset-1px before:rounded-[calc(1.6rem-1px)] before:border before:border-white/6 before:content-[''] after:pointer-events-none after:absolute after:inset-x-0 after:top-0 after:h-px after:bg-linear-to-r after:from-transparent after:via-white/30 after:to-transparent after:content-['']",
"group relative h-full overflow-hidden rounded-[1.6rem] border border-white/8 bg-[linear-gradient(160deg,rgba(255,255,255,0.08),rgba(255,255,255,0.02)_28%,rgba(8,10,20,0.92)_100%)] shadow-[0_14px_42px_rgba(0,0,0,0.3)] transition-[transform,opacity] duration-700 ease-out before:pointer-events-none before:absolute before:inset-1px before:rounded-[calc(1.6rem-1px)] before:border before:border-white/6 before:content-[''] after:pointer-events-none after:absolute after:inset-x-0 after:top-0 after:h-px after:bg-linear-to-r after:from-transparent after:via-white/30 after:to-transparent after:content-['']",
isVisible ? "translate-y-0 opacity-100" : "translate-y-8 opacity-0",
className,
)}
style={{ transitionDelay: `${animationDelay}ms` }}
>
<div className="pointer-events-none absolute inset-0 opacity-[0.14] bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.12),transparent_32%),linear-gradient(180deg,rgba(255,255,255,0.04),transparent_40%),linear-gradient(rgba(255,255,255,0.08)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.08)_1px,transparent_1px)] bg-position-[center,center,center,center] bg-size-[auto,auto,32px_32px,32px_32px] mask-[linear-gradient(180deg,rgba(0,0,0,0.75),transparent_92%)]" />
<div className={cn("pointer-events-none absolute -left-14 -top-14 h-36 w-36 rounded-full blur-2xl opacity-95 transition-transform duration-700", toneStyle.orb)} />
<div className={cn("pointer-events-none absolute inset-0 bg-linear-to-br opacity-90", toneStyle.glow)} />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_bottom_right,rgba(8,10,20,0),rgba(8,10,20,0.58)_58%,rgba(8,10,20,0.9))]" />
<div className={cn("pointer-events-none absolute -left-10 -top-10 h-28 w-28 rounded-full blur-2xl opacity-95 transition-transform duration-700", toneStyle.orb)} />
<div className={cn("pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.02),transparent_24%)] bg-linear-to-br opacity-90", toneStyle.glow)} />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_bottom_right,rgba(8,10,20,0),rgba(8,10,20,0.52)_54%,rgba(8,10,20,0.88))]" />

<div className={cn("absolute inset-0 z-10 flex flex-col justify-start items-center gap-4 p-5 md:p-6", contentClassName)}>
{children}
Expand Down
Loading