import { config, saveSettings, defaultConfig, setConfig } from './config.js'; import { cols, rows, selectedType, setSelectedType, clearGrid, canvas, cellSize, brushSize, setBrushSize } from './state.js'; import { spawnCell } from './physics.js'; import { stepOnce } from './index.js'; export const elementCategories = [ { title: 'Solids', items: [ { type: 'sand', color: '#e8b656', label: 'Sand' }, { type: 'dirt', color: '#8b5a2b', label: 'Dirt' }, { type: 'rock', color: '#7a7e85', label: 'Rock' }, { type: 'wood', color: '#704020', label: 'Wood' }, { type: 'ice', color: '#bce8f5', label: 'Ice', textColor: '#000' } ] }, { title: 'Liquids', items: [ { type: 'water', color: '#2582eb', label: 'Water' }, { type: 'lava', color: '#ff5a00', label: 'Lava' }, { type: 'oil', color: '#382f23', label: 'Oil' }, { type: 'acid', color: '#68f518', label: 'Acid', textColor: '#000' } ] }, { title: 'Organics & Gas', items: [ { type: 'plant', color: '#228B22', label: 'Plant' }, { type: 'grass', color: '#38b000', label: 'Grass' }, { type: 'fire', color: '#ff6600', label: 'Fire' }, { type: 'steam', color: '#dcf3f9', label: 'Steam', textColor: '#000' }, { type: 'smoke', color: '#4a4e54', label: 'Smoke' } ] }, { title: 'Explosives & Tools', items: [ { type: 'explosive', color: '#b71c1c', label: 'TNT' }, { type: 'eraser', color: '#555560', label: 'Eraser', title: 'Or right-click to erase' }, { type: 'clear', color: '#2a2a30', label: 'Clear All' } ] } ]; let lastPos = null; export function setLastPos(pos) { lastPos = pos; } let brushSizeSlider = null; let brushSizeValLabel = null; export function updateBrushUI() { if (brushSizeSlider) brushSizeSlider.value = brushSize; if (brushSizeValLabel) brushSizeValLabel.textContent = `${brushSize}px`; } export function setupUI() { // Settings Drawer const drawer = document.getElementById('settingsContent'); const toggle = document.getElementById('toggleDrawer'); const drawerContainer = document.getElementById('settingsDrawer'); const pauseBtn = document.getElementById('togglePause'); const stepBtn = document.getElementById('btnStep'); toggle.addEventListener('click', () => { drawerContainer.classList.toggle('open'); }); function updatePauseBtn() { pauseBtn.textContent = config.time.paused ? '▶' : '⏸'; } pauseBtn.addEventListener('click', () => { config.time.paused = !config.time.paused; saveSettings(); updatePauseBtn(); }); updatePauseBtn(); if (stepBtn) { stepBtn.addEventListener('click', () => { if (config.time.paused) { stepOnce(); } else { config.time.paused = true; saveSettings(); updatePauseBtn(); stepOnce(); } }); } renderSettings(drawer); // Elements Drawer const elDrawer = document.getElementById('elementContent'); const elToggle = document.getElementById('toggleElements'); const elContainer = document.getElementById('elementDrawer'); elToggle.addEventListener('click', () => { elContainer.classList.toggle('closed'); }); renderElements(elDrawer); } function selectElement(type) { setSelectedType(type); document.querySelectorAll('.element-btn').forEach(b => { if (b.dataset.type === type) { b.classList.add('selected'); } else { b.classList.remove('selected'); } }); } function renderElements(container) { container.innerHTML = ''; // Brush Size Header Control const brushBox = document.createElement('div'); brushBox.className = 'brush-size-box'; const brushHeader = document.createElement('div'); brushHeader.className = 'brush-size-header'; brushHeader.innerHTML = `Brush Size`; brushSizeValLabel = document.createElement('span'); brushSizeValLabel.className = 'brush-val'; brushSizeValLabel.textContent = `${brushSize}px`; brushHeader.appendChild(brushSizeValLabel); brushSizeSlider = document.createElement('input'); brushSizeSlider.type = 'range'; brushSizeSlider.min = '1'; brushSizeSlider.max = '25'; brushSizeSlider.value = brushSize; brushSizeSlider.className = 'brush-slider'; brushSizeSlider.addEventListener('input', (e) => { const val = parseInt(e.target.value, 10); setBrushSize(val); config.brush.size = val; saveSettings(); brushSizeValLabel.textContent = `${val}px`; }); brushBox.appendChild(brushHeader); brushBox.appendChild(brushSizeSlider); container.appendChild(brushBox); // Categorized Buttons (clean without any hotkey numbers) elementCategories.forEach(category => { const catDiv = document.createElement('div'); catDiv.className = 'element-category'; const catTitle = document.createElement('div'); catTitle.className = 'category-title'; catTitle.textContent = category.title; catDiv.appendChild(catTitle); category.items.forEach(btn => { const button = document.createElement('button'); button.className = 'element-btn'; button.dataset.type = btn.type; if (btn.type === selectedType) button.classList.add('selected'); button.style.backgroundColor = btn.color; if (btn.textColor) button.style.color = btn.textColor; const labelSpan = document.createElement('span'); labelSpan.textContent = btn.label; button.appendChild(labelSpan); button.onclick = () => { if (btn.type === 'clear') { if (confirm('Clear the entire canvas?')) { clearGrid(); } } else { selectElement(btn.type); } }; catDiv.appendChild(button); }); container.appendChild(catDiv); }); } export function renderSettings(container) { container.innerHTML = ''; function addControl(group, key, subkey, def) { const ctrlDiv = document.createElement('div'); const labelEl = document.createElement('label'); let val = subkey ? config[group]?.[key]?.[subkey] : config[group]?.[key]; labelEl.textContent = def.label; const input = document.createElement('input'); input.type = def.type || 'range'; if (def.type === 'checkbox') { ctrlDiv.className = 'setting-checkbox'; input.checked = !!val; input.addEventListener('change', (e) => { if (subkey) config[group][key][subkey] = e.target.checked; else config[group][key] = e.target.checked; saveSettings(); }); ctrlDiv.append(input, labelEl); } else if (def.type === 'radio') { const fieldset = document.createElement('fieldset'); const legend = document.createElement('legend'); legend.textContent = def.label; fieldset.appendChild(legend); def.options.forEach(option => { const radioId = `radio-${group}-${key}-${option}`; const radioContainer = document.createElement('div'); radioContainer.style.display = 'inline-block'; radioContainer.style.marginRight = '12px'; const radioInput = document.createElement('input'); radioInput.type = 'radio'; radioInput.name = `radio-${group}-${key}`; radioInput.value = option; radioInput.id = radioId; if (val === option) radioInput.checked = true; radioInput.addEventListener('change', (e) => { config[group][key] = e.target.value; saveSettings(); }); const radioLabel = document.createElement('label'); radioLabel.textContent = option.charAt(0).toUpperCase() + option.slice(1); radioLabel.setAttribute('for', radioId); radioLabel.style.marginLeft = '4px'; radioContainer.append(radioInput, radioLabel); fieldset.appendChild(radioContainer); }); return fieldset; } else { ctrlDiv.className = 'setting-control'; input.min = def.min; input.max = def.max; input.step = def.step; input.value = val !== undefined ? val : def.min; const valueSpan = document.createElement('span'); valueSpan.className = 'control-val'; valueSpan.textContent = typeof val === 'number' ? val.toString() : (val || '0'); input.addEventListener('input', (e) => { const newVal = parseFloat(e.target.value); if (subkey) config[group][key][subkey] = newVal; else config[group][key] = newVal; saveSettings(); valueSpan.textContent = newVal.toFixed(Math.max(def.step.toString().split('.')[1]?.length || 0, 1)); }); ctrlDiv.append(labelEl, input, valueSpan); } return ctrlDiv; } // Complete settings matching all original groups plus all new elements const groups = { time: [ { key: 'speed', label: 'Speed', min: 0, max: 20, step: 1 } ], global: [ { key: 'gravity', label: 'Gravity', min: -1.0, max: 2.0, step: 0.1 }, { key: 'drag', label: 'Drag', min: 0.1, max: 1.0, step: 0.05 } ], explosive: [ { key: 'force', label: 'Blast Force', min: 0, max: 20, step: 1 }, { key: 'radius', label: 'Blast Reach', min: 1, max: 50, step: 1 }, { key: 'destroy', subkey: 'rock', type: 'checkbox', label: 'Destroy Rock' }, { key: 'destroy', subkey: 'wood', type: 'checkbox', label: 'Destroy Wood' }, { key: 'destroy', subkey: 'dirt', type: 'checkbox', label: 'Destroy Dirt' }, { key: 'destroy', subkey: 'sand', type: 'checkbox', label: 'Destroy Sand' }, { key: 'destroy', subkey: 'plant', type: 'checkbox', label: 'Destroy Plant' }, { key: 'destroy', subkey: 'grass', type: 'checkbox', label: 'Destroy Grass' }, { key: 'destroy', subkey: 'water', type: 'checkbox', label: 'Destroy Water' }, { key: 'destroy', subkey: 'acid', type: 'checkbox', label: 'Destroy Acid' }, { key: 'destroy', subkey: 'ice', type: 'checkbox', label: 'Destroy Ice' }, { key: 'destroy', subkey: 'lava', type: 'checkbox', label: 'Destroy Lava' }, { key: 'destroy', subkey: 'oil', type: 'checkbox', label: 'Destroy Oil' } ], acid: [ { key: 'dissolveChance', label: 'Dissolve Rate', min: 0, max: 1.0, step: 0.01 }, { key: 'sinkChance', label: 'Sink Rate', min: 0, max: 1.0, step: 0.05 }, { key: 'dispersion', label: 'Spread Speed', min: 1, max: 10, step: 1 }, { key: 'explodeTNT', type: 'checkbox', label: 'Explode TNT' }, { key: 'dissolve', subkey: 'rock', type: 'checkbox', label: 'Dissolve Rock' }, { key: 'dissolve', subkey: 'wood', type: 'checkbox', label: 'Dissolve Wood' }, { key: 'dissolve', subkey: 'dirt', type: 'checkbox', label: 'Dissolve Dirt' }, { key: 'dissolve', subkey: 'sand', type: 'checkbox', label: 'Dissolve Sand' }, { key: 'dissolve', subkey: 'plant', type: 'checkbox', label: 'Dissolve Plant' }, { key: 'dissolve', subkey: 'grass', type: 'checkbox', label: 'Dissolve Grass' }, { key: 'dissolve', subkey: 'water', type: 'checkbox', label: 'Infect Water' }, { key: 'dissolve', subkey: 'ice', type: 'checkbox', label: 'Dissolve Ice' }, { key: 'dissolve', subkey: 'oil', type: 'checkbox', label: 'Dissolve Oil' } ], fire: [ { key: 'burnChance', label: 'Fire Decay Rate', min: 0, max: 1.0, step: 0.01 } ], water: [ { key: 'flowChance', label: 'Flow Rate', min: 0, max: 1.0, step: 0.05 }, { key: 'dispersion', label: 'Spread Speed', min: 1, max: 10, step: 1 } ], lava: [ { key: 'flowChance', label: 'Flow Rate', min: 0, max: 1.0, step: 0.05 }, { key: 'dispersion', label: 'Spread Speed', min: 1, max: 6, step: 1 }, { key: 'coolingChance', label: 'Cooling Rate', min: 0, max: 1.0, step: 0.01 }, { key: 'sinkChance', label: 'Sink in Water', min: 0, max: 1.0, step: 0.05 } ], oil: [ { key: 'flowChance', label: 'Flow Rate', min: 0, max: 1.0, step: 0.05 }, { key: 'dispersion', label: 'Spread Speed', min: 1, max: 8, step: 1 }, { key: 'burnChance', label: 'Burn Rate', min: 0, max: 1.0, step: 0.01 }, { key: 'sinkChance', label: 'Water Sink Rate', min: 0, max: 1.0, step: 0.05 }, { key: 'floatChance', label: 'Oil Float Rate', min: 0, max: 1.0, step: 0.05 } ], ice: [ { key: 'meltChance', label: 'Melt Rate', min: 0, max: 1.0, step: 0.01 }, { key: 'freezeChance', label: 'Freeze Rate', min: 0, max: 1.0, step: 0.01 } ], steam: [ { key: 'condenseChance', label: 'Condense Rate', min: 0, max: 1.0, step: 0.01 } ], smoke: [ { key: 'dissipateChance', label: 'Dissipate Rate', min: 0, max: 1.0, step: 0.01 } ], plant: [ { key: 'growChance', label: 'Growth Rate', min: 0, max: 1.0, step: 0.01 }, { key: 'woodificationChance', label: 'Wood Rate', min: 0, max: 1.0, step: 0.01 }, { key: 'burnChance', label: 'Burn Rate', min: 0, max: 1.0, step: 0.05 } ], grass: [ { key: 'growChance', label: 'Growth Rate', min: 0, max: 1.0, step: 0.01 }, { key: 'burnChance', label: 'Burn Rate', min: 0, max: 1.0, step: 0.05 } ], wood: [ { key: 'burnChance', label: 'Burn Rate', min: 0, max: 1.0, step: 0.01 } ], visual: [ { key: 'style', type: 'radio', label: 'Style', options: ['blocky', 'bloby', 'fuzzy'] }, { key: 'colorNoise', type: 'checkbox', label: 'Multi-Tone Shading' } ] }; for (const [groupName, settings] of Object.entries(groups)) { const groupDiv = document.createElement('div'); groupDiv.className = 'setting-group'; const title = document.createElement('h3'); title.textContent = groupName.charAt(0).toUpperCase() + groupName.slice(1); groupDiv.appendChild(title); settings.forEach(s => { groupDiv.appendChild(addControl(groupName, s.key, s.subkey, s)); }); container.appendChild(groupDiv); } // Reset Defaults const resetBtn = document.createElement('button'); resetBtn.className = 'reset-defaults-btn'; resetBtn.textContent = '↺ Reset to Defaults'; resetBtn.onclick = () => { setConfig(JSON.parse(JSON.stringify(defaultConfig))); setBrushSize(defaultConfig.brush.size); saveSettings(); updateBrushUI(); renderSettings(container); }; container.appendChild(resetBtn); } // Bresenham's line algorithm for continuous stroke drawing without gaps export function handleInput(e, overrideType = null) { const rect = canvas.getBoundingClientRect(); const typeToSpawn = overrideType || selectedType; function getGridCoords(clientX, clientY) { const x = clientX - rect.left; const y = clientY - rect.top; return { col: Math.floor(x / cellSize), row: Math.floor(y / cellSize) }; } function stampCircle(centerCol, centerRow) { const r = brushSize; const rSq = r * r; for (let di = -r; di <= r; di++) { for (let dj = -r; dj <= r; dj++) { if (di * di + dj * dj <= rSq) { const c = centerCol + di; const ro = centerRow + dj; if (c >= 0 && c < cols && ro >= 0 && ro < rows) { if (r <= 2 || Math.random() > 0.08) { spawnCell(c, ro, typeToSpawn); } } } } } } function drawLine(p0, p1) { let x0 = p0.col, y0 = p0.row; let x1 = p1.col, y1 = p1.row; const dx = Math.abs(x1 - x0); const dy = Math.abs(y1 - y0); const sx = x0 < x1 ? 1 : -1; const sy = y0 < y1 ? 1 : -1; let err = dx - dy; while (true) { stampCircle(x0, y0); if (x0 === x1 && y0 === y1) break; const e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } } if (e.type.startsWith('mouse')) { const current = getGridCoords(e.clientX, e.clientY); if (lastPos && (lastPos.col !== current.col || lastPos.row !== current.row)) { drawLine(lastPos, current); } else { stampCircle(current.col, current.row); } lastPos = current; } else if (e.type.startsWith('touch')) { for (let i = 0; i < e.touches.length; i++) { const t = e.touches[i]; const current = getGridCoords(t.clientX, t.clientY); if (lastPos) { drawLine(lastPos, current); } else { stampCircle(current.col, current.row); } lastPos = current; } } }