Add geometric element
This commit is contained in:
@@ -18,6 +18,10 @@
|
||||
--font-cursive: var(--font-cursive);
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-y: overlay;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ export default function Home() {
|
||||
<main>
|
||||
<Landing />
|
||||
|
||||
<section id="about" className="min-h-[150vh] px-8 py-16 md:px-16 lg:px-24">
|
||||
<section id="about" className="px-8 py-16 md:px-16 lg:px-24">
|
||||
<div className="mx-auto max-w-4xl space-y-16">
|
||||
<div className="space-y-8">
|
||||
<h2 className="font-bold font-heading text-5xl md:text-7xl">About</h2>
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import {
|
||||
BLUE,
|
||||
CYAN,
|
||||
DRIFT_AMOUNT,
|
||||
EDGE_APPEAR_SPEED,
|
||||
EDGE_DISAPPEAR_SPEED,
|
||||
NEIGHBORS,
|
||||
PROXIMITY_THRESHOLD,
|
||||
REPEL_STRENGTH,
|
||||
SPRING_DAMPING,
|
||||
SPRING_MASS,
|
||||
SPRING_STIFFNESS,
|
||||
VIOLET,
|
||||
} from "./constants";
|
||||
import { type Color, EdgePhase, type EdgeState, type Point } from "./types";
|
||||
|
||||
type Cursor = { x: number; y: number };
|
||||
|
||||
export const updatePoints = (
|
||||
points: Point[],
|
||||
elapsed: number,
|
||||
deltaTime: number,
|
||||
cursor: Cursor,
|
||||
rect: DOMRect,
|
||||
scaleX: number,
|
||||
scaleY: number,
|
||||
isTouchDevice: boolean,
|
||||
) => {
|
||||
for (const point of points) {
|
||||
const driftX = Math.sin(elapsed * point.driftSpeedX + point.driftPhaseX) * DRIFT_AMOUNT;
|
||||
const driftY = Math.sin(elapsed * point.driftSpeedY + point.driftPhaseY) * DRIFT_AMOUNT;
|
||||
|
||||
point.targetOffsetX = 0;
|
||||
point.targetOffsetY = 0;
|
||||
|
||||
if (!isTouchDevice && cursor.x !== 0) {
|
||||
const pointScreenX = rect.left + (point.baseX + driftX) * scaleX;
|
||||
const pointScreenY = rect.top + (point.baseY + driftY) * scaleY;
|
||||
const dx = cursor.x - pointScreenX;
|
||||
const dy = cursor.y - pointScreenY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < PROXIMITY_THRESHOLD && distance > 0) {
|
||||
const influence = (1 - distance / PROXIMITY_THRESHOLD) ** 2;
|
||||
point.targetOffsetX = -(dx / distance) * influence * REPEL_STRENGTH;
|
||||
point.targetOffsetY = -(dy / distance) * influence * REPEL_STRENGTH;
|
||||
}
|
||||
}
|
||||
|
||||
const forceX = (point.targetOffsetX - point.currentOffsetX) * SPRING_STIFFNESS;
|
||||
const forceY = (point.targetOffsetY - point.currentOffsetY) * SPRING_STIFFNESS;
|
||||
const dampingX = point.velocityX * SPRING_DAMPING;
|
||||
const dampingY = point.velocityY * SPRING_DAMPING;
|
||||
|
||||
point.velocityX += ((forceX - dampingX) / SPRING_MASS) * deltaTime;
|
||||
point.velocityY += ((forceY - dampingY) / SPRING_MASS) * deltaTime;
|
||||
point.currentOffsetX += point.velocityX * deltaTime;
|
||||
point.currentOffsetY += point.velocityY * deltaTime;
|
||||
|
||||
point.x = point.baseX + driftX + point.currentOffsetX;
|
||||
point.y = point.baseY + driftY + point.currentOffsetY;
|
||||
}
|
||||
};
|
||||
|
||||
export const findCurrentEdges = (points: Point[]): Set<string> => {
|
||||
const currentEdges = new Set<string>();
|
||||
|
||||
for (const point of points) {
|
||||
const distances = points
|
||||
.filter((p) => p.id !== point.id)
|
||||
.map((other) => ({
|
||||
other,
|
||||
dist: Math.sqrt((point.x - other.x) ** 2 + (point.y - other.y) ** 2),
|
||||
}))
|
||||
.sort((a, b) => a.dist - b.dist);
|
||||
|
||||
for (let i = 0; i < Math.min(NEIGHBORS, distances.length); i++) {
|
||||
const other = distances[i].other;
|
||||
const edgeKey = point.id < other.id ? `${point.id}-${other.id}` : `${other.id}-${point.id}`;
|
||||
currentEdges.add(edgeKey);
|
||||
}
|
||||
}
|
||||
|
||||
return currentEdges;
|
||||
};
|
||||
|
||||
export const updateEdges = (
|
||||
edges: Map<string, EdgeState>,
|
||||
currentEdges: Set<string>,
|
||||
deltaTime: number,
|
||||
) => {
|
||||
// Mark edges that should disappear
|
||||
for (const [key, edge] of edges) {
|
||||
if (!currentEdges.has(key) && edge.phase !== EdgePhase.Disappearing) {
|
||||
edge.phase = EdgePhase.Disappearing;
|
||||
edge.progress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new edges or resurrect disappearing ones
|
||||
for (const key of currentEdges) {
|
||||
const existing = edges.get(key);
|
||||
if (!existing) {
|
||||
const [id1, id2] = key.split("-").map(Number);
|
||||
edges.set(key, { id1, id2, phase: EdgePhase.Appearing, progress: 0 });
|
||||
} else if (existing.phase === EdgePhase.Disappearing) {
|
||||
existing.phase = EdgePhase.Appearing;
|
||||
existing.progress = 1 - existing.progress;
|
||||
}
|
||||
}
|
||||
|
||||
// Animate edges and remove fully disappeared ones
|
||||
const toRemove: string[] = [];
|
||||
for (const [key, edge] of edges) {
|
||||
if (edge.phase === EdgePhase.Appearing) {
|
||||
edge.progress += EDGE_APPEAR_SPEED * deltaTime;
|
||||
if (edge.progress >= 1) {
|
||||
edge.progress = 1;
|
||||
edge.phase = EdgePhase.Visible;
|
||||
}
|
||||
} else if (edge.phase === EdgePhase.Disappearing) {
|
||||
edge.progress += EDGE_DISAPPEAR_SPEED * deltaTime;
|
||||
if (edge.progress >= 1) {
|
||||
toRemove.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key of toRemove) {
|
||||
edges.delete(key);
|
||||
}
|
||||
};
|
||||
|
||||
const getEdgeColor = (id1: number, id2: number): Color => {
|
||||
const colorIndex = (id1 + id2) % 3;
|
||||
return colorIndex === 0 ? CYAN : colorIndex === 1 ? VIOLET : BLUE;
|
||||
};
|
||||
|
||||
const drawLine = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
color: Color,
|
||||
opacity: number,
|
||||
) => {
|
||||
ctx.strokeStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${opacity})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
export const drawEdges = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
edges: Map<string, EdgeState>,
|
||||
points: Point[],
|
||||
scaleX: number,
|
||||
scaleY: number,
|
||||
) => {
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
for (const [, edge] of edges) {
|
||||
const p1 = points[edge.id1];
|
||||
const p2 = points[edge.id2];
|
||||
|
||||
const x1 = p1.x * scaleX;
|
||||
const y1 = p1.y * scaleY;
|
||||
const x2 = p2.x * scaleX;
|
||||
const y2 = p2.y * scaleY;
|
||||
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
|
||||
const dist = Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
|
||||
const maxDist = 200;
|
||||
const baseOpacity = Math.max(0, 1 - dist / maxDist) * 0.6;
|
||||
const color = getEdgeColor(edge.id1, edge.id2);
|
||||
|
||||
if (edge.phase === EdgePhase.Appearing) {
|
||||
const t = edge.progress;
|
||||
|
||||
// First half: draw from p1 towards center
|
||||
const seg1End = Math.min(t * 2, 1);
|
||||
drawLine(ctx, x1, y1, x1 + (cx - x1) * seg1End, y1 + (cy - y1) * seg1End, color, baseOpacity);
|
||||
|
||||
// Second half: draw from p2 towards center
|
||||
if (t > 0.5) {
|
||||
const seg2End = (t - 0.5) * 2;
|
||||
drawLine(
|
||||
ctx,
|
||||
x2,
|
||||
y2,
|
||||
x2 + (cx - x2) * (1 - seg2End),
|
||||
y2 + (cy - y2) * (1 - seg2End),
|
||||
color,
|
||||
baseOpacity,
|
||||
);
|
||||
}
|
||||
} else if (edge.phase === EdgePhase.Disappearing) {
|
||||
const t = edge.progress;
|
||||
const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
||||
const dirX = len > 0 ? (x2 - x1) / len : 0;
|
||||
const dirY = len > 0 ? (y2 - y1) / len : 0;
|
||||
|
||||
if (t < 0.5) {
|
||||
// First half: gap grows from center
|
||||
const gapSize = t * 2;
|
||||
const halfLen = Math.sqrt((cx - x1) ** 2 + (cy - y1) ** 2);
|
||||
const gapHalf = halfLen * gapSize;
|
||||
|
||||
drawLine(ctx, x1, y1, cx - dirX * gapHalf, cy - dirY * gapHalf, color, baseOpacity);
|
||||
drawLine(ctx, cx + dirX * gapHalf, cy + dirY * gapHalf, x2, y2, color, baseOpacity);
|
||||
} else {
|
||||
// Second half: segments shrink and fade
|
||||
const shrink = (t - 0.5) * 2;
|
||||
const fadeOpacity = baseOpacity * (1 - shrink);
|
||||
|
||||
drawLine(
|
||||
ctx,
|
||||
x1,
|
||||
y1,
|
||||
x1 + (cx - x1) * (1 - shrink),
|
||||
y1 + (cy - y1) * (1 - shrink),
|
||||
color,
|
||||
fadeOpacity,
|
||||
);
|
||||
drawLine(
|
||||
ctx,
|
||||
x2 + (cx - x2) * (1 - shrink),
|
||||
y2 + (cy - y2) * (1 - shrink),
|
||||
x2,
|
||||
y2,
|
||||
color,
|
||||
fadeOpacity,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Visible - draw full line
|
||||
drawLine(ctx, x1, y1, x2, y2, color, baseOpacity);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const drawPoints = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
points: Point[],
|
||||
scaleX: number,
|
||||
scaleY: number,
|
||||
) => {
|
||||
for (const point of points) {
|
||||
const x = point.x * scaleX;
|
||||
const y = point.y * scaleY;
|
||||
const color = point.id % 2 === 0 ? CYAN : VIOLET;
|
||||
ctx.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Color } from "./types";
|
||||
|
||||
export const DRIFT_AMOUNT = 25;
|
||||
export const NEIGHBORS = 3;
|
||||
export const PROXIMITY_THRESHOLD = 250;
|
||||
export const REPEL_STRENGTH = 80;
|
||||
export const SPRING_STIFFNESS = 180;
|
||||
export const SPRING_DAMPING = 12;
|
||||
export const SPRING_MASS = 1;
|
||||
export const EDGE_APPEAR_SPEED = 4;
|
||||
export const EDGE_DISAPPEAR_SPEED = 5;
|
||||
export const CYAN: Color = { r: 68, g: 211, b: 220 };
|
||||
export const VIOLET: Color = { r: 145, g: 82, b: 211 };
|
||||
export const BLUE: Color = { r: 107, g: 147, b: 216 };
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { drawEdges, drawPoints, findCurrentEdges, updateEdges, updatePoints } from "./animation";
|
||||
import { INITIAL_POINTS } from "./points";
|
||||
import type { EdgeState, Point } from "./types";
|
||||
|
||||
export const DynamicCanvas = () => {
|
||||
const [isTouchDevice, setIsTouchDevice] = useState(false);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const pointsRef = useRef<Point[]>(INITIAL_POINTS.map((p) => ({ ...p })));
|
||||
const edgesRef = useRef<Map<string, EdgeState>>(new Map());
|
||||
const cursorRef = useRef({ x: 0, y: 0 });
|
||||
const lastTimeRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
setIsTouchDevice(window.matchMedia("(pointer: coarse)").matches);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTouchDevice) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
cursorRef.current.x = e.clientX;
|
||||
cursorRef.current.y = e.clientY;
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
return () => window.removeEventListener("mousemove", handleMouseMove);
|
||||
}, [isTouchDevice]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let animationId: number;
|
||||
const startTime = performance.now();
|
||||
lastTimeRef.current = startTime;
|
||||
|
||||
const animate = (time: number) => {
|
||||
const elapsed = time - startTime;
|
||||
const deltaTime = Math.min((time - lastTimeRef.current) / 1000, 0.1);
|
||||
lastTimeRef.current = time;
|
||||
|
||||
const points = pointsRef.current;
|
||||
const edges = edgesRef.current;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const targetWidth = Math.round(rect.width * dpr);
|
||||
const targetHeight = Math.round(rect.height * dpr);
|
||||
|
||||
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
}
|
||||
|
||||
const scaleX = rect.width / 1000;
|
||||
const scaleY = rect.height / 500;
|
||||
|
||||
updatePoints(
|
||||
points,
|
||||
elapsed,
|
||||
deltaTime,
|
||||
cursorRef.current,
|
||||
rect,
|
||||
scaleX,
|
||||
scaleY,
|
||||
isTouchDevice,
|
||||
);
|
||||
|
||||
const currentEdges = findCurrentEdges(points);
|
||||
updateEdges(edges, currentEdges, deltaTime);
|
||||
|
||||
ctx.clearRect(0, 0, rect.width, rect.height);
|
||||
drawEdges(ctx, edges, points, scaleX, scaleY);
|
||||
drawPoints(ctx, points, scaleX, scaleY);
|
||||
|
||||
animationId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationId = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationId);
|
||||
};
|
||||
}, [isTouchDevice]);
|
||||
|
||||
return <canvas ref={canvasRef} className="h-full w-full" />;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useReducedMotion } from "motion/react";
|
||||
import { DynamicCanvas } from "./dynamic-canvas";
|
||||
import { StaticFallback } from "./static-fallback";
|
||||
|
||||
type Props = {
|
||||
wrapperHeight?: string;
|
||||
canvasHeight?: string;
|
||||
};
|
||||
|
||||
export const GeometricElement = ({ wrapperHeight = "0px", canvasHeight = "100vh" }: Props) => {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const isReducedMotion = shouldReduceMotion ?? false;
|
||||
|
||||
return (
|
||||
<div className="relative w-full overflow-visible" style={{ height: wrapperHeight }}>
|
||||
<div
|
||||
className="-z-10 -translate-y-1/2 mask-[linear-gradient(to_bottom,transparent,black_50%,transparent)] absolute inset-x-0 top-1/2"
|
||||
style={{ height: canvasHeight }}
|
||||
>
|
||||
{isReducedMotion ? <StaticFallback /> : <DynamicCanvas />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { Point } from "./types";
|
||||
|
||||
const seededRandom = (seed: number) => {
|
||||
const x = Math.sin(seed * 9999) * 10000;
|
||||
return x - Math.floor(x);
|
||||
};
|
||||
|
||||
export const generatePoints = (): Point[] => {
|
||||
const points: Point[] = [];
|
||||
const numPoints = 40;
|
||||
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const rand = (offset: number) => seededRandom(i * 100 + offset);
|
||||
const baseX = 50 + rand(1) * 900;
|
||||
const baseY = 30 + rand(2) * 440;
|
||||
|
||||
points.push({
|
||||
id: i,
|
||||
baseX,
|
||||
baseY,
|
||||
driftPhaseX: rand(3) * Math.PI * 2,
|
||||
driftPhaseY: rand(4) * Math.PI * 2,
|
||||
driftSpeedX: 0.00015 + rand(5) * 0.00025,
|
||||
driftSpeedY: 0.00015 + rand(6) * 0.00025,
|
||||
x: baseX,
|
||||
y: baseY,
|
||||
velocityX: 0,
|
||||
velocityY: 0,
|
||||
targetOffsetX: 0,
|
||||
targetOffsetY: 0,
|
||||
currentOffsetX: 0,
|
||||
currentOffsetY: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return points;
|
||||
};
|
||||
|
||||
export const INITIAL_POINTS = generatePoints();
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { NEIGHBORS } from "./constants";
|
||||
import { INITIAL_POINTS } from "./points";
|
||||
import type { Point } from "./types";
|
||||
|
||||
export const StaticFallback = () => {
|
||||
const points = INITIAL_POINTS;
|
||||
const edges: Array<{ p1: Point; p2: Point }> = [];
|
||||
|
||||
for (const point of points) {
|
||||
const distances = points
|
||||
.filter((p) => p.id !== point.id)
|
||||
.map((other) => ({
|
||||
other,
|
||||
dist: Math.sqrt((point.baseX - other.baseX) ** 2 + (point.baseY - other.baseY) ** 2),
|
||||
}))
|
||||
.sort((a, b) => a.dist - b.dist);
|
||||
|
||||
for (let i = 0; i < Math.min(NEIGHBORS, distances.length); i++) {
|
||||
const other = distances[i].other;
|
||||
if (point.id < other.id) {
|
||||
edges.push({ p1: point, p2: other });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 1000 500"
|
||||
className="h-full w-full"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{edges.map(({ p1, p2 }) => (
|
||||
<line
|
||||
key={`${p1.id}-${p2.id}`}
|
||||
x1={p1.baseX}
|
||||
y1={p1.baseY}
|
||||
x2={p2.baseX}
|
||||
y2={p2.baseY}
|
||||
stroke={(p1.id + p2.id) % 2 === 0 ? "var(--accent-cyan)" : "var(--accent-violet)"}
|
||||
strokeWidth={1}
|
||||
opacity={0.4}
|
||||
/>
|
||||
))}
|
||||
{points.map((point) => (
|
||||
<circle
|
||||
key={point.id}
|
||||
cx={point.baseX}
|
||||
cy={point.baseY}
|
||||
r={2}
|
||||
fill={point.id % 2 === 0 ? "var(--accent-cyan)" : "var(--accent-violet)"}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
export type Point = {
|
||||
id: number;
|
||||
baseX: number;
|
||||
baseY: number;
|
||||
driftPhaseX: number;
|
||||
driftPhaseY: number;
|
||||
driftSpeedX: number;
|
||||
driftSpeedY: number;
|
||||
x: number;
|
||||
y: number;
|
||||
velocityX: number;
|
||||
velocityY: number;
|
||||
targetOffsetX: number;
|
||||
targetOffsetY: number;
|
||||
currentOffsetX: number;
|
||||
currentOffsetY: number;
|
||||
};
|
||||
|
||||
export enum EdgePhase {
|
||||
Appearing = 0,
|
||||
Visible = 1,
|
||||
Disappearing = 2,
|
||||
}
|
||||
|
||||
export type EdgeState = {
|
||||
id1: number;
|
||||
id2: number;
|
||||
phase: EdgePhase;
|
||||
progress: number;
|
||||
};
|
||||
|
||||
export type Color = { r: number; g: number; b: number };
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LandingClient } from "./landing-client";
|
||||
import { GeometricElement } from "./geometric-element";
|
||||
import { Logotype } from "./logotype";
|
||||
|
||||
const letterSegments: Record<string, [number, number, number, number][]> = {
|
||||
l: [
|
||||
@@ -111,8 +112,10 @@ const allPaths = letterLayout.reduce<{ d: string; key: string }[]>(
|
||||
|
||||
export const Landing = () => {
|
||||
return (
|
||||
<section id="landing">
|
||||
<LandingClient paths={allPaths} />
|
||||
<section id="landing" className="relative flex flex-col">
|
||||
<Logotype paths={allPaths} />
|
||||
|
||||
<GeometricElement />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ const LogotypePath = ({
|
||||
d: string;
|
||||
scrollProgress: MotionValue<number>;
|
||||
}) => {
|
||||
const strokeDashoffset = useTransform(scrollProgress, [0, 0.6], [0, 1]);
|
||||
const strokeDashoffset = useTransform(scrollProgress, [0, 0.5], [0, 1]);
|
||||
|
||||
return (
|
||||
<m.path
|
||||
@@ -33,16 +33,16 @@ type Props = {
|
||||
paths: { d: string; key: string }[];
|
||||
};
|
||||
|
||||
export const LandingClient = ({ paths }: Props) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
export const Logotype = ({ paths }: Props) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: containerRef,
|
||||
target: ref,
|
||||
offset: ["start start", "end start"],
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="flex min-h-screen w-full items-center justify-center">
|
||||
<div ref={ref} className="flex min-h-screen w-full items-center justify-center">
|
||||
<m.svg
|
||||
role="img"
|
||||
aria-label="luisdralves"
|
||||
@@ -53,9 +53,54 @@ export const RadialReveal = ({ targetSection, origin, onComplete }: Props) => {
|
||||
|
||||
overlayRef.current.innerHTML = "";
|
||||
overlayRef.current.appendChild(clone);
|
||||
|
||||
setIsReady(true);
|
||||
}, [targetSection]);
|
||||
|
||||
// Continuously sync canvas content during the reveal animation
|
||||
useEffect(() => {
|
||||
if (!isReady || hasCompleted) return;
|
||||
|
||||
const overlay = overlayRef.current;
|
||||
if (!overlay) return;
|
||||
|
||||
// Cache canvas pairs once at effect start
|
||||
const allCanvases = document.querySelectorAll("canvas");
|
||||
const originalCanvases: HTMLCanvasElement[] = [];
|
||||
for (const canvas of allCanvases) {
|
||||
if (!overlay.contains(canvas)) {
|
||||
originalCanvases.push(canvas as HTMLCanvasElement);
|
||||
}
|
||||
}
|
||||
const clonedCanvases = Array.from(overlay.querySelectorAll("canvas")) as HTMLCanvasElement[];
|
||||
|
||||
let animationId: number;
|
||||
|
||||
const syncCanvases = () => {
|
||||
originalCanvases.forEach((original, index) => {
|
||||
const cloned = clonedCanvases[index];
|
||||
if (!cloned || original.width === 0 || original.height === 0) return;
|
||||
|
||||
const clonedCtx = cloned.getContext("2d");
|
||||
if (!clonedCtx) return;
|
||||
|
||||
if (cloned.width !== original.width) cloned.width = original.width;
|
||||
if (cloned.height !== original.height) cloned.height = original.height;
|
||||
|
||||
clonedCtx.clearRect(0, 0, cloned.width, cloned.height);
|
||||
clonedCtx.drawImage(original, 0, 0);
|
||||
});
|
||||
|
||||
animationId = requestAnimationFrame(syncCanvases);
|
||||
};
|
||||
|
||||
animationId = requestAnimationFrame(syncCanvases);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationId);
|
||||
};
|
||||
}, [isReady, hasCompleted]);
|
||||
|
||||
const maxRadius =
|
||||
typeof window !== "undefined"
|
||||
? Math.hypot(
|
||||
|
||||
Reference in New Issue
Block a user