Skip to content

Commit 84363e0

Browse files
Edwin ChanEdwin Chan
authored andcommitted
Use Anime.js for app animations
1 parent 41e9ede commit 84363e0

8 files changed

Lines changed: 135 additions & 56 deletions

File tree

CENTRAL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Stack: Next.js, NextAuth (Google + dev bypass), Prisma, KaTeX for math, Lucide i
3333

3434
### Recently landed
3535

36+
- **Anime.js animation pass** — the shared `MathCurveLoader` now drives SVG path drawing, rotation, and dot pulses with Anime.js v4 instead of CSS keyframes, and `TypewriterGreeting` uses Anime.js for its text tick/caret motion. Dependency: `animejs` in `package.json`. Reduced-motion users still get static loader/caret output.
3637
- **Problem-set writeups**`/problem-sets/[slug]/writeups` lets signed-in users post LaTeX/HTML solution notes with up to four images, then sort by latest/top and upvote/downvote posts. Authors can delete their own writeups with confirmation; admins can delete any writeup. `/writeups` is the sidebar directory for latest/top writeups and problem-set search. Persistence uses new `Writeup`, `WriteupImage`, and `WriteupVote` Prisma models plus `lib/writeup-images.ts`; image bytes are stored through the existing storage layer and streamed via `/api/files/[id]`.
3738
- **Class announcements** — teachers/admins post title/body announcements from `/classes` to one or more classes. `Announcement` persists the message with an implicit class relation; `POST /api/admin/announcements` enforces `admin:users` and teacher ownership. Dashboard loads targeted announcements fresh on page render and pins them above the hero.
3839
- **Profile mastery heatmap**`app/users/[username]/page.tsx` now renders a GitHub-style yearly heatmap between authored tasks and the set grid. It counts days where the profile user mastered one or more sets with best-day intensity capped at 5.

DBSMO/Projects/dbsmo/Components.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
date: 2026-06-26
3-
updated: 2026-06-28
3+
updated: 2026-06-29
44
type: components
55
tags: [project, architecture, components, ui, dbsmo]
66
ai-first: true
@@ -22,7 +22,8 @@ This note maps important [[dbsmo]] UI/components to their source files and usage
2222
- `AuthButton` in `app/auth-button.tsx`: sign-in/sign-out control. It renders Google sign-in, optional bypass buttons, session badge, and profile avatar link.
2323
- `Avatar` in `app/avatar.tsx`: shared avatar display; deterministic fallback initial/tint helper lives in `lib/avatar.ts`.
2424
- `ThemeToggle` in `app/theme-toggle.tsx`: theme control used on dashboard and problem set pages.
25-
- `TypewriterGreeting` in `app/typewriter-greeting.tsx`: animated greeting used by dashboard and configured in settings.
25+
- `MathCurveLoader` in `app/math-curve-loader.tsx`: shared inline loading indicator used across admin, problem set, FTW, classes, settings, and profile actions. It renders random math-curve SVG variants and uses Anime.js v4 for path drawing, rotor spin, and dot pulses while respecting `prefers-reduced-motion`.
26+
- `TypewriterGreeting` in `app/typewriter-greeting.tsx`: animated greeting used by dashboard and configured in settings. It keeps the existing typed/deleted text state machine and uses Anime.js v4 for subtle text tick and caret motion.
2627

2728
## Student Dashboard and Catalog
2829

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ Session updates from the CodeGraph/Second Brain indexing pass onward:
4343
- Added problem-set writeups with LaTeX/HTML text, image uploads, latest/top sorting, and upvote/downvote voting. Writeups are reachable from the set header beside the bookmark action and remain accessible regardless of submission status.
4444
- Added owner/admin writeup deletion with an in-card confirmation step, plus a sidebar Writeups page for browsing latest/top writeups and searching by problem set.
4545
- Added class announcements: teachers/admins can post messages to one or more classes, and students see targeted messages pinned at the top of the dashboard on page load.
46+
- Switched the shared math curve loader and dashboard typewriter/caret motion to Anime.js v4 while keeping reduced-motion fallbacks.
4647

4748
© 2026 Cosmic Crusader

app/globals.css

Lines changed: 2 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2232,7 +2232,6 @@ img.avatar {
22322232
}
22332233

22342234
.math-curve-loader-rotor {
2235-
animation: math-curve-spin calc(var(--loader-duration) * 1.7) linear infinite;
22362235
transform-origin: 50px 50px;
22372236
}
22382237

@@ -2250,14 +2249,11 @@ img.avatar {
22502249
}
22512250

22522251
.math-curve-loader-path {
2253-
animation: math-curve-dash var(--loader-duration) ease-in-out infinite;
22542252
opacity: 0.92;
2255-
stroke-dasharray: 24 76;
22562253
stroke-width: 7;
22572254
}
22582255

22592256
.math-curve-loader-dot {
2260-
animation: math-curve-pulse calc(var(--loader-duration) * 0.7) ease-in-out infinite;
22612257
fill: currentColor;
22622258
opacity: 0.55;
22632259
transform-box: fill-box;
@@ -2274,42 +2270,12 @@ img.avatar {
22742270
opacity: 0.5;
22752271
}
22762272

2277-
@keyframes math-curve-spin {
2278-
to {
2279-
transform: rotate(360deg);
2280-
}
2281-
}
2282-
2283-
@keyframes math-curve-dash {
2284-
0% {
2285-
stroke-dashoffset: 100;
2286-
}
2287-
2288-
50% {
2289-
stroke-dasharray: 38 62;
2290-
}
2291-
2292-
100% {
2293-
stroke-dashoffset: 0;
2294-
}
2295-
}
2296-
2297-
@keyframes math-curve-pulse {
2298-
0%,
2299-
100% {
2300-
transform: scale(0.55);
2301-
}
2302-
2303-
50% {
2304-
transform: scale(1.12);
2305-
}
2306-
}
23072273

23082274
@media (prefers-reduced-motion: reduce) {
23092275
.math-curve-loader-rotor,
23102276
.math-curve-loader-path,
23112277
.math-curve-loader-dot {
2312-
animation: none;
2278+
transform: none !important;
23132279
}
23142280
}
23152281

@@ -4088,24 +4054,13 @@ html.dark .confetti-triangles span {
40884054
color: var(--color-pink);
40894055
margin-left: 1px;
40904056
opacity: 1;
4091-
animation: typewriter-cursor-blink 1s steps(1, end) infinite;
4057+
transform-origin: center;
40924058
}
40934059

40944060
.typewriter-cursor.is-typing {
4095-
animation: none;
40964061
opacity: 1;
40974062
}
40984063

4099-
@keyframes typewriter-cursor-blink {
4100-
0%,
4101-
49% {
4102-
opacity: 1;
4103-
}
4104-
50%,
4105-
100% {
4106-
opacity: 0;
4107-
}
4108-
}
41094064

41104065
.hero-copy h2 .typewriter-greeting {
41114066
min-height: 1.2em;

app/math-curve-loader.tsx

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

3-
import { useEffect, useState } from "react";
3+
import { animate, createDrawable, stagger } from "animejs";
4+
import { useEffect, useRef, useState } from "react";
45
import type { CSSProperties } from "react";
56

67
type Point = {
@@ -124,6 +125,9 @@ const CURVE_VARIANTS: CurveVariant[] = CURVE_POINTS.map((curve) => ({
124125

125126
export function MathCurveLoader({ size = 18, label = "Loading", className }: MathCurveLoaderProps) {
126127
const [variantIndex, setVariantIndex] = useState(0);
128+
const rotorRef = useRef<SVGGElement>(null);
129+
const pathRef = useRef<SVGPathElement>(null);
130+
const dotsRef = useRef<SVGCircleElement[]>([]);
127131
const variant = CURVE_VARIANTS[variantIndex] ?? CURVE_VARIANTS[0];
128132

129133
useEffect(() => {
@@ -134,6 +138,49 @@ export function MathCurveLoader({ size = 18, label = "Loading", className }: Mat
134138
return () => window.cancelAnimationFrame(frame);
135139
}, []);
136140

141+
useEffect(() => {
142+
const rotor = rotorRef.current;
143+
const path = pathRef.current;
144+
const dots = dotsRef.current;
145+
146+
if (!rotor || !path || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
147+
return;
148+
}
149+
150+
const [drawable] = createDrawable(path, 0, 0);
151+
drawable.setAttribute("draw", "0 0");
152+
const drawAnimation = animate(drawable, {
153+
draw: "0 1",
154+
duration: variant.durationMs,
155+
ease: "inOutSine",
156+
loop: true,
157+
alternate: true,
158+
});
159+
const spinAnimation = animate(rotor, {
160+
rotate: "360deg",
161+
duration: variant.durationMs * 1.7,
162+
ease: "linear",
163+
loop: true,
164+
});
165+
const dotAnimation = dots.length
166+
? animate(dots, {
167+
scale: [0.5, 1.15],
168+
opacity: [0.42, 0.9],
169+
duration: variant.durationMs * 0.7,
170+
delay: stagger(85, { from: "center" }),
171+
ease: "inOutSine",
172+
loop: true,
173+
alternate: true,
174+
})
175+
: null;
176+
177+
return () => {
178+
drawAnimation.revert();
179+
spinAnimation.revert();
180+
dotAnimation?.revert();
181+
};
182+
}, [variant]);
183+
137184
return (
138185
<span
139186
aria-label={label}
@@ -142,23 +189,29 @@ export function MathCurveLoader({ size = 18, label = "Loading", className }: Mat
142189
style={
143190
{
144191
"--loader-size": `${size}px`,
145-
"--loader-duration": `${variant.durationMs}ms`,
146192
} as CSSProperties
147193
}
148194
title={variant.name}
149195
>
150196
<svg viewBox="0 0 100 100" fill="none" aria-hidden="true">
151-
<g className="math-curve-loader-rotor">
197+
<g className="math-curve-loader-rotor" ref={rotorRef}>
152198
<path className="math-curve-loader-track" d={variant.path} pathLength={100} />
153-
<path className="math-curve-loader-path" d={variant.path} pathLength={100} />
199+
<path
200+
className="math-curve-loader-path"
201+
d={variant.path}
202+
pathLength={100}
203+
ref={pathRef}
204+
/>
154205
{variant.particles.map((point, index) => (
155206
<circle
156207
className={`math-curve-loader-dot ${variant.className}`}
157208
cx={point.x}
158209
cy={point.y}
159210
key={`${variant.name}-${index}`}
211+
ref={(node) => {
212+
if (node) dotsRef.current[index] = node;
213+
}}
160214
r={index === 0 ? 4.6 : 2.8}
161-
style={{ animationDelay: `${index * -110}ms` }}
162215
/>
163216
))}
164217
</g>

app/typewriter-greeting.tsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

3-
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
3+
import { animate } from "animejs";
4+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
45

56
const DEFAULT_TYPE_SPEED_MS = 42;
67
const DEFAULT_DELETE_SPEED_MS = 22;
@@ -100,6 +101,8 @@ function GreetingTyper({ name }: { name: string }) {
100101
const [greetingIndex, setGreetingIndex] = useState(INITIAL_GREETING_INDEX);
101102
const [charIndex, setCharIndex] = useState(0);
102103
const [isDeleting, setIsDeleting] = useState(false);
104+
const textRef = useRef<HTMLSpanElement>(null);
105+
const cursorRef = useRef<HTMLSpanElement>(null);
103106
const storedSpeeds = useSyncExternalStore(
104107
subscribeToTypewriterSpeeds,
105108
getTypewriterSpeedsSnapshot,
@@ -151,11 +154,52 @@ function GreetingTyper({ name }: { name: string }) {
151154
speeds.typeSpeed,
152155
]);
153156

157+
useEffect(() => {
158+
const text = textRef.current;
159+
160+
if (!text || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
161+
return;
162+
}
163+
164+
const textAnimation = animate(text, {
165+
opacity: [0.72, 1],
166+
translateY: [2, 0],
167+
duration: isDeleting ? 90 : 140,
168+
ease: "outQuad",
169+
});
170+
171+
return () => {
172+
textAnimation.revert();
173+
};
174+
}, [charIndex, isDeleting]);
175+
176+
useEffect(() => {
177+
const cursor = cursorRef.current;
178+
179+
if (!cursor || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
180+
return;
181+
}
182+
183+
const cursorAnimation = animate(cursor, {
184+
opacity: [1, 0.16],
185+
scaleY: [1, 0.72],
186+
duration: 620,
187+
ease: "steps(2)",
188+
loop: true,
189+
alternate: true,
190+
});
191+
192+
return () => {
193+
cursorAnimation.revert();
194+
};
195+
}, []);
196+
154197
return (
155198
<span className="typewriter-greeting" aria-label={activeGreeting}>
156-
{activeGreeting.slice(0, charIndex)}
199+
<span ref={textRef}>{activeGreeting.slice(0, charIndex)}</span>
157200
<span
158201
className={`typewriter-cursor${charIndex < activeGreeting.length || isDeleting ? " is-typing" : ""}`}
202+
ref={cursorRef}
159203
aria-hidden="true"
160204
>
161205
|

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"@prisma/adapter-pg": "^7.8.0",
2626
"@prisma/client": "latest",
2727
"@types/katex": "^0.16.8",
28+
"animejs": "^4.5.0",
2829
"csv-parse": "^6.2.1",
2930
"dotenv": "^17.4.2",
3031
"js-yaml": "latest",

0 commit comments

Comments
 (0)