1- import React , { useState , useEffect , useRef } from 'react' ;
1+ import React , { useState , useEffect , useRef , useCallback } from 'react' ;
22import { Link } from 'react-router-dom' ;
33import {
4- AcornIcon ,
54 ArrowLeftIcon ,
65 DownloadSimpleIcon ,
76 UploadSimpleIcon ,
87} from '@phosphor-icons/react' ;
98import useSeo from '../../hooks/useSeo' ;
9+ import { useToast } from '../../hooks/useToast' ;
1010import CustomDropdown from '../../components/CustomDropdown' ;
1111import BreadcrumbTitle from '../../components/BreadcrumbTitle' ;
1212
@@ -41,11 +41,15 @@ const TcgCardGeneratorPage = () => {
4141 ] ,
4242 } ) ;
4343
44+ const { addToast } = useToast ( ) ;
45+
4446 // --- State ---
4547 const [ cardName , setCardName ] = useState ( 'Cyber Dragon' ) ;
4648 const [ hp , setHp ] = useState ( '250' ) ;
4749 const [ background , setBackground ] = useState ( 'Techno 1' ) ;
4850 const [ image , setImage ] = useState ( null ) ;
51+ const [ imageDimensions , setImageDimensions ] = useState ( null ) ; // Stores { width, height }
52+ const [ loadedImgElement , setLoadedImgElement ] = useState ( null ) ; // Stores the actual Image object
4953
5054 const [ generation , setGeneration ] = useState ( 'Gen 5 - Neo Tokyo' ) ;
5155 const [ type , setType ] = useState ( 'Machine / Dragon' ) ;
@@ -65,6 +69,17 @@ const TcgCardGeneratorPage = () => {
6569 const canvasRef = useRef ( null ) ;
6670 const fileInputRef = useRef ( null ) ;
6771
72+ // Preload image object when image string changes
73+ useEffect ( ( ) => {
74+ if ( image ) {
75+ const img = new Image ( ) ;
76+ img . src = image ;
77+ img . onload = ( ) => setLoadedImgElement ( img ) ;
78+ } else {
79+ setLoadedImgElement ( null ) ;
80+ }
81+ } , [ image ] ) ;
82+
6883 // --- Canvas Drawing Helper Functions ---
6984 const drawRoundedRect = ( ctx , x , y , width , height , radius ) => {
7085 ctx . beginPath ( ) ;
@@ -101,22 +116,8 @@ const TcgCardGeneratorPage = () => {
101116 return currentY + lineHeight ;
102117 } ;
103118
104- // --- Main Draw Logic ---
105- useEffect ( ( ) => {
106- const canvas = canvasRef . current ;
107- if ( ! canvas ) return ;
108- const ctx = canvas . getContext ( '2d' ) ;
109-
110- // High DPI scaling
111- const dpr = window . devicePixelRatio || 1 ;
112- const logicalWidth = 420 ;
113- const logicalHeight = 620 ; // Increased height slightly
114- canvas . width = logicalWidth * dpr ;
115- canvas . height = logicalHeight * dpr ;
116- ctx . scale ( dpr , dpr ) ;
117- canvas . style . width = `${ logicalWidth } px` ;
118- canvas . style . height = `${ logicalHeight } px` ;
119-
119+ // --- Main Draw Logic (Memoized) ---
120+ const drawCard = useCallback ( ( ctx , logicalWidth , logicalHeight ) => {
120121 // --- Background ---
121122 const selectedBg = backgroundOptions . find ( b => b . value === background ) || backgroundOptions [ 0 ] ;
122123
@@ -189,7 +190,7 @@ const TcgCardGeneratorPage = () => {
189190
190191 // Text: Name
191192 ctx . fillStyle = '#00ffff' ;
192- ctx . font = 'bold 22px "Courier New ", monospace ' ;
193+ ctx . font = 'bold 22px "Arvo ", serif ' ;
193194 ctx . textAlign = 'left' ;
194195 ctx . shadowColor = '#00ffff' ;
195196 ctx . shadowBlur = 8 ;
@@ -198,7 +199,7 @@ const TcgCardGeneratorPage = () => {
198199
199200 // Text: HP
200201 ctx . fillStyle = '#ff0055' ;
201- ctx . font = 'bold 22px "Courier New ", monospace ' ;
202+ ctx . font = 'bold 22px "Arvo ", serif ' ;
202203 ctx . textAlign = 'right' ;
203204 ctx . shadowColor = '#ff0055' ;
204205 ctx . shadowBlur = 8 ;
@@ -216,42 +217,29 @@ const TcgCardGeneratorPage = () => {
216217 ctx . fillRect ( imgX - 2 , imgY - 2 , imgW + 4 , imgH + 4 ) ;
217218
218219 // Draw Image
219- if ( image ) {
220- const img = new Image ( ) ;
221- img . src = image ;
222-
223- const drawImageCover = ( img ) => {
224- const srcRatio = img . width / img . height ;
225- const destRatio = imgW / imgH ;
226- let sx , sy , sWidth , sHeight ;
227-
228- if ( srcRatio > destRatio ) {
229- // Image is wider than destination: crop width
230- sHeight = img . height ;
231- sWidth = img . height * destRatio ;
232- sx = ( img . width - sWidth ) / 2 ;
233- sy = 0 ;
234- } else {
235- // Image is taller than destination: crop height
236- sWidth = img . width ;
237- sHeight = img . width / destRatio ;
238- sx = 0 ;
239- sy = ( img . height - sHeight ) / 2 ;
240- }
241-
242- ctx . drawImage ( img , sx , sy , sWidth , sHeight , imgX , imgY , imgW , imgH ) ;
243- } ;
244-
245- if ( img . complete ) {
246- drawImageCover ( img ) ;
220+ if ( loadedImgElement ) {
221+ // Object-fit: cover logic
222+ const srcRatio = loadedImgElement . width / loadedImgElement . height ;
223+ const destRatio = imgW / imgH ;
224+ let sx , sy , sWidth , sHeight ;
225+
226+ if ( srcRatio > destRatio ) {
227+ sHeight = loadedImgElement . height ;
228+ sWidth = loadedImgElement . height * destRatio ;
229+ sx = ( loadedImgElement . width - sWidth ) / 2 ;
230+ sy = 0 ;
247231 } else {
248- img . onload = ( ) => drawImageCover ( img ) ;
232+ sWidth = loadedImgElement . width ;
233+ sHeight = loadedImgElement . width / destRatio ;
234+ sx = 0 ;
235+ sy = ( loadedImgElement . height - sHeight ) / 2 ;
249236 }
237+ ctx . drawImage ( loadedImgElement , sx , sy , sWidth , sHeight , imgX , imgY , imgW , imgH ) ;
250238 } else {
251239 ctx . fillStyle = '#222' ;
252240 ctx . fillRect ( imgX , imgY , imgW , imgH ) ;
253241 ctx . fillStyle = '#555' ;
254- ctx . font = '16px "Courier New ", monospace ' ;
242+ ctx . font = '16px "Arvo ", serif ' ;
255243 ctx . textAlign = 'center' ;
256244 ctx . fillText ( 'UPLOAD IMAGE' , logicalWidth / 2 , imgY + imgH / 2 ) ;
257245 }
@@ -288,7 +276,7 @@ const TcgCardGeneratorPage = () => {
288276 // --- Content Fields ---
289277 let currentY = imgY + imgH + 20 ;
290278 const labelWidth = 100 ;
291- const valueX = 40 + labelWidth ;
279+ // const valueX = 40 + labelWidth;
292280
293281 const drawField = ( label , value , color = '#fff' ) => {
294282 // Label Box
@@ -351,15 +339,36 @@ const TcgCardGeneratorPage = () => {
351339
352340 // Illustrator (Bottom Left)
353341 ctx . fillStyle = '#fff' ;
354- ctx . font = '10px "Courier New ", monospace ' ;
342+ ctx . font = '10px "Arvo ", serif ' ;
355343 ctx . textAlign = 'left' ;
356344 ctx . fillText ( `ILLUS: ${ illustrator . toUpperCase ( ) } ` , 25 , footerY ) ;
357345
358346 // Card Info (Bottom Right)
359347 const date = new Date ( ) . toLocaleDateString ( ) ;
360348 ctx . textAlign = 'right' ;
361349 ctx . fillText ( `${ date } | FEZCODEX | ${ cardNumber } /${ totalCards } ` , logicalWidth - 25 , footerY ) ;
362- } , [ cardName , hp , background , image , generation , type , attack , defense , cost , description , illustrator , cardNumber , totalCards ] ) ;
350+ } , [ cardName , hp , background , loadedImgElement , generation , type , attack , defense , cost , description , illustrator , cardNumber , totalCards ] ) ;
351+
352+ // --- useEffect to update canvas ---
353+ useEffect ( ( ) => {
354+ const canvas = canvasRef . current ;
355+ if ( ! canvas ) return ;
356+ const ctx = canvas . getContext ( '2d' ) ;
357+
358+ const dpr = window . devicePixelRatio || 1 ;
359+ const logicalWidth = 420 ;
360+ const logicalHeight = 620 ;
361+
362+ canvas . width = logicalWidth * dpr ;
363+ canvas . height = logicalHeight * dpr ;
364+ canvas . style . width = `${ logicalWidth } px` ;
365+ canvas . style . height = `${ logicalHeight } px` ;
366+
367+ ctx . save ( ) ;
368+ ctx . scale ( dpr , dpr ) ;
369+ drawCard ( ctx , logicalWidth , logicalHeight ) ;
370+ ctx . restore ( ) ;
371+ } , [ drawCard ] ) ; // Depend only on drawCard which already depends on all state
363372
364373 const handleImageUpload = ( e ) => {
365374 const file = e . target . files [ 0 ] ;
@@ -368,6 +377,7 @@ const TcgCardGeneratorPage = () => {
368377 reader . onload = ( event ) => {
369378 const img = new Image ( ) ;
370379 img . onload = ( ) => {
380+ setImageDimensions ( { width : img . width , height : img . height } ) ;
371381 setImage ( event . target . result ) ;
372382 } ;
373383 img . src = event . target . result ;
@@ -381,11 +391,23 @@ const TcgCardGeneratorPage = () => {
381391 } ;
382392
383393 const downloadCard = ( ) => {
384- const canvas = canvasRef . current ;
385- if ( ! canvas ) return ;
394+ addToast ( { title : 'Downloading...' , message : 'Generating high-resolution card.' , duration : 3000 } ) ;
395+ // High Resolution Download
396+ const scaleFactor = 3 ; // 3x resolution (1260x1860)
397+ const width = 420 ;
398+ const height = 620 ;
399+
400+ const canvas = document . createElement ( 'canvas' ) ;
401+ canvas . width = width * scaleFactor ;
402+ canvas . height = height * scaleFactor ;
403+ const ctx = canvas . getContext ( '2d' ) ;
404+
405+ ctx . scale ( scaleFactor , scaleFactor ) ;
406+ drawCard ( ctx , width , height ) ;
407+
386408 const link = document . createElement ( 'a' ) ;
387- link . download = `${ cardName . replace ( / \s + / g, '_' ) } _card .png` ;
388- link . href = canvas . toDataURL ( ) ;
409+ link . download = `${ cardName . replace ( / \s + / g, '_' ) } _card_HD .png` ;
410+ link . href = canvas . toDataURL ( 'image/png' ) ;
389411 link . click ( ) ;
390412 } ;
391413
@@ -403,13 +425,13 @@ const TcgCardGeneratorPage = () => {
403425 { /* Header */ }
404426 < div className = "mb-10 text-center" >
405427 < Link to = "/apps" className = "text-article hover:underline flex items-center justify-center gap-2 text-lg mb-4" >
406- < ArrowLeftIcon size = { 24 } /> Back to Apps
428+ < ArrowLeftIcon size = { 24 } /> Back to Apps
407429 </ Link >
408430 < BreadcrumbTitle title = "Techno TCG Maker" slug = "tcg" />
409431 < p className = "text-gray-500 max-w-xl mx-auto mb-4" >
410432 Design your own futuristic trading cards.
411433 </ p >
412- < hr className = "border-gray-700" />
434+ < hr className = "border-gray-700" />
413435 </ div >
414436 < div className = "flex flex-col xl:flex-row gap-10 items-start justify-center" >
415437 { /* --- Left Column: Editor --- */ }
@@ -459,6 +481,11 @@ const TcgCardGeneratorPage = () => {
459481 < div className = "flex items-center gap-4" >
460482 < div className = "flex-1" >
461483 < p className = "text-sm text-gray-400" > Upload a cyberpunk or futuristic image for best results.</ p >
484+ { imageDimensions && (
485+ < p className = "text-xs text-gray-500 mt-1" >
486+ Dimensions: { imageDimensions . width } x { imageDimensions . height } px
487+ </ p >
488+ ) }
462489 </ div >
463490 < div className = "flex-shrink-0" >
464491 < input
@@ -597,13 +624,13 @@ const TcgCardGeneratorPage = () => {
597624 < div className = "mt-8 flex gap-4 w-full max-w-[420px]" >
598625 < button
599626 onClick = { downloadCard }
600- className = "flex-1 flex items-center justify-center gap-2 px-6 py-4 rounded-xl text-lg font-bold bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white shadow-lg hover:shadow-cyan-900/30 transition-all transform hover:-translate-y-0.5 "
627+ className = "flex-1 flex items-center justify-center gap-2 px-6 py-4 rounded-md text-lg font-arvo transition-colors border bg-tb text-app border-app-alpha-50 hover:bg-app/15 "
601628 >
602- < DownloadSimpleIcon size = { 24 } weight = "bold" /> Download Card
629+ < DownloadSimpleIcon size = { 24 } weight = "bold" /> Download HD
603630 </ button >
604631 </ div >
605632 < p className = "mt-4 text-xs text-gray-500" >
606- High-resolution PNG output.
633+ High-resolution (3x) PNG output.
607634 </ p >
608635 </ div >
609636 </ div >
0 commit comments