1024 × 768
0%
image-generation.tsx
styles.css
prompt.md
Collapse
Reset
Copy Code
Download
Run
1
"use client";
2
3
import React, { useEffect, useRef } from "react";
4
import { cn } from "@/lib/utils";
5
6
export interface ImageGenerationProps {
7
className?: string;
8
}
9
10
export function ImageGeneration({ className }: ImageGenerationProps) {
11
const canvasRef = useRef<HTMLCanvasElement>(null);
12
const progressRef = useRef<HTMLSpanElement>(null);
13
14
useEffect(() => {
15
const canvas = canvasRef.current;
16
if (!canvas) return;
17
const ctx = canvas.getContext("2d", { alpha: false });
18
if (!ctx) return;
19
20
let width = 0;
21
let height = 0;
22
let dpr = 1;
23
24
const resize = () => {
25
const rect = canvas.parentElement?.getBoundingClientRect();
26
if (rect) {
27
dpr = window.devicePixelRatio || 1;
28
width = rect.width;
29
height = rect.height;
30
canvas.width = width * dpr;
31
canvas.height = height * dpr;
32
ctx.scale(dpr, dpr);
33
}
34
};
35
36
const ro = new ResizeObserver(resize);
37
if (canvas.parentElement) {
38
ro.observe(canvas.parentElement);
39
}
40
resize();
41
42
let frameId: number;
43
const startTime = Date.now();
44
const segmentDuration = 4500; // 4.5s per movement segment (much slower)
45
const totalSegments = 8;
46
const duration = segmentDuration * totalSegments;
47
48
const keyframes = [
49
{x: -1, y: -1}, // 0: Top-Left
50
{x: 0, y: 0}, // 1: Center
51
{x: 1, y: 1}, // 2: Bottom-Right
52
{x: 0, y: 0}, // 3: Center
53
{x: 1, y: -1}, // 4: Top-Right
54
{x: 0, y: 0}, // 5: Center
55
{x: -1, y: 1}, // 6: Bottom-Left
56
{x: 0, y: 0} // 7: Center
57
];
58
59
const render = () => {
60
const now = Date.now();
61
const elapsed = now - startTime;
62
const cycleTime = elapsed % duration;
63
64
const segment = Math.floor(cycleTime / segmentDuration);
65
const rawT = (cycleTime % segmentDuration) / segmentDuration;
66
67
// Smooth cosine interpolation for the segment
68
const t = (1 - Math.cos(rawT * Math.PI)) / 2;
69
70
const start = keyframes[segment];
71
const end = keyframes[(segment + 1) % totalSegments];
72
73
const kx = start.x + (end.x - start.x) * t;
74
const ky = start.y + (end.y - start.y) * t;
75
76
// Distance from center (1 = corners, 0 = center)
77
const dist = Math.max(Math.abs(kx), Math.abs(ky));
78
const p = 1 - dist;
79
80
// Update progress text without triggering React re-renders
81
if (progressRef.current) {
82
progressRef.current.innerText = `${Math.floor(p * 100)}%`;
83
}
84
85
const isDark = document.documentElement.classList.contains('dark');
86
87
// Draw background
88
ctx.fillStyle = isDark ? '#111111' : '#fafafa';
89
ctx.fillRect(0, 0, width, height);
90
91
const rgb = isDark ? '255, 255, 255' : '0, 0, 0';
92
93
const spacing = 12;
94
const cx = width / 2;
95
const cy = height / 2;
96
97
const dx = cx * kx;
98
const dy = cy * ky;
99
100
// Radius shrinks as they converge in the center
101
const baseRadius = 50 + dist * 70;
102
103
// 2 moving glow points, perfectly opposite each other
104
const pt1 = { x: cx + dx, y: cy + dy, radius: baseRadius };
105
const pt2 = { x: cx - dx, y: cy - dy, radius: baseRadius };
106
107
// Draw grid
108
for (let x = spacing / 2; x < width; x += spacing) {
109
for (let y = spacing / 2; y < height; y += spacing) {
110
let totalInfluence = 0;
111
for (const pt of [pt1, pt2]) {
112
const d = Math.hypot(x - pt.x, y - pt.y);
113
// Gaussian-like falloff
114
const influence = Math.exp(- (d * d) / (pt.radius * pt.radius));
115
totalInfluence += influence;
116
}
117
const clampedInfluence = Math.min(1, totalInfluence);
118
119
const minSize = 0.5;
120
const maxSize = 2.5;
121
const size = minSize + clampedInfluence * (maxSize - minSize);
122
123
const minAlpha = isDark ? 0.08 : 0.04;
124
const maxAlpha = isDark ? 0.95 : 0.8;
125
const alpha = minAlpha + clampedInfluence * (maxAlpha - minAlpha);
126
127
ctx.fillStyle = `rgba(${rgb}, ${alpha})`;
128
ctx.beginPath();
129
ctx.arc(x, y, size, 0, Math.PI * 2);
130
ctx.fill();
131
}
132
}
133
134
frameId = requestAnimationFrame(render);
135
};
136
137
frameId = requestAnimationFrame(render);
138
139
return () => {
140
cancelAnimationFrame(frameId);
141
ro.disconnect();
142
};
143
}, []);
144
145
return (
146
<div className={cn("relative w-full max-w-[360px] h-[260px] rounded-[18px] overflow-hidden border border-zinc-200 dark:border-white/10 bg-zinc-50 dark:bg-[#111111] shadow-sm", className)}>
147
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
148
149
{/* Top right resolution badge */}
150
<div className="absolute top-4 right-4 rounded-full bg-white/80 dark:bg-black/60 border border-zinc-200 dark:border-white/10 px-2.5 py-1 font-mono text-[11px] text-zinc-600 dark:text-white/80 backdrop-blur-md flex items-center justify-center">
151
1024 × 768
152
</div>
153
154
{/* Bottom left progress badge */}
155
<div className="absolute bottom-4 left-4 rounded-full bg-zinc-900/90 dark:bg-black/80 border border-black/10 dark:border-white/10 px-2.5 py-1 font-mono text-[11px] text-white backdrop-blur-md min-w-[42px] flex items-center justify-center tabular-nums">
156
<span ref={progressRef}>0%</span>
157
</div>
158
</div>
159
);
160
}
161

