6363 margin-bottom : 10px ;
6464 }
6565
66+ .upload-zone .dragover {
67+ border-color : var (--accent-color );
68+ background : rgba (79 , 70 , 229 , 0.12 );
69+ }
70+
6671 /* The Stage */
6772 .stage {
6873 position : relative;
109114 padding : 15px ; border : 1px solid # ff00ff ; border-radius : 8px ;
110115 display : none; z-index : 1000 ; width : 250px ;
111116 }
117+
118+ # mobileWarning {
119+ display : none;
120+ width : 100% ;
121+ max-width : 1100px ;
122+ box-sizing : border-box;
123+ margin : 20px 0 ;
124+ padding : 16px ;
125+ border-radius : 10px ;
126+ border : 1px solid # b45309 ;
127+ background : # 2b1d0e ;
128+ color : # fde68a ;
129+ font-weight : 600 ;
130+ line-height : 1.5 ;
131+ }
132+
133+ # dpiWarning {
134+ grid-column : 1 / -1 ;
135+ display : none;
136+ box-sizing : border-box;
137+ padding : 12px ;
138+ border-radius : 8px ;
139+ border : 1px solid # b45309 ;
140+ background : # 2b1d0e ;
141+ color : # fde68a ;
142+ line-height : 1.4 ;
143+ }
112144 </ style >
113145</ head >
114146< body >
115147
148+ < div id ="mobileWarning ">
149+ Mobile is not fully supported for this tool. Please use a desktop browser for accurate scaling and print output.
150+ </ div >
151+
116152 < div class ="controls ">
117- < div class ="upload-zone " onclick ="document.getElementById('artUpload').click() ">
118- < span style ="color:var(--accent-color) "> FILE:</ span > CLICK TO UPLOAD DESIGN (.PNG)
119- < input type ="file " id ="artUpload " accept ="image/png " style ="display:none ">
153+ < div class ="upload-zone " id =" uploadZone " onclick ="document.getElementById('artUpload').click() ">
154+ < span style ="color:var(--accent-color) "> FILE:</ span > CLICK OR DRAG TO UPLOAD DESIGN (.PNG, .SVG )
155+ < input type ="file " id ="artUpload " accept ="image/png,image/svg+xml,.svg " style ="display:none ">
120156 </ div >
121157
158+ < div id ="dpiWarning "> </ div >
159+
122160 < div class ="control-group " style ="grid-column: 1/-1; flex-direction: row; align-items: center; ">
123161 < input type ="checkbox " id ="showGuides " style ="width:18px; height:18px; ">
124162 < label for ="showGuides " style ="margin-left:8px; text-transform: none; color:#eee; "> Show Center Guides</ label >
@@ -162,19 +200,82 @@ <h4 style="color:#ff00ff; margin:0 0 10px 0;">DEV ALIGNMENT</h4>
162200 // --- USER DEFAULTS ---
163201 const ALIGN_DEF = { x : - 1 , y : 390 , s : 154 } ;
164202
165- const MM_W = 90 , MM_H = 42 , DPI_WS = 360 , DPI_EXP = 600 , IN_MM = 25.4 ;
203+ const MM_W = 90 , MM_H = 42 , DPI_WS = 360 , DPI_EXP = 600 , MIN_SOURCE_DPI = 600 , IN_MM = 25.4 ;
166204 const canvas = document . getElementById ( 'mainCanvas' ) ;
167205 const ctx = canvas . getContext ( '2d' ) ;
168206 const bg = document . getElementById ( 'gamepadBg' ) ;
207+ const dpiWarning = document . getElementById ( 'dpiWarning' ) ;
208+ const uploadZone = document . getElementById ( 'uploadZone' ) ;
209+ const artUpload = document . getElementById ( 'artUpload' ) ;
210+ const scaleRange = document . getElementById ( 'rScale' ) ;
211+ const scaleNum = document . getElementById ( 'nScale' ) ;
212+ const MIN_SCALE = parseFloat ( scaleRange . min ) ;
213+ const MAX_SCALE_DEFAULT = parseFloat ( scaleRange . max ) ;
169214
170215 const wsW = Math . round ( ( MM_W / IN_MM ) * DPI_WS ) ;
171216 const wsH = Math . round ( ( MM_H / IN_MM ) * DPI_WS ) ;
172217 canvas . width = wsW ; canvas . height = wsH ;
173218
174219 let thickImg = new Image ( ) , userArt = null ;
175220 let state = { x : wsW / 2 , y : wsH / 2 , s : 1 } ;
221+ let maxAllowedScale = MAX_SCALE_DEFAULT ;
222+
223+ function isMobileDevice ( ) {
224+ const ua = navigator . userAgent || '' ;
225+ const hasMobileUA = / A n d r o i d | w e b O S | i P h o n e | i P a d | i P o d | B l a c k B e r r y | I E M o b i l e | O p e r a M i n i / i. test ( ua ) ;
226+ const likelyTouchLayout = window . matchMedia ( '(max-width: 900px)' ) . matches && window . matchMedia ( '(pointer: coarse)' ) . matches ;
227+ return hasMobileUA || likelyTouchLayout ;
228+ }
229+
230+ function setMobileUnsupportedUI ( ) {
231+ document . getElementById ( 'mobileWarning' ) . style . display = 'block' ;
232+ document . querySelector ( '.controls' ) . style . display = 'none' ;
233+ document . querySelector ( '.stage' ) . style . display = 'none' ;
234+ }
235+
236+ function clampScale ( value ) {
237+ return Math . max ( MIN_SCALE , Math . min ( maxAllowedScale , value ) ) ;
238+ }
239+
240+ function setScale ( value ) {
241+ const clamped = clampScale ( value ) ;
242+ state . s = clamped ;
243+ scaleRange . value = clamped . toFixed ( 2 ) ;
244+ scaleNum . value = clamped . toFixed ( 2 ) ;
245+ draw ( ) ;
246+ }
247+
248+ function setScaleLimit ( maxScale ) {
249+ maxAllowedScale = Math . max ( MIN_SCALE , Math . min ( MAX_SCALE_DEFAULT , maxScale ) ) ;
250+ scaleRange . max = maxAllowedScale . toFixed ( 2 ) ;
251+ setScale ( state . s ) ;
252+ }
253+
254+ function showDpiWarning ( message ) {
255+ dpiWarning . style . display = 'block' ;
256+ dpiWarning . textContent = message ;
257+ }
258+
259+ function hideDpiWarning ( ) {
260+ dpiWarning . style . display = 'none' ;
261+ dpiWarning . textContent = '' ;
262+ }
263+
264+ function getResolutionBasedDpi ( img ) {
265+ // Estimate source DPI from pixel resolution against print size.
266+ const widthInches = MM_W / IN_MM ;
267+ const heightInches = MM_H / IN_MM ;
268+ const dpiX = img . width / widthInches ;
269+ const dpiY = img . height / heightInches ;
270+ return Math . min ( dpiX , dpiY ) ;
271+ }
176272
177273 window . onload = ( ) => {
274+ if ( isMobileDevice ( ) ) {
275+ setMobileUnsupportedUI ( ) ;
276+ return ;
277+ }
278+
178279 // Init Dev Sliders
179280 document . getElementById ( 'rBgX' ) . value = ALIGN_DEF . x ;
180281 document . getElementById ( 'rBgY' ) . value = ALIGN_DEF . y ;
@@ -200,10 +301,10 @@ <h4 style="color:#ff00ff; margin:0 0 10px 0;">DEV ALIGNMENT</h4>
200301 const r = document . getElementById ( rangeId ) ;
201302 const n = document . getElementById ( numId ) ;
202303 const update = ( val ) => {
203- if ( prop === 's' ) state . s = parseFloat ( val ) ;
304+ if ( prop === 's' ) setScale ( parseFloat ( val ) ) ;
204305 if ( prop === 'x' ) state . x = ( wsW / 2 ) + parseInt ( val ) ;
205306 if ( prop === 'y' ) state . y = ( wsH / 2 ) + parseInt ( val ) ;
206- draw ( ) ;
307+ if ( prop !== 's' ) draw ( ) ;
207308 } ;
208309 r . oninput = ( ) => { n . value = r . value ; update ( r . value ) ; } ;
209310 n . oninput = ( ) => { r . value = n . value ; update ( n . value ) ; } ;
@@ -220,16 +321,71 @@ <h4 style="color:#ff00ff; margin:0 0 10px 0;">DEV ALIGNMENT</h4>
220321
221322 window . onkeydown = ( e ) => { if ( e . key . toLowerCase ( ) === 'd' ) { const d = document . getElementById ( 'devConsole' ) ; d . style . display = d . style . display === 'block' ?'none' :'block' ; } } ;
222323
223- document . getElementById ( 'artUpload' ) . onchange = ( e ) => {
324+ function handleImportedFile ( file ) {
325+ if ( ! file ) return ;
326+
327+ const fileName = file . name || '' ;
328+ const fileType = file . type || '' ;
329+ const isSvg = fileType === 'image/svg+xml' || fileName . toLowerCase ( ) . endsWith ( '.svg' ) ;
330+ const isPng = fileType === 'image/png' || fileName . toLowerCase ( ) . endsWith ( '.png' ) ;
331+ if ( ! isSvg && ! isPng ) {
332+ showDpiWarning ( 'Unsupported file type. Please upload PNG or SVG.' ) ;
333+ return ;
334+ }
335+
224336 const reader = new FileReader ( ) ;
225- reader . onload = ( ev ) => {
337+ new Promise ( ( resolve ) => {
338+ reader . onload = ( ev ) => resolve ( ev . target . result ) ;
339+ reader . readAsDataURL ( file ) ;
340+ } ) . then ( ( dataUrl ) => {
226341 const img = new Image ( ) ;
227- img . onload = ( ) => { userArt = img ; draw ( ) ; } ;
228- img . src = ev . target . result ;
229- } ;
230- reader . readAsDataURL ( e . target . files [ 0 ] ) ;
342+ img . onload = ( ) => {
343+ userArt = img ;
344+
345+ if ( isSvg ) {
346+ setScaleLimit ( MAX_SCALE_DEFAULT ) ;
347+ setScale ( 1 ) ;
348+ hideDpiWarning ( ) ;
349+ } else {
350+ const detectedDpi = getResolutionBasedDpi ( img ) ;
351+ const safeScale = detectedDpi / MIN_SOURCE_DPI ;
352+ setScaleLimit ( safeScale ) ;
353+ if ( detectedDpi < MIN_SOURCE_DPI ) {
354+ setScale ( safeScale ) ;
355+ showDpiWarning ( `This image resolution provides about ${ detectedDpi . toFixed ( 0 ) } DPI for the print area, below the recommended ${ MIN_SOURCE_DPI } DPI. It has been auto-scaled to ${ safeScale . toFixed ( 2 ) } x to maintain ${ MIN_SOURCE_DPI } DPI effective quality, and scaling above this limit is disabled.` ) ;
356+ } else {
357+ setScale ( 1 ) ;
358+ hideDpiWarning ( ) ;
359+ }
360+ }
361+
362+ draw ( ) ;
363+ } ;
364+ img . src = dataUrl ;
365+ } ) ;
366+ }
367+
368+ artUpload . onchange = ( e ) => {
369+ handleImportedFile ( e . target . files [ 0 ] ) ;
370+ artUpload . value = '' ;
231371 } ;
232372
373+ uploadZone . addEventListener ( 'dragover' , ( e ) => {
374+ e . preventDefault ( ) ;
375+ uploadZone . classList . add ( 'dragover' ) ;
376+ } ) ;
377+
378+ uploadZone . addEventListener ( 'dragleave' , ( ) => {
379+ uploadZone . classList . remove ( 'dragover' ) ;
380+ } ) ;
381+
382+ uploadZone . addEventListener ( 'drop' , ( e ) => {
383+ e . preventDefault ( ) ;
384+ uploadZone . classList . remove ( 'dragover' ) ;
385+ const file = e . dataTransfer && e . dataTransfer . files ? e . dataTransfer . files [ 0 ] : null ;
386+ handleImportedFile ( file ) ;
387+ } ) ;
388+
233389 function draw ( ) {
234390 ctx . clearRect ( 0 , 0 , wsW , wsH ) ;
235391 if ( userArt ) {
0 commit comments