| <!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; } |
| /* Embed mode (?embed=1): hide UI chrome; host page drives |
| * the demo via postMessage. */ |
| html.embed #controls, |
| html.embed #fps, |
| html.embed #help, |
| html.embed #help-mobile, |
| html.embed #text-btn { display: none !important; } |
| </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 |
| 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 · 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); |
| /* Skip zero-sized updates -- iframe-hosted case runs |
| * this at parse time before layout, and installing a |
| * 0x0 drawing buffer then leaves GL stuck at 0x0 even |
| * after the canvas later gets a real size. Leaving |
| * canvas.width / .height alone keeps the default 300x150 |
| * buffer until ResizeObserver fires with real dims. */ |
| if (!width || !height) return; |
| 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); |
| /* When embedded in an iframe, script executes before the |
| * iframe has done layout, so clientWidth/clientHeight |
| * are 0 and resizeCanvas() above installs a 0x0 GL |
| * backbuffer. A plain 'resize' window event never fires |
| * just because the iframe sized itself, so watch the |
| * canvas with ResizeObserver and pick up its first real |
| * size whenever it appears. */ |
| if (typeof ResizeObserver !== 'undefined') { |
| new ResizeObserver(function () { |
| resizeCanvas(); |
| /* Also dispatch a window resize: emscripten's GLFW |
| * port listens for that to re-fire its framebuffer |
| * size callback into wasm, which re-sets glViewport |
| * and forces a redraw at the correct size. */ |
| window.dispatchEvent(new Event('resize')); |
| requestRedraw(); |
| }).observe(canvas); |
| } |
| 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 + location.search; |
| }); |
| |
| /* 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); |
| |
| /* URL parameters: forwarded to C main() as argv so main can |
| * apply them before first frame (no default-demo flash). */ |
| var urlParams = new URLSearchParams(location.search); |
| var moduleArgs = []; |
| var urlText = urlParams.get('text'); |
| var urlFont = urlParams.get('font'); |
| if (urlText) moduleArgs.push('--text=' + urlText); |
| if (urlFont) moduleArgs.push('--font=' + urlFont); |
| |
| /* Embed mode: hide the demo's own chrome (font picker, |
| * text button, backend selector, help overlays) so a host |
| * page embedding this in an iframe can drive it purely |
| * through postMessage. The canvas-only view keeps the |
| * FPS counter and the loading overlay. */ |
| /* Any presence of ?embed (with or without a value, except |
| * explicit =0 / =false) turns on embed mode. */ |
| var embedMode = urlParams.has('embed') && |
| urlParams.get('embed') !== '0' && |
| urlParams.get('embed') !== 'false'; |
| if (embedMode) { |
| document.documentElement.classList.add('embed'); |
| } |
| |
| var Module = { |
| canvas: canvas, |
| arguments: moduleArgs, |
| 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'); |
| |
| var textOnOpen = ''; |
| textBtn.addEventListener('click', function () { |
| var ptr = _web_get_text(); |
| textOnOpen = UTF8ToString(ptr); |
| textArea.value = textOnOpen; |
| 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; } |
| if (text !== textOnOpen) { |
| applyText(text); |
| /* Reflect custom text in the URL so it's copy/pasteable. */ |
| var url = new URL(location.href); |
| url.searchParams.set('text', text); |
| history.replaceState(null, '', url.toString()); |
| } |
| 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); |
| } |
| |
| function applyText(text) { |
| var len = lengthBytesUTF8(text) + 1; |
| var buf = _malloc(len); |
| stringToUTF8(text, buf, len); |
| _web_set_text(buf); |
| _free(buf); |
| requestRedraw(); |
| } |
| |
| function applyFontBytes(bytes) { |
| var data = new Uint8Array(bytes); |
| var buf = _malloc(data.length); |
| HEAPU8.set(data, buf); |
| _web_load_font(buf, data.length); |
| _free(buf); |
| } |
| |
| function applyVariations(settings) { |
| var len = lengthBytesUTF8(settings) + 1; |
| var buf = _malloc(len); |
| stringToUTF8(settings, buf, len); |
| _web_set_variations(buf); |
| _free(buf); |
| requestRedraw(); |
| } |
| |
| function applyFeatures(settings) { |
| var len = lengthBytesUTF8(settings) + 1; |
| var buf = _malloc(len); |
| stringToUTF8(settings, buf, len); |
| _web_set_features(buf); |
| _free(buf); |
| requestRedraw(); |
| } |
| |
| function applyPalette(idx) { |
| _web_set_palette(idx >>> 0); |
| requestRedraw(); |
| } |
| |
| /* Embed-mode message protocol: host page drives the |
| * demo with { kind: 'text', value } and |
| * { kind: 'font', bytes: ArrayBuffer }. No origin |
| * check -- hb-gpu-demo is read-only w.r.t. the host. */ |
| window.addEventListener('message', function (e) { |
| var d = e.data; |
| if (!d || typeof d !== 'object') return; |
| if (d.kind === 'text' && typeof d.value === 'string') |
| applyText(d.value); |
| else if (d.kind === 'font' && d.bytes) |
| applyFontBytes(d.bytes); |
| else if (d.kind === 'variations' && typeof d.value === 'string') |
| applyVariations(d.value); |
| else if (d.kind === 'palette' && typeof d.value === 'number') |
| applyPalette(d.value); |
| else if (d.kind === 'features' && typeof d.value === 'string') |
| applyFeatures(d.value); |
| else if (d.kind === 'dark' && typeof d.value === 'boolean') |
| _web_set_dark(d.value ? 1 : 0); |
| }); |
| |
| /* Tell any parent frame we're ready to receive |
| * messages. Host may have queued them during load. */ |
| if (window.parent && window.parent !== window) |
| window.parent.postMessage({ kind: 'ready' }, '*'); |
| |
| /* ?text= is applied pre-main via Module.arguments; here we |
| * handle the async ?font= fetch. Text (if any) is re-applied |
| * after the font loads. |
| * |
| * For WebGPU, init_demo is async -- wait for the C side to |
| * signal readiness before calling exported functions. */ |
| function startFontFetch () { |
| if (!urlFont) return; |
| fetch(urlFont) |
| .then(function (r) { |
| if (!r.ok) throw new Error('HTTP ' + r.status); |
| return r.arrayBuffer(); |
| }) |
| .then(function (ab) { |
| var data = new Uint8Array(ab); |
| var buf = _malloc(data.length); |
| HEAPU8.set(data, buf); |
| _web_load_font(buf, data.length); |
| _free(buf); |
| if (urlText) applyText(urlText); |
| }) |
| .catch(function (err) { |
| console.warn('?font= fetch failed: ' + err.message); |
| }); |
| } |
| if (isWebGPU) { |
| window._webDemoReady = startFontFetch; |
| } else { |
| startFontFetch (); |
| } |
| } |
| }; |
| </script> |
| {{{ SCRIPT }}} |
| </body> |
| </html> |