@@ -549,8 +751,11 @@
LES CERCLES
Trigger Settings
@@ -612,6 +817,13 @@
Settings
+
+
+
+
+
+
+
@@ -683,30 +895,37 @@
CUSTOM SCALE
const UNDO_LIMIT = 20;
const undoStack = [];
+const redoStack = [];
function createUndoSnapshot() {
return {
planets: STATE.planets.map(p => ({ ...p })),
triggers: STATE.triggers.map(t => ({ ...t })),
+ projectiles: STATE.projectiles.map(o => ({ ...o })),
root: STATE.root,
scale: STATE.scale,
customScale: [...STATE.customScale],
globalSpeed: STATE.globalSpeed,
gateTranspose: STATE.gateTranspose,
direction: STATE.direction,
- isPaused: STATE.isPaused
+ isPaused: STATE.isPaused,
+ gateSpinActive: STATE.gateSpinActive,
+ gateSpinRPM: STATE.gateSpinRPM,
+ gateSpinDirection: STATE.gateSpinDirection
};
}
function pushUndoState() {
undoStack.push(createUndoSnapshot());
if (undoStack.length > UNDO_LIMIT) undoStack.shift();
- updateUndoButton();
+ redoStack.length = 0;
+ updateUndoRedoButtons();
}
function applyUndoSnapshot(snapshot) {
STATE.planets = snapshot.planets.map(p => ({ ...p }));
STATE.triggers = snapshot.triggers.map(t => ({ ...t }));
+ STATE.projectiles = (snapshot.projectiles || []).map(o => ({ ...o }));
STATE.root = snapshot.root;
STATE.scale = snapshot.scale;
STATE.customScale = [...snapshot.customScale];
@@ -714,6 +933,9 @@ CUSTOM SCALE
STATE.gateTranspose = snapshot.gateTranspose ?? 0;
STATE.direction = snapshot.direction;
STATE.isPaused = snapshot.isPaused;
+ STATE.gateSpinActive = snapshot.gateSpinActive ?? false;
+ STATE.gateSpinRPM = snapshot.gateSpinRPM ?? 10;
+ STATE.gateSpinDirection = snapshot.gateSpinDirection ?? (STATE.direction * -1);
STATE.orbPanelPrevSlingshot = null;
STATE.editingId = null;
@@ -735,23 +957,36 @@ CUSTOM SCALE
const transposeRange = document.getElementById('transpose-range');
if (transposeRange) transposeRange.value = STATE.gateTranspose;
const transposeLabel = document.getElementById('transpose-label');
- if (transposeLabel) transposeLabel.innerText = STATE.gateTranspose;
+ if (transposeLabel) transposeLabel.innerText = formatSliderValue('signed', STATE.gateTranspose);
syncGateUI();
+ updateGateSpinUI();
updatePlayButton();
updatePlanetList();
+ refreshSliderDisplays();
}
function undoLastMove() {
if (undoStack.length === 0) return;
+ redoStack.push(createUndoSnapshot());
const snapshot = undoStack.pop();
applyUndoSnapshot(snapshot);
- updateUndoButton();
+ updateUndoRedoButtons();
+}
+
+function redoLastMove() {
+ if (redoStack.length === 0) return;
+ undoStack.push(createUndoSnapshot());
+ const snapshot = redoStack.pop();
+ applyUndoSnapshot(snapshot);
+ updateUndoRedoButtons();
}
-function updateUndoButton() {
+function updateUndoRedoButtons() {
const btn = document.getElementById('btn-undo');
if (!btn) return;
btn.disabled = undoStack.length === 0;
+ const redoBtn = document.getElementById('btn-redo');
+ if (redoBtn) redoBtn.disabled = redoStack.length === 0;
}
/**
@@ -987,6 +1222,7 @@ CUSTOM SCALE
const canvas = document.getElementById('mainCanvas');
const ctx = canvas.getContext('2d');
let canvasW, canvasH, centerX, centerY;
+let isMobileLayout = null;
function resize() {
canvasW = window.innerWidth;
@@ -996,11 +1232,43 @@ CUSTOM SCALE
centerX = canvasW/2;
centerY = canvasH/2;
const dim = Math.min(canvasW, canvasH);
- STATE.orbitRadius = dim * (dim < 600 ? 0.35 : 0.35);
+ STATE.orbitRadius = dim * (dim < 600 ? 0.25 : 0.35);
+ updateSidePanelsForViewport();
}
window.addEventListener('resize', resize);
resize();
+function updateSidePanelsForViewport() {
+ const isMobile = window.innerWidth <= 768;
+ const layoutChanged = isMobileLayout !== isMobile;
+ isMobileLayout = isMobile;
+ const notesPanel = document.getElementById('notes-panel');
+ const gatesPanel = document.getElementById('gates-panel');
+ const sideControls = document.getElementById('side-controls');
+ const planetList = document.getElementById('planet-list-wrap');
+ if (!notesPanel || !gatesPanel) return;
+ if (layoutChanged) {
+ notesPanel.open = !isMobile;
+ gatesPanel.open = !isMobile;
+ }
+
+ if (sideControls && planetList) {
+ const baseTop = centerY + STATE.orbitRadius;
+ const panelOffset = isMobile ? 80 : 90;
+ planetList.style.top = `${baseTop + 10}px`;
+ planetList.style.left = '50%';
+ planetList.style.transform = 'translateX(-50%)';
+ planetList.style.bottom = 'auto';
+
+ sideControls.style.top = `${baseTop + panelOffset}px`;
+ sideControls.style.left = '50%';
+ sideControls.style.right = 'auto';
+ sideControls.style.bottom = 'auto';
+ sideControls.style.transform = 'translateX(-50%)';
+ sideControls.style.alignItems = 'stretch';
+ }
+}
+
function toggleAudio() { AudioEngine.toggle(); }
function updateMasterVolume(value) {
@@ -1009,6 +1277,69 @@ CUSTOM SCALE
AudioEngine.setVolume(volume);
}
+function formatSliderValue(format, value) {
+ const num = parseFloat(value);
+ if (Number.isNaN(num)) return value;
+ switch (format) {
+ case 'percent':
+ return `${Math.round(num * 100)}%`;
+ case 'percent-100':
+ return `${Math.round(num * 100)}%`;
+ case 'speed':
+ return `${num.toFixed(1)}x`;
+ case 'rpm':
+ return `${Math.round(num)} RPM`;
+ case 'rpm-fine':
+ return num.toFixed(2);
+ case 'signed': {
+ const intVal = Math.round(num);
+ return intVal > 0 ? `+${intVal}` : `${intVal}`;
+ }
+ case 'int':
+ return `${Math.round(num)}`;
+ default:
+ return `${num}`;
+ }
+}
+
+function updateSliderValue(input) {
+ if (!input) return;
+ const targetId = input.dataset.valueTarget;
+ if (!targetId) return;
+ const target = document.getElementById(targetId);
+ if (!target) return;
+ const format = input.dataset.format || 'raw';
+ target.textContent = formatSliderValue(format, input.value);
+ updateSliderFill(input);
+}
+
+function updateSliderFill(input) {
+ const container = input.closest('.slider-pill');
+ if (!container) return;
+ const min = parseFloat(input.min || 0);
+ const max = parseFloat(input.max || 100);
+ const val = parseFloat(input.value || 0);
+ const percent = max > min ? ((val - min) / (max - min)) * 100 : 0;
+ const clamped = Math.max(0, Math.min(100, percent));
+ container.style.setProperty('--slider-fill', `${clamped}%`);
+}
+
+function refreshSliderDisplays() {
+ const ids = [
+ 'volume-range',
+ 'speed-range',
+ 'gate-range',
+ 'transpose-range',
+ 'gate-speed-range',
+ 'p-rpm-range',
+ 'p-vol'
+ ];
+ ids.forEach(id => {
+ const input = document.getElementById(id);
+ if (input) updateSliderValue(input);
+ });
+}
+
function updatePlayButton() {
const btn = document.getElementById('btn-play');
if (!btn) return;
@@ -1021,7 +1352,19 @@ CUSTOM SCALE
const dirBtn = document.getElementById('btn-gate-dir');
if (dirBtn) dirBtn.innerText = STATE.gateSpinDirection === 1 ? "CW" : "CCW";
const speedRange = document.getElementById('gate-speed-range');
- if (speedRange) speedRange.value = STATE.gateSpinRPM;
+ if (speedRange) {
+ speedRange.value = STATE.gateSpinRPM;
+ updateSliderValue(speedRange);
+ }
+}
+
+const gateSpeedRange = document.getElementById('gate-speed-range');
+if (gateSpeedRange) {
+ const stopGateSpeedAdjust = () => { gateSpeedIsAdjusting = false; };
+ gateSpeedRange.addEventListener('change', stopGateSpeedAdjust);
+ gateSpeedRange.addEventListener('pointerup', stopGateSpeedAdjust);
+ gateSpeedRange.addEventListener('touchend', stopGateSpeedAdjust, { passive: true });
+ gateSpeedRange.addEventListener('blur', stopGateSpeedAdjust);
}
function togglePlay() {
@@ -1040,16 +1383,23 @@ CUSTOM SCALE
}
function toggleGateSpin() {
+ pushUndoState();
STATE.gateSpinActive = !STATE.gateSpinActive;
updateGateSpinUI();
}
function toggleGateSpinDirection() {
+ pushUndoState();
STATE.gateSpinDirection *= -1;
updateGateSpinUI();
}
+let gateSpeedIsAdjusting = false;
function updateGateSpinSpeed(val) {
+ if (!gateSpeedIsAdjusting) {
+ pushUndoState();
+ gateSpeedIsAdjusting = true;
+ }
STATE.gateSpinRPM = Math.min(60, Math.max(1, parseFloat(val)));
updateGateSpinUI();
}
@@ -1058,7 +1408,10 @@ CUSTOM SCALE
const gateCount = STATE.triggers.length;
document.getElementById('gate-label').innerText = gateCount;
const gateRange = document.getElementById('gate-range');
- if (gateRange) gateRange.value = gateCount;
+ if (gateRange) {
+ gateRange.value = gateCount;
+ updateSliderValue(gateRange);
+ }
}
function updatePlanetList() {
@@ -1086,7 +1439,21 @@ CUSTOM SCALE
function openGlobal() {
document.getElementById('g-root').value = STATE.root;
document.getElementById('g-scale').value = STATE.scale;
- document.getElementById('global-panel').style.display = 'block';
+ const panel = document.getElementById('global-panel');
+ const settingsButton = document.getElementById('settings-button');
+ const container = document.getElementById('game-container');
+ panel.style.display = 'block';
+ if (panel && settingsButton && container) {
+ const btnRect = settingsButton.getBoundingClientRect();
+ const containerRect = container.getBoundingClientRect();
+ const desiredTop = btnRect.bottom - containerRect.top + 8;
+ const desiredLeft = btnRect.left - containerRect.left;
+ const maxLeft = containerRect.width - panel.offsetWidth - 10;
+ panel.style.top = `${Math.max(10, desiredTop)}px`;
+ panel.style.left = `${Math.max(10, Math.min(desiredLeft, maxLeft))}px`;
+ }
+ document.getElementById('modal-overlay').classList.add('active');
+ refreshSliderDisplays();
}
function applyGlobal() {
@@ -1153,7 +1520,6 @@ CUSTOM SCALE
notes.forEach((n, i) => {
const div = document.createElement('div');
div.className = 'key-circle';
- if (n.includes('#')) div.classList.add('black-key');
div.innerText = n;
if (STATE.customScale.includes(i)) {
@@ -1250,14 +1616,16 @@ CUSTOM SCALE
const transposeRange = document.getElementById('transpose-range');
if (transposeRange) transposeRange.value = STATE.gateTranspose;
const transposeLabel = document.getElementById('transpose-label');
- if (transposeLabel) transposeLabel.innerText = STATE.gateTranspose;
+ if (transposeLabel) transposeLabel.innerText = formatSliderValue('signed', STATE.gateTranspose);
syncGateUI();
updateGateSpinUI();
+ refreshSliderDisplays();
updatePlanetList();
closePanel('global-panel');
undoStack.length = 0;
- updateUndoButton();
+ redoStack.length = 0;
+ updateUndoRedoButtons();
alert("Pattern Loaded!");
} catch(err) {
console.error(err);
@@ -1275,7 +1643,9 @@ CUSTOM SCALE
if (p) {
const effectiveRPM = p.rpm * STATE.globalSpeed;
document.getElementById('p-rpm').value = effectiveRPM.toFixed(2);
- document.getElementById('p-rpm-range').value = effectiveRPM.toFixed(2);
+ const rpmRange = document.getElementById('p-rpm-range');
+ rpmRange.value = effectiveRPM.toFixed(2);
+ updateSliderValue(rpmRange);
}
}
}
@@ -1311,20 +1681,36 @@ CUSTOM SCALE
pushUndoState();
STATE.gateTranspose = clampShift(parseInt(val, 10) || 0);
const transposeRange = document.getElementById('transpose-range');
- if (transposeRange) transposeRange.value = STATE.gateTranspose;
+ if (transposeRange) {
+ transposeRange.value = STATE.gateTranspose;
+ updateSliderValue(transposeRange);
+ }
const transposeLabel = document.getElementById('transpose-label');
- if (transposeLabel) transposeLabel.innerText = STATE.gateTranspose;
+ if (transposeLabel) transposeLabel.innerText = formatSliderValue('signed', STATE.gateTranspose);
if (STATE.editingTriggerId) {
const t = STATE.triggers.find(x => x.id === STATE.editingTriggerId);
if (t) {
const shiftRange = document.getElementById('t-shift-range');
const shiftLabel = document.getElementById('t-shift-label');
- if (shiftRange) shiftRange.value = clampShift(t.shift);
+ if (shiftRange) {
+ shiftRange.value = clampShift(t.shift);
+ updateSliderValue(shiftRange);
+ }
if (shiftLabel) shiftLabel.innerText = getEffectiveTriggerShift(t.shift);
}
}
}
+function adjustGlobalTranspose(delta) {
+ const next = clampShift((STATE.gateTranspose || 0) + delta);
+ updateGlobalTranspose(next);
+ const transposeRange = document.getElementById('transpose-range');
+ if (transposeRange) {
+ transposeRange.value = next;
+ updateSliderValue(transposeRange);
+ }
+}
+
function addPlanet() {
if(STATE.planets.length >= 32) return;
pushUndoState();
@@ -1375,8 +1761,12 @@ CUSTOM SCALE
const effectiveRPM = p.rpm * STATE.globalSpeed;
document.getElementById('p-rpm').value = effectiveRPM.toFixed(2);
- document.getElementById('p-rpm-range').value = effectiveRPM.toFixed(2);
- document.getElementById('p-vol').value = p.velocity;
+ const rpmRange = document.getElementById('p-rpm-range');
+ rpmRange.value = effectiveRPM.toFixed(2);
+ updateSliderValue(rpmRange);
+ const volumeRange = document.getElementById('p-vol');
+ volumeRange.value = p.velocity;
+ updateSliderValue(volumeRange);
panel.style.display = 'block';
}
@@ -1392,7 +1782,9 @@ CUSTOM SCALE
closePanel('orb-panel');
closePanel('planet-panel');
STATE.editingTriggerId = t.id;
- document.getElementById('t-shift-range').value = clampShift(t.shift);
+ const shiftRange = document.getElementById('t-shift-range');
+ shiftRange.value = clampShift(t.shift);
+ updateSliderValue(shiftRange);
document.getElementById('t-shift-label').innerText = getEffectiveTriggerShift(t.shift);
document.getElementById('trigger-panel').style.display = 'block';
}
@@ -1427,6 +1819,7 @@ CUSTOM SCALE
document.getElementById('t-shift-range').oninput = (e) => {
const val = clampShift(parseInt(e.target.value, 10) || 0);
+ updateSliderValue(e.target);
const t = STATE.triggers.find(x => x.id === STATE.editingTriggerId);
if (t) {
pushUndoState();
@@ -1474,6 +1867,8 @@ CUSTOM SCALE
const count = parseInt(val);
pushUndoState();
document.getElementById('gate-label').innerText = count;
+ const gateRange = document.getElementById('gate-range');
+ if (gateRange) updateSliderValue(gateRange);
const previousShifts = STATE.triggers
.slice()
.sort((a, b) => a.angle - b.angle)
@@ -1539,7 +1934,10 @@ CUSTOM SCALE
const inputVal = parseFloat(e.target.value);
updatePlanetRPM(inputVal);
const range = document.getElementById('p-rpm-range');
- if (range && !isNaN(inputVal)) range.value = inputVal;
+ if (range && !isNaN(inputVal)) {
+ range.value = inputVal;
+ updateSliderValue(range);
+ }
};
document.getElementById('p-rpm-range').oninput = (e) => {
@@ -1547,15 +1945,20 @@ CUSTOM SCALE
updatePlanetRPM(inputVal);
const numberInput = document.getElementById('p-rpm');
if (numberInput && !isNaN(inputVal)) numberInput.value = inputVal.toFixed(2);
+ updateSliderValue(e.target);
};
document.getElementById('p-vol').oninput = (e) => {
const p = STATE.planets.find(x => x.id === STATE.editingId);
if(p) p.velocity = parseFloat(e.target.value);
+ updateSliderValue(e.target);
};
function closePanel(id) {
document.getElementById(id).style.display = 'none';
+ if (id === 'global-panel') {
+ document.getElementById('modal-overlay').classList.remove('active');
+ }
if(id === 'planet-panel') STATE.editingId = null;
if(id === 'trigger-panel') STATE.editingTriggerId = null;
if(id === 'orb-panel') {
@@ -1817,6 +2220,7 @@ CUSTOM SCALE
// Spawn New Orb (Limit 32)
if (STATE.projectiles.length < 32) {
+ pushUndoState();
STATE.projectiles.push({
id: Date.now() + Math.random(),
x: centerX,
@@ -2184,6 +2588,7 @@ CUSTOM SCALE
AudioEngine.init();
MidiEngine.init();
updatePlanetList();
+ refreshSliderDisplays();
STATE.isRunning = true;
lastTime = 0;
document.getElementById('start-overlay').style.display = 'none';