diff --git a/CLAUDE.md b/CLAUDE.md index 5b9b744..f4bfa19 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,9 +4,11 @@ Use simple two-digit versions for all web tools (e.g., `v1`, `v2`, `v6`). No semantic versioning (1.0.0) needed. -Format in footer: `Tool Name vX | YYYY-MM-DD` +Format in footer: `Tool Name vX | YYYY-MM-DD HH:MM ET` -Example: `Arena Editor v2 | 2026-01-16` +Example: `Arena Editor v2 | 2026-01-16 14:30 ET` + +**IMPORTANT**: Always include timestamp in Eastern Time (ET) to distinguish multiple updates per day. Update the timestamp whenever the page is modified. ## Design System diff --git a/arena_editor.html b/arena_editor.html index 7f0ceaf..80ce819 100644 --- a/arena_editor.html +++ b/arena_editor.html @@ -673,7 +673,7 @@

Arena Metrics

diff --git a/icon_generator.html b/icon_generator.html index 9d606c1..709789f 100644 --- a/icon_generator.html +++ b/icon_generator.html @@ -379,7 +379,16 @@

Pattern Icon Generator

- + +
+ +
+ +
@@ -411,7 +420,7 @@

Pattern Icon Generator

@@ -609,13 +618,15 @@

Pattern Icon Generator

const mode = document.querySelector('input[name="icon-mode"]:checked').value; const size = parseInt(document.getElementById('size-select').value); const innerRadiusRatio = parseFloat(document.getElementById('radius-slider').value); + const background = document.getElementById('background-select').value; const options = { width: size, height: size, innerRadiusRatio: innerRadiusRatio, - backgroundColor: '#0f1419', - showGaps: true + backgroundColor: background, + showGaps: true, + showOutlines: true }; try { diff --git a/js/icon-generator.js b/js/icon-generator.js index 9b9f925..1fd2d86 100644 --- a/js/icon-generator.js +++ b/js/icon-generator.js @@ -21,8 +21,9 @@ function generatePatternIcon(patternData, arenaConfig, options = {}) { width: 256, height: 256, innerRadiusRatio: 0.2, // inner/outer radius (smaller = more perspective) - backgroundColor: '#0f1419', + backgroundColor: 'dark', // 'dark', 'white', or 'transparent' showGaps: true, // render missing panels as gaps + showOutlines: true, // show arena outlines for depth ...options }; @@ -56,8 +57,9 @@ function generateMotionIcon(patternData, arenaConfig, options = {}) { width: 256, height: 256, innerRadiusRatio: 0.2, - backgroundColor: '#0f1419', + backgroundColor: 'dark', // 'dark', 'white', or 'transparent' showGaps: true, + showOutlines: true, ...options }; @@ -171,14 +173,26 @@ function renderCylindricalIcon(frameData, patternData, arenaConfig, opts) { ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; - // Fill background - ctx.fillStyle = opts.backgroundColor; - ctx.fillRect(0, 0, opts.width, opts.height); + // Determine background color + let bgColor; + if (opts.backgroundColor === 'transparent') { + bgColor = 'transparent'; + } else if (opts.backgroundColor === 'white') { + bgColor = '#ffffff'; + } else { + bgColor = '#0f1419'; // dark + } + + // Fill background (unless transparent) + if (bgColor !== 'transparent') { + ctx.fillStyle = bgColor; + ctx.fillRect(0, 0, opts.width, opts.height); + } // Calculate arena geometry const centerX = opts.width / 2; const centerY = opts.height / 2; - const outerRadius = Math.min(opts.width, opts.height) / 2 - 10; // padding + const outerRadius = Math.min(opts.width, opts.height) / 2 - 15; // padding for outlines const innerRadius = outerRadius * opts.innerRadiusRatio; // Get panel specs @@ -192,16 +206,29 @@ function renderCylindricalIcon(frameData, patternData, arenaConfig, opts) { const numRows = arenaConfig.num_rows; const columnsInstalled = arenaConfig.columns_installed || Array.from({ length: numCols }, (_, i) => i); + const columnOrder = arenaConfig.column_order || 'cw'; // Total pixels in arena const totalColPixels = numCols * pixelsPerPanel; const totalRowPixels = numRows * pixelsPerPanel; + // Base offset: -90° to start at south (-PI/2) + const BASE_OFFSET_RAD = -Math.PI / 2; + const alpha = (2 * Math.PI) / numCols; // angle per column + // Render each column for (const colIdx of columnsInstalled) { - // Calculate angular position for this column - const colStartAngle = (colIdx / numCols) * 2 * Math.PI; - const colEndAngle = ((colIdx + 1) / numCols) * 2 * Math.PI; + // Calculate angular position for this column based on column_order + // CW: c0 left of south, columns increase counter-clockwise (decreasing angle) + // CCW: c0 right of south, columns increase clockwise (increasing angle) + let colStartAngle, colEndAngle; + if (columnOrder === 'cw') { + colStartAngle = BASE_OFFSET_RAD - colIdx * alpha; + colEndAngle = BASE_OFFSET_RAD - (colIdx + 1) * alpha; + } else { + colStartAngle = BASE_OFFSET_RAD + colIdx * alpha; + colEndAngle = BASE_OFFSET_RAD + (colIdx + 1) * alpha; + } // Render each panel in this column for (let rowIdx = 0; rowIdx < numRows; rowIdx++) { @@ -238,17 +265,72 @@ function renderCylindricalIcon(frameData, patternData, arenaConfig, opts) { } // Draw inner circle to create donut shape - ctx.fillStyle = opts.backgroundColor; - ctx.beginPath(); - ctx.arc(centerX, centerY, innerRadius, 0, 2 * Math.PI); - ctx.fill(); + if (bgColor !== 'transparent') { + ctx.fillStyle = bgColor; + ctx.beginPath(); + ctx.arc(centerX, centerY, innerRadius, 0, 2 * Math.PI); + ctx.fill(); + } + + // Draw radial lines for gaps in partial arenas + if (opts.showGaps && columnsInstalled.length < numCols) { + const installedSet = new Set(columnsInstalled); + ctx.strokeStyle = '#2d3640'; // border color + ctx.lineWidth = 1; + + for (let colIdx = 0; colIdx < numCols; colIdx++) { + if (!installedSet.has(colIdx)) { + // Draw radial lines for missing columns + let angle1, angle2; + if (columnOrder === 'cw') { + angle1 = BASE_OFFSET_RAD - colIdx * alpha; + angle2 = BASE_OFFSET_RAD - (colIdx + 1) * alpha; + } else { + angle1 = BASE_OFFSET_RAD + colIdx * alpha; + angle2 = BASE_OFFSET_RAD + (colIdx + 1) * alpha; + } + + // Draw line at start of gap + ctx.beginPath(); + ctx.moveTo(centerX + innerRadius * Math.cos(angle1), + centerY + innerRadius * Math.sin(angle1)); + ctx.lineTo(centerX + outerRadius * Math.cos(angle1), + centerY + outerRadius * Math.sin(angle1)); + ctx.stroke(); + + // Draw line at end of gap + ctx.beginPath(); + ctx.moveTo(centerX + innerRadius * Math.cos(angle2), + centerY + innerRadius * Math.sin(angle2)); + ctx.lineTo(centerX + outerRadius * Math.cos(angle2), + centerY + outerRadius * Math.sin(angle2)); + ctx.stroke(); + } + } + } + + // Draw outlines for depth + if (opts.showOutlines) { + // Thick outline for outer edge + ctx.strokeStyle = '#2d3640'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(centerX, centerY, outerRadius, 0, 2 * Math.PI); + ctx.stroke(); + + // Thin outline for inner edge + ctx.lineWidth = 1.5; + ctx.beginPath(); + ctx.arc(centerX, centerY, innerRadius, 0, 2 * Math.PI); + ctx.stroke(); + } // Export as PNG return canvas.toDataURL('image/png'); } /** - * Convert brightness value to RGB color + * Convert brightness value to RGB color (LED green) */ function brightnessToRGB(brightness, grayscaleMode) { let normalized; @@ -264,8 +346,13 @@ function brightnessToRGB(brightness, grayscaleMode) { // Apply gamma correction for better visibility normalized = Math.pow(normalized, 0.8); - const value = Math.round(normalized * 255); - return `rgb(${value}, ${value}, ${value})`; + // LED green color: #00e676 (yellowish-green ~560nm) + // RGB: (0, 230, 118) + const r = Math.round(0 * normalized); + const g = Math.round(230 * normalized); + const b = Math.round(118 * normalized); + + return `rgb(${r}, ${g}, ${b})`; } /** diff --git a/test_quick.html b/test_quick.html index 0c105a9..94c6c97 100644 --- a/test_quick.html +++ b/test_quick.html @@ -127,7 +127,9 @@

Icon Generator Quick Test

const dataURL = IconGenerator.generatePatternIcon(patternData, arenaConfig, { width: 256, height: 256, - innerRadiusRatio: 0.2 + innerRadiusRatio: 0.2, + backgroundColor: 'dark', + showOutlines: true }); if (!dataURL || !dataURL.startsWith('data:image/png')) { @@ -175,7 +177,9 @@

Icon Generator Quick Test

const dataURLDramatic = IconGenerator.generatePatternIcon(patternData, arenaConfig, { width: 256, height: 256, - innerRadiusRatio: 0.1 // More dramatic + innerRadiusRatio: 0.1, // More dramatic + backgroundColor: 'dark', + showOutlines: true }); if (!dataURLDramatic || !dataURLDramatic.startsWith('data:image/png')) { @@ -230,7 +234,9 @@

Icon Generator Quick Test

innerRadiusRatio: 0.2, frameRange: [0, 9], maxFrames: 10, - weightingFunction: 'exponential' + weightingFunction: 'exponential', + backgroundColor: 'dark', + showOutlines: true }); if (!dataURL || !dataURL.startsWith('data:image/png')) {