MaartenGr's picture
Upload 4 files
69e68c1 verified
Raw
History Blame Contribute Delete
21 kB
<!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>
<!-- Clean, Modern Typography -->
<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 */
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;
}
/* Cards & Containers */
.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);
}
/* Explanation Details Section */
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 Section */
.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 & Grid Checkbox Bar */
.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;
}
/* Centered Horizontal Menu Bar for N_max */
.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 Display Area */
#results-area {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
overflow: visible;
}
/* Stacked layout centered on page */
.stacked-list {
display: flex;
flex-direction: column;
align-items: center;
gap: 36px;
width: 100%;
}
/* Card expands naturally to show exact, unconstrained width without any inner scrollbars */
.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 wrapper allows true actual resolution display without scroll constraints */
.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);
/* True 1:1 pixel accuracy is applied via style in JS */
}
/* Grid Overlay Canvas */
.grid-overlay {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
border-radius: 6px;
}
/* Tables */
.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 state */
.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>
<!-- Header & Controls bounded nicely -->
<div class="header-wrapper">
<header>
<h1>Gemma 4 - Vision Token Budget</h1>
<p class="subtitle">Aspect-Ratio Preserving Image Resizing</p>
</header>
<!-- Explanation Details Section with image.png / sample_paper.png -->
<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>
<!-- Upload & Controls Card -->
<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>
<!-- Populated dynamically -->
</div>
<label class="checkbox-label">
<input type="checkbox" id="toggle-grid" checked>
<span>Show 48 × 48 px Token Grid</span>
</label>
</div>
</section>
<!-- Centered Horizontal Menu Bar for N_max -->
<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>
<!-- Results Area expands naturally for unconstrained true resolution -->
<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>
// Gemma 4 Macro-Block Constants
const P = 16;
const K = 3;
const M = P * K; // 48
const BUDGETS = [70, 140, 280, 560, 1120];
// State
let currentImage = null;
let selectedOption = 'All'; // Default N_max option
let showGridOverlay = true;
let processedResults = {};
// DOM Elements
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');
// Preset configurations
const PRESETS = [
{ name: 'Sample Paper', src: 'sample_paper.png', fallback: 'image.png' },
{ name: 'Sample 12B', src: 'sample_12b.png' }
];
function init() {
setupPresets();
setupEventListeners();
// Load initial preset
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() {
// Drag over
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]);
}
});
// Click dropzone
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();
});
// Paste
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;
}
}
});
// Menu Bar clicks
menuBar.addEventListener('click', (e) => {
if (e.target.classList.contains('menu-btn')) {
const budget = e.target.getAttribute('data-budget');
selectMenuOption(budget);
}
});
// Grid checkbox
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);
}
// Create Original Card (always full actual resolution centered without constraints, no grid overlay) and place at the bottom
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';
// Clone or draw image/canvas to a display canvas with true actual pixel dimensions unconstrained
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);
// Create Table using safe DOM methods
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)'; // Bright Red Grid
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>