Skip to content

Commit 640d243

Browse files
committed
feat: login/signup background
1 parent 641c2e2 commit 640d243

File tree

6 files changed

+321
-15
lines changed

6 files changed

+321
-15
lines changed

app/(auth)/login/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { LoginForm } from '@/components/login-form'
22
import type { Metadata } from 'next'
33
import { SITE_NAME } from '@/lib/consts'
4+
import { AnimatedBooksBackground } from '@/components/ui/animated-books-background'
45

56
export const metadata: Metadata = {
67
title: `Login · ${SITE_NAME}`,
@@ -17,7 +18,8 @@ export default async function Page({
1718
const { email, from = '/' } = await searchParams
1819
return (
1920
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
20-
<div className="w-full max-w-sm">
21+
<AnimatedBooksBackground />
22+
<div className="relative w-full max-w-sm">
2123
<LoginForm email={email} from={from} />
2224
</div>
2325
</div>

app/(auth)/signup/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { SignUpForm } from '@/components/signup-form'
22
import type { Metadata } from 'next'
33
import { SITE_NAME } from '@/lib/consts'
4+
import { AnimatedBooksBackground } from '@/components/ui/animated-books-background'
45

56
export const metadata: Metadata = {
67
title: `Signup · ${SITE_NAME}`,
@@ -9,7 +10,8 @@ export const metadata: Metadata = {
910
export default async function Page() {
1011
return (
1112
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
12-
<div className="w-full max-w-sm">
13+
<AnimatedBooksBackground />
14+
<div className="relative w-full max-w-sm">
1315
<SignUpForm />
1416
</div>
1517
</div>

components/forgot-password-form.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,7 @@ export function ForgotPasswordForm({
7979

8080
<div className="mt-4 text-center text-sm">
8181
Don&apos;t have an account?{' '}
82-
<Link
83-
href="/signup"
84-
className="underline underline-offset-4"
85-
replace
86-
>
82+
<Link href="/signup" className="underline underline-offset-4">
8783
Sign up
8884
</Link>
8985
</div>

components/login-form.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ export function LoginForm({
4545

4646
return (
4747
<div className={cn('flex flex-col gap-6', className)} {...props}>
48-
<Card>
48+
<Card className="backdrop-blur-md bg-background/40">
4949
<CardHeader>
5050
<CardTitle className="text-2xl">Login</CardTitle>
5151
<CardDescription>
5252
Enter your email below to login to your account
5353
</CardDescription>
5454
{state.error && (
55-
<div className="text-sm text-red-400">{state.error}</div>
55+
<div className="text-sm text-destructive">{state.error}</div>
5656
)}
5757
</CardHeader>
5858
<CardContent>
@@ -107,11 +107,7 @@ export function LoginForm({
107107
</div>
108108
<div className="mt-4 text-center text-sm">
109109
Don&apos;t have an account?{' '}
110-
<Link
111-
href="/signup"
112-
className="underline underline-offset-4"
113-
replace
114-
>
110+
<Link href="/signup" className="underline underline-offset-4">
115111
Sign up
116112
</Link>
117113
</div>

components/signup-form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function SignUpForm({
3333
)
3434
return (
3535
<div className={cn('flex flex-col gap-6', className)} {...props}>
36-
<Card>
36+
<Card className="backdrop-blur-md bg-background/40">
3737
<CardHeader>
3838
<CardTitle className="text-2xl">Sign Up</CardTitle>
3939
<CardDescription>
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
'use client'
2+
3+
import { useEffect, useRef } from 'react'
4+
5+
interface Book {
6+
x: number
7+
y: number
8+
z: number
9+
width: number
10+
height: number
11+
depth: number
12+
rotationX: number
13+
rotationY: number
14+
rotationZ: number
15+
speedX: number
16+
speedY: number
17+
speedZ: number
18+
rotationSpeedX: number
19+
rotationSpeedY: number
20+
rotationSpeedZ: number
21+
color: string
22+
opacity: number
23+
}
24+
25+
export function AnimatedBooksBackground() {
26+
const canvasRef = useRef<HTMLCanvasElement>(null)
27+
28+
useEffect(() => {
29+
const canvas = canvasRef.current
30+
if (!canvas) return
31+
32+
const ctx = canvas.getContext('2d')
33+
if (!ctx) return
34+
35+
let animationFrameId: number
36+
let books: Book[] = []
37+
38+
const LIGHT_COLORS = [
39+
'#10b981',
40+
'#059669',
41+
'#047857',
42+
'#6366f1',
43+
'#4f46e5',
44+
'#7c3aed',
45+
'#2563eb',
46+
'#0891b2',
47+
] as const
48+
49+
const DARK_COLORS = [
50+
'#0ea5e9',
51+
'#0369a1',
52+
'#3730a3',
53+
'#312e81',
54+
'#4c1d95',
55+
'#155e75',
56+
'#047857',
57+
'#0f766e',
58+
] as const
59+
60+
const LIGHT_GRADIENT = ['#ecfdf5', '#d1fae5', '#a7f3d0'] as const
61+
const DARK_GRADIENT = ['#0f172a', '#111827', '#1f2937'] as const
62+
63+
const LIGHT_OPACITY_RANGE: [number, number] = [0.18, 0.45]
64+
const DARK_OPACITY_RANGE: [number, number] = [0.08, 0.28]
65+
66+
let colorPalette: string[] = []
67+
let gradientStops: string[] = []
68+
let opacityRange: [number, number] = [0, 0]
69+
70+
const gradientCss = (stops: readonly string[]) =>
71+
`linear-gradient(135deg, ${stops
72+
.map((stop, idx) => `${stop} ${(idx / (stops.length - 1)) * 100}%`)
73+
.join(', ')})`
74+
75+
const randomBetween = (min: number, max: number) =>
76+
min + Math.random() * (max - min)
77+
78+
const detectDarkMode = () => {
79+
const html = document.documentElement
80+
const style = getComputedStyle(html)
81+
return (
82+
html.classList.contains('dark') ||
83+
style.getPropertyValue('color-scheme').trim() === 'dark'
84+
)
85+
}
86+
87+
const applyThemeConfig = () => {
88+
const isDark = detectDarkMode()
89+
colorPalette = Array.from(isDark ? DARK_COLORS : LIGHT_COLORS)
90+
gradientStops = Array.from(isDark ? DARK_GRADIENT : LIGHT_GRADIENT)
91+
opacityRange = [
92+
...(isDark ? DARK_OPACITY_RANGE : LIGHT_OPACITY_RANGE),
93+
] as [number, number]
94+
canvas.style.background = gradientCss(gradientStops)
95+
96+
books.forEach((book) => {
97+
book.color =
98+
colorPalette[Math.floor(Math.random() * colorPalette.length)]
99+
book.opacity = randomBetween(opacityRange[0], opacityRange[1])
100+
})
101+
}
102+
103+
const resize = () => {
104+
canvas.width = window.innerWidth
105+
canvas.height = window.innerHeight
106+
}
107+
108+
const createBook = (): Book => {
109+
const size = Math.random() * 60 + 40
110+
return {
111+
x: Math.random() * canvas.width,
112+
y: Math.random() * canvas.height,
113+
z: Math.random() * 1000,
114+
width: size,
115+
height: size * 1.4,
116+
depth: size * 0.3,
117+
rotationX: Math.random() * Math.PI * 2,
118+
rotationY: Math.random() * Math.PI * 2,
119+
rotationZ: Math.random() * Math.PI * 2,
120+
speedX: (Math.random() - 0.5) * 0.5,
121+
speedY: (Math.random() - 0.5) * 0.5,
122+
speedZ: (Math.random() - 0.5) * 2,
123+
rotationSpeedX: (Math.random() - 0.5) * 0.02,
124+
rotationSpeedY: (Math.random() - 0.5) * 0.02,
125+
rotationSpeedZ: (Math.random() - 0.5) * 0.02,
126+
color: colorPalette[Math.floor(Math.random() * colorPalette.length)],
127+
opacity: randomBetween(opacityRange[0], opacityRange[1]),
128+
}
129+
}
130+
131+
const init = () => {
132+
resize()
133+
applyThemeConfig()
134+
books = Array.from({ length: 30 }, createBook)
135+
}
136+
137+
const project = (x: number, y: number, z: number) => {
138+
const camera = 800
139+
const scale = camera / (camera + z)
140+
return {
141+
x: canvas.width / 2 + (x - canvas.width / 2) * scale,
142+
y: canvas.height / 2 + (y - canvas.height / 2) * scale,
143+
scale,
144+
z,
145+
}
146+
}
147+
148+
// Sequential rotation (X -> Y -> Z). More robust and readable.
149+
const rotatePoint = (
150+
x: number,
151+
y: number,
152+
z: number,
153+
rotX: number,
154+
rotY: number,
155+
rotZ: number
156+
) => {
157+
// Rotate around X
158+
const cx = Math.cos(rotX),
159+
sx = Math.sin(rotX)
160+
const y1 = y * cx - z * sx
161+
const z1 = y * sx + z * cx
162+
const x1 = x
163+
164+
// Rotate around Y
165+
const cy = Math.cos(rotY),
166+
sy = Math.sin(rotY)
167+
const x2 = x1 * cy + z1 * sy
168+
const z2 = -x1 * sy + z1 * cy
169+
const y2 = y1
170+
171+
// Rotate around Z
172+
const cz = Math.cos(rotZ),
173+
sz = Math.sin(rotZ)
174+
const x3 = x2 * cz - y2 * sz
175+
const y3 = x2 * sz + y2 * cz
176+
const z3 = z2
177+
178+
return { x: x3, y: y3, z: z3 }
179+
}
180+
181+
const drawBook = (book: Book) => {
182+
const corners = [
183+
[-book.width / 2, -book.height / 2, -book.depth / 2],
184+
[book.width / 2, -book.height / 2, -book.depth / 2],
185+
[book.width / 2, book.height / 2, -book.depth / 2],
186+
[-book.width / 2, book.height / 2, -book.depth / 2],
187+
[-book.width / 2, -book.height / 2, book.depth / 2],
188+
[book.width / 2, -book.height / 2, book.depth / 2],
189+
[book.width / 2, book.height / 2, book.depth / 2],
190+
[-book.width / 2, book.height / 2, book.depth / 2],
191+
]
192+
193+
// Rotate and translate each corner, keep world z for depth sorting
194+
const rotatedWorld = corners.map(([cx, cy, cz]) => {
195+
const r = rotatePoint(
196+
cx,
197+
cy,
198+
cz,
199+
book.rotationX,
200+
book.rotationY,
201+
book.rotationZ
202+
)
203+
return { x: book.x + r.x, y: book.y + r.y, z: book.z + r.z }
204+
})
205+
206+
const projectedCorners = rotatedWorld.map((p) => project(p.x, p.y, p.z))
207+
208+
const faces = [
209+
{ indices: [0, 1, 2, 3], brightness: 1.0 },
210+
{ indices: [4, 5, 6, 7], brightness: 0.6 },
211+
{ indices: [0, 4, 7, 3], brightness: 0.8 },
212+
{ indices: [1, 2, 6, 5], brightness: 0.8 },
213+
{ indices: [0, 1, 5, 4], brightness: 0.7 },
214+
{ indices: [3, 2, 6, 7], brightness: 0.9 },
215+
]
216+
217+
// Use average world Z (rotatedWorld.z) for depth sorting (nearer = larger z -> drawn last)
218+
const facesWithDepth = faces.map((face) => {
219+
const avgZ =
220+
face.indices.reduce((sum, i) => sum + rotatedWorld[i].z, 0) / 4
221+
return { ...face, depth: avgZ }
222+
})
223+
facesWithDepth.sort((a, b) => a.depth - b.depth)
224+
225+
facesWithDepth.forEach((face) => {
226+
const points = face.indices.map((i) => projectedCorners[i])
227+
228+
ctx.beginPath()
229+
ctx.moveTo(points[0].x, points[0].y)
230+
for (let i = 1; i < points.length; i++) {
231+
ctx.lineTo(points[i].x, points[i].y)
232+
}
233+
ctx.closePath()
234+
235+
const r = Number.parseInt(book.color.slice(1, 3), 16)
236+
const g = Number.parseInt(book.color.slice(3, 5), 16)
237+
const b = Number.parseInt(book.color.slice(5, 7), 16)
238+
const finalOpacity = book.opacity * face.brightness
239+
ctx.fillStyle = `rgba(${Math.floor(r * face.brightness)}, ${Math.floor(
240+
g * face.brightness
241+
)}, ${Math.floor(b * face.brightness)}, ${finalOpacity})`
242+
ctx.fill()
243+
ctx.strokeStyle = `rgba(0, 0, 0, ${finalOpacity * 0.1})`
244+
ctx.lineWidth = 1
245+
ctx.stroke()
246+
})
247+
}
248+
249+
const update = () => {
250+
books.forEach((book) => {
251+
book.x += book.speedX
252+
book.y += book.speedY
253+
book.z += book.speedZ
254+
book.rotationX += book.rotationSpeedX
255+
book.rotationY += book.rotationSpeedY
256+
book.rotationZ += book.rotationSpeedZ
257+
258+
if (book.x < -200) book.x = canvas.width + 200
259+
if (book.x > canvas.width + 200) book.x = -200
260+
if (book.y < -200) book.y = canvas.height + 200
261+
if (book.y > canvas.height + 200) book.y = -200
262+
if (book.z < -500) book.z = 1000
263+
if (book.z > 1000) book.z = -500
264+
})
265+
}
266+
267+
const draw = () => {
268+
const gradient = ctx.createLinearGradient(
269+
0,
270+
0,
271+
canvas.width,
272+
canvas.height
273+
)
274+
gradientStops.forEach((color, idx) => {
275+
const position =
276+
gradientStops.length === 1 ? 0 : idx / (gradientStops.length - 1)
277+
gradient.addColorStop(position, color)
278+
})
279+
ctx.fillStyle = gradient
280+
ctx.fillRect(0, 0, canvas.width, canvas.height)
281+
282+
const sortedBooks = [...books].sort((a, b) => a.z - b.z)
283+
sortedBooks.forEach(drawBook)
284+
}
285+
286+
const animate = () => {
287+
update()
288+
draw()
289+
animationFrameId = requestAnimationFrame(animate)
290+
}
291+
292+
init()
293+
animate()
294+
295+
window.addEventListener('resize', resize)
296+
297+
return () => {
298+
window.removeEventListener('resize', resize)
299+
cancelAnimationFrame(animationFrameId)
300+
}
301+
}, [])
302+
303+
return (
304+
<canvas
305+
ref={canvasRef}
306+
className="fixed inset-0 w-full h-full -z-10"
307+
style={{ background: 'transparent' }}
308+
/>
309+
)
310+
}

0 commit comments

Comments
 (0)