Skip to content
Open
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
126 changes: 100 additions & 26 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,36 @@
}
input[type=checkbox] { width: auto; }

input.collision-toggle {
appearance: none;
-webkit-appearance: none;
width: 20px;
height: 20px;
border: 1px solid var(--ink-color);
border-radius: 2px;
background: var(--bg-color);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
}

input.collision-toggle::after {
content: "✕";
font-size: 14px;
color: #000;
opacity: 0;
}

input.collision-toggle:checked::after {
opacity: 1;
}

input.collision-toggle:disabled {
opacity: 0.4;
cursor: not-allowed;
}

.btn-group { display: flex; gap: 10px; margin-top: 25px; }
.btn-full { flex: 1; padding: 12px; cursor: pointer; border: 1px solid #ccc; background: var(--bg-color); color: var(--ink-color); font-weight: 600; font-size: 0.9em; border-radius: 3px; }
.btn-danger { color: #d00; border-color: #ecc; }
Expand Down Expand Up @@ -806,10 +836,10 @@ <h1 class="title">LES CERCLES</h1>
<details class="side-panel" id="notes-panel">
<summary class="panel-header-row">
<span class="panel-title">NOTES</span>
<button id="btn-dir" class="btn-text" onclick="event.stopPropagation(); toggleDirection()">CCW</button>
<button id="btn-play" class="btn-text btn-fixed" onclick="event.stopPropagation(); togglePlay()">PLAY</button>
<button id="btn-dir" class="btn-text" onclick="event.stopPropagation(); toggleDirection()">CW</button>
<button class="btn-text" onclick="event.stopPropagation(); syncRPMs()">SYNC</button>
<button class="btn-text" onclick="event.stopPropagation(); evenSpacePlanets()">EVEN</button>
<button id="btn-play" class="btn-text btn-fixed" onclick="event.stopPropagation(); togglePlay()">PLAY</button>
</summary>
<div class="side-panel-content">
<div class="panel-slider-row">
Expand All @@ -831,7 +861,7 @@ <h1 class="title">LES CERCLES</h1>
<button id="btn-gate-play" class="btn-text btn-fixed" onclick="event.stopPropagation(); toggleGateSpin()">PLAY</button>
<button class="btn-text" onclick="event.stopPropagation(); randomizeGateTranspose()">RND TRNS</button>
<button class="btn-text" onclick="event.stopPropagation(); randomizeGateSpacing()">RND SPC</button>
<button id="btn-gate-dir" class="btn-text" onclick="event.stopPropagation(); toggleGateSpinDirection()">CW</button>
<button id="btn-gate-dir" class="btn-text" onclick="event.stopPropagation(); toggleGateSpinDirection()">CCW</button>
</summary>
<div class="side-panel-content">
<div class="panel-slider-row">
Expand All @@ -841,10 +871,6 @@ <h1 class="title">LES CERCLES</h1>
<span class="slider-pill__value" id="gate-label">1</span>
<input id="gate-range" type="range" min="1" max="32" step="1" value="1" data-value-target="gate-label" data-format="int" oninput="updateGates(this.value); updateSliderValue(this)">
</div>
<div class="control-buttons-row">
<button class="btn-text btn-small" onclick="event.stopPropagation(); adjustGateCount(-1)">-1</button>
<button class="btn-text btn-small" onclick="event.stopPropagation(); adjustGateCount(1)">+1</button>
</div>
</div>

<div class="macro-slider-container">
Expand Down Expand Up @@ -885,11 +911,13 @@ <h1 class="title">LES CERCLES</h1>
<option value="notes">NOTES</option>
<option value="gates">GATES</option>
<option value="circle">CIRCLE</option>
<option value="orb" data-requires-collision="true" disabled>ORB</option>
<option value="orb-circle" data-requires-collision="true" disabled>ORB + CIRCLE</option>
</select>
</div>
<div class="control-row">
<label>Collision</label>
<input type="checkbox" id="o-collision-inline">
<input type="checkbox" id="o-collision-inline" class="collision-toggle">
</div>
</div>
</details>
Expand Down Expand Up @@ -948,11 +976,13 @@ <h2>Orb Settings</h2>
<option value="notes">NOTES</option>
<option value="gates">GATES</option>
<option value="circle">CIRCLE</option>
<option value="orb" data-requires-collision="true" disabled>ORB</option>
<option value="orb-circle" data-requires-collision="true" disabled>ORB + CIRCLE</option>
</select>
</div>
<div class="control-row">
<label>Collision</label>
<input type="checkbox" id="o-collision">
<input type="checkbox" id="o-collision" class="collision-toggle">
</div>
<div class="btn-group">
<button class="btn-full" onclick="closePanel('orb-panel')">DONE</button>
Expand Down Expand Up @@ -1122,11 +1152,7 @@ <h2>CUSTOM SCALE</h2>
document.getElementById('trigger-panel').style.display = 'none';
document.getElementById('orb-panel').style.display = 'none';

const btn = document.getElementById('btn-dir');
const isCW = STATE.direction === 1;
btn.innerText = isCW ? "CW" : "CCW";
if (isCW) document.body.classList.add('dark-mode');
else document.body.classList.remove('dark-mode');
updateDirectionUI();

const speedRange = document.getElementById('speed-range');
if (speedRange) speedRange.value = STATE.globalSpeed;
Expand Down Expand Up @@ -1458,11 +1484,44 @@ <h2>CUSTOM SCALE</h2>
AudioEngine.setVolume(volume);
}

function getDirectionToggleLabel(direction) {
return direction === 1 ? "CCW" : "CW";
}

function updateDirectionUI() {
const btn = document.getElementById('btn-dir');
const isCW = STATE.direction === 1;
if (btn) btn.innerText = getDirectionToggleLabel(STATE.direction);
if (isCW) document.body.classList.add('dark-mode');
else document.body.classList.remove('dark-mode');
}

function updateOrbTriggerOptions() {
const triggerMode = STATE.orbSettings.triggerMode || 'notes';
const collisionEnabled = !!STATE.orbSettings.collision;
const selects = [document.getElementById('o-trigger'), document.getElementById('o-trigger-inline')];
selects.forEach((select) => {
if (!select) return;
select.querySelectorAll('option[data-requires-collision="true"]').forEach(option => {
option.disabled = !collisionEnabled;
});
});

if (!collisionEnabled && (triggerMode === 'orb' || triggerMode === 'orb-circle')) {
STATE.orbSettings.triggerMode = 'notes';
}

selects.forEach((select) => {
if (select) select.value = STATE.orbSettings.triggerMode;
});
}

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;
updateOrbTriggerOptions();
}

function formatSliderValue(format, value) {
Expand Down Expand Up @@ -1538,7 +1597,7 @@ <h2>CUSTOM SCALE</h2>
const btn = document.getElementById('btn-gate-play');
if (btn) btn.innerText = STATE.gateSpinActive ? "PAUSE" : "PLAY";
const dirBtn = document.getElementById('btn-gate-dir');
if (dirBtn) dirBtn.innerText = STATE.gateSpinDirection === 1 ? "CW" : "CCW";
if (dirBtn) dirBtn.innerText = getDirectionToggleLabel(STATE.gateSpinDirection);
const speedRange = document.getElementById('gate-speed-range');
if (speedRange) {
speedRange.value = STATE.gateSpinRPM;
Expand All @@ -1563,11 +1622,7 @@ <h2>CUSTOM SCALE</h2>
function toggleDirection() {
pushUndoState();
STATE.direction *= -1;
const btn = document.getElementById('btn-dir');
const isCW = STATE.direction === 1;
btn.innerText = isCW ? "CW" : "CCW";
if(isCW) document.body.classList.add('dark-mode');
else document.body.classList.remove('dark-mode');
updateDirectionUI();
}

function toggleGateSpin() {
Expand Down Expand Up @@ -1804,10 +1859,7 @@ <h2>CUSTOM SCALE</h2>
STATE.globalSpeed = data.globalSpeed;
STATE.gateTranspose = data.gateTranspose || 0;
STATE.direction = data.direction || -1;
const isCW = STATE.direction === 1;
document.getElementById('btn-dir').innerText = isCW ? "CW" : "CCW";
if(isCW) document.body.classList.add('dark-mode');
else document.body.classList.remove('dark-mode');
updateDirectionUI();
if (data.gateSpinRPM) STATE.gateSpinRPM = data.gateSpinRPM;
if (data.gateSpinDirection) STATE.gateSpinDirection = data.gateSpinDirection;
if (!data.gateSpinDirection) STATE.gateSpinDirection = STATE.direction * -1;
Expand Down Expand Up @@ -2146,6 +2198,7 @@ <h2>CUSTOM SCALE</h2>
STATE.orbSettings.triggerMode = e.target.value;
const modalTrigger = document.getElementById('o-trigger');
if (modalTrigger) modalTrigger.value = e.target.value;
updateOrbTriggerOptions();
};
}

Expand All @@ -2155,6 +2208,7 @@ <h2>CUSTOM SCALE</h2>
STATE.orbSettings.collision = e.target.checked;
const modalCollision = document.getElementById('o-collision');
if (modalCollision) modalCollision.checked = e.target.checked;
updateOrbTriggerOptions();
};
}

Expand Down Expand Up @@ -2463,7 +2517,7 @@ <h2>CUSTOM SCALE</h2>
vx: dx * power,
vy: dy * power,
color: getRandomPaletteColor(),
pitch: STATE.orbSettings.triggerMode === 'notes'
pitch: ['notes', 'orb', 'orb-circle'].includes(STATE.orbSettings.triggerMode)
? null
: MusicTheory.getRandomNoteInRange(STATE.root, STATE.scale, 2, 5),
cooldown: 0,
Expand Down Expand Up @@ -2575,6 +2629,13 @@ <h2>CUSTOM SCALE</h2>
orb.cooldown = 0.2;
}
}
if (STATE.orbSettings.triggerMode === 'orb-circle') {
if (!orb.pitch) assignOrbPitch(orb);
if (orb.cooldown <= 0) {
playOrbNote(orb);
orb.cooldown = 0.2;
}
}
}

if (STATE.orbSettings.triggerMode === 'gates') {
Expand Down Expand Up @@ -2613,6 +2674,9 @@ <h2>CUSTOM SCALE</h2>
});

if (STATE.orbSettings.collision) {
const orbTriggerMode = STATE.orbSettings.triggerMode;
const isOrbCollisionMode = orbTriggerMode === 'orb' || orbTriggerMode === 'orb-circle';
const shouldAssignPitchOnCollision = isOrbCollisionMode || orbTriggerMode === 'circle';
for (let i = 0; i < STATE.projectiles.length; i++) {
const orbA = STATE.projectiles[i];
for (let j = i + 1; j < STATE.projectiles.length; j++) {
Expand All @@ -2639,9 +2703,19 @@ <h2>CUSTOM SCALE</h2>
orbA.y -= ny * overlap;
orbB.x += nx * overlap;
orbB.y += ny * overlap;
if (STATE.orbSettings.triggerMode !== 'notes') {
if (shouldAssignPitchOnCollision) {
assignOrbPitch(orbA);
assignOrbPitch(orbB);
if (orbTriggerMode === 'orb') {
if (orbA.cooldown <= 0) {
playOrbNote(orbA);
orbA.cooldown = 0.2;
}
if (orbB.cooldown <= 0) {
playOrbNote(orbB);
orbB.cooldown = 0.2;
}
}
}
orbA.color = getRandomPaletteColor();
orbB.color = getRandomPaletteColor();
Expand Down