@@ -549,8 +802,11 @@
LES CERCLES
Trigger Settings
@@ -612,6 +868,13 @@
Settings
+
+
+
+
+
+
+
@@ -683,30 +946,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 +984,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 +1008,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 updateUndoButton() {
+function redoLastMove() {
+ if (redoStack.length === 0) return;
+ undoStack.push(createUndoSnapshot());
+ const snapshot = redoStack.pop();
+ applyUndoSnapshot(snapshot);
+ updateUndoRedoButtons();
+}
+
+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 +1273,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;
@@ -994,13 +1281,27 @@ CUSTOM SCALE
canvas.width = canvasW;
canvas.height = canvasH;
centerX = canvasW/2;
- centerY = canvasH/2;
+ centerY = canvasW < 768 ? canvasH * 0.32 : canvasH / 2;
const dim = Math.min(canvasW, canvasH);
- STATE.orbitRadius = dim * (dim < 600 ? 0.35 : 0.35);
+ STATE.orbitRadius = dim * (dim < 600 ? 0.3 : 0.35);
+ updateSidePanelsForViewport();
}
window.addEventListener('resize', resize);
resize();
+function updateSidePanelsForViewport() {
+ const isMobile = window.innerWidth <= 768;
+ if (isMobileLayout === isMobile) return;
+ isMobileLayout = isMobile;
+ const notesPanel = document.getElementById('notes-panel');
+ const gatesPanel = document.getElementById('gates-panel');
+ const orbsPanel = document.getElementById('orbs-panel');
+ if (!notesPanel || !gatesPanel || !orbsPanel) return;
+ notesPanel.open = !isMobile;
+ gatesPanel.open = !isMobile;
+ orbsPanel.open = !isMobile;
+}
+
function toggleAudio() { AudioEngine.toggle(); }
function updateMasterVolume(value) {
@@ -1009,6 +1310,76 @@ CUSTOM SCALE
AudioEngine.setVolume(volume);
}
+function syncOrbInlineControls() {
+ const triggerInline = document.getElementById('o-trigger-inline');
+ const collisionInline = document.getElementById('o-collision-inline');
+ if (triggerInline) triggerInline.value = STATE.orbSettings.triggerMode || 'notes';
+ if (collisionInline) collisionInline.checked = !!STATE.orbSettings.collision;
+}
+
+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 +1392,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 +1423,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 +1448,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 +1479,30 @@ 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 (window.innerWidth <= 768) {
+ panel.style.top = '50%';
+ panel.style.left = '50%';
+ panel.style.transform = 'translate(-50%, -50%)';
+ } else {
+ panel.style.transform = 'none';
+ }
+ 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;
+ if (window.innerWidth > 768) {
+ 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 +1569,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)) {
@@ -1196,6 +1611,10 @@ CUSTOM SCALE
openOrbPanel();
}
+function toggleSlingshotArm() {
+ STATE.slingshot.active = !STATE.slingshot.active;
+}
+
function savePattern() {
const data = {
planets: STATE.planets,
@@ -1250,14 +1669,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 +1696,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 +1734,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 +1814,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 +1835,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';
}
@@ -1410,6 +1855,7 @@ CUSTOM SCALE
triggerSel.value = target.triggerMode;
collisionToggle.checked = !!target.collision;
+ syncOrbInlineControls();
if (STATE.orbPanelPrevSlingshot === null) {
STATE.orbPanelPrevSlingshot = STATE.slingshot.active;
@@ -1427,6 +1873,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 +1921,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)
@@ -1522,12 +1971,32 @@ CUSTOM SCALE
document.getElementById('o-trigger').onchange = (e) => {
STATE.orbSettings.triggerMode = e.target.value;
+ syncOrbInlineControls();
};
document.getElementById('o-collision').onchange = (e) => {
STATE.orbSettings.collision = e.target.checked;
+ syncOrbInlineControls();
};
+const orbTriggerInline = document.getElementById('o-trigger-inline');
+if (orbTriggerInline) {
+ orbTriggerInline.onchange = (e) => {
+ STATE.orbSettings.triggerMode = e.target.value;
+ const modalTrigger = document.getElementById('o-trigger');
+ if (modalTrigger) modalTrigger.value = e.target.value;
+ };
+}
+
+const orbCollisionInline = document.getElementById('o-collision-inline');
+if (orbCollisionInline) {
+ orbCollisionInline.onchange = (e) => {
+ STATE.orbSettings.collision = e.target.checked;
+ const modalCollision = document.getElementById('o-collision');
+ if (modalCollision) modalCollision.checked = e.target.checked;
+ };
+}
+
function updatePlanetRPM(inputVal) {
const p = STATE.planets.find(x => x.id === STATE.editingId);
if (p && !isNaN(inputVal) && inputVal > 0) {
@@ -1539,7 +2008,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 +2019,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 +2294,7 @@ CUSTOM SCALE
// Spawn New Orb (Limit 32)
if (STATE.projectiles.length < 32) {
+ pushUndoState();
STATE.projectiles.push({
id: Date.now() + Math.random(),
x: centerX,
@@ -2180,22 +2658,28 @@ CUSTOM SCALE
});
}
-document.getElementById('start-overlay').addEventListener('click', () => {
+function initializeApp() {
AudioEngine.init();
MidiEngine.init();
updatePlanetList();
+ syncOrbInlineControls();
+ refreshSliderDisplays();
STATE.isRunning = true;
lastTime = 0;
document.getElementById('start-overlay').style.display = 'none';
updatePlayButton();
updateGateSpinUI();
requestAnimationFrame(loop);
+}
+
+document.getElementById('start-overlay').addEventListener('click', () => {
+ initializeApp();
});
// Touch to start overlay
document.getElementById('start-overlay').addEventListener('touchstart', (e) => {
e.preventDefault();
- document.getElementById('start-overlay').click();
+ initializeApp();
}, {passive: false});