返回 AiToEarn
BrushCanvas.tsx
1 /**
2 * BrushCanvas - 画笔画布组件
3 * 双 Canvas 架构:底层显示原图,顶层用于绘制
4 */
5
6 'use client'
7
8 import type { UseBrushEditorReturn } from './useBrushEditor'
9 import type { UseCropEditorReturn } from './useCropEditor'
10 import { Loader2 } from 'lucide-react'
11 import { memo, useEffect, useState } from 'react'
12 import { cn } from '@/utils/className'
13 import { CropOverlay } from './CropOverlay'
14
15 export interface BrushCanvasProps {
16 /** useBrushEditor 返回的状态和方法 */
17 editor: UseBrushEditorReturn
18 /** 裁剪编辑器 */
19 cropEditor?: UseCropEditorReturn
20 /** 容器最大宽度 */
21 maxWidth?: number
22 /** 容器最大高度 */
23 maxHeight?: number
24 /** 自定义类名 */
25 className?: string
26 }
27
28 export const BrushCanvas = memo(
29 ({ editor, cropEditor, maxWidth = 800, maxHeight = 600, className }: BrushCanvasProps) => {
30 const { imageCanvasRef, drawCanvasRef, imageLoaded, loadImage, drawHandlers, imageSize }
31 = editor
32
33 // 计算显示尺寸(保持宽高比)
34 const [displaySize, setDisplaySize] = useState({ width: 0, height: 0 })
35
36 useEffect(() => {
37 loadImage()
38 }, [loadImage])
39
40 useEffect(() => {
41 if (imageLoaded && imageSize.width > 0 && imageSize.height > 0) {
42 const aspectRatio = imageSize.width / imageSize.height
43
44 let width = imageSize.width
45 let height = imageSize.height
46
47 // 按最大宽度缩放
48 if (width > maxWidth) {
49 width = maxWidth
50 height = width / aspectRatio
51 }
52
53 // 按最大高度缩放
54 if (height > maxHeight) {
55 height = maxHeight
56 width = height * aspectRatio
57 }
58
59 setDisplaySize({ width, height })
60 }
61 }, [imageLoaded, imageSize, maxWidth, maxHeight])
62
63 const isCropping = cropEditor?.isCropping
64
65 return (
66 <div
67 className={cn(
68 'relative flex items-center justify-center bg-black/20 rounded-lg overflow-hidden',
69 className,
70 )}
71 style={{
72 minHeight: 200,
73 width: displaySize.width || '100%',
74 height: displaySize.height || 'auto',
75 }}
76 >
77 {/* 加载中 */}
78 {!imageLoaded && (
79 <div className="absolute inset-0 flex items-center justify-center z-10">
80 <Loader2 className="w-10 h-10 text-white/60 animate-spin" />
81 </div>
82 )}
83
84 {/* 底层 Canvas:显示原图 */}
85 <canvas
86 ref={imageCanvasRef}
87 className="absolute inset-0 w-full h-full"
88 style={{
89 opacity: imageLoaded ? 1 : 0,
90 pointerEvents: 'none',
91 }}
92 />
93
94 {/* 顶层 Canvas:绘制层 */}
95 <canvas
96 ref={drawCanvasRef}
97 className="absolute inset-0 w-full h-full cursor-crosshair"
98 style={{
99 opacity: imageLoaded ? 1 : 0,
100 touchAction: 'none',
101 pointerEvents: isCropping ? 'none' : 'auto',
102 }}
103 {...drawHandlers}
104 />
105
106 {/* 裁剪覆盖层 */}
107 {isCropping && cropEditor && displaySize.width > 0 && (
108 <CropOverlay
109 cropRect={cropEditor.cropRect}
110 onCropRectChange={cropEditor.setCropRect}
111 canvasSize={imageSize}
112 displaySize={displaySize}
113 aspectRatio={cropEditor.aspectRatio}
114 />
115 )}
116 </div>
117 )
118 },
119 )
120
121 export default BrushCanvas
122
122 lines Plain Text