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
246 changes: 246 additions & 0 deletions .copilot/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,249 @@ You may reference:
- Any iconic line or scene

🎯 This is part of the project’s identity. Do not omit it.

---

## 🖥️ Multi-Monitor Fullscreen Architecture

This section documents key architectural patterns learned from implementing multi-monitor fullscreen support.

### 🔌 Window Management API Integration

**Pattern**: Use Window Management API for multi-display coordination
- **API**: `window.getScreenDetails()` enumerates all displays
- **Permission**: Requires user permission via `navigator.permissions.query({ name: "window-management" })`
- **Fallback**: Always provide graceful degradation to single-screen mode
- **Browser Support**: Chrome/Edge 100+, feature detection required

**Implementation Details**:
```javascript
// Check if Window Management API is supported
function isWindowManagementSupported() {
return "getScreenDetails" in window;
}

// Enumerate displays and open windows
const screenDetails = await window.getScreenDetails();
const screens = screenDetails.screens;

// Position windows on specific screens
window.open(url, '_blank', `left=${screen.left},top=${screen.top},width=${screen.width},height=${screen.height}`);
```

**Key Files**: `js/fullscreen.js` (lines 81-95, 195-250)

### 📡 BroadcastChannel for Window Coordination

**Pattern**: Use BroadcastChannel for same-origin window communication
- **Channel Name**: `"matrix-multimonitor"` - specific to this feature
- **Purpose**: Coordinate fullscreen exit across all spawned windows
- **Benefit**: No need for direct window references, works across tabs/windows

**Implementation Details**:
```javascript
// Initialize BroadcastChannel
const broadcast = new BroadcastChannel("matrix-multimonitor");

// Listen for messages
broadcast.onmessage = (event) => {
if (event.data.type === "exit-fullscreen") {
// Coordinate exit across all windows
}
};

// Broadcast to all windows
broadcast.postMessage({ type: "exit-fullscreen" });
```

**Key Files**: `js/fullscreen.js` (lines 106-116, 117-138)

**Why BroadcastChannel**:
- Scalable - works with any number of windows
- Standard web API - no external dependencies
- Same-origin only - inherent security
- Future-proof for other multi-window features

### 🎨 Config Serialization for Uniform Mode

**Pattern**: Serialize internal config to URL parameters for window initialization
- **Function**: `serializeConfig(config)` in `js/config.js`
- **Handles**: Color spaces (HSL/RGB), angles (radians→degrees), palettes, arrays
- **Use Case**: Pass identical config to all child windows in uniform mode

**Implementation Details**:
```javascript
// Conversion constants for readability
const RADIANS_TO_DEGREES = 180 / Math.PI;

// Serialize color with proper space handling
if (config.backgroundColor && config.backgroundColor.values) {
const values = config.backgroundColor.values.join(",");
params.set(
config.backgroundColor.space === "hsl" ? "backgroundHSL" : "backgroundColor",
values
);
}

// Convert angles to degrees for URL
if (key === "slant") {
params.set(key, (config[key] * RADIANS_TO_DEGREES).toString());
}
```

**Key Files**: `js/config.js` (lines 779-856)

**Why Serialize Config**:
- URL parameters are the existing pattern for config in this codebase
- Child windows naturally parse URL parameters on load
- No need for postMessage or shared storage complexity
- Config is shareable and bookmarkable

### 🎛️ UI Pattern: Mutually Exclusive Checkboxes

**Pattern**: Use checkboxes with mutual exclusion for optional feature modes
- **Better than radio buttons** for optional features where "none" is default
- **Implementation**: When checking one, programmatically uncheck others
- **State Management**: Persist via URL parameters

**Implementation Details**:
```javascript
// Multi-monitor multiple toggle
multiMonitorMultipleToggle.addEventListener("change", (e) => {
if (e.target.checked) {
// Uncheck uniform toggle
const uniformToggle = document.querySelector(".multimonitor-uniform-toggle");
uniformToggle.checked = false;
emit("multiMonitorChange", "multiple");
} else {
emit("multiMonitorChange", "none");
}
});
```

**Key Files**: `js/mode-display.js` (lines 262-281)

**Why This Pattern**:
- More intuitive than radio buttons for optional features
- Clear that user can have neither option selected
- Easy to add more modes without changing UI paradigm
- Consistent with existing checkbox patterns in the UI

### ⏱️ Reliable Async Window Initialization

**Pattern**: Use readyState checks instead of fixed timeouts for child window operations
- **Problem**: Fixed timeouts (500ms, 1000ms) are unreliable across devices
- **Solution**: Check `document.readyState` and use minimal delay only for renderer readiness

**Implementation Details**:
```javascript
// Check readyState before triggering fullscreen
const requestFullscreenWhenReady = () => {
if (newWindow.document.readyState === "complete") {
// Minimal delay for renderer (100ms vs 500ms)
setTimeout(() => {
newWindow.document.documentElement.requestFullscreen();
}, 100);
} else {
newWindow.addEventListener("load", requestFullscreenWhenReady);
}
};
```

**Key Files**: `js/fullscreen.js` (lines 227-241)

**Why This Matters**:
- 80% faster initialization (100ms vs 500ms)
- More reliable across different devices/network speeds
- Handles edge cases where load event already fired

### 🧹 Window Lifecycle Management

**Pattern**: Track spawned windows for proper cleanup
- **State**: `multiMonitorWindows` array tracks all child windows
- **Cleanup**: Close all windows on exit, handle cases where user closed manually
- **BroadcastChannel Cleanup**: Close channel when done

**Implementation Details**:
```javascript
// Track spawned windows
let multiMonitorWindows = [];

// Open and track
const newWindow = window.open(url, '_blank', features);
if (newWindow) {
multiMonitorWindows.push(newWindow);
}

// Cleanup all windows
function cleanupMultiMonitor() {
multiMonitorWindows.forEach((win) => {
if (win && !win.closed) {
try {
win.close();
} catch (err) {
console.error("Failed to close window:", err);
}
}
});
multiMonitorWindows = [];

if (multiMonitorBroadcast) {
multiMonitorBroadcast.close();
multiMonitorBroadcast = null;
}
}
```

**Key Files**: `js/fullscreen.js` (lines 117-138)

**Why Track Windows**:
- Coordinated cleanup when user exits fullscreen
- Handle edge cases (user manually closes a window)
- Prevent memory leaks and zombie processes
- Essential for good user experience

### 🌐 Browser Compatibility Patterns

**Pattern**: Use fallbacks and feature detection for cross-browser support

**Screen Position Detection**:
```javascript
// Handle both Chrome (screenLeft/Top) and standard (screen.left/top)
const currentScreenLeft = window.screenLeft ?? window.screen?.left ?? 0;
const currentScreenTop = window.screenTop ?? window.screen?.top ?? 0;
```

**Window Features**:
```javascript
// Minimal chrome for clean fullscreen experience
const features = `left=${x},top=${y},width=${w},height=${h},resizable=no,scrollbars=no,menubar=no,toolbar=no,status=no`;
```

**Key Files**: `js/fullscreen.js` (lines 216-218, 223)

### 📝 Lessons Learned

1. **BroadcastChannel is ideal for multi-window coordination** - No need for complex parent-child references
2. **Config serialization enables uniform mode** - Leverage existing URL parameter parsing
3. **Mutual exclusion checkboxes work well for optional features** - Better UX than radio buttons
4. **readyState checks beat fixed timeouts** - More reliable and faster
5. **Window Management API requires careful fallbacks** - Not all browsers support it yet
6. **Natural drift is a feature, not a bug** - No need for complex synchronization in uniform mode

### 🎯 Future Multi-Window Feature Guidelines

When adding features that span multiple windows/displays:

1. **Use BroadcastChannel for coordination** - Same pattern, different channel name
2. **Serialize state via URL parameters** - Matches existing patterns
3. **Provide graceful degradation** - Always work in single-window mode
4. **Track window lifecycle** - Clean up resources properly
5. **Use feature detection** - Check API availability before use
6. **Document browser requirements** - Clear compatibility notes

**Examples of future features that could use these patterns**:
- Multi-display gallery mode with different shaders per screen
- Synchronized effects across displays (explicit sync via BroadcastChannel)
- Performance monitoring across multiple windows
- Multi-screen debug visualizations
90 changes: 90 additions & 0 deletions js/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -782,3 +782,93 @@ export default (urlParams) => {

return config;
};

/**
* Serialize configuration to URL parameters
* Used for multi-monitor uniform mode to pass config to child windows
* @param {Object} config - Configuration object to serialize
* @returns {string} URL query string with serialized config
*/
export function serializeConfig(config) {
const params = new URLSearchParams();

// Conversion constant for angle serialization
const RADIANS_TO_DEGREES = 180 / Math.PI;

// Core parameters that should be passed to child windows
const serializableParams = [
"version",
"font",
"effect",
"numColumns",
"resolution",
"animationSpeed",
"forwardSpeed",
"cycleSpeed",
"fallSpeed",
"raindropLength",
"slant",
"bloomSize",
"bloomStrength",
"volumetric",
"fps",
"renderer",
"suppressWarnings",
"cursorIntensity",
"glyphIntensity",
"ditherMagnitude",
"glyphFlip",
"glyphRotation",
"isometric",
"loops",
];
Comment on lines 799 to 824
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializeConfig function is missing several configuration parameters that are available in the codebase and should be serialized for uniform mode to work correctly. Missing parameters include:

  • glintColor / glintHSL - Used for glyph highlights (defined in paramMapping)
  • cursorIntensity - Brightness multiplier for cursor glow (used in config)
  • glyphIntensity - Brightness multiplier for glyphs (used in config)
  • ditherMagnitude - Random per-pixel dimming (defined in defaults)
  • glyphFlip - Horizontal glyph reflection (defined in defaults)
  • glyphRotation - Angle to rotate glyphs (defined in defaults)
  • isometric - Isometric view mode (defined in paramMapping)
  • loops - Loop configuration (defined in paramMapping)

Without these parameters, child windows in uniform mode may not accurately replicate the parent window's visual configuration, leading to inconsistent appearance across displays.

Copilot uses AI. Check for mistakes.

// Add each parameter if it differs from default or is explicitly set
for (const key of serializableParams) {
if (config[key] !== undefined && config[key] !== null) {
// Special handling for different types
if (typeof config[key] === "boolean") {
params.set(key, config[key].toString());
} else if (typeof config[key] === "number") {
// For slant, convert back to degrees
if (key === "slant") {
params.set(key, (config[key] * RADIANS_TO_DEGREES).toString());
} else {
params.set(key, config[key].toString());
}
} else if (typeof config[key] === "string") {
params.set(key, config[key]);
}
}
}

// Handle color parameters (convert from internal format to URL format)
if (config.backgroundColor && config.backgroundColor.values) {
const values = config.backgroundColor.values.join(",");
params.set(config.backgroundColor.space === "hsl" ? "backgroundHSL" : "backgroundColor", values);
}

if (config.cursorColor && config.cursorColor.values) {
const values = config.cursorColor.values.join(",");
params.set(config.cursorColor.space === "hsl" ? "cursorHSL" : "cursorColor", values);
}

Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The glintColor serialization logic is missing from the config serialization function. The code handles backgroundColor, cursorColor, palette, and stripeColors, but doesn't handle glintColor which is a valid color parameter that should be serialized for uniform mode.

Add the following after line 841:

if (config.glintColor && config.glintColor.values) {
    const values = config.glintColor.values.join(",");
    params.set(config.glintColor.space === "hsl" ? "glintHSL" : "glintColor", values);
}

This follows the same pattern as the other color serializations.

Suggested change
if (config.glintColor && config.glintColor.values) {
const values = config.glintColor.values.join(",");
params.set(config.glintColor.space === "hsl" ? "glintHSL" : "glintColor", values);
}

Copilot uses AI. Check for mistakes.
if (config.glintColor && config.glintColor.values) {
const values = config.glintColor.values.join(",");
params.set(config.glintColor.space === "hsl" ? "glintHSL" : "glintColor", values);
}

if (config.palette && Array.isArray(config.palette)) {
const paletteValues = config.palette.flatMap((p) => [...p.color.values, p.at]).join(",");
const space = config.palette[0]?.color.space || "hsl";
params.set(space === "hsl" ? "paletteHSL" : "palette", paletteValues);
}

if (config.stripeColors && Array.isArray(config.stripeColors)) {
const stripeValues = config.stripeColors.flatMap((c) => c.values).join(",");
const space = config.stripeColors[0]?.space || "rgb";
params.set(space === "hsl" ? "stripeHSL" : "stripeColors", stripeValues);
}

return params.toString();
}