11<!doctype html>
22< html >
3+ < meta
4+ name ="viewport "
5+ content ="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no "
6+ />
37 < head >
48 < style >
59 canvas {
2226 color-scheme : dark;
2327 background-color : # 15171c ;
2428 color : white;
29+ margin-left : 0px ;
2530 }
2631 # textarea {
2732 width : 95% ;
3540 }
3641 # canvasContainer {
3742 position : relative;
38- width : 960px ; /* 1920 * 0.5 */
39- height : 540px ; /* 1080 * 0.5 */
40- border : 2px solid # 333 ;
43+ width : 100vw ;
44+ height : 90vh ;
45+ overflow : hidden;
46+ }
47+
48+ # scaler {
49+ position : absolute;
50+ top : 0 ;
51+ left : 0 ;
52+ width : 1920px ;
53+ height : 1080px ;
54+ transform-origin : top left;
4155 }
4256
4357 # player {
87101 .actions {
88102 text-align : right;
89103 }
104+ canvas {
105+ border : none;
106+ }
90107 </ style >
91108 < link
92109 href ="https://fonts.googleapis.com/icon?family=Material+Icons "
@@ -210,12 +227,11 @@ <h3>Select an image</h3>
210227 </ form >
211228 </ dialog >
212229 </ div >
213- < div
214- id ="canvasContainer "
215- style ="border: 2px solid #333; display: inline-block "
216- >
217- < div id ="player "> </ div >
218- < canvas id ="editorCanvas " style ="display: none "> </ canvas >
230+ < div id ="canvasContainer " style ="display: inline-block ">
231+ < div id ="scaler ">
232+ < div id ="player "> </ div >
233+ < canvas id ="editorCanvas " style ="display: none "> </ canvas >
234+ </ div >
219235 </ div >
220236
221237 < textarea style ="display: none " id ="textarea " rows ="10 "> </ textarea >
@@ -224,19 +240,22 @@ <h3>Select an image</h3>
224240 const dialog = document . getElementById ( "imageDialog" ) ;
225241 const grid = document . getElementById ( "imageGrid" ) ;
226242 const form = document . getElementById ( "imageUploadForm" ) ;
227-
228- fabric . FabricObject . customProperties = [ "dataId" , "counterName" ] ;
229- let data = { } ;
230243 const playPauseButton = document . getElementById ( "playPauseButton" ) ;
231244 const player = new Twitch . Player ( "player" , {
232- width : 1920 / 2 ,
233- height : 1080 / 2 ,
245+ width : 1920 ,
246+ height : 1080 ,
234247 channel : "sweetbabooo_o" ,
235248 parent : [ "localhost" , "talkingpanda.dev" ] ,
236249 autoplay : true ,
237250 muted : true ,
238251 } ) ;
239- player . addEventListener ( Twitch . Player . PLAYING , ( ) => {
252+ let viewScale = 1 ;
253+ let panX = 0 ;
254+ let panY = 0 ;
255+ fabric . FabricObject . customProperties = [ "dataId" , "counterName" ] ;
256+ let data = { } ;
257+
258+ player . addEventListener ( Twitch . Player . READY , ( ) => {
240259 if ( ! canvas ) initCanvas ( ) ;
241260 } ) ;
242261
@@ -286,14 +305,191 @@ <h3>Select an image</h3>
286305 ctx . drawImage ( deleteImg , - size / 2 , - size / 2 , size , size ) ;
287306 ctx . restore ( ) ;
288307 }
308+
309+ function enablePinchAndPan ( canvas ) {
310+ let lastDist = null ;
311+ let lastMid = null ;
312+ let startScale = viewScale ;
313+ let startPanX = panX ;
314+ let startPanY = panY ;
315+
316+ const el = canvas . upperCanvasEl ;
317+
318+ el . addEventListener (
319+ "touchstart" ,
320+ ( e ) => {
321+ if ( e . touches . length === 2 ) {
322+ e . preventDefault ( ) ;
323+
324+ lastDist = getDistance ( e . touches [ 0 ] , e . touches [ 1 ] ) ;
325+ lastMid = getMidpoint ( e . touches [ 0 ] , e . touches [ 1 ] ) ;
326+
327+ startScale = viewScale ;
328+ startPanX = panX ;
329+ startPanY = panY ;
330+ canvas . selection = false ;
331+ canvas . discardActiveObject ( ) ;
332+ }
333+ } ,
334+ { passive : false } ,
335+ ) ;
336+
337+ el . addEventListener (
338+ "touchmove" ,
339+ ( e ) => {
340+ if ( e . touches . length !== 2 || lastDist === null ) return ;
341+
342+ e . preventDefault ( ) ;
343+
344+ const dist = getDistance ( e . touches [ 0 ] , e . touches [ 1 ] ) ;
345+ const mid = getMidpoint ( e . touches [ 0 ] , e . touches [ 1 ] ) ;
346+
347+ /* ----- SCALE ----- */
348+ if ( Math . abs ( dist - lastDist ) > 25 ) {
349+ const scaleFactor = dist / lastDist ;
350+ const oldScale = viewScale ;
351+ viewScale = clamp ( startScale * scaleFactor , 0.25 , 3 ) ;
352+ zoomAtCursor ( mid . x , mid . y , oldScale , viewScale ) ;
353+ } else {
354+ /* ----- PAN ----- */
355+ panX = startPanX + ( mid . x - lastMid . x ) ;
356+ panY = startPanY + ( mid . y - lastMid . y ) ;
357+ }
358+
359+ clampPan ( ) ;
360+ applyTransform ( ) ;
361+ } ,
362+ { passive : false } ,
363+ ) ;
364+
365+ el . addEventListener ( "touchend" , ( ) => {
366+ lastDist = null ;
367+ lastMid = null ;
368+ canvas . selection = true ;
369+ } ) ;
370+ }
371+
372+ function clampPan ( ) {
373+ const container = document . getElementById ( "canvasContainer" ) ;
374+
375+ const maxX = 0 ;
376+ const maxY = 0 ;
377+
378+ const minX = container . clientWidth - 1920 * viewScale ;
379+ const minY = container . clientHeight - 1080 * viewScale ;
380+
381+ panX = clamp ( panX , minX , maxX ) ;
382+ panY = clamp ( panY , minY , maxY ) ;
383+ }
384+
385+ function clamp ( v , min , max ) {
386+ return Math . min ( Math . max ( v , min ) , max ) ;
387+ }
388+
389+ function getDistance ( t1 , t2 ) {
390+ const dx = t2 . clientX - t1 . clientX ;
391+ const dy = t2 . clientY - t1 . clientY ;
392+ return Math . hypot ( dx , dy ) ;
393+ }
394+
395+ function getMidpoint ( t1 , t2 ) {
396+ return {
397+ x : ( t1 . clientX + t2 . clientX ) / 2 ,
398+ y : ( t1 . clientY + t2 . clientY ) / 2 ,
399+ } ;
400+ }
401+
402+ function enableMousePanAndZoom ( canvas ) {
403+ const el = canvas . upperCanvasEl ;
404+
405+ let isPanning = false ;
406+ let startX = 0 ;
407+ let startY = 0 ;
408+ let startPanX = 0 ;
409+ let startPanY = 0 ;
410+
411+ el . addEventListener ( "mousedown" , ( e ) => {
412+ if ( e . button !== 1 ) return ; // Mouse3 only
413+ e . preventDefault ( ) ;
414+
415+ isPanning = true ;
416+ startX = e . clientX ;
417+ startY = e . clientY ;
418+ startPanX = panX ;
419+ startPanY = panY ;
420+ canvas . selection = false ;
421+ canvas . discardActiveObject ( ) ;
422+ } ) ;
423+
424+ window . addEventListener ( "mousemove" , ( e ) => {
425+ if ( ! isPanning ) return ;
426+
427+ panX = startPanX + ( e . clientX - startX ) ;
428+ panY = startPanY + ( e . clientY - startY ) ;
429+
430+ clampPan ( ) ;
431+ applyTransform ( ) ;
432+ } ) ;
433+
434+ window . addEventListener ( "mouseup" , ( e ) => {
435+ if ( e . button === 1 ) {
436+ isPanning = false ;
437+ canvas . selection = true ;
438+ }
439+ } ) ;
440+
441+ /* ---------- CTRL + SCROLL ZOOM ---------- */
442+
443+ el . addEventListener (
444+ "wheel" ,
445+ ( e ) => {
446+ if ( ! e . ctrlKey ) return ;
447+
448+ e . preventDefault ( ) ;
449+
450+ const zoomIntensity = 0.0015 ;
451+ const delta = - e . deltaY * zoomIntensity ;
452+
453+ const oldScale = viewScale ;
454+ viewScale = clamp ( viewScale * ( 1 + delta ) , 0.5 , 3 ) ;
455+
456+ zoomAtCursor ( e . clientX , e . clientY , oldScale , viewScale ) ;
457+ clampPan ( ) ;
458+ applyTransform ( ) ;
459+ } ,
460+ { passive : false } ,
461+ ) ;
462+ }
463+
464+ function zoomAtCursor ( clientX , clientY , oldScale , newScale ) {
465+ const container = document . getElementById ( "canvasContainer" ) ;
466+ const rect = container . getBoundingClientRect ( ) ;
467+
468+ const cx = clientX - rect . left ;
469+ const cy = clientY - rect . top ;
470+
471+ const scaleRatio = newScale / oldScale ;
472+
473+ panX = cx - scaleRatio * ( cx - panX ) ;
474+ panY = cy - scaleRatio * ( cy - panY ) ;
475+ }
476+
477+ function applyTransform ( ) {
478+ const scaler = document . getElementById ( "scaler" ) ;
479+
480+ scaler . style . transform = `translate(${ panX } px, ${ panY } px) scale(${ viewScale } )` ;
481+
482+ canvas . requestRenderAll ( ) ;
483+ }
484+
289485 function initCanvas ( ) {
290486 document . getElementById ( "editorCanvas" ) . style . display = "block" ;
291487 canvas = new fabric . Canvas ( "editorCanvas" , {
292488 width : 1920 ,
293489 height : 1080 ,
294490 } ) ;
295- canvas . setZoom ( 0.5 ) ;
296- canvas . setDimensions ( { width : 1920 * 0.5 , height : 1080 * 0.5 } ) ;
491+ enablePinchAndPan ( canvas ) ;
492+ enableMousePanAndZoom ( canvas ) ;
297493 getModText ( ) . then ( ( ) => {
298494 events . forEach ( ( event ) => canvas . on ( event , ( ) => saveState ( ) ) ) ;
299495 } ) ;
@@ -474,7 +670,8 @@ <h3>Select an image</h3>
474670
475671 async function getModText ( ) {
476672 if ( ! mode ) {
477- await getModTextCanvas ( ) ;
673+ if ( ! canvas ) initCanvas ( ) ;
674+ else await getModTextCanvas ( ) ;
478675 return ;
479676 }
480677 try {
0 commit comments