Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion arena_editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ <h2>Arena Metrics</h2>

<footer>
<p><a href="https://github.com/reiserlab/webDisplayTools" target="_blank">Reiser Lab</a> | PanelDisplayTools</p>
<p class="version">Arena Editor v3.1 | 2026-01-30</p>
<p class="version">Arena Editor v3.1 | 2026-01-30 15:00 ET</p>
</footer>
</div>

Expand Down
19 changes: 15 additions & 4 deletions icon_generator.html
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,16 @@ <h1>Pattern Icon Generator</h1>

<div class="control-group">
<label>Inner Radius <span class="range-value" id="radius-value">0.20</span></label>
<input type="range" id="radius-slider" min="0.1" max="0.4" step="0.01" value="0.2">
<input type="range" id="radius-slider" min="0.1" max="0.75" step="0.01" value="0.2">
</div>

<div class="control-group">
<label>Background</label>
<select id="background-select">
<option value="dark">Dark</option>
<option value="white">White</option>
<option value="transparent">Transparent</option>
</select>
</div>
</div>

Expand Down Expand Up @@ -411,7 +420,7 @@ <h1>Pattern Icon Generator</h1>
</div>

<footer>
<p>Pattern Icon Generator v1 | 2026-02-01</p>
<p>Pattern Icon Generator v0.2 | 2026-02-01 10:29 ET</p>
<p><a href="index.html">← Back to Tools</a></p>
</footer>
</div>
Expand Down Expand Up @@ -609,13 +618,15 @@ <h1>Pattern Icon Generator</h1>
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 {
Expand Down
119 changes: 103 additions & 16 deletions js/icon-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand Down Expand Up @@ -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
};

Expand Down Expand Up @@ -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
Expand All @@ -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++) {
Expand Down Expand Up @@ -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;
Expand All @@ -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})`;
}

/**
Expand Down
12 changes: 9 additions & 3 deletions test_quick.html
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ <h1>Icon Generator Quick Test</h1>
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')) {
Expand Down Expand Up @@ -175,7 +177,9 @@ <h1>Icon Generator Quick Test</h1>
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')) {
Expand Down Expand Up @@ -230,7 +234,9 @@ <h1>Icon Generator Quick Test</h1>
innerRadiusRatio: 0.2,
frameRange: [0, 9],
maxFrames: 10,
weightingFunction: 'exponential'
weightingFunction: 'exponential',
backgroundColor: 'dark',
showOutlines: true
});

if (!dataURL || !dataURL.startsWith('data:image/png')) {
Expand Down