| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Gemma 4 - Vision Token Budget</title> |
| |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
| <style> |
| :root { |
| --bg-page: #f8fafc; |
| --bg-card: #ffffff; |
| --text-main: #1e293b; |
| --text-sub: #475569; |
| --text-muted: #64748b; |
| --border-color: #e2e8f0; |
| --border-hover: #cbd5e1; |
| --accent: #2563eb; |
| --accent-light: #eff6ff; |
| --radius: 10px; |
| --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05); |
| } |
| |
| * { |
| box-sizing: border-box; |
| margin: 0; |
| padding: 0; |
| } |
| |
| body { |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; |
| background-color: var(--bg-page); |
| color: var(--text-main); |
| line-height: 1.6; |
| padding: 40px 20px; |
| } |
| |
| .header-wrapper { |
| max-width: 1100px; |
| margin: 0 auto; |
| } |
| |
| |
| header { |
| text-align: center; |
| margin-bottom: 35px; |
| } |
| |
| h1 { |
| font-size: 2.2rem; |
| font-weight: 700; |
| color: var(--text-main); |
| letter-spacing: -0.02em; |
| } |
| |
| .subtitle { |
| font-size: 1.1rem; |
| color: var(--text-sub); |
| margin-top: 6px; |
| font-weight: 500; |
| } |
| |
| |
| .card { |
| background-color: var(--bg-card); |
| border: 1px solid var(--border-color); |
| border-radius: var(--radius); |
| padding: 24px; |
| margin-bottom: 24px; |
| box-shadow: var(--shadow); |
| } |
| |
| |
| details.explanation-card { |
| background-color: var(--bg-card); |
| border: 1px solid var(--border-color); |
| border-radius: var(--radius); |
| padding: 16px 20px; |
| margin-bottom: 28px; |
| box-shadow: var(--shadow); |
| } |
| |
| details.explanation-card summary { |
| cursor: pointer; |
| font-weight: 600; |
| font-size: 1.05rem; |
| color: var(--text-main); |
| user-select: none; |
| } |
| |
| details.explanation-card summary:hover { |
| color: var(--accent); |
| } |
| |
| .explanation-img { |
| max-width: 100%; |
| height: auto; |
| margin-top: 16px; |
| border-radius: 6px; |
| border: 1px solid var(--border-color); |
| display: block; |
| } |
| |
| |
| .upload-controls { |
| display: grid; |
| grid-template-columns: 1fr; |
| gap: 20px; |
| } |
| |
| #drop-zone { |
| width: 100%; |
| min-height: 160px; |
| border: 2px dashed #cbd5e1; |
| border-radius: var(--radius); |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| text-align: center; |
| padding: 24px; |
| background-color: #fcfcfd; |
| cursor: pointer; |
| transition: all 0.2s ease; |
| } |
| |
| #drop-zone:hover, #drop-zone.hover { |
| border-color: var(--accent); |
| background-color: var(--accent-light); |
| } |
| |
| .drop-title { |
| font-size: 1.05rem; |
| font-weight: 600; |
| color: var(--text-main); |
| } |
| |
| .drop-sub { |
| font-size: 0.85rem; |
| color: var(--text-muted); |
| margin-top: 4px; |
| } |
| |
| |
| .presets-and-options { |
| display: flex; |
| flex-wrap: wrap; |
| align-items: center; |
| justify-content: space-between; |
| gap: 16px; |
| padding-top: 16px; |
| border-top: 1px solid var(--border-color); |
| } |
| |
| .presets-group { |
| display: flex; |
| flex-wrap: wrap; |
| align-items: center; |
| gap: 10px; |
| } |
| |
| .presets-label { |
| font-size: 0.9rem; |
| font-weight: 600; |
| color: var(--text-sub); |
| margin-right: 4px; |
| } |
| |
| .preset-btn { |
| background-color: #f1f5f9; |
| border: 1px solid var(--border-color); |
| border-radius: 6px; |
| padding: 7px 14px; |
| font-size: 0.85rem; |
| font-weight: 500; |
| color: var(--text-main); |
| cursor: pointer; |
| transition: all 0.2s ease; |
| } |
| |
| .preset-btn:hover { |
| background-color: #e2e8f0; |
| border-color: #94a3b8; |
| } |
| |
| .checkbox-label { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| font-size: 0.9rem; |
| font-weight: 600; |
| color: var(--text-main); |
| cursor: pointer; |
| user-select: none; |
| } |
| |
| .checkbox-label input[type="checkbox"] { |
| width: 18px; |
| height: 18px; |
| accent-color: var(--accent); |
| cursor: pointer; |
| } |
| |
| |
| .menu-bar { |
| display: flex; |
| justify-content: center; |
| flex-wrap: wrap; |
| gap: 10px; |
| margin: 30px 0; |
| } |
| |
| .menu-btn { |
| background-color: var(--bg-card); |
| border: 1px solid var(--border-color); |
| border-radius: 8px; |
| padding: 10px 24px; |
| font-size: 1rem; |
| font-weight: 600; |
| color: var(--text-sub); |
| cursor: pointer; |
| transition: all 0.2s ease; |
| box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); |
| } |
| |
| .menu-btn:hover { |
| color: var(--text-main); |
| border-color: #94a3b8; |
| background-color: #f8fafc; |
| } |
| |
| .menu-btn.active { |
| background-color: var(--accent); |
| color: #ffffff; |
| border-color: var(--accent); |
| box-shadow: 0 4px 10px rgba(37, 99, 235, 0.25); |
| } |
| |
| |
| #results-area { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| width: 100%; |
| overflow: visible; |
| } |
| |
| |
| .stacked-list { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 36px; |
| width: 100%; |
| } |
| |
| |
| .image-card { |
| background-color: var(--bg-card); |
| border: 1px solid var(--border-color); |
| border-radius: var(--radius); |
| padding: 28px 24px; |
| box-shadow: var(--shadow); |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| width: auto; |
| min-width: 650px; |
| max-width: none; |
| } |
| |
| .image-card h3 { |
| font-size: 1.3rem; |
| font-weight: 700; |
| color: var(--text-main); |
| margin-bottom: 20px; |
| width: 100%; |
| text-align: center; |
| border-bottom: 1px solid var(--border-color); |
| padding-bottom: 14px; |
| } |
| |
| |
| .canvas-box { |
| position: relative; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| margin: 10px auto 24px; |
| overflow: visible; |
| width: auto; |
| max-width: none; |
| } |
| |
| canvas { |
| border: 1px solid #cbd5e1; |
| border-radius: 6px; |
| display: block; |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| |
| } |
| |
| |
| .grid-overlay { |
| position: absolute; |
| top: 0; |
| left: 0; |
| pointer-events: none; |
| border-radius: 6px; |
| } |
| |
| |
| .info-table { |
| width: 100%; |
| max-width: 650px; |
| margin: 0 auto; |
| border-collapse: collapse; |
| border: 1px solid var(--border-color); |
| border-radius: 6px; |
| overflow: hidden; |
| font-size: 0.95rem; |
| } |
| |
| .info-table th, |
| .info-table td { |
| text-align: left; |
| padding: 12px 16px; |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .info-table tr:last-child th, |
| .info-table tr:last-child td { |
| border-bottom: none; |
| } |
| |
| .info-table th { |
| background-color: #f8fafc; |
| font-weight: 600; |
| color: var(--text-sub); |
| width: 55%; |
| } |
| |
| .info-table td { |
| color: var(--text-main); |
| font-weight: 500; |
| } |
| |
| |
| .empty-message { |
| text-align: center; |
| padding: 50px 20px; |
| color: var(--text-muted); |
| font-size: 1.05rem; |
| background-color: var(--bg-card); |
| border: 1px dashed #cbd5e1; |
| border-radius: var(--radius); |
| width: 100%; |
| max-width: 1100px; |
| } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <div class="header-wrapper"> |
| <header> |
| <h1>Gemma 4 - Vision Token Budget</h1> |
| <p class="subtitle">Aspect-Ratio Preserving Image Resizing</p> |
| </header> |
|
|
| |
| <details class="explanation-card"> |
| <summary>Explanation Aspect Preserving Resize to Sequence Length (a.k.a. Visual Budget)</summary> |
| <img src="sample_paper.png" onerror="this.onerror=null; this.src='image.png';" alt="Explanation Aspect Preserving Resize to Sequence Length (a.k.a. Visual Budget)" class="explanation-img"> |
| </details> |
|
|
| |
| <section class="card upload-controls"> |
| <div id="drop-zone"> |
| <div class="drop-title">Drag & Drop an image here, or Paste (Ctrl+V) from clipboard</div> |
| <div class="drop-sub">Click anywhere in this box to browse local files</div> |
| </div> |
|
|
| <div class="presets-and-options"> |
| <div class="presets-group" id="preset-buttons-container"> |
| <span class="presets-label">Presets:</span> |
| |
| </div> |
|
|
| <label class="checkbox-label"> |
| <input type="checkbox" id="toggle-grid" checked> |
| <span>Show 48 × 48 px Token Grid</span> |
| </label> |
| </div> |
| </section> |
|
|
| |
| <nav class="menu-bar" id="menu-bar"> |
| <button class="menu-btn" data-budget="70">70</button> |
| <button class="menu-btn" data-budget="140">140</button> |
| <button class="menu-btn" data-budget="280">280</button> |
| <button class="menu-btn" data-budget="560">560</button> |
| <button class="menu-btn" data-budget="1120">1120</button> |
| <button class="menu-btn active" data-budget="All">All</button> |
| </nav> |
| </div> |
|
|
| |
| <main id="results-area"> |
| <div class="empty-message" id="empty-message"> |
| Please upload or select an image to visualize the aspect-ratio preserving resize. |
| </div> |
| <div id="display-content" style="width: 100%;"></div> |
| </main> |
|
|
| <script> |
| |
| const P = 16; |
| const K = 3; |
| const M = P * K; |
| const BUDGETS = [70, 140, 280, 560, 1120]; |
| |
| |
| let currentImage = null; |
| let selectedOption = 'All'; |
| let showGridOverlay = true; |
| let processedResults = {}; |
| |
| |
| const dropZone = document.getElementById('drop-zone'); |
| const presetButtonsContainer = document.getElementById('preset-buttons-container'); |
| const toggleGridCheckbox = document.getElementById('toggle-grid'); |
| const menuBar = document.getElementById('menu-bar'); |
| const emptyMessage = document.getElementById('empty-message'); |
| const displayContent = document.getElementById('display-content'); |
| |
| |
| const PRESETS = [ |
| { name: 'Sample Paper', src: 'sample_paper.png', fallback: 'image.png' }, |
| { name: 'Sample 12B', src: 'sample_12b.png' } |
| ]; |
| |
| function init() { |
| setupPresets(); |
| setupEventListeners(); |
| |
| loadPreset(PRESETS[0]); |
| } |
| |
| function setupPresets() { |
| presetButtonsContainer.replaceChildren(); |
| |
| PRESETS.forEach(preset => { |
| const btn = document.createElement('button'); |
| btn.className = 'preset-btn'; |
| btn.textContent = preset.name; |
| btn.addEventListener('click', () => loadPreset(preset)); |
| presetButtonsContainer.appendChild(btn); |
| }); |
| } |
| |
| function setupEventListeners() { |
| |
| dropZone.addEventListener('dragover', (e) => { |
| e.preventDefault(); |
| dropZone.classList.add('hover'); |
| }); |
| |
| dropZone.addEventListener('dragleave', () => { |
| dropZone.classList.remove('hover'); |
| }); |
| |
| dropZone.addEventListener('drop', (e) => { |
| e.preventDefault(); |
| dropZone.classList.remove('hover'); |
| const files = e.dataTransfer.files; |
| if (files.length > 0) { |
| handleFile(files[0]); |
| } |
| }); |
| |
| |
| dropZone.addEventListener('click', () => { |
| const input = document.createElement('input'); |
| input.type = 'file'; |
| input.accept = 'image/*'; |
| input.onchange = (e) => { |
| if (e.target.files.length > 0) { |
| handleFile(e.target.files[0]); |
| } |
| }; |
| input.click(); |
| }); |
| |
| |
| document.addEventListener('paste', (e) => { |
| const items = (e.clipboardData || e.originalEvent.clipboardData).items; |
| for (let i = 0; i < items.length; i++) { |
| if (items[i].type.indexOf('image') !== -1) { |
| const file = items[i].getAsFile(); |
| handleFile(file); |
| break; |
| } |
| } |
| }); |
| |
| |
| menuBar.addEventListener('click', (e) => { |
| if (e.target.classList.contains('menu-btn')) { |
| const budget = e.target.getAttribute('data-budget'); |
| selectMenuOption(budget); |
| } |
| }); |
| |
| |
| toggleGridCheckbox.addEventListener('change', (e) => { |
| showGridOverlay = e.target.checked; |
| renderResults(); |
| }); |
| } |
| |
| function handleFile(file) { |
| if (!file.type.startsWith('image/')) { |
| alert('Not an image file'); |
| return; |
| } |
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| const img = new Image(); |
| img.onload = () => processImage(img); |
| img.src = e.target.result; |
| }; |
| reader.readAsDataURL(file); |
| } |
| |
| function loadPreset(preset) { |
| if (preset.src) { |
| const img = new Image(); |
| img.onload = () => processImage(img); |
| img.onerror = () => { |
| if (preset.fallback) { |
| const fbImg = new Image(); |
| fbImg.onload = () => processImage(fbImg); |
| fbImg.onerror = () => alert('Please ensure image files (sample_paper.png or image.png) are uploaded to your static hosting directory.'); |
| fbImg.src = preset.fallback; |
| } else { |
| alert('Please ensure ' + preset.src + ' is uploaded to your static hosting directory.'); |
| } |
| }; |
| img.src = preset.src; |
| } |
| } |
| |
| function processImage(img) { |
| currentImage = img; |
| processedResults = {}; |
| |
| BUDGETS.forEach(nMax => { |
| processedResults[nMax] = resizeImage(img, nMax); |
| }); |
| |
| emptyMessage.style.display = 'none'; |
| renderResults(); |
| } |
| |
| function resizeImage(img, nMax) { |
| const w = img.naturalWidth; |
| const h = img.naturalHeight; |
| |
| const m = M; |
| const t = nMax * m * m; |
| const f = Math.sqrt(t / (h * w)); |
| const hIdeal = f * h; |
| const wIdeal = f * w; |
| |
| let hTarget = Math.floor(hIdeal / m) * m; |
| let wTarget = Math.floor(wIdeal / m) * m; |
| |
| hTarget = Math.max(m, hTarget); |
| wTarget = Math.max(m, wTarget); |
| |
| const canvas = document.createElement('canvas'); |
| canvas.width = wTarget; |
| canvas.height = hTarget; |
| const ctx = canvas.getContext('2d'); |
| |
| ctx.imageSmoothingEnabled = true; |
| ctx.imageSmoothingQuality = 'high'; |
| ctx.drawImage(img, 0, 0, wTarget, hTarget); |
| |
| const targetPatches = (hTarget / P) * (wTarget / P); |
| const actualTokens = Math.min(Math.floor(targetPatches / (K * K)), nMax); |
| |
| return { |
| canvas: canvas, |
| width: wTarget, |
| height: hTarget, |
| actualTokens: actualTokens |
| }; |
| } |
| |
| function selectMenuOption(option) { |
| selectedOption = option; |
| |
| const buttons = menuBar.querySelectorAll('.menu-btn'); |
| buttons.forEach(btn => { |
| if (btn.getAttribute('data-budget') === option) { |
| btn.classList.add('active'); |
| } else { |
| btn.classList.remove('active'); |
| } |
| }); |
| |
| renderResults(); |
| } |
| |
| function renderResults() { |
| if (!currentImage) return; |
| displayContent.replaceChildren(); |
| |
| const w = currentImage.naturalWidth; |
| const h = currentImage.naturalHeight; |
| const origNMax = (w * h) / (M * M); |
| const origTokens = Math.floor(w / M) * Math.floor(h / M); |
| |
| const stackedDiv = document.createElement('div'); |
| stackedDiv.className = 'stacked-list'; |
| |
| if (selectedOption === 'All') { |
| BUDGETS.forEach(nMax => { |
| const data = processedResults[nMax]; |
| const card = createCardElement( |
| `N_max = ${nMax}`, |
| data.canvas, |
| data.width, |
| data.height, |
| [ |
| { label: 'Target Dimensions', value: `${data.width} x ${data.height}` }, |
| { label: 'Actual Tokens', value: `${data.actualTokens}` } |
| ], |
| true |
| ); |
| stackedDiv.appendChild(card); |
| }); |
| } else { |
| const nMax = parseInt(selectedOption, 10); |
| const data = processedResults[nMax]; |
| const resizedCard = createCardElement( |
| `N_max = ${nMax}`, |
| data.canvas, |
| data.width, |
| data.height, |
| [ |
| { label: 'Target Dimensions', value: `${data.width} x ${data.height}` }, |
| { label: 'Actual Tokens', value: `${data.actualTokens}` } |
| ], |
| true |
| ); |
| stackedDiv.appendChild(resizedCard); |
| } |
| |
| |
| const origCard = createCardElement( |
| 'Original Image', |
| currentImage, |
| w, |
| h, |
| [ |
| { label: 'Dimensions', value: `${w} x ${h}` }, |
| { label: 'Equivalent N_max (w*h/m²)', value: origNMax.toFixed(2) }, |
| { label: 'Actual Tokens (floor(w/m)*floor(h/m))', value: `${origTokens}` } |
| ], |
| false |
| ); |
| stackedDiv.appendChild(origCard); |
| |
| displayContent.appendChild(stackedDiv); |
| } |
| |
| function createCardElement(titleText, imgOrCanvas, width, height, tableRows, allowGrid = true) { |
| const card = document.createElement('div'); |
| card.className = 'image-card'; |
| |
| const h3 = document.createElement('h3'); |
| h3.textContent = titleText; |
| card.appendChild(h3); |
| |
| const canvasBox = document.createElement('div'); |
| canvasBox.className = 'canvas-box'; |
| |
| |
| const displayCanvas = document.createElement('canvas'); |
| displayCanvas.width = width; |
| displayCanvas.height = height; |
| displayCanvas.style.width = width + 'px'; |
| displayCanvas.style.height = height + 'px'; |
| const ctx = displayCanvas.getContext('2d'); |
| ctx.imageSmoothingEnabled = true; |
| ctx.imageSmoothingQuality = 'high'; |
| ctx.drawImage(imgOrCanvas, 0, 0, width, height); |
| |
| canvasBox.appendChild(displayCanvas); |
| |
| if (showGridOverlay && allowGrid) { |
| const overlay = createGridOverlay(width, height); |
| canvasBox.appendChild(overlay); |
| } |
| |
| card.appendChild(canvasBox); |
| |
| |
| const table = document.createElement('table'); |
| table.className = 'info-table'; |
| |
| tableRows.forEach(row => { |
| const tr = document.createElement('tr'); |
| const th = document.createElement('th'); |
| th.textContent = row.label; |
| const td = document.createElement('td'); |
| td.textContent = row.value; |
| tr.appendChild(th); |
| tr.appendChild(td); |
| table.appendChild(tr); |
| }); |
| |
| card.appendChild(table); |
| return card; |
| } |
| |
| function createGridOverlay(width, height) { |
| const canvas = document.createElement('canvas'); |
| canvas.className = 'grid-overlay'; |
| canvas.width = width; |
| canvas.height = height; |
| canvas.style.width = width + 'px'; |
| canvas.style.height = height + 'px'; |
| const ctx = canvas.getContext('2d'); |
| |
| ctx.strokeStyle = 'rgba(239, 68, 68, 0.75)'; |
| ctx.lineWidth = 1.25; |
| |
| for (let x = 0; x <= width; x += M) { |
| ctx.beginPath(); |
| ctx.moveTo(x, 0); |
| ctx.lineTo(x, height); |
| ctx.stroke(); |
| } |
| |
| for (let y = 0; y <= height; y += M) { |
| ctx.beginPath(); |
| ctx.moveTo(0, y); |
| ctx.lineTo(width, y); |
| ctx.stroke(); |
| } |
| |
| return canvas; |
| } |
| |
| window.addEventListener('DOMContentLoaded', init); |
| </script> |
| </body> |
| </html> |