@docubook/create 1.16.1 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,324 +0,0 @@
1
- "use client";
2
-
3
- import React, { useEffect, useRef, useState } from "react";
4
- import { renderToString } from "react-dom/server";
5
-
6
- interface Icon {
7
- x: number;
8
- y: number;
9
- z: number;
10
- scale: number;
11
- opacity: number;
12
- id: number;
13
- }
14
-
15
- interface IconCloudProps {
16
- icons?: React.ReactNode[];
17
- images?: string[];
18
- }
19
-
20
- function easeOutCubic(t: number): number {
21
- return 1 - Math.pow(1 - t, 3);
22
- }
23
-
24
- export function IconCloud({ icons, images }: IconCloudProps) {
25
- const canvasRef = useRef<HTMLCanvasElement>(null);
26
- const [iconPositions, setIconPositions] = useState<Icon[]>([]);
27
- const [rotation] = useState({ x: 0, y: 0 });
28
- const [isDragging, setIsDragging] = useState(false);
29
- const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 });
30
- const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
31
- const [targetRotation, setTargetRotation] = useState<{
32
- x: number;
33
- y: number;
34
- startX: number;
35
- startY: number;
36
- distance: number;
37
- startTime: number;
38
- duration: number;
39
- } | null>(null);
40
- const animationFrameRef = useRef<number>();
41
- const rotationRef = useRef(rotation);
42
- const iconCanvasesRef = useRef<HTMLCanvasElement[]>([]);
43
- const imagesLoadedRef = useRef<boolean[]>([]);
44
-
45
- // Create icon canvases once when icons/images change
46
- useEffect(() => {
47
- if (!icons && !images) return;
48
-
49
- const items = icons || images || [];
50
- imagesLoadedRef.current = new Array(items.length).fill(false);
51
-
52
- const newIconCanvases = items.map((item, index) => {
53
- const offscreen = document.createElement("canvas");
54
- offscreen.width = 40;
55
- offscreen.height = 40;
56
- const offCtx = offscreen.getContext("2d");
57
-
58
- if (offCtx) {
59
- if (images) {
60
- // Handle image URLs directly
61
- const img = new Image();
62
- img.crossOrigin = "anonymous";
63
- img.src = items[index] as string;
64
- img.onload = () => {
65
- offCtx.clearRect(0, 0, offscreen.width, offscreen.height);
66
-
67
- // Create circular clipping path
68
- offCtx.beginPath();
69
- offCtx.arc(20, 20, 20, 0, Math.PI * 2);
70
- offCtx.closePath();
71
- offCtx.clip();
72
-
73
- // Draw the image
74
- offCtx.drawImage(img, 0, 0, 40, 40);
75
-
76
- imagesLoadedRef.current[index] = true;
77
- };
78
- } else {
79
- // Handle SVG icons
80
- offCtx.scale(0.4, 0.4);
81
- const svgString = renderToString(item as React.ReactElement);
82
- const img = new Image();
83
- img.src = "data:image/svg+xml;base64," + btoa(svgString);
84
- img.onload = () => {
85
- offCtx.clearRect(0, 0, offscreen.width, offscreen.height);
86
- offCtx.drawImage(img, 0, 0);
87
- imagesLoadedRef.current[index] = true;
88
- };
89
- }
90
- }
91
- return offscreen;
92
- });
93
-
94
- iconCanvasesRef.current = newIconCanvases;
95
- }, [icons, images]);
96
-
97
- // Generate initial icon positions on a sphere
98
- useEffect(() => {
99
- const items = icons || images || [];
100
- const newIcons: Icon[] = [];
101
- const numIcons = items.length || 20;
102
-
103
- // Fibonacci sphere parameters
104
- const offset = 2 / numIcons;
105
- const increment = Math.PI * (3 - Math.sqrt(5));
106
-
107
- for (let i = 0; i < numIcons; i++) {
108
- const y = i * offset - 1 + offset / 2;
109
- const r = Math.sqrt(1 - y * y);
110
- const phi = i * increment;
111
-
112
- const x = Math.cos(phi) * r;
113
- const z = Math.sin(phi) * r;
114
-
115
- newIcons.push({
116
- x: x * 100,
117
- y: y * 100,
118
- z: z * 100,
119
- scale: 1,
120
- opacity: 1,
121
- id: i,
122
- });
123
- }
124
- setIconPositions(newIcons);
125
- }, [icons, images]);
126
-
127
- // Handle mouse events
128
- const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
129
- const rect = canvasRef.current?.getBoundingClientRect();
130
- if (!rect || !canvasRef.current) return;
131
-
132
- const x = e.clientX - rect.left;
133
- const y = e.clientY - rect.top;
134
-
135
- const ctx = canvasRef.current.getContext("2d");
136
- if (!ctx) return;
137
-
138
- iconPositions.forEach((icon) => {
139
- const cosX = Math.cos(rotationRef.current.x);
140
- const sinX = Math.sin(rotationRef.current.x);
141
- const cosY = Math.cos(rotationRef.current.y);
142
- const sinY = Math.sin(rotationRef.current.y);
143
-
144
- const rotatedX = icon.x * cosY - icon.z * sinY;
145
- const rotatedZ = icon.x * sinY + icon.z * cosY;
146
- const rotatedY = icon.y * cosX + rotatedZ * sinX;
147
-
148
- const screenX = canvasRef.current!.width / 2 + rotatedX;
149
- const screenY = canvasRef.current!.height / 2 + rotatedY;
150
-
151
- const scale = (rotatedZ + 200) / 300;
152
- const radius = 20 * scale;
153
- const dx = x - screenX;
154
- const dy = y - screenY;
155
-
156
- if (dx * dx + dy * dy < radius * radius) {
157
- const targetX = -Math.atan2(
158
- icon.y,
159
- Math.sqrt(icon.x * icon.x + icon.z * icon.z),
160
- );
161
- const targetY = Math.atan2(icon.x, icon.z);
162
-
163
- const currentX = rotationRef.current.x;
164
- const currentY = rotationRef.current.y;
165
- const distance = Math.sqrt(
166
- Math.pow(targetX - currentX, 2) + Math.pow(targetY - currentY, 2),
167
- );
168
-
169
- const duration = Math.min(2000, Math.max(800, distance * 1000));
170
-
171
- setTargetRotation({
172
- x: targetX,
173
- y: targetY,
174
- startX: currentX,
175
- startY: currentY,
176
- distance,
177
- startTime: performance.now(),
178
- duration,
179
- });
180
- return;
181
- }
182
- });
183
-
184
- setIsDragging(true);
185
- setLastMousePos({ x: e.clientX, y: e.clientY });
186
- };
187
-
188
- const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
189
- const rect = canvasRef.current?.getBoundingClientRect();
190
- if (rect) {
191
- const x = e.clientX - rect.left;
192
- const y = e.clientY - rect.top;
193
- setMousePos({ x, y });
194
- }
195
-
196
- if (isDragging) {
197
- const deltaX = e.clientX - lastMousePos.x;
198
- const deltaY = e.clientY - lastMousePos.y;
199
-
200
- rotationRef.current = {
201
- x: rotationRef.current.x + deltaY * 0.002,
202
- y: rotationRef.current.y + deltaX * 0.002,
203
- };
204
-
205
- setLastMousePos({ x: e.clientX, y: e.clientY });
206
- }
207
- };
208
-
209
- const handleMouseUp = () => {
210
- setIsDragging(false);
211
- };
212
-
213
- // Animation and rendering
214
- useEffect(() => {
215
- const canvas = canvasRef.current;
216
- const ctx = canvas?.getContext("2d");
217
- if (!canvas || !ctx) return;
218
-
219
- const animate = () => {
220
- ctx.clearRect(0, 0, canvas.width, canvas.height);
221
-
222
- const centerX = canvas.width / 2;
223
- const centerY = canvas.height / 2;
224
- const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);
225
- const dx = mousePos.x - centerX;
226
- const dy = mousePos.y - centerY;
227
- const distance = Math.sqrt(dx * dx + dy * dy);
228
- const speed = 0.003 + (distance / maxDistance) * 0.01;
229
-
230
- if (targetRotation) {
231
- const elapsed = performance.now() - targetRotation.startTime;
232
- const progress = Math.min(1, elapsed / targetRotation.duration);
233
- const easedProgress = easeOutCubic(progress);
234
-
235
- rotationRef.current = {
236
- x:
237
- targetRotation.startX +
238
- (targetRotation.x - targetRotation.startX) * easedProgress,
239
- y:
240
- targetRotation.startY +
241
- (targetRotation.y - targetRotation.startY) * easedProgress,
242
- };
243
-
244
- if (progress >= 1) {
245
- setTargetRotation(null);
246
- }
247
- } else if (!isDragging) {
248
- rotationRef.current = {
249
- x: rotationRef.current.x + (dy / canvas.height) * speed,
250
- y: rotationRef.current.y + (dx / canvas.width) * speed,
251
- };
252
- }
253
-
254
- iconPositions.forEach((icon, index) => {
255
- const cosX = Math.cos(rotationRef.current.x);
256
- const sinX = Math.sin(rotationRef.current.x);
257
- const cosY = Math.cos(rotationRef.current.y);
258
- const sinY = Math.sin(rotationRef.current.y);
259
-
260
- const rotatedX = icon.x * cosY - icon.z * sinY;
261
- const rotatedZ = icon.x * sinY + icon.z * cosY;
262
- const rotatedY = icon.y * cosX + rotatedZ * sinX;
263
-
264
- const scale = (rotatedZ + 200) / 300;
265
- const opacity = Math.max(0.2, Math.min(1, (rotatedZ + 150) / 200));
266
-
267
- ctx.save();
268
- ctx.translate(
269
- canvas.width / 2 + rotatedX,
270
- canvas.height / 2 + rotatedY,
271
- );
272
- ctx.scale(scale, scale);
273
- ctx.globalAlpha = opacity;
274
-
275
- if (icons || images) {
276
- // Only try to render icons/images if they exist
277
- if (
278
- iconCanvasesRef.current[index] &&
279
- imagesLoadedRef.current[index]
280
- ) {
281
- ctx.drawImage(iconCanvasesRef.current[index], -20, -20, 40, 40);
282
- }
283
- } else {
284
- // Show numbered circles if no icons/images are provided
285
- ctx.beginPath();
286
- ctx.arc(0, 0, 20, 0, Math.PI * 2);
287
- ctx.fillStyle = "#4444ff";
288
- ctx.fill();
289
- ctx.fillStyle = "white";
290
- ctx.textAlign = "center";
291
- ctx.textBaseline = "middle";
292
- ctx.font = "16px Arial";
293
- ctx.fillText(`${icon.id + 1}`, 0, 0);
294
- }
295
-
296
- ctx.restore();
297
- });
298
- animationFrameRef.current = requestAnimationFrame(animate);
299
- };
300
-
301
- animate();
302
-
303
- return () => {
304
- if (animationFrameRef.current) {
305
- cancelAnimationFrame(animationFrameRef.current);
306
- }
307
- };
308
- }, [icons, images, iconPositions, isDragging, mousePos, targetRotation]);
309
-
310
- return (
311
- <canvas
312
- ref={canvasRef}
313
- width={400}
314
- height={400}
315
- onMouseDown={handleMouseDown}
316
- onMouseMove={handleMouseMove}
317
- onMouseUp={handleMouseUp}
318
- onMouseLeave={handleMouseUp}
319
- className="rounded-full"
320
- aria-label="Interactive 3D Icon Cloud"
321
- role="img"
322
- />
323
- );
324
- }