[READ-ONLY] One Calendar is a privacy-first calendar web app built with Next.js. It has modern security features, including e2ee, password-protected sharing, and self-destructing share links 📅
calendar.xyehr.cn
nextjs
2.8 kB
103 lines
1'use client'
2
3import { useEffect, useRef } from 'react'
4
5export function AnimatedSphere() {
6 const canvasRef = useRef<HTMLCanvasElement>(null)
7 const frameRef = useRef(0)
8
9 useEffect(() => {
10 const canvas = canvasRef.current
11 if (!canvas) return
12
13 const ctx = canvas.getContext('2d')
14 if (!ctx) return
15
16 const chars = '░▒▓█▀▄▌▐│─┤├┴┬╭╮╰╯'
17 let time = 0
18
19 const resize = () => {
20 const dpr = window.devicePixelRatio || 1
21 const rect = canvas.getBoundingClientRect()
22 canvas.width = rect.width * dpr
23 canvas.height = rect.height * dpr
24 ctx.scale(dpr, dpr)
25 }
26
27 resize()
28 window.addEventListener('resize', resize)
29
30 const render = () => {
31 const rect = canvas.getBoundingClientRect()
32 ctx.clearRect(0, 0, rect.width, rect.height)
33
34 const centerX = rect.width / 2
35 const centerY = rect.height / 2
36 const radius = Math.min(rect.width, rect.height) * 0.525
37
38 ctx.font = '12px monospace'
39 ctx.textAlign = 'center'
40 ctx.textBaseline = 'middle'
41
42 const step = 12
43 const points: { x: number; y: number; z: number; char: string }[] = []
44
45 // Generate sphere points
46 for (let phi = 0; phi < Math.PI * 2; phi += 0.15) {
47 for (let theta = 0; theta < Math.PI; theta += 0.15) {
48 const x = Math.sin(theta) * Math.cos(phi + time * 0.5)
49 const y = Math.sin(theta) * Math.sin(phi + time * 0.5)
50 const z = Math.cos(theta)
51
52 // Rotate around Y axis
53 const rotY = time * 0.3
54 const newX = x * Math.cos(rotY) - z * Math.sin(rotY)
55 const newZ = x * Math.sin(rotY) + z * Math.cos(rotY)
56
57 // Rotate around X axis
58 const rotX = time * 0.2
59 const newY = y * Math.cos(rotX) - newZ * Math.sin(rotX)
60 const finalZ = y * Math.sin(rotX) + newZ * Math.cos(rotX)
61
62 const depth = (finalZ + 1) / 2
63 const charIndex = Math.floor(depth * (chars.length - 1))
64
65 points.push({
66 x: centerX + newX * radius,
67 y: centerY + newY * radius,
68 z: finalZ,
69 char: chars[charIndex],
70 })
71 }
72 }
73
74 // Sort by z for depth
75 points.sort((a, b) => a.z - b.z)
76
77 // Draw points
78 points.forEach((point) => {
79 const alpha = 0.2 + (point.z + 1) * 0.4
80 ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`
81 ctx.fillText(point.char, point.x, point.y)
82 })
83
84 time += 0.02
85 frameRef.current = requestAnimationFrame(render)
86 }
87
88 render()
89
90 return () => {
91 window.removeEventListener('resize', resize)
92 cancelAnimationFrame(frameRef.current)
93 }
94 }, [])
95
96 return (
97 <canvas
98 ref={canvasRef}
99 className="w-full h-full"
100 style={{ display: 'block' }}
101 />
102 )
103}