| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> |
| <title>HarfBuzz GPU Demo</title> |
| <script> |
| if (location.pathname.indexOf('webgpu') !== -1) |
| document.title = 'HarfBuzz WebGPU Demo'; |
| else |
| document.title = 'HarfBuzz WebGL Demo'; |
| </script> |
| <style> |
| body { margin: 0; background: #fff; overflow: hidden; touch-action: none; } |
| #loading { position: fixed; inset: 0; z-index: 100; |
| display: flex; justify-content: center; align-items: center; |
| background: #fff; color: #999; font: 32px monospace; } |
| canvas { display: block; width: 100vw; height: 100vh; } |
| #fps { position: fixed; top: 8px; right: 12px; color: #999; |
| font: 14px monospace; pointer-events: none; z-index: 1; } |
| #help { position: fixed; bottom: 12px; left: 12px; color: #999; |
| font: 12px monospace; pointer-events: none; z-index: 1; |
| line-height: 1.6; } |
| #controls { position: fixed; top: 8px; left: 12px; z-index: 1; |
| display: flex; gap: 6px; } |
| @media (hover: none) { #controls { display: none; } |
| #help-desktop { display: none; } } |
| @media (hover: hover) { #help-mobile { display: none; } } |
| #controls label { color: #fff; font: 12px monospace; cursor: pointer; |
| padding: 4px 8px; border: 1px solid #888; border-radius: 4px; |
| background: rgba(128,128,128,0.5); |
| text-shadow: 0 1px 2px rgba(0,0,0,0.8); } |
| #controls label:hover { border-color: #ccc; background: rgba(128,128,128,0.7); } |
| #controls input[type=file] { display: none; } |
| #controls button { color: #fff; font: 12px monospace; cursor: pointer; |
| padding: 4px 8px; border: 1px solid #888; border-radius: 4px; |
| background: rgba(128,128,128,0.5); |
| text-shadow: 0 1px 2px rgba(0,0,0,0.8); } |
| #controls button:hover { border-color: #ccc; background: rgba(128,128,128,0.7); } |
| #controls select { color: #fff; font: 12px monospace; cursor: pointer; |
| padding: 3px 4px; border: 1px solid #888; border-radius: 4px; |
| background: rgba(128,128,128,0.5); |
| text-shadow: 0 1px 2px rgba(0,0,0,0.8); } |
| #drop-overlay { display: none; position: fixed; inset: 0; z-index: 10; |
| background: rgba(0,0,0,0.6); color: #fff; |
| font: 24px monospace; justify-content: center; |
| align-items: center; } |
| #text-modal { display: none; position: fixed; inset: 0; z-index: 20; |
| background: rgba(0,0,0,0.7); justify-content: center; |
| align-items: center; } |
| #text-modal-inner { background: #333; border: 1px solid #666; border-radius: 8px; |
| padding: 16px; width: 90vw; max-width: 1600px; } |
| #text-modal textarea { width: 100%; height: 70vh; background: #222; color: #ccc; |
| border: 1px solid #555; border-radius: 4px; |
| font: 14px monospace; padding: 8px; resize: vertical; |
| outline: none; box-sizing: border-box; } |
| #text-modal textarea:focus { border-color: #999; } |
| #text-modal-buttons { margin-top: 10px; text-align: right; } |
| #text-modal-buttons button { color: #fff; font: 13px monospace; cursor: pointer; |
| padding: 6px 16px; border: 1px solid #888; |
| border-radius: 4px; background: rgba(128,128,128,0.5); |
| margin-left: 8px; } |
| #text-modal-buttons button:hover { border-color: #ccc; background: rgba(128,128,128,0.7); } |
| #help-modal { display: none; position: fixed; inset: 0; z-index: 20; |
| background: rgba(0,0,0,0.7); justify-content: center; |
| align-items: center; } |
| #help-modal pre { background: #333; color: #ccc; font: 14px monospace; |
| padding: 24px 32px; border-radius: 8px; border: 1px solid #666; |
| line-height: 1.8; } |
| </style> |
| </head> |
| <body> |
| <div id="loading">loading</div> |
| <div id="fps"></div> |
| <div id="controls"> |
| <label>font <input type="file" id="font-input" accept=".ttf,.otf"></label> |
| <button id="text-btn">text</button> |
| <select id="backend-sel"> |
| <option value="index.html">WebGL</option> |
| <option value="webgpu.html">WebGPU</option> |
| </select> |
| </div> |
| <div id="drop-overlay">drop font file</div> |
| <div id="text-modal"> |
| <div id="text-modal-inner"> |
| <textarea id="text-area"></textarea> |
| <div id="text-modal-buttons"> |
| <button id="text-cancel">cancel</button> |
| <button id="text-apply">apply</button> |
| </div> |
| </div> |
| </div> |
| <div id="help-modal"><pre> |
| Keyboard: Mouse: |
| space toggle animation drag pan |
| b dark mode scroll zoom |
| g gamma (2.2/none) right-drag rotate |
| s stem darkening click toggle animation |
| d debug heatmap dblclick reset view |
| r reset view middle-drag zoom |
| =/- zoom in/out |
| [ ] { } stretch Touch: |
| arrows pan drag pan |
| h j k l pan (vim) pinch zoom |
| / clean screen 3-finger rotate |
| ? this help tap animate |
| |
| Shortcuts: Text editor: |
| t edit text Ctrl+Enter apply |
| f load font Esc cancel |
| w switch backend |
| R reload page <a href="https://github.com/harfbuzz/harfbuzz" style="color:#88f">HarfBuzz</a> |
| </pre></div> |
| <div id="help"> |
| <span id="help-desktop">drag: pan · scroll: zoom · click: animate · dblclick: reset · right-drag: rotate<br> |
| space: animate · b: dark mode · g: gamma · s: stem · d: debug · r: reset · ?: help</span> |
| <span id="help-mobile">drag: pan · pinch: zoom/rotate · 3-finger: 3D rotate<br> |
| tap: animate · dbl-tap+drag: zoom · long-press: reset</span> |
| </div> |
| <canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas> |
| <script> |
| var canvas = document.getElementById('canvas'); |
| function resizeCanvas () { |
| var dpr = window.devicePixelRatio || 1; |
| var width = Math.round(canvas.clientWidth * dpr); |
| var height = Math.round(canvas.clientHeight * dpr); |
| if (canvas.width !== width) |
| canvas.width = width; |
| if (canvas.height !== height) |
| canvas.height = height; |
| } |
| function requestRedraw () { |
| if (typeof _web_request_redraw === 'function') |
| _web_request_redraw(); |
| } |
| function handleResume () { |
| resizeCanvas(); |
| requestRedraw(); |
| } |
| resizeCanvas(); |
| window.addEventListener('resize', resizeCanvas); |
| window.addEventListener('focus', handleResume); |
| window.addEventListener('pageshow', handleResume); |
| document.addEventListener('visibilitychange', function () { |
| if (!document.hidden) |
| handleResume(); |
| }); |
| canvas.addEventListener('webglcontextrestored', handleResume); |
| |
| /* Backend selector */ |
| var isWebGPU = location.pathname.indexOf('webgpu') !== -1; |
| var backendSel = document.getElementById('backend-sel'); |
| if (isWebGPU) |
| backendSel.value = 'webgpu.html'; |
| backendSel.addEventListener('change', function () { |
| backendSel.blur(); |
| location.href = backendSel.value; |
| }); |
| |
| /* Early WebGL2 availability check */ |
| if (!isWebGPU && !canvas.getContext('webgl2')) { |
| var el = document.getElementById('loading'); |
| el.innerHTML = 'WebGL2 not available.<br>' + |
| 'Check that hardware acceleration is enabled in your browser settings.<br>' + |
| '<a href="https://webglreport.com/?v=2" style="color:#88f">WebGL2 support status</a>'; |
| el.style.font = '18px sans-serif'; |
| el.style.color = '#666'; |
| el.style.textAlign = 'center'; |
| el.style.flexDirection = 'column'; |
| window._webglError = true; |
| } |
| |
| /* Early WebGPU availability check */ |
| if (isWebGPU && !navigator.gpu) { |
| var el = document.getElementById('loading'); |
| var html = 'WebGPU not available in this browser.<br>' + |
| '<a href="https://github.com/gpuweb/gpuweb/wiki/Implementation-Status" ' + |
| 'style="color:#88f">Browser support status</a>'; |
| if (navigator.userAgent.indexOf('Linux') !== -1 && |
| navigator.userAgent.indexOf('Chrome') !== -1) |
| html += '<br><br>On Linux Chrome, try restarting with:<br>' + |
| '<code style="font-size:13px">google-chrome --enable-unsafe-webgpu --ozone-platform=x11 ' + |
| '--use-angle=vulkan --enable-features=Vulkan,VulkanFromANGLE</code>'; |
| el.innerHTML = html; |
| el.style.font = '18px sans-serif'; |
| el.style.color = '#666'; |
| el.style.textAlign = 'center'; |
| el.style.flexDirection = 'column'; |
| window._webgpuError = true; |
| } |
| |
| var fpsEl = document.getElementById('fps'); |
| var frames = 0, lastTime = performance.now(); |
| (function fpsLoop () { |
| requestAnimationFrame(fpsLoop); |
| frames++; |
| var now = performance.now(); |
| if (now - lastTime >= 1000) { |
| fpsEl.textContent = Math.round(frames * 1000 / (now - lastTime)) + ' fps'; |
| frames = 0; |
| lastTime = now; |
| } |
| })(); |
| |
| /* Multi-touch: 2-finger pinch → zoom, 3-finger drag → 3D rotate. */ |
| var pinchDist = 0; |
| var pinchAngle = 0; |
| var pinchCX = 0, pinchCY = 0; |
| var gestureX = 0, gestureY = 0; |
| var pinchActive = false; |
| var singlePanActive = false; |
| var threeFingerActive = false; |
| var skipNextPinchMove = false; |
| var longPressTimer = null; |
| var longPressFired = false; |
| var lastTapTime = 0; |
| var doubleTapZoom = false; |
| var doubleTapY = 0; |
| function swallowTouch (e) { |
| e.preventDefault(); |
| e.stopImmediatePropagation(); |
| } |
| if (!isWebGPU) canvas.addEventListener('touchstart', function (e) { |
| if (e.touches.length === 1) { |
| var now = performance.now(); |
| if (now - lastTapTime < 300) { |
| /* Double-tap: start zoom drag. |
| * Undo animation toggle from first tap. */ |
| e.preventDefault(); |
| if (typeof _web_toggle_animation === 'function') |
| _web_toggle_animation(); |
| doubleTapZoom = true; |
| doubleTapY = e.touches[0].clientY; |
| lastTapTime = 0; |
| if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } |
| } else { |
| lastTapTime = now; |
| longPressX = e.touches[0].clientX; |
| longPressY = e.touches[0].clientY; |
| longPressTimer = setTimeout(function () { |
| longPressTimer = null; |
| longPressFired = true; |
| if (typeof _web_reset === 'function') _web_reset(); |
| }, 500); |
| } |
| } else if (longPressTimer) { |
| clearTimeout(longPressTimer); |
| longPressTimer = null; |
| } |
| if (e.touches.length === 2) { |
| swallowTouch(e); |
| pinchActive = true; |
| singlePanActive = false; |
| var t = e.touches; |
| pinchDist = Math.hypot(t[1].clientX - t[0].clientX, |
| t[1].clientY - t[0].clientY); |
| pinchAngle = Math.atan2(t[1].clientY - t[0].clientY, |
| t[1].clientX - t[0].clientX); |
| pinchCX = (t[0].clientX + t[1].clientX) / 2; |
| pinchCY = (t[0].clientY + t[1].clientY) / 2; |
| gestureX = pinchCX; |
| gestureY = pinchCY; |
| if (typeof _web_cancel_gesture === 'function') |
| _web_cancel_gesture(); |
| canvas.dispatchEvent(new MouseEvent('mouseup', { |
| button: 0, clientX: pinchCX, clientY: pinchCY |
| })); |
| skipNextPinchMove = false; |
| } else if (e.touches.length >= 3) { |
| swallowTouch(e); |
| pinchActive = false; |
| singlePanActive = false; |
| threeFingerActive = true; |
| skipNextPinchMove = false; |
| var t = e.touches; |
| var cx = (t[0].clientX + t[1].clientX + t[2].clientX) / 3; |
| var cy = (t[0].clientY + t[1].clientY + t[2].clientY) / 3; |
| gestureX = cx; |
| gestureY = cy; |
| if (typeof _web_cancel_gesture === 'function') |
| _web_cancel_gesture(); |
| canvas.dispatchEvent(new MouseEvent('mouseup', { |
| button: 0, clientX: cx, clientY: cy |
| })); |
| /* Synthesize right-button press at centroid */ |
| canvas.dispatchEvent(new MouseEvent('mousedown', { |
| button: 2, clientX: cx, clientY: cy |
| })); |
| } |
| }, {passive: false, capture: true}); |
| var longPressX = 0, longPressY = 0; |
| if (!isWebGPU) canvas.addEventListener('touchmove', function (e) { |
| if (singlePanActive && e.touches.length === 1) { |
| swallowTouch(e); |
| gestureX = e.touches[0].clientX; |
| gestureY = e.touches[0].clientY; |
| canvas.dispatchEvent(new MouseEvent('mousemove', { |
| clientX: gestureX, clientY: gestureY |
| })); |
| return; |
| } |
| if (longPressTimer && e.touches.length === 1) { |
| var dx = e.touches[0].clientX - longPressX; |
| var dy = e.touches[0].clientY - longPressY; |
| if (dx*dx + dy*dy > 100) { clearTimeout(longPressTimer); longPressTimer = null; } |
| } |
| if (doubleTapZoom && e.touches.length === 1) { |
| swallowTouch(e); |
| var dy = e.touches[0].clientY - doubleTapY; |
| doubleTapY = e.touches[0].clientY; |
| canvas.dispatchEvent(new WheelEvent('wheel', { |
| deltaY: dy * 5, |
| clientX: e.touches[0].clientX, |
| clientY: e.touches[0].clientY |
| })); |
| return; |
| } |
| if (e.touches.length >= 3 && threeFingerActive) { |
| swallowTouch(e); |
| pinchActive = false; |
| var t = e.touches; |
| var cx = (t[0].clientX + t[1].clientX + t[2].clientX) / 3; |
| var cy = (t[0].clientY + t[1].clientY + t[2].clientY) / 3; |
| gestureX = cx; |
| gestureY = cy; |
| canvas.dispatchEvent(new MouseEvent('mousemove', { |
| clientX: cx, clientY: cy |
| })); |
| return; |
| } |
| if (e.touches.length === 2) { |
| swallowTouch(e); |
| pinchActive = true; |
| var t = e.touches; |
| var cx = (t[0].clientX + t[1].clientX) / 2; |
| var cy = (t[0].clientY + t[1].clientY) / 2; |
| var dist = Math.hypot(t[1].clientX - t[0].clientX, |
| t[1].clientY - t[0].clientY); |
| var angle = Math.atan2(t[1].clientY - t[0].clientY, |
| t[1].clientX - t[0].clientX); |
| if (skipNextPinchMove) { |
| pinchDist = dist; |
| pinchAngle = angle; |
| pinchCX = cx; |
| pinchCY = cy; |
| gestureX = cx; |
| gestureY = cy; |
| skipNextPinchMove = false; |
| return; |
| } |
| var dAngle = angle - pinchAngle; |
| if (dAngle > Math.PI) dAngle -= 2 * Math.PI; |
| if (dAngle < -Math.PI) dAngle += 2 * Math.PI; |
| var factor = pinchDist > 0 ? dist / pinchDist : 1; |
| if (typeof _web_pinch === 'function') |
| _web_pinch(cx - pinchCX, cy - pinchCY, |
| factor, dAngle, |
| cx, cy, canvas.clientWidth, canvas.clientHeight); |
| else { |
| /* Fallback: zoom only via wheel */ |
| var delta = (factor - 1) * 5; |
| canvas.dispatchEvent(new WheelEvent('wheel', { deltaY: -delta * 120 })); |
| } |
| pinchDist = dist; |
| pinchAngle = angle; |
| pinchCX = cx; pinchCY = cy; |
| gestureX = cx; |
| gestureY = cy; |
| } |
| }, {passive: false, capture: true}); |
| if (!isWebGPU) canvas.addEventListener('touchend', function (e) { |
| var pinchEnding = pinchActive && e.touches.length < 2; |
| var singlePanEnding = singlePanActive && e.touches.length === 0; |
| if (threeFingerActive || pinchEnding || singlePanEnding || |
| skipNextPinchMove || |
| e.touches.length >= 2 || doubleTapZoom) |
| swallowTouch(e); |
| if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } |
| if (longPressFired) { |
| longPressFired = false; |
| e.preventDefault(); /* Don't let GLFW see this as a click */ |
| } |
| doubleTapZoom = false; |
| if (threeFingerActive && e.touches.length < 3) { |
| threeFingerActive = false; |
| var releaseX = gestureX, releaseY = gestureY; |
| if (e.touches.length === 2) { |
| var t = e.touches; |
| releaseX = (t[0].clientX + t[1].clientX) / 2; |
| releaseY = (t[0].clientY + t[1].clientY) / 2; |
| pinchDist = Math.hypot(t[1].clientX - t[0].clientX, |
| t[1].clientY - t[0].clientY); |
| pinchAngle = Math.atan2(t[1].clientY - t[0].clientY, |
| t[1].clientX - t[0].clientX); |
| pinchCX = releaseX; |
| pinchCY = releaseY; |
| gestureX = releaseX; |
| gestureY = releaseY; |
| pinchActive = true; |
| skipNextPinchMove = true; |
| } else { |
| pinchActive = false; |
| skipNextPinchMove = false; |
| } |
| if (typeof _web_cancel_gesture === 'function') |
| _web_cancel_gesture(); |
| canvas.dispatchEvent(new MouseEvent('mouseup', { |
| button: 2, clientX: releaseX, clientY: releaseY |
| })); |
| } else if (pinchEnding) { |
| pinchActive = false; |
| skipNextPinchMove = false; |
| if (e.touches.length === 1) { |
| /* Hand single-finger pan off from the remaining touch point. */ |
| singlePanActive = true; |
| gestureX = e.touches[0].clientX; |
| gestureY = e.touches[0].clientY; |
| canvas.dispatchEvent(new MouseEvent('mousemove', { |
| clientX: gestureX, |
| clientY: gestureY |
| })); |
| canvas.dispatchEvent(new MouseEvent('mousedown', { |
| button: 0, |
| clientX: gestureX, |
| clientY: gestureY |
| })); |
| } |
| } else if (singlePanEnding) { |
| singlePanActive = false; |
| if (typeof _web_cancel_gesture === 'function') |
| _web_cancel_gesture(); |
| canvas.dispatchEvent(new MouseEvent('mouseup', { |
| button: 0, clientX: gestureX, clientY: gestureY |
| })); |
| } else if (e.touches.length < 2) { |
| skipNextPinchMove = false; |
| } |
| }, {passive: false, capture: true}); |
| if (!isWebGPU) canvas.addEventListener('touchcancel', function (e) { |
| var handledGesture = threeFingerActive || pinchActive || |
| singlePanActive || |
| skipNextPinchMove || |
| e.touches.length >= 2 || doubleTapZoom; |
| if (handledGesture) |
| swallowTouch(e); |
| if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } |
| longPressFired = false; |
| doubleTapZoom = false; |
| pinchActive = false; |
| if (singlePanActive) { |
| singlePanActive = false; |
| if (typeof _web_cancel_gesture === 'function') |
| _web_cancel_gesture(); |
| canvas.dispatchEvent(new MouseEvent('mouseup', { |
| button: 0, clientX: gestureX, clientY: gestureY |
| })); |
| } |
| skipNextPinchMove = false; |
| if (threeFingerActive) { |
| threeFingerActive = false; |
| if (typeof _web_cancel_gesture === 'function') |
| _web_cancel_gesture(); |
| canvas.dispatchEvent(new MouseEvent('mouseup', { |
| button: 2, clientX: gestureX, clientY: gestureY |
| })); |
| } |
| }, {passive: false, capture: true}); |
| |
| /* Block Emscripten/GLFW from eating key events while the text modal is open. |
| * Registered before Module so it runs before Emscripten's capture-phase handlers. */ |
| var textModalEl = document.getElementById('text-modal'); |
| function blockKeysIfModal(e) { |
| if (textModalEl.style.display !== 'flex') { |
| if (e.key === '?') { |
| if (e.type === 'keydown') { |
| var hm = document.getElementById('help-modal'); |
| hm.style.display = hm.style.display === 'flex' ? 'none' : 'flex'; |
| } |
| e.preventDefault(); |
| e.stopImmediatePropagation(); |
| return; |
| } |
| if (e.key === '/') { |
| if (e.type === 'keydown') { |
| var h = document.getElementById('help'); |
| var fp = document.getElementById('fps'); |
| var c = document.getElementById('controls'); |
| var hidden = h.style.display === 'none'; |
| h.style.display = hidden ? '' : 'none'; |
| fp.style.display = hidden ? '' : 'none'; |
| if (c) c.style.display = hidden ? '' : 'none'; |
| } |
| e.preventDefault(); |
| e.stopImmediatePropagation(); |
| return; |
| } |
| if (e.key === 'Escape') { |
| var hm = document.getElementById('help-modal'); |
| if (hm.style.display === 'flex') { |
| hm.style.display = 'none'; |
| e.stopImmediatePropagation(); |
| return; |
| } |
| } |
| if (e.key === 'R') { |
| location.reload(); |
| return; |
| } |
| if (e.key === 't') { |
| e.preventDefault(); |
| if (e.type === 'keydown') |
| document.getElementById('text-btn').click(); |
| e.stopImmediatePropagation(); |
| return; |
| } |
| if (e.type === 'keydown' && e.key === 'f') { |
| document.getElementById('font-input').click(); |
| e.stopImmediatePropagation(); |
| } |
| if (e.type === 'keydown' && e.key === 'w') { |
| var sel = document.getElementById('backend-sel'); |
| sel.value = sel.value === 'webgpu.html' ? 'index.html' : 'webgpu.html'; |
| location.href = sel.value; |
| e.stopImmediatePropagation(); |
| } |
| return; |
| } |
| if (e.key === 'Escape') { textModalEl.style.display = 'none'; e.stopImmediatePropagation(); return; } |
| if (e.key === 'Enter' && (e.shiftKey || e.ctrlKey || e.metaKey)) { |
| document.getElementById('text-apply').click(); |
| e.stopImmediatePropagation(); return; |
| } |
| e.stopImmediatePropagation(); |
| } |
| window.addEventListener('keydown', blockKeysIfModal, true); |
| window.addEventListener('keyup', blockKeysIfModal, true); |
| window.addEventListener('keypress', blockKeysIfModal, true); |
| |
| var Module = { |
| canvas: canvas, |
| onRuntimeInitialized: function () { |
| if (!window._webgpuError && !window._webglError) { |
| document.getElementById('loading').style.display = 'none'; |
| document.body.style.background = '#222'; |
| } |
| console.log('HarfBuzz GPU Demo loaded'); |
| |
| /* Font file input */ |
| var fontInput = document.getElementById('font-input'); |
| fontInput.addEventListener('change', function () { |
| if (fontInput.files.length) |
| loadFontFile(fontInput.files[0]); |
| }); |
| |
| /* Drag and drop */ |
| var overlay = document.getElementById('drop-overlay'); |
| document.addEventListener('dragover', function (e) { |
| e.preventDefault(); |
| overlay.style.display = 'flex'; |
| }); |
| document.addEventListener('dragleave', function (e) { |
| if (e.relatedTarget === null) |
| overlay.style.display = 'none'; |
| }); |
| document.addEventListener('drop', function (e) { |
| e.preventDefault(); |
| overlay.style.display = 'none'; |
| if (e.dataTransfer.files.length) |
| loadFontFile(e.dataTransfer.files[0]); |
| }); |
| |
| /* Text editor modal */ |
| var textModal = document.getElementById('text-modal'); |
| var textArea = document.getElementById('text-area'); |
| var textBtn = document.getElementById('text-btn'); |
| |
| textBtn.addEventListener('click', function () { |
| var ptr = _web_get_text(); |
| textArea.value = UTF8ToString(ptr); |
| textModal.style.display = 'flex'; |
| textArea.focus(); |
| }); |
| |
| document.getElementById('text-cancel').addEventListener('click', function () { |
| textModal.style.display = 'none'; |
| }); |
| |
| document.getElementById('text-apply').addEventListener('click', function () { |
| var text = textArea.value.trim(); |
| if (!text) { textModal.style.display = 'none'; return; } |
| var len = lengthBytesUTF8(text) + 1; |
| var buf = _malloc(len); |
| stringToUTF8(text, buf, len); |
| _web_set_text(buf); |
| _free(buf); |
| textModal.style.display = 'none'; |
| }); |
| |
| |
| /* Prevent mouse events on the modal from reaching the canvas/GLFW. */ |
| textModal.addEventListener('mousedown', function (e) { e.stopPropagation(); }); |
| textModal.addEventListener('mouseup', function (e) { e.stopPropagation(); }); |
| textModal.addEventListener('click', function (e) { e.stopPropagation(); }); |
| |
| function loadFontFile(file) { |
| var reader = new FileReader(); |
| reader.onload = function () { |
| var data = new Uint8Array(reader.result); |
| var buf = _malloc(data.length); |
| HEAPU8.set(data, buf); |
| _web_load_font(buf, data.length); |
| _free(buf); |
| }; |
| reader.readAsArrayBuffer(file); |
| } |
| } |
| }; |
| </script> |
| {{{ SCRIPT }}} |
| </body> |
| </html> |