Initial commit: DJ Visualizer
Electron app with audio-reactive fractal/abstract visuals on an external display. Dual-window setup (control panel + fullscreen output), 8 WebGL scenes, audio from file/live input/system audio, image and scrolling-text overlays. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
b32ea47ae5
14 files changed
+5748
No files matched your search
@@ -0,0 +1,156 @@
|
||||
const { app, BrowserWindow, screen, ipcMain, systemPreferences } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
let controlWin = null; // panel with all the controls (primary screen)
|
||||
let outputWin = null; // pure visualization (external screen, fullscreen)
|
||||
|
||||
function externalDisplay() {
|
||||
const primary = screen.getPrimaryDisplay();
|
||||
return screen.getAllDisplays().find(d => d.id !== primary.id) || primary;
|
||||
}
|
||||
|
||||
function createWindows() {
|
||||
const primary = screen.getPrimaryDisplay();
|
||||
const ext = externalDisplay();
|
||||
const hasExternal = ext.id !== primary.id;
|
||||
|
||||
console.log(`[DJV] Schermi rilevati: ${screen.getAllDisplays().length}` +
|
||||
` — esterno ${hasExternal ? 'SÌ' : 'NO'} (output su ${hasExternal ? 'monitor esterno' : 'principale, finestra'})`);
|
||||
|
||||
// --- Control window (the panel) ---
|
||||
controlWin = new BrowserWindow({
|
||||
width: 580,
|
||||
height: 880,
|
||||
minWidth: 420,
|
||||
x: primary.bounds.x + 60,
|
||||
y: primary.bounds.y + 60,
|
||||
title: 'DJ Visualizer — Controlli',
|
||||
backgroundColor: '#0e0e16',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
additionalArguments: ['--role=control']
|
||||
}
|
||||
});
|
||||
controlWin.loadFile(path.join(__dirname, 'src', 'control.html'));
|
||||
|
||||
// --- Output window (the visuals) ---
|
||||
outputWin = new BrowserWindow({
|
||||
width: hasExternal ? ext.bounds.width : 960,
|
||||
height: hasExternal ? ext.bounds.height : 600,
|
||||
x: hasExternal ? ext.bounds.x : primary.bounds.x + 520,
|
||||
y: hasExternal ? ext.bounds.y : primary.bounds.y + 60,
|
||||
title: 'DJ Visualizer — Output',
|
||||
backgroundColor: '#000000',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
additionalArguments: ['--role=output'],
|
||||
// Let audio loaded via IPC play without a click inside the output window.
|
||||
autoplayPolicy: 'no-user-gesture-required'
|
||||
}
|
||||
});
|
||||
outputWin.loadFile(path.join(__dirname, 'src', 'output.html'));
|
||||
if (hasExternal) {
|
||||
outputWin.once('ready-to-show', () => placeOutputOnDisplay(ext, true));
|
||||
}
|
||||
|
||||
controlWin.on('closed', () => {
|
||||
controlWin = null;
|
||||
if (outputWin) outputWin.close();
|
||||
app.quit();
|
||||
});
|
||||
outputWin.on('closed', () => { outputWin = null; });
|
||||
}
|
||||
|
||||
async function ensureMicAccess() {
|
||||
if (process.platform !== 'darwin') return;
|
||||
try {
|
||||
const status = systemPreferences.getMediaAccessStatus('microphone');
|
||||
if (status !== 'granted') await systemPreferences.askForMediaAccess('microphone');
|
||||
} catch (e) { /* getUserMedia will prompt again if needed */ }
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
await ensureMicAccess();
|
||||
createWindows();
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindows();
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
|
||||
|
||||
// --- IPC routing between the two windows --------------------------------
|
||||
|
||||
// Control -> Output (commands).
|
||||
ipcMain.on('ctl', (_e, msg) => {
|
||||
if (outputWin && !outputWin.isDestroyed()) outputWin.webContents.send('ctl', msg);
|
||||
});
|
||||
|
||||
// Output -> Control (reports: meters, device list, play state).
|
||||
ipcMain.on('rpt', (_e, msg) => {
|
||||
if (controlWin && !controlWin.isDestroyed()) controlWin.webContents.send('rpt', msg);
|
||||
});
|
||||
|
||||
// --- Display management --------------------------------------------------
|
||||
|
||||
ipcMain.handle('displays:list', () => {
|
||||
const primary = screen.getPrimaryDisplay();
|
||||
return screen.getAllDisplays().map((d, i) => ({
|
||||
id: d.id,
|
||||
index: i,
|
||||
label: d.label || `Display ${i + 1}`,
|
||||
bounds: d.bounds,
|
||||
isPrimary: d.id === primary.id
|
||||
}));
|
||||
});
|
||||
|
||||
// Place the output window on a given display. On macOS we use *simple*
|
||||
// fullscreen (a borderless window covering the screen) instead of native
|
||||
// fullscreen: native fullscreen creates a Space tied to whichever display the
|
||||
// window happens to be on and is unreliable when moving between monitors.
|
||||
function placeOutputOnDisplay(target, fullscreen) {
|
||||
if (!outputWin || !target) return;
|
||||
// Leave any current fullscreen first so the window can be repositioned.
|
||||
if (process.platform === 'darwin') {
|
||||
if (outputWin.isSimpleFullScreen()) outputWin.setSimpleFullScreen(false);
|
||||
} else if (outputWin.isFullScreen()) {
|
||||
outputWin.setFullScreen(false);
|
||||
}
|
||||
|
||||
// Move onto the target display.
|
||||
outputWin.setBounds({ ...target.bounds });
|
||||
outputWin.show();
|
||||
outputWin.focus();
|
||||
|
||||
if (!fullscreen) return;
|
||||
// Cover the target display once the move has settled.
|
||||
setTimeout(() => {
|
||||
if (!outputWin) return;
|
||||
outputWin.setBounds({ ...target.bounds });
|
||||
if (process.platform === 'darwin') outputWin.setSimpleFullScreen(true);
|
||||
else outputWin.setFullScreen(true);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Move the OUTPUT window to a display and go fullscreen there.
|
||||
ipcMain.handle('output:moveTo', (_e, displayId) => {
|
||||
const target = screen.getAllDisplays().find(d => d.id === displayId);
|
||||
if (!target) return false;
|
||||
placeOutputOnDisplay(target, true);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('output:toggleFullscreen', () => {
|
||||
if (!outputWin) return false;
|
||||
if (process.platform === 'darwin') {
|
||||
const next = !outputWin.isSimpleFullScreen();
|
||||
outputWin.setSimpleFullScreen(next);
|
||||
return next;
|
||||
}
|
||||
outputWin.setFullScreen(!outputWin.isFullScreen());
|
||||
return outputWin.isFullScreen();
|
||||
});
|
||||
Reference in new issue
Block a user