1+ import React , { useRef , useEffect , useCallback } from 'react' ;
2+
3+ const CanvasPreview = ( {
4+ text,
5+ author,
6+ width,
7+ height,
8+ backgroundColor,
9+ textColor,
10+ fontFamily,
11+ fontSize,
12+ fontWeight,
13+ textAlign,
14+ padding,
15+ lineHeight,
16+ backgroundImage,
17+ overlayOpacity,
18+ overlayColor,
19+ themeType, // 'standard', 'wordbox', 'outline', 'newspaper'
20+ onDownload,
21+ triggerDownload // boolean to trigger download effect
22+ } ) => {
23+ const canvasRef = useRef ( null ) ;
24+
25+ // Helper to wrap text
26+ const getWrappedLines = ( ctx , text , maxWidth ) => {
27+ const words = text . split ( ' ' ) ;
28+ const lines = [ ] ;
29+ let currentLine = words [ 0 ] ;
30+
31+ for ( let i = 1 ; i < words . length ; i ++ ) {
32+ const word = words [ i ] ;
33+ const width = ctx . measureText ( currentLine + " " + word ) . width ;
34+ if ( width < maxWidth ) {
35+ currentLine += " " + word ;
36+ } else {
37+ lines . push ( currentLine ) ;
38+ currentLine = word ;
39+ }
40+ }
41+ lines . push ( currentLine ) ;
42+ return lines ;
43+ } ;
44+
45+ const drawImageCover = useCallback ( ( ctx , img , w , h ) => {
46+ const imgRatio = img . width / img . height ;
47+ const canvasRatio = w / h ;
48+ let renderW , renderH , offsetX , offsetY ;
49+
50+ if ( imgRatio > canvasRatio ) {
51+ renderH = h ;
52+ renderW = h * imgRatio ;
53+ offsetX = ( w - renderW ) / 2 ;
54+ offsetY = 0 ;
55+ } else {
56+ renderW = w ;
57+ renderH = w / imgRatio ;
58+ offsetX = 0 ;
59+ offsetY = ( h - renderH ) / 2 ;
60+ }
61+ ctx . drawImage ( img , offsetX , offsetY , renderW , renderH ) ;
62+ } , [ ] ) ;
63+
64+ const drawNewspaperBg = useCallback ( ( ctx , w , h ) => {
65+ // Clear canvas for transparency around torn edges
66+ ctx . clearRect ( 0 , 0 , w , h ) ;
67+
68+ const pad = 60 ; // Padding from canvas edge for the paper
69+
70+ ctx . beginPath ( ) ;
71+ ctx . moveTo ( pad , pad ) ;
72+
73+ // Top edge (ragged)
74+ for ( let x = pad ; x <= w - pad ; x += 5 ) {
75+ ctx . lineTo ( x , pad + ( Math . random ( ) - 0.5 ) * 8 ) ;
76+ }
77+
78+ // Right edge (ragged)
79+ for ( let y = pad ; y <= h - pad ; y += 5 ) {
80+ ctx . lineTo ( w - pad + ( Math . random ( ) - 0.5 ) * 8 , y ) ;
81+ }
82+
83+ // Bottom edge (ragged)
84+ for ( let x = w - pad ; x >= pad ; x -= 5 ) {
85+ ctx . lineTo ( x , h - pad + ( Math . random ( ) - 0.5 ) * 8 ) ;
86+ }
87+
88+ // Left edge (ragged)
89+ for ( let y = h - pad ; y >= pad ; y -= 5 ) {
90+ ctx . lineTo ( pad + ( Math . random ( ) - 0.5 ) * 8 , y ) ;
91+ }
92+ ctx . closePath ( ) ;
93+
94+ // Shadow for depth (Outer glow)
95+ ctx . save ( ) ;
96+ ctx . shadowColor = "rgba(0,0,0,0.6)" ;
97+ ctx . shadowBlur = 25 ;
98+ ctx . shadowOffsetX = 10 ;
99+ ctx . shadowOffsetY = 15 ;
100+ ctx . fillStyle = backgroundColor ;
101+ ctx . fill ( ) ;
102+ ctx . restore ( ) ;
103+
104+ // 1. Apply Texture (Noise) to the filled shape
105+ const imageData = ctx . getImageData ( 0 , 0 , w , h ) ;
106+ const data = imageData . data ;
107+ for ( let i = 0 ; i < data . length ; i += 4 ) {
108+ // Only apply noise where alpha > 0 (inside the filled shape)
109+ if ( data [ i + 3 ] > 0 ) {
110+ const noise = ( Math . random ( ) - 0.5 ) * 20 ;
111+ data [ i ] = Math . min ( 255 , Math . max ( 0 , data [ i ] + noise ) ) ;
112+ data [ i + 1 ] = Math . min ( 255 , Math . max ( 0 , data [ i + 1 ] + noise ) ) ;
113+ data [ i + 2 ] = Math . min ( 255 , Math . max ( 0 , data [ i + 2 ] + noise ) ) ;
114+ }
115+ }
116+ ctx . putImageData ( imageData , 0 , 0 ) ;
117+
118+ // 2. Aged Vignette
119+ ctx . save ( ) ;
120+ ctx . globalCompositeOperation = 'source-atop' ; // Only draw on top of existing paper
121+ const gradient = ctx . createRadialGradient ( w / 2 , h / 2 , w / 3 , w / 2 , h / 2 , w * 0.8 ) ;
122+ gradient . addColorStop ( 0 , "rgba(0,0,0,0)" ) ;
123+ gradient . addColorStop ( 1 , "rgba(139, 69, 19, 0.2)" ) ; // Sepia tint
124+ ctx . fillStyle = gradient ;
125+ ctx . fillRect ( 0 , 0 , w , h ) ;
126+ ctx . restore ( ) ;
127+ } , [ backgroundColor ] ) ;
128+
129+ const drawContent = useCallback ( ( ctx ) => {
130+ // 3. Text Configuration
131+ ctx . fillStyle = textColor ;
132+ ctx . font = `${ fontWeight } ${ fontSize } px "${ fontFamily } "` ;
133+ ctx . textBaseline = 'top' ;
134+
135+ const maxWidth = width - ( padding * 2 ) ;
136+ const lines = getWrappedLines ( ctx , text , maxWidth ) ;
137+ const totalTextHeight = lines . length * ( fontSize * lineHeight ) ;
138+
139+ // Vertical Center Calculation
140+ let startY = ( height - totalTextHeight ) / 2 ;
141+
142+ // Adjust if there is an author
143+ if ( author ) {
144+ startY -= ( fontSize * 0.8 ) ;
145+ }
146+
147+ // Drawing Text
148+ lines . forEach ( ( line , index ) => {
149+ const lineWidth = ctx . measureText ( line ) . width ;
150+ let x ;
151+ if ( textAlign === 'center' ) x = ( width - lineWidth ) / 2 ;
152+ else if ( textAlign === 'right' ) x = width - padding - lineWidth ;
153+ else x = padding ;
154+
155+ const y = startY + ( index * fontSize * lineHeight ) ;
156+
157+ if ( themeType === 'wordbox' ) {
158+ const bgPadding = fontSize * 0.2 ;
159+ ctx . save ( ) ;
160+ ctx . fillStyle = textColor ;
161+ ctx . fillRect ( x - bgPadding , y - bgPadding , lineWidth + ( bgPadding * 2 ) , ( fontSize * lineHeight ) ) ;
162+
163+ ctx . fillStyle = backgroundColor ;
164+ ctx . fillText ( line , x , y ) ;
165+ ctx . restore ( ) ;
166+ } else {
167+ ctx . fillText ( line , x , y ) ;
168+ }
169+ } ) ;
170+
171+ // 4. Draw Author
172+ if ( author ) {
173+ const authorFontSize = fontSize * 0.5 ;
174+ ctx . font = `italic ${ fontWeight } ${ authorFontSize } px "${ fontFamily } "` ;
175+ const authorY = startY + totalTextHeight + ( fontSize * 1.5 ) ;
176+ const authorWidth = ctx . measureText ( "- " + author ) . width ;
177+
178+ let authorX ;
179+ if ( textAlign === 'center' ) authorX = ( width - authorWidth ) / 2 ;
180+ else if ( textAlign === 'right' ) authorX = width - padding - authorWidth ;
181+ else authorX = padding ;
182+
183+ ctx . fillText ( "- " + author , authorX , authorY ) ;
184+ }
185+ } , [ textColor , fontWeight , fontSize , fontFamily , width , padding , text , lineHeight , height , author , textAlign , themeType , backgroundColor ] ) ;
186+
187+ const draw = useCallback ( ( ) => {
188+ const canvas = canvasRef . current ;
189+ if ( ! canvas ) return ;
190+ const ctx = canvas . getContext ( '2d' ) ;
191+
192+ // 1. Setup Canvas
193+ canvas . width = width ;
194+ canvas . height = height ;
195+
196+ // 2. Background
197+ if ( themeType === 'newspaper' ) {
198+ drawNewspaperBg ( ctx , width , height ) ;
199+ } else {
200+ ctx . fillStyle = backgroundColor ;
201+ ctx . fillRect ( 0 , 0 , width , height ) ;
202+ }
203+
204+ if ( backgroundImage ) {
205+ const img = new Image ( ) ;
206+ img . src = backgroundImage ;
207+ if ( img . complete ) {
208+ drawImageCover ( ctx , img , width , height ) ;
209+ } else {
210+ img . onload = ( ) => {
211+ drawImageCover ( ctx , img , width , height ) ;
212+ drawContent ( ctx ) ;
213+ }
214+ }
215+ }
216+
217+ // Overlay
218+ if ( overlayOpacity > 0 ) {
219+ ctx . fillStyle = overlayColor || '#000000' ;
220+ ctx . globalAlpha = overlayOpacity ;
221+ ctx . fillRect ( 0 , 0 , width , height ) ;
222+ ctx . globalAlpha = 1.0 ;
223+ }
224+
225+ drawContent ( ctx ) ;
226+ } , [ width , height , backgroundColor , backgroundImage , overlayOpacity , overlayColor , drawImageCover , drawContent , themeType , drawNewspaperBg ] ) ;
227+ useEffect ( ( ) => {
228+ draw ( ) ;
229+ document . fonts . ready . then ( draw ) ;
230+ } , [ draw ] ) ;
231+
232+ useEffect ( ( ) => {
233+ if ( triggerDownload && onDownload && canvasRef . current ) {
234+ onDownload ( canvasRef . current . toDataURL ( 'image/png' ) ) ;
235+ }
236+ } , [ triggerDownload , onDownload ] ) ;
237+
238+ return (
239+ < div className = "w-full flex justify-center items-center overflow-hidden bg-[#111] border border-white/10 rounded-lg p-4" >
240+ < canvas
241+ ref = { canvasRef }
242+ style = { {
243+ maxWidth : '100%' ,
244+ height : 'auto' ,
245+ boxShadow : '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)'
246+ } }
247+ />
248+ </ div >
249+ ) ;
250+ } ;
251+
252+ export default CanvasPreview ;
0 commit comments