Skip to content

Instantly share code, notes, and snippets.

@ArthurDelannoyazerty
Last active December 2, 2025 11:30
Show Gist options
  • Select an option

  • Save ArthurDelannoyazerty/8685727a77fb2d0a901d561428e2d337 to your computer and use it in GitHub Desktop.

Select an option

Save ArthurDelannoyazerty/8685727a77fb2d0a901d561428e2d337 to your computer and use it in GitHub Desktop.
tinling & stride & overlap image viz / calculator
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sentinel-2 Vector Planner v2</title>
<style>
:root {
--bg: #121212;
--panel: #1e1e1e;
--text: #e0e0e0;
--accent: #00d4aa; /* Teal for safe */
--warn: #ffb74d; /* Orange for heavy */
--danger: #ff5252; /* Red for overload */
--grid: #333;
}
body {
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg);
color: var(--text);
margin: 0;
display: flex;
height: 100vh;
overflow: hidden;
}
/* Sidebar */
.controls {
width: 340px;
background-color: var(--panel);
padding: 25px;
display: flex;
flex-direction: column;
gap: 25px;
box-shadow: 4px 0 15px rgba(0,0,0,0.4);
overflow-y: auto;
border-right: 1px solid #333;
z-index: 10;
}
h2 { margin: 0 0 10px 0; font-size: 1.1rem; color: #fff; letter-spacing: 0.5px; }
h3 { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; color: #777; margin-bottom: 8px; border-bottom: 1px solid #333; padding-bottom: 4px;}
.input-group { margin-bottom: 12px; }
label { font-size: 0.8rem; color: #aaa; display: flex; justify-content: space-between; margin-bottom: 4px;}
input[type="number"], input[type="range"] {
background: #2a2a2a;
border: 1px solid #444;
color: white;
padding: 8px;
border-radius: 4px;
width: 100%;
font-family: monospace;
box-sizing: border-box;
}
input[type="number"]:focus { outline: none; border-color: var(--accent); }
/* Charts Section */
.chart-container {
background: #252525;
padding: 15px;
border-radius: 8px;
border: 1px solid #333;
}
.chart-label { display: flex; justify-content: space-between; font-size: 0.85rem; margin-bottom: 5px; }
.chart-val { font-weight: bold; font-family: monospace; }
.bar-bg {
background: #111;
height: 10px;
border-radius: 5px;
overflow: hidden;
position: relative;
}
.bar-fill {
height: 100%;
background: var(--accent);
width: 0%;
transition: width 0.3s ease, background-color 0.3s ease;
}
/* Legend */
.info-box {
font-size: 0.75rem;
color: #888;
line-height: 1.4;
background: rgba(0,0,0,0.2);
padding: 10px;
border-radius: 4px;
}
/* Canvas Area */
.viz-container {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
background-image:
radial-gradient(#333 1px, transparent 1px);
background-size: 20px 20px;
background-color: var(--bg);
position: relative;
}
canvas {
box-shadow: 0 0 50px rgba(0,0,0,0.5);
border: 1px solid #444;
}
.overlay-stats {
position: absolute;
top: 20px;
right: 20px;
text-align: right;
pointer-events: none;
}
.big-stat { font-size: 2rem; font-weight: bold; color: var(--text); text-shadow: 0 2px 4px rgba(0,0,0,0.8); }
.sub-stat { font-size: 0.9rem; color: #aaa; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
</style>
</head>
<body>
<div class="controls">
<div>
<h2>Vector Architect</h2>
<div style="font-size: 0.75rem; color: #666;">Sentinel-2 Optimization Tool</div>
</div>
<div>
<h3>1. Sentinel Product (Source)</h3>
<div class="input-group">
<label>Width (px)</label>
<input type="number" id="imgW" value="10980">
</div>
<!-- Hidden Height for simplicity, assumed square usually, but code handles rect -->
<input type="hidden" id="imgH" value="10980">
</div>
<div>
<h3>2. Model Input (Target)</h3>
<div class="input-group">
<label>Tile Size (px)</label>
<input type="number" id="tileS" value="224">
</div>
</div>
<div>
<h3>3. Tiling Strategy</h3>
<div class="input-group">
<label>
<span>Stride (Step Size)</span>
<span id="strideVal" style="color:var(--accent)">610 px</span>
</label>
<input type="range" id="strideRange" min="50" max="2000" value="610">
</div>
<div class="input-group">
<label>
<span>Overlap (px)</span>
<span style="font-size:0.7em; opacity:0.7;">(Negative = Gap)</span>
</label>
<input type="number" id="overlapInput" value="-386">
</div>
</div>
<div>
<h3>4. Live Metrics</h3>
<div class="chart-container">
<div class="chart-label">
<span>Vector Count</span>
<span class="chart-val" id="txtCount">324</span>
</div>
<div class="bar-bg">
<div class="bar-fill" id="barCount"></div>
</div>
<div style="font-size:0.7rem; color:#666; margin-top:4px; text-align:right;">Target: < 500</div>
</div>
<div class="chart-container" style="margin-top:10px;">
<div class="chart-label">
<span>Pixel Coverage</span>
<span class="chart-val" id="txtCov">13%</span>
</div>
<div class="bar-bg">
<div class="bar-fill" id="barCov"></div>
</div>
</div>
</div>
<div class="info-box">
<strong style="color:var(--text)">Logic Check:</strong><br>
<span id="logicMsg">Sampling mode active. High efficiency.</span>
</div>
</div>
<div class="viz-container">
<canvas id="canvas"></canvas>
<div class="overlay-stats">
<div class="big-stat" id="bigTotal">324</div>
<div class="sub-stat">Total Vectors</div>
</div>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// UI Elements
const ui = {
imgW: document.getElementById('imgW'),
imgH: document.getElementById('imgH'),
tileS: document.getElementById('tileS'),
strideRange: document.getElementById('strideRange'),
strideVal: document.getElementById('strideVal'),
overlapInput: document.getElementById('overlapInput'),
// Charts
txtCount: document.getElementById('txtCount'),
barCount: document.getElementById('barCount'),
txtCov: document.getElementById('txtCov'),
barCov: document.getElementById('barCov'),
// Msgs
logicMsg: document.getElementById('logicMsg'),
bigTotal: document.getElementById('bigTotal')
};
// --- Interaction Logic ---
// 1. When Slider moves (Stride changes) -> Update Overlap
ui.strideRange.addEventListener('input', () => {
const S = parseInt(ui.strideRange.value);
const T = parseInt(ui.tileS.value);
ui.strideVal.textContent = S + " px";
// Formula: Overlap = Tile - Stride
ui.overlapInput.value = T - S;
draw();
});
// 2. When Overlap Input changes -> Update Stride
ui.overlapInput.addEventListener('input', () => {
const O = parseInt(ui.overlapInput.value);
const T = parseInt(ui.tileS.value);
// Formula: Stride = Tile - Overlap
let newStride = T - O;
if(newStride < 1) newStride = 1; // Safety
ui.strideRange.value = newStride;
ui.strideVal.textContent = newStride + " px";
draw();
});
// 3. Other inputs
[ui.imgW, ui.tileS].forEach(el => el.addEventListener('input', () => {
// If Tile changes, we need to recalc relationship based on current slider
const S = parseInt(ui.strideRange.value);
const T = parseInt(ui.tileS.value);
ui.overlapInput.value = T - S;
draw();
}));
function draw() {
// --- Calculations ---
const W = parseInt(ui.imgW.value);
const H = parseInt(ui.imgH.value);
const T = parseInt(ui.tileS.value);
const S = parseInt(ui.strideRange.value);
const Overlap = parseInt(ui.overlapInput.value);
// Count (Convolution formula)
const countX = Math.floor((W - T) / S) + 1;
const countY = Math.floor((H - T) / S) + 1;
const total = countX * countY;
// Coverage %
// Simple approximation: (Vectors * TileArea) / TotalArea
// Note: If overlapping, this goes > 100% which indicates redundancy
const totalArea = W * H;
const vectorArea = total * (T * T);
let covPct = (vectorArea / totalArea) * 100;
// --- Update Charts & text ---
// 1. Vector Count Chart
ui.txtCount.textContent = total.toLocaleString();
ui.bigTotal.textContent = total.toLocaleString();
// Logic for scaling the bar (Assuming 2000 is "Too Many")
let countFill = (total / 2000) * 100;
if(countFill > 100) countFill = 100;
ui.barCount.style.width = countFill + "%";
// Color Coding
if(total <= 400) {
ui.barCount.style.backgroundColor = "var(--accent)"; // Green
ui.logicMsg.innerHTML = "Sampling Mode.<br>Storage Optimized.";
ui.logicMsg.style.color = "var(--accent)";
} else if (total <= 1000) {
ui.barCount.style.backgroundColor = "var(--warn)"; // Orange
ui.logicMsg.innerHTML = "High Density.<br>Heavy Storage Cost.";
ui.logicMsg.style.color = "var(--warn)";
} else {
ui.barCount.style.backgroundColor = "var(--danger)"; // Red
ui.logicMsg.innerHTML = "Extremely Heavy.<br>Not recommended for DB.";
ui.logicMsg.style.color = "var(--danger)";
}
// 2. Coverage Chart
ui.txtCov.textContent = covPct.toFixed(1) + "%";
let covFill = covPct;
if(covFill > 100) covFill = 100;
ui.barCov.style.width = covFill + "%";
if(Overlap > 0) {
ui.txtCov.textContent += " (Redundant)";
ui.barCov.style.backgroundColor = "#aaa";
} else {
ui.barCov.style.backgroundColor = "var(--accent)";
}
// --- Canvas Drawing ---
// Fit canvas to container
const padding = 60;
const containerW = canvas.parentElement.clientWidth - padding;
const containerH = canvas.parentElement.clientHeight - padding;
const scale = Math.min(containerW / W, containerH / H);
canvas.width = W * scale;
canvas.height = H * scale;
// Background (Gray, not red)
ctx.fillStyle = '#2d2d2d';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Border
ctx.strokeStyle = '#555';
ctx.lineWidth = 2;
ctx.strokeRect(0, 0, canvas.width, canvas.height);
// Draw Vectors
// We use a slight opacity to visualize overlap if it exists
ctx.fillStyle = 'rgba(0, 212, 170, 0.3)'; // Teal translucent
ctx.strokeStyle = 'rgba(0, 212, 170, 0.8)'; // Teal border
// Performance: Don't render borders if thousands of tiles
if(total > 3000) ctx.strokeStyle = 'rgba(0,0,0,0)';
for (let x = 0; x < countX; x++) {
for (let y = 0; y < countY; y++) {
const screenX = (x * S) * scale;
const screenY = (y * S) * scale;
const screenT = T * scale;
ctx.fillRect(screenX, screenY, screenT, screenT);
ctx.strokeRect(screenX, screenY, screenT, screenT);
}
}
}
// Init
draw();
window.addEventListener('resize', draw);
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Universal Tiling Planner v2</title>
<style>
:root {
--bg: #121212;
--panel: #1e1e1e;
--text: #e0e0e0;
--accent: #3b82f6; /* Blue */
--tile: #10b981; /* Emerald */
--input-bg: #2a2a2a;
--border: #333;
}
body {
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg);
color: var(--text);
margin: 0;
display: flex;
height: 100vh;
overflow: hidden;
}
/* Sidebar */
.controls {
width: 380px;
background-color: var(--panel);
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
box-shadow: 4px 0 15px rgba(0,0,0,0.4);
overflow-y: auto;
border-right: 1px solid var(--border);
z-index: 10;
}
h2 { margin: 0 0 5px 0; font-size: 1.2rem; color: #fff; }
.subtitle { font-size: 0.75rem; color: #777; margin-bottom: 10px; }
h3 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 1px;
color: #888;
margin-bottom: 10px;
border-bottom: 1px solid var(--border);
padding-bottom: 4px;
}
.input-group { margin-bottom: 15px; }
.label-row {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 0.85rem;
color: #ccc;
}
/* Flex container for Slider + Number Box */
.control-row {
display: flex;
align-items: center;
gap: 10px;
}
input[type="range"] {
flex-grow: 1;
cursor: pointer;
height: 6px;
background: #444;
border-radius: 3px;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: var(--accent);
border-radius: 50%;
cursor: pointer;
}
input[type="number"] {
background: var(--input-bg);
border: 1px solid #444;
color: var(--accent);
padding: 5px;
border-radius: 4px;
width: 70px; /* Fixed width for precision box */
font-family: monospace;
text-align: right;
}
input[type="number"]:focus {
outline: none;
border-color: var(--accent);
}
/* Canvas Area */
.viz-container {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
background-image:
linear-gradient(#222 1px, transparent 1px),
linear-gradient(90deg, #222 1px, transparent 1px);
background-size: 40px 40px;
background-color: var(--bg);
position: relative;
}
#mainCanvas {
box-shadow: 0 0 50px rgba(0,0,0,0.5);
border: 1px solid #444;
}
.overlay-stats {
position: absolute;
top: 20px;
right: 20px;
text-align: right;
pointer-events: none;
background: rgba(0,0,0,0.7);
padding: 15px;
border-radius: 8px;
backdrop-filter: blur(4px);
border: 1px solid #444;
}
.big-stat { font-size: 2.5rem; font-weight: bold; color: white; line-height: 1; }
.sub-stat { font-size: 0.9rem; color: #bbb; margin-top: 5px;}
/* Analysis Chart Container */
.analysis-box {
background: #252525;
padding: 10px;
border-radius: 8px;
border: 1px solid #333;
height: 200px;
display: flex;
flex-direction: column;
position: relative;
}
#chartCanvas {
width: 100%;
height: 100%;
cursor: crosshair;
}
</style>
</head>
<body>
<div class="controls">
<div>
<h2>Vector Architect</h2>
<div class="subtitle">Universal Tiling & Padding Planner</div>
</div>
<!-- 1. Source Dimensions -->
<div>
<h3>1. Source Dimensions (px)</h3>
<div class="input-group">
<div class="label-row"><span>Width</span></div>
<div class="control-row">
<input type="range" id="slW" min="100" max="10000" step="10" value="1000">
<input type="number" id="numW" value="1000">
</div>
</div>
<div class="input-group">
<div class="label-row"><span>Height</span></div>
<div class="control-row">
<input type="range" id="slH" min="100" max="10000" step="10" value="1000">
<input type="number" id="numH" value="1000">
</div>
</div>
</div>
<!-- 2. Configuration -->
<div>
<h3>2. Tiling Configuration</h3>
<div class="input-group">
<div class="label-row"><span>Tile Size (Kernel)</span></div>
<div class="control-row">
<!-- Note: Tile size typically doesn't need a massive slider, usually 64-1024 -->
<input type="range" id="slTile" min="32" max="1024" step="8" value="224">
<input type="number" id="numTile" value="224">
</div>
</div>
<div class="input-group">
<div class="label-row"><span>Padding (Zero-pad)</span></div>
<div class="control-row">
<input type="range" id="slPad" min="0" max="500" value="0">
<input type="number" id="numPad" value="0">
</div>
</div>
<div class="input-group">
<div class="label-row"><span>Stride</span></div>
<div class="control-row">
<input type="range" id="slStride" min="10" max="1024" value="100">
<input type="number" id="numStride" value="100">
</div>
</div>
</div>
<!-- 3. Analysis -->
<div style="flex-grow:1; display:flex; flex-direction:column;">
<h3>3. Impact Analysis (Log Scale)</h3>
<div class="analysis-box">
<canvas id="chartCanvas"></canvas>
</div>
</div>
</div>
<div class="viz-container">
<canvas id="mainCanvas"></canvas>
<div class="overlay-stats">
<div class="big-stat" id="statTotal">0</div>
<div class="sub-stat">Total Vectors</div>
<div class="sub-stat" id="statGrid" style="font-size:0.8rem; margin-top:2px;">0 x 0 grid</div>
</div>
</div>
<script>
// --- UI State Management ---
// Helper to bind slider and number box together
function bindInputs(sliderId, numberId, onChange) {
const s = document.getElementById(sliderId);
const n = document.getElementById(numberId);
s.addEventListener('input', () => {
n.value = s.value;
onChange();
});
n.addEventListener('input', () => {
// Validation limits based on slider attributes
let val = parseInt(n.value);
if(isNaN(val)) return;
// Optional: Clamp logic (commented out to allow free typing, but slider visual might cap)
// if(val < parseInt(s.min)) val = parseInt(s.min);
// if(val > parseInt(s.max)) val = parseInt(s.max);
s.value = val;
onChange();
});
return { s, n };
}
const app = {
w: bindInputs('slW', 'numW', updateAll),
h: bindInputs('slH', 'numH', updateAll),
t: bindInputs('slTile', 'numTile', updateAll),
p: bindInputs('slPad', 'numPad', updateAll),
str: bindInputs('slStride', 'numStride', updateAll),
stats: {
total: document.getElementById('statTotal'),
grid: document.getElementById('statGrid')
}
};
const mainCanvas = document.getElementById('mainCanvas');
const mainCtx = mainCanvas.getContext('2d');
const chartCanvas = document.getElementById('chartCanvas');
const chartCtx = chartCanvas.getContext('2d');
// Store chart data for hover effects
let chartDataPoints = [];
let mouseX = -1;
let isHoveringChart = false;
function getState() {
return {
w: parseInt(app.w.n.value) || 100,
h: parseInt(app.h.n.value) || 100,
t: parseInt(app.t.n.value) || 64,
p: parseInt(app.p.n.value) || 0,
s: parseInt(app.str.n.value) || 10
};
}
function calculateVectors(w, h, t, p, s) {
if (s < 1) s = 1;
const dimW = w + (p * 2);
const dimH = h + (p * 2);
// Convolution floor logic
const xCount = Math.floor((dimW - t) / s) + 1;
const yCount = Math.floor((dimH - t) / s) + 1;
const x = Math.max(0, xCount);
const y = Math.max(0, yCount);
return { x, y, total: x * y };
}
function updateAll() {
drawViz();
prepareChartData(); // Calculate the curve
drawChart(); // Render the curve + hover state
}
// --- Visualization (Main Canvas) ---
function drawViz() {
const s = getState();
const calcs = calculateVectors(s.w, s.h, s.t, s.p, s.s);
// Update Text Stats
app.stats.total.innerText = calcs.total.toLocaleString();
app.stats.grid.innerText = `${calcs.x} cols x ${calcs.y} rows`;
// Fit logic
const totalDrawW = s.w + (s.p * 2);
const totalDrawH = s.h + (s.p * 2);
const paddingMargin = 50;
const containerW = mainCanvas.parentElement.clientWidth - paddingMargin;
const containerH = mainCanvas.parentElement.clientHeight - paddingMargin;
const scale = Math.min(containerW / totalDrawW, containerH / totalDrawH);
mainCanvas.width = totalDrawW * scale;
mainCanvas.height = totalDrawH * scale;
mainCtx.clearRect(0,0, mainCanvas.width, mainCanvas.height);
// 1. Padding Zone (Dashed)
mainCtx.strokeStyle = '#555';
mainCtx.setLineDash([5, 5]);
mainCtx.lineWidth = 1;
mainCtx.strokeRect(0, 0, mainCanvas.width, mainCanvas.height);
// 2. Source Image (Blue)
const imgX = s.p * scale;
const imgY = s.p * scale;
const imgW = s.w * scale;
const imgH = s.h * scale;
mainCtx.fillStyle = 'rgba(59, 130, 246, 0.1)';
mainCtx.fillRect(imgX, imgY, imgW, imgH);
mainCtx.setLineDash([]);
mainCtx.lineWidth = 2;
mainCtx.strokeStyle = '#3b82f6';
mainCtx.strokeRect(imgX, imgY, imgW, imgH);
// 3. Tiles (Green)
mainCtx.strokeStyle = 'rgba(16, 185, 129, 0.8)';
mainCtx.fillStyle = 'rgba(16, 185, 129, 0.2)';
// Performance mode
const highCount = calcs.total > 3000;
if(highCount) {
mainCtx.lineWidth = 0.5;
mainCtx.strokeStyle = 'rgba(16, 185, 129, 0.5)';
mainCtx.fillStyle = 'transparent';
}
const tileScreenS = s.t * scale;
const strideScreen = s.s * scale;
// Limit drawing loop to prevent freeze on insane inputs
if(calcs.total < 50000) {
mainCtx.beginPath();
for(let x = 0; x < calcs.x; x++) {
for(let y = 0; y < calcs.y; y++) {
const drawX = x * strideScreen;
const drawY = y * strideScreen;
mainCtx.rect(drawX, drawY, tileScreenS, tileScreenS);
}
}
if(!highCount) mainCtx.fill();
mainCtx.stroke();
} else {
// Too many to draw
mainCtx.fillStyle = 'rgba(255, 100, 100, 0.5)';
mainCtx.font = "20px Arial";
mainCtx.fillText("Rendering Limit Exceeded (>50k)", 20, 30);
}
}
// --- Chart Logic ---
// 1. Calculate Data Points (Separated from render for performance)
function prepareChartData() {
const s = getState();
chartDataPoints = [];
// Graph range: Stride 10 -> TileSize * 1.5
const startStride = 10;
const endStride = Math.floor(s.t * 1.5);
const step = Math.max(1, Math.floor((endStride - startStride) / 100)); // ~100 points
for (let st = startStride; st <= endStride; st += step) {
const res = calculateVectors(s.w, s.h, s.t, s.p, st);
chartDataPoints.push({ stride: st, count: res.total });
}
}
// 2. Render Chart
function drawChart() {
const cw = chartCanvas.parentElement.clientWidth;
const ch = chartCanvas.parentElement.clientHeight;
chartCanvas.width = cw;
chartCanvas.height = ch;
const ctx = chartCtx;
ctx.clearRect(0, 0, cw, ch);
if (chartDataPoints.length === 0) return;
// Find min/max for scaling
// Y-Axis: Logarithmic
// X-Axis: Linear
let maxCount = 0;
let minCount = Infinity;
chartDataPoints.forEach(p => {
if(p.count > maxCount) maxCount = p.count;
if(p.count < minCount) minCount = p.count;
});
if(maxCount === 0) maxCount = 1;
// Log(0) is undefined, so ensure min is at least 1 for log math
const logMin = Math.log10(Math.max(1, minCount));
const logMax = Math.log10(Math.max(10, maxCount)); // Ensure some height
const xMin = chartDataPoints[0].stride;
const xMax = chartDataPoints[chartDataPoints.length - 1].stride;
const pad = 30;
const graphW = cw - pad * 2;
const graphH = ch - pad * 2;
// Helper to map coordinates
const getX = (stride) => pad + ((stride - xMin) / (xMax - xMin)) * graphW;
const getY = (count) => {
// Log Scale Normalization
const val = Math.log10(Math.max(1, count));
const pct = (val - logMin) / (logMax - logMin || 1);
return (ch - pad) - (pct * graphH);
};
// Draw Line
ctx.beginPath();
ctx.strokeStyle = '#3b82f6';
ctx.lineWidth = 2;
chartDataPoints.forEach((pt, i) => {
const x = getX(pt.stride);
const y = getY(pt.count);
if(i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
// Draw Current Position Dot (Real State)
const currentS = getState().s;
const currentTotal = calculateVectors(getState().w, getState().h, getState().t, getState().p, currentS).total;
if (currentS >= xMin && currentS <= xMax) {
const cx = getX(currentS);
const cy = getY(currentTotal);
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(cx, cy, 4, 0, Math.PI * 2);
ctx.fill();
}
// --- Interaction: Hover Overlay ---
if (isHoveringChart && mouseX >= pad && mouseX <= cw - pad) {
// 1. Find nearest data point based on mouseX
// Inverse Map X to Stride
const ratio = (mouseX - pad) / graphW;
const targetStride = xMin + (ratio * (xMax - xMin));
// Find closest point in array
const closest = chartDataPoints.reduce((prev, curr) => {
return (Math.abs(curr.stride - targetStride) < Math.abs(prev.stride - targetStride) ? curr : prev);
});
const cx = getX(closest.stride);
const cy = getY(closest.count);
// 2. Draw Crosshair
ctx.beginPath();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
// Vertical line
ctx.moveTo(cx, pad);
ctx.lineTo(cx, ch - pad);
// Horizontal line
ctx.moveTo(pad, cy);
ctx.lineTo(cw - pad, cy);
ctx.stroke();
ctx.setLineDash([]);
// 3. Draw Tooltip Box
const text = `Stride: ${closest.stride} | Count: ${closest.count.toLocaleString()}`;
ctx.font = "12px monospace";
const tw = ctx.measureText(text).width + 10;
const th = 24;
let tx = cx + 10;
let ty = cy - 30;
// Boundary checks
if(tx + tw > cw) tx = cx - tw - 10;
if(ty < 0) ty = cy + 10;
ctx.fillStyle = "rgba(0,0,0,0.8)";
ctx.fillRect(tx, ty, tw, th);
ctx.strokeStyle = "#555";
ctx.strokeRect(tx, ty, tw, th);
ctx.fillStyle = "#fff";
ctx.fillText(text, tx + 5, ty + 16);
// Highlight dot
ctx.fillStyle = "var(--accent)";
ctx.beginPath();
ctx.arc(cx, cy, 5, 0, Math.PI*2);
ctx.fill();
}
}
// --- Chart Event Listeners ---
chartCanvas.addEventListener('mousemove', (e) => {
const rect = chartCanvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
isHoveringChart = true;
drawChart(); // Redraw to show overlay
});
chartCanvas.addEventListener('mouseleave', () => {
isHoveringChart = false;
drawChart(); // Redraw to clear overlay
});
window.addEventListener('resize', updateAll);
// Init
updateAll();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Precision Tiling Planner</title>
<style>
:root {
--bg: #121212;
--panel: #1e1e1e;
--border: #333;
--text: #e0e0e0;
--accent: #3b82f6; /* Blue */
--tile: #10b981; /* Emerald */
--chart-line: #f59e0b; /* Amber */
--grid: #333;
}
body {
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg);
color: var(--text);
margin: 0;
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
/* --- Top Section: Controls & Viz --- */
.top-section {
display: flex;
flex: 1;
min-height: 0; /* allows flex shrink */
}
/* Sidebar Controls */
.controls {
width: 320px;
background-color: var(--panel);
padding: 20px;
overflow-y: auto;
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 20px;
box-shadow: 2px 0 10px rgba(0,0,0,0.3);
z-index: 10;
}
h2 { margin: 0 0 5px 0; font-size: 1.1rem; color: #fff; }
.subtitle { font-size: 0.75rem; color: #777; margin-bottom: 5px; }
h3 { font-size: 0.75rem; text-transform: uppercase; color: #888; border-bottom: 1px solid #333; padding-bottom: 5px; margin-bottom: 10px; }
.control-group { margin-bottom: 15px; }
.label-row { display: flex; justify-content: space-between; font-size: 0.8rem; color: #ccc; margin-bottom: 5px; }
/* Dual Input Styling */
.dual-input { display: flex; align-items: center; gap: 8px; }
input[type="range"] { flex-grow: 1; cursor: pointer; }
input[type="number"] {
width: 70px;
background: #2a2a2a;
border: 1px solid #444;
color: var(--accent);
padding: 4px 6px;
border-radius: 4px;
font-family: monospace;
text-align: right;
}
input[type="number"]:focus { outline: none; border-color: var(--accent); }
/* Visualization Area */
.viz-container {
flex-grow: 1;
position: relative;
background-image: radial-gradient(#2d2d2d 1px, transparent 1px);
background-size: 20px 20px;
background-color: #181818;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
#mainCanvas {
box-shadow: 0 0 30px rgba(0,0,0,0.5);
border: 1px solid #444;
}
/* --- Bottom Section: Analytics --- */
.bottom-panel {
height: 220px;
background-color: var(--panel);
border-top: 1px solid var(--border);
display: flex;
padding: 0;
box-shadow: 0 -4px 15px rgba(0,0,0,0.2);
z-index: 20;
}
/* Stats Grid (Left of Bottom) */
.stats-container {
width: 320px; /* Match sidebar width */
padding: 20px;
border-right: 1px solid var(--border);
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 15px;
}
.stat-box { display: flex; flex-direction: column; justify-content: center; }
.stat-label { font-size: 0.75rem; color: #888; text-transform: uppercase; margin-bottom: 4px; }
.stat-val { font-size: 1.1rem; font-weight: bold; color: #fff; font-family: monospace; }
.stat-sub { font-size: 0.7rem; color: #666; }
/* Chart Area (Right of Bottom) */
.chart-container {
flex-grow: 1;
padding: 15px 20px;
display: flex;
flex-direction: column;
position: relative;
}
.chart-header {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: #aaa;
margin-bottom: 5px;
}
#chartCanvas {
width: 100%;
height: 100%;
background: rgba(0,0,0,0.2);
border-radius: 4px;
cursor: crosshair;
}
</style>
</head>
<body>
<!-- TOP SECTION: Controls + Viz -->
<div class="top-section">
<!-- Controls Sidebar -->
<div class="controls">
<div>
<h2>Vector Architect</h2>
<div class="subtitle">Precision Tiling Tool</div>
</div>
<div>
<h3>Source Dimensions</h3>
<div class="control-group">
<div class="label-row"><span>Width (px)</span></div>
<div class="dual-input">
<input type="range" id="sl_W" min="100" max="8000" value="1000">
<input type="number" id="nb_W" value="1000">
</div>
</div>
<div class="control-group">
<div class="label-row"><span>Height (px)</span></div>
<div class="dual-input">
<input type="range" id="sl_H" min="100" max="8000" value="1000">
<input type="number" id="nb_H" value="1000">
</div>
</div>
</div>
<div>
<h3>Configuration</h3>
<div class="control-group">
<div class="label-row"><span>Tile Size (px)</span></div>
<div class="dual-input">
<input type="range" id="sl_T" min="32" max="1024" value="224">
<input type="number" id="nb_T" value="224">
</div>
</div>
<div class="control-group">
<div class="label-row"><span>Padding (px)</span></div>
<div class="dual-input">
<input type="range" id="sl_P" min="0" max="500" value="0">
<input type="number" id="nb_P" value="0">
</div>
</div>
<div class="control-group">
<div class="label-row"><span>Stride (px)</span></div>
<div class="dual-input">
<input type="range" id="sl_S" min="10" max="1024" value="100">
<input type="number" id="nb_S" value="100">
</div>
</div>
</div>
</div>
<!-- Canvas Viz -->
<div class="viz-container">
<canvas id="mainCanvas"></canvas>
</div>
</div>
<!-- BOTTOM SECTION: Stats & Chart -->
<div class="bottom-panel">
<!-- Stats Grid -->
<div class="stats-container">
<div class="stat-box">
<div class="stat-label">Total Tiles</div>
<div class="stat-val" id="valTotal" style="color: var(--accent)">0</div>
<div class="stat-sub">Vectors Generated</div>
</div>
<div class="stat-box">
<div class="stat-label">Grid Layout</div>
<div class="stat-val" id="valGrid">0 x 0</div>
<div class="stat-sub">Cols x Rows</div>
</div>
<div class="stat-box">
<div class="stat-label">Abs. Coverage</div>
<div class="stat-val" id="valAbsCov">0</div>
<div class="stat-sub">Total Pixels (Sq)</div>
</div>
<div class="stat-box">
<div class="stat-label">Coverage %</div>
<div class="stat-val" id="valPctCov">0%</div>
<div class="stat-sub" id="subPctCov">of source area</div>
</div>
</div>
<!-- Interactive Log Chart -->
<div class="chart-container">
<div class="chart-header">
<strong>Impact Analysis</strong>
<span id="chart-tooltip" style="color:var(--chart-line); font-family:monospace;">Hover chart for details</span>
</div>
<canvas id="chartCanvas"></canvas>
</div>
</div>
<script>
// --- Elements & Inputs ---
// Helper to get sync'd elements
const getEl = (id) => document.getElementById(id);
const pairs = [
{ sl: getEl('sl_W'), nb: getEl('nb_W'), key: 'w' },
{ sl: getEl('sl_H'), nb: getEl('nb_H'), key: 'h' },
{ sl: getEl('sl_T'), nb: getEl('nb_T'), key: 't' },
{ sl: getEl('sl_P'), nb: getEl('nb_P'), key: 'p' },
{ sl: getEl('sl_S'), nb: getEl('nb_S'), key: 's' },
];
const ui = {
valTotal: getEl('valTotal'),
valGrid: getEl('valGrid'),
valAbsCov: getEl('valAbsCov'),
valPctCov: getEl('valPctCov'),
subPctCov: getEl('subPctCov'),
chartTip: getEl('chart-tooltip')
};
const mainCanvas = getEl('mainCanvas');
const mainCtx = mainCanvas.getContext('2d');
const chartCanvas = getEl('chartCanvas');
const chartCtx = chartCanvas.getContext('2d');
// Chart Data Cache
let chartDataPoints = [];
let chartMaxY = 0;
let chartMinY = 0;
// --- Synchronization Logic ---
pairs.forEach(pair => {
// Slider changes Number
pair.sl.addEventListener('input', () => {
pair.nb.value = pair.sl.value;
update();
});
// Number changes Slider
pair.nb.addEventListener('input', () => {
pair.sl.value = pair.nb.value;
update();
});
});
function getState() {
return {
w: parseInt(getEl('nb_W').value) || 100,
h: parseInt(getEl('nb_H').value) || 100,
t: parseInt(getEl('nb_T').value) || 64,
p: parseInt(getEl('nb_P').value) || 0,
s: parseInt(getEl('nb_S').value) || 50
};
}
// --- Core Math ---
function calculateStats(w, h, t, p, s) {
if (s < 1) s = 1;
// Padded Dimensions
const effW = w + (p * 2);
const effH = h + (p * 2);
// Grid calculation
// Formula: floor((W - K) / S) + 1
// Note: If K > W, count is usually 0, but standard sliding window might take 1 if padded.
// Here we assume simple sliding window logic.
const cols = effW < t ? 0 : Math.floor((effW - t) / s) + 1;
const rows = effH < t ? 0 : Math.floor((effH - t) / s) + 1;
const total = cols * rows;
// Coverage Stats
const areaTile = t * t;
const totalCoveredPx = total * areaTile;
const sourceArea = w * h;
const pct = sourceArea > 0 ? (totalCoveredPx / sourceArea) * 100 : 0;
return { cols, rows, total, totalCoveredPx, pct };
}
// --- Main Update Loop ---
function update() {
const state = getState();
const stats = calculateStats(state.w, state.h, state.t, state.p, state.s);
// 1. Update Text Stats
ui.valTotal.innerText = stats.total.toLocaleString();
ui.valGrid.innerText = `${stats.cols} x ${stats.rows}`;
// Intl.NumberFormat for nice comma separation
ui.valAbsCov.innerText = new Intl.NumberFormat().format(stats.totalCoveredPx) + " px²";
ui.valPctCov.innerText = stats.pct.toFixed(1) + "%";
if(stats.pct > 100) {
ui.valPctCov.style.color = "#f59e0b"; // Warn color for heavy overlap
ui.subPctCov.innerText = "High Overlap (>100%)";
} else {
ui.valPctCov.style.color = "#fff";
ui.subPctCov.innerText = "of source area";
}
// 2. Draw Viz
drawVisualization(state, stats);
// 3. Prepare Chart Data (but don't draw on mouse move, only on input change)
prepareChartData(state);
drawChart(); // Initial draw without mouse info
}
// --- Visualization ---
function drawVisualization(st, stats) {
// Setup Canvas Size
const padDraw = 40;
const containerW = mainCanvas.parentElement.clientWidth - padDraw;
const containerH = mainCanvas.parentElement.clientHeight - padDraw;
const totalW = st.w + (st.p * 2);
const totalH = st.h + (st.p * 2);
// Scale
const scale = Math.min(containerW / totalW, containerH / totalH);
mainCanvas.width = totalW * scale;
mainCanvas.height = totalH * scale;
mainCtx.clearRect(0,0, mainCanvas.width, mainCanvas.height);
// 1. Padding Area (Dashed Box)
mainCtx.strokeStyle = '#555';
mainCtx.setLineDash([5, 5]);
mainCtx.lineWidth = 1;
mainCtx.strokeRect(0, 0, mainCanvas.width, mainCanvas.height);
// 2. Source Image (Blue Fill)
const imgX = st.p * scale;
const imgY = st.p * scale;
const imgW = st.w * scale;
const imgH = st.h * scale;
mainCtx.fillStyle = 'rgba(59, 130, 246, 0.15)';
mainCtx.fillRect(imgX, imgY, imgW, imgH);
mainCtx.setLineDash([]);
mainCtx.strokeStyle = '#3b82f6';
mainCtx.lineWidth = 2;
mainCtx.strokeRect(imgX, imgY, imgW, imgH);
// 3. Tiles (Green)
mainCtx.strokeStyle = 'rgba(16, 185, 129, 0.6)';
// Optimize fill if too many
mainCtx.fillStyle = stats.total > 1500 ? 'transparent' : 'rgba(16, 185, 129, 0.15)';
// If extremely high count, reduce stroke opacity
if(stats.total > 5000) mainCtx.strokeStyle = 'rgba(16, 185, 129, 0.2)';
const tSize = st.t * scale;
const stride = st.s * scale;
mainCtx.lineWidth = 1;
// Prevent browser freeze on massive numbers
if(stats.total < 20000) {
for(let c=0; c<stats.cols; c++) {
for(let r=0; r<stats.rows; r++) {
mainCtx.fillRect(c*stride, r*stride, tSize, tSize);
mainCtx.strokeRect(c*stride, r*stride, tSize, tSize);
}
}
}
}
// --- Chart Logic ---
function prepareChartData(st) {
chartDataPoints = [];
// Generate points for Stride = 10 to Stride = TileSize * 1.5
const maxStride = Math.floor(st.t * 1.5);
const startStride = 10;
// To make graph performant, don't calc every integer if range is huge
const step = maxStride > 500 ? 5 : 2;
for(let s = startStride; s <= maxStride; s += step) {
const res = calculateStats(st.w, st.h, st.t, st.p, s);
chartDataPoints.push({ s: s, count: res.total });
}
// Find Min/Max for Log scaling
const counts = chartDataPoints.map(p => p.count);
// Avoid log(0)
chartMinY = Math.max(1, Math.min(...counts));
chartMaxY = Math.max(10, Math.max(...counts));
}
function drawChart(mouseX = -1) {
const cw = chartCanvas.parentElement.clientWidth - 40; // padding inside container
const ch = chartCanvas.parentElement.clientHeight - 30; // header space
chartCanvas.width = cw;
chartCanvas.height = ch;
const ctx = chartCtx;
ctx.clearRect(0, 0, cw, ch);
if(chartDataPoints.length === 0) return;
// Log Math Helpers
const logMin = Math.log10(chartMinY);
const logMax = Math.log10(chartMaxY);
const logRange = logMax - logMin;
const getY = (count) => {
const val = Math.max(1, count);
const logVal = Math.log10(val);
const pct = (logVal - logMin) / (logRange || 1); // avoid div 0
return ch - (pct * ch); // Flip Y
};
const getX = (strideIndex) => {
return (strideIndex / (chartDataPoints.length - 1)) * cw;
};
// Draw Line
ctx.beginPath();
ctx.strokeStyle = '#f59e0b';
ctx.lineWidth = 2;
chartDataPoints.forEach((pt, i) => {
const x = getX(i);
const y = getY(pt.count);
if(i===0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
// Draw Current Stride Position Dot
const state = getState();
const currentPt = chartDataPoints.find(p => Math.abs(p.s - state.s) < 3)
|| chartDataPoints.reduce((prev, curr) => Math.abs(curr.s - state.s) < Math.abs(prev.s - state.s) ? curr : prev);
if(currentPt) {
const idx = chartDataPoints.indexOf(currentPt);
const cx = getX(idx);
const cy = getY(currentPt.count);
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(cx, cy, 5, 0, Math.PI*2);
ctx.fill();
}
// --- Mouse Interaction ---
if(mouseX >= 0) {
// Find closest index to mouseX
const pct = mouseX / cw;
let idx = Math.floor(pct * (chartDataPoints.length - 1));
if(idx < 0) idx = 0;
if(idx >= chartDataPoints.length) idx = chartDataPoints.length - 1;
const hoverPt = chartDataPoints[idx];
const hx = getX(idx);
const hy = getY(hoverPt.count);
// Draw Crosshair
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(hx, 0); ctx.lineTo(hx, ch);
ctx.moveTo(0, hy); ctx.lineTo(cw, hy);
ctx.stroke();
// Draw Tooltip text on UI (not canvas text for crispness, but here we update DOM)
ui.chartTip.innerHTML = `Stride: <span style="color:#fff">${hoverPt.s}px</span> | Count: <span style="color:#fff">${hoverPt.count.toLocaleString()}</span>`;
// Draw circle at hover
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.beginPath();
ctx.arc(hx, hy, 4, 0, Math.PI*2);
ctx.fill();
} else {
// Reset tooltip
ui.chartTip.innerHTML = "Hover chart for details";
}
}
// Chart Mouse Events
chartCanvas.addEventListener('mousemove', (e) => {
const rect = chartCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
drawChart(x);
});
chartCanvas.addEventListener('mouseleave', () => {
drawChart(-1);
});
// Init
update();
window.addEventListener('resize', () => {
update();
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment