2026-10-22 14:05:51 +00:00
|
|
|
import { config, saveSettings, defaultConfig, setConfig } from './config.js';
|
2026-10-22 14:45:19 +00:00
|
|
|
import { cols, rows, selectedType, setSelectedType, clearGrid, canvas, cellSize, brushSize, setBrushSize } from './state.js';
|
2026-10-22 14:05:51 +00:00
|
|
|
import { spawnCell } from './physics.js';
|
2026-10-22 14:45:19 +00:00
|
|
|
import { stepOnce } from './index.js';
|
2026-10-22 14:05:51 +00:00
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
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' }
|
|
|
|
|
]
|
|
|
|
|
}
|
2026-10-22 14:05:51 +00:00
|
|
|
];
|
|
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
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`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 14:05:51 +00:00
|
|
|
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');
|
2026-10-22 14:45:19 +00:00
|
|
|
const stepBtn = document.getElementById('btnStep');
|
2026-10-22 14:05:51 +00:00
|
|
|
|
|
|
|
|
toggle.addEventListener('click', () => {
|
|
|
|
|
drawerContainer.classList.toggle('open');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function updatePauseBtn() {
|
2026-10-22 14:45:19 +00:00
|
|
|
pauseBtn.textContent = config.time.paused ? '▶' : '⏸';
|
2026-10-22 14:05:51 +00:00
|
|
|
}
|
2026-10-22 14:45:19 +00:00
|
|
|
|
2026-10-22 14:05:51 +00:00
|
|
|
pauseBtn.addEventListener('click', () => {
|
|
|
|
|
config.time.paused = !config.time.paused;
|
|
|
|
|
saveSettings();
|
|
|
|
|
updatePauseBtn();
|
|
|
|
|
});
|
|
|
|
|
updatePauseBtn();
|
|
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
if (stepBtn) {
|
|
|
|
|
stepBtn.addEventListener('click', () => {
|
|
|
|
|
if (config.time.paused) {
|
|
|
|
|
stepOnce();
|
|
|
|
|
} else {
|
|
|
|
|
config.time.paused = true;
|
|
|
|
|
saveSettings();
|
|
|
|
|
updatePauseBtn();
|
|
|
|
|
stepOnce();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 14:05:51 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 14:05:51 +00:00
|
|
|
function renderElements(container) {
|
|
|
|
|
container.innerHTML = '';
|
2026-10-22 14:45:19 +00:00
|
|
|
|
|
|
|
|
// 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 = `<span>Brush Size</span>`;
|
|
|
|
|
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);
|
2026-10-22 14:05:51 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function renderSettings(container) {
|
2026-10-22 14:45:19 +00:00
|
|
|
container.innerHTML = '';
|
2026-10-22 14:05:51 +00:00
|
|
|
|
|
|
|
|
function addControl(group, key, subkey, def) {
|
2026-10-22 14:45:19 +00:00
|
|
|
const ctrlDiv = document.createElement('div');
|
2026-10-22 14:05:51 +00:00
|
|
|
|
|
|
|
|
const labelEl = document.createElement('label');
|
2026-10-22 14:45:19 +00:00
|
|
|
let val = subkey ? config[group]?.[key]?.[subkey] : config[group]?.[key];
|
2026-10-22 14:05:51 +00:00
|
|
|
labelEl.textContent = def.label;
|
2026-10-22 14:45:19 +00:00
|
|
|
|
2026-10-22 14:05:51 +00:00
|
|
|
const input = document.createElement('input');
|
|
|
|
|
input.type = def.type || 'range';
|
2026-10-22 14:45:19 +00:00
|
|
|
|
2026-10-22 14:05:51 +00:00
|
|
|
if (def.type === 'checkbox') {
|
2026-10-22 14:45:19 +00:00
|
|
|
ctrlDiv.className = 'setting-checkbox';
|
|
|
|
|
input.checked = !!val;
|
2026-10-22 14:05:51 +00:00
|
|
|
input.addEventListener('change', (e) => {
|
|
|
|
|
if (subkey) config[group][key][subkey] = e.target.checked;
|
|
|
|
|
else config[group][key] = e.target.checked;
|
|
|
|
|
saveSettings();
|
|
|
|
|
});
|
2026-10-22 14:45:19 +00:00
|
|
|
ctrlDiv.append(input, labelEl);
|
2026-10-22 14:05:51 +00:00
|
|
|
} else if (def.type === 'radio') {
|
|
|
|
|
const fieldset = document.createElement('fieldset');
|
|
|
|
|
const legend = document.createElement('legend');
|
|
|
|
|
legend.textContent = def.label;
|
|
|
|
|
fieldset.appendChild(legend);
|
2026-10-22 14:45:19 +00:00
|
|
|
|
2026-10-22 14:05:51 +00:00
|
|
|
def.options.forEach(option => {
|
2026-10-22 14:45:19 +00:00
|
|
|
const radioId = `radio-${group}-${key}-${option}`;
|
2026-10-22 14:05:51 +00:00
|
|
|
const radioContainer = document.createElement('div');
|
|
|
|
|
radioContainer.style.display = 'inline-block';
|
2026-10-22 14:45:19 +00:00
|
|
|
radioContainer.style.marginRight = '12px';
|
2026-10-22 14:05:51 +00:00
|
|
|
|
|
|
|
|
const radioInput = document.createElement('input');
|
|
|
|
|
radioInput.type = 'radio';
|
2026-10-22 14:45:19 +00:00
|
|
|
radioInput.name = `radio-${group}-${key}`;
|
2026-10-22 14:05:51 +00:00
|
|
|
radioInput.value = option;
|
|
|
|
|
radioInput.id = radioId;
|
|
|
|
|
if (val === option) radioInput.checked = true;
|
2026-10-22 14:45:19 +00:00
|
|
|
|
2026-10-22 14:05:51 +00:00
|
|
|
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);
|
2026-10-22 14:45:19 +00:00
|
|
|
radioLabel.style.marginLeft = '4px';
|
2026-10-22 14:05:51 +00:00
|
|
|
|
|
|
|
|
radioContainer.append(radioInput, radioLabel);
|
|
|
|
|
fieldset.appendChild(radioContainer);
|
|
|
|
|
});
|
|
|
|
|
return fieldset;
|
|
|
|
|
} else {
|
2026-10-22 14:45:19 +00:00
|
|
|
ctrlDiv.className = 'setting-control';
|
2026-10-22 14:05:51 +00:00
|
|
|
input.min = def.min;
|
|
|
|
|
input.max = def.max;
|
|
|
|
|
input.step = def.step;
|
2026-10-22 14:45:19 +00:00
|
|
|
input.value = val !== undefined ? val : def.min;
|
2026-10-22 14:05:51 +00:00
|
|
|
const valueSpan = document.createElement('span');
|
2026-10-22 14:45:19 +00:00
|
|
|
valueSpan.className = 'control-val';
|
|
|
|
|
valueSpan.textContent = typeof val === 'number' ? val.toString() : (val || '0');
|
|
|
|
|
|
2026-10-22 14:05:51 +00:00
|
|
|
input.addEventListener('input', (e) => {
|
|
|
|
|
const newVal = parseFloat(e.target.value);
|
|
|
|
|
if (subkey) config[group][key][subkey] = newVal;
|
|
|
|
|
else config[group][key] = newVal;
|
|
|
|
|
saveSettings();
|
2026-10-22 14:45:19 +00:00
|
|
|
valueSpan.textContent = newVal.toFixed(Math.max(def.step.toString().split('.')[1]?.length || 0, 1));
|
2026-10-22 14:05:51 +00:00
|
|
|
});
|
2026-10-22 14:45:19 +00:00
|
|
|
ctrlDiv.append(labelEl, input, valueSpan);
|
2026-10-22 14:05:51 +00:00
|
|
|
}
|
2026-10-22 14:45:19 +00:00
|
|
|
|
|
|
|
|
return ctrlDiv;
|
2026-10-22 14:05:51 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
// Complete settings matching all original groups plus all new elements
|
2026-10-22 14:05:51 +00:00
|
|
|
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' },
|
2026-10-22 14:45:19 +00:00
|
|
|
{ key: 'destroy', subkey: 'sand', type: 'checkbox', label: 'Destroy Sand' },
|
2026-10-22 14:05:51 +00:00
|
|
|
{ key: 'destroy', subkey: 'plant', type: 'checkbox', label: 'Destroy Plant' },
|
2026-10-22 14:45:19 +00:00
|
|
|
{ key: 'destroy', subkey: 'grass', type: 'checkbox', label: 'Destroy Grass' },
|
2026-10-22 14:05:51 +00:00
|
|
|
{ key: 'destroy', subkey: 'water', type: 'checkbox', label: 'Destroy Water' },
|
2026-10-22 14:45:19 +00:00
|
|
|
{ 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' }
|
2026-10-22 14:05:51 +00:00
|
|
|
],
|
|
|
|
|
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 },
|
2026-10-22 14:45:19 +00:00
|
|
|
{ key: 'dispersion', label: 'Spread Speed', min: 1, max: 10, step: 1 },
|
2026-10-22 14:05:51 +00:00
|
|
|
{ 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' },
|
2026-10-22 14:45:19 +00:00
|
|
|
{ key: 'dissolve', subkey: 'sand', type: 'checkbox', label: 'Dissolve Sand' },
|
2026-10-22 14:05:51 +00:00
|
|
|
{ key: 'dissolve', subkey: 'plant', type: 'checkbox', label: 'Dissolve Plant' },
|
2026-10-22 14:45:19 +00:00
|
|
|
{ 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' }
|
2026-10-22 14:05:51 +00:00
|
|
|
],
|
|
|
|
|
fire: [
|
|
|
|
|
{ key: 'burnChance', label: 'Fire Decay Rate', min: 0, max: 1.0, step: 0.01 }
|
|
|
|
|
],
|
|
|
|
|
water: [
|
2026-10-22 14:45:19 +00:00
|
|
|
{ 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 }
|
2026-10-22 14:05:51 +00:00
|
|
|
],
|
|
|
|
|
steam: [
|
|
|
|
|
{ key: 'condenseChance', label: 'Condense Rate', min: 0, max: 1.0, step: 0.01 }
|
|
|
|
|
],
|
2026-10-22 14:45:19 +00:00
|
|
|
smoke: [
|
|
|
|
|
{ key: 'dissipateChance', label: 'Dissipate Rate', min: 0, max: 1.0, step: 0.01 }
|
|
|
|
|
],
|
2026-10-22 14:05:51 +00:00
|
|
|
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 }
|
|
|
|
|
],
|
2026-10-22 14:45:19 +00:00
|
|
|
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 }
|
|
|
|
|
],
|
2026-10-22 14:05:51 +00:00
|
|
|
wood: [
|
|
|
|
|
{ key: 'burnChance', label: 'Burn Rate', min: 0, max: 1.0, step: 0.01 }
|
|
|
|
|
],
|
|
|
|
|
visual: [
|
2026-10-22 14:45:19 +00:00
|
|
|
{ key: 'style', type: 'radio', label: 'Style', options: ['blocky', 'bloby', 'fuzzy'] },
|
|
|
|
|
{ key: 'colorNoise', type: 'checkbox', label: 'Multi-Tone Shading' }
|
2026-10-22 14:05:51 +00:00
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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);
|
2026-10-22 14:45:19 +00:00
|
|
|
|
2026-10-22 14:05:51 +00:00
|
|
|
settings.forEach(s => {
|
|
|
|
|
groupDiv.appendChild(addControl(groupName, s.key, s.subkey, s));
|
|
|
|
|
});
|
|
|
|
|
container.appendChild(groupDiv);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
// Reset Defaults
|
2026-10-22 14:05:51 +00:00
|
|
|
const resetBtn = document.createElement('button');
|
2026-10-22 14:45:19 +00:00
|
|
|
resetBtn.className = 'reset-defaults-btn';
|
|
|
|
|
resetBtn.textContent = '↺ Reset to Defaults';
|
2026-10-22 14:05:51 +00:00
|
|
|
resetBtn.onclick = () => {
|
|
|
|
|
setConfig(JSON.parse(JSON.stringify(defaultConfig)));
|
2026-10-22 14:45:19 +00:00
|
|
|
setBrushSize(defaultConfig.brush.size);
|
2026-10-22 14:05:51 +00:00
|
|
|
saveSettings();
|
2026-10-22 14:45:19 +00:00
|
|
|
updateBrushUI();
|
2026-10-22 14:05:51 +00:00
|
|
|
renderSettings(container);
|
|
|
|
|
};
|
|
|
|
|
container.appendChild(resetBtn);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
// Bresenham's line algorithm for continuous stroke drawing without gaps
|
|
|
|
|
export function handleInput(e, overrideType = null) {
|
2026-10-22 14:05:51 +00:00
|
|
|
const rect = canvas.getBoundingClientRect();
|
2026-10-22 14:45:19 +00:00
|
|
|
const typeToSpawn = overrideType || selectedType;
|
2026-10-22 14:05:51 +00:00
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
function getGridCoords(clientX, clientY) {
|
|
|
|
|
const x = clientX - rect.left;
|
|
|
|
|
const y = clientY - rect.top;
|
2026-10-22 14:05:51 +00:00
|
|
|
return {
|
2026-10-22 14:45:19 +00:00
|
|
|
col: Math.floor(x / cellSize),
|
|
|
|
|
row: Math.floor(y / cellSize)
|
2026-10-22 14:05:51 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 14:45:19 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-22 14:05:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-22 14:45:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-22 14:05:51 +00:00
|
|
|
|
|
|
|
|
if (e.type.startsWith('mouse')) {
|
2026-10-22 14:45:19 +00:00
|
|
|
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;
|
2026-10-22 14:05:51 +00:00
|
|
|
} else if (e.type.startsWith('touch')) {
|
|
|
|
|
for (let i = 0; i < e.touches.length; i++) {
|
|
|
|
|
const t = e.touches[i];
|
2026-10-22 14:45:19 +00:00
|
|
|
const current = getGridCoords(t.clientX, t.clientY);
|
|
|
|
|
if (lastPos) {
|
|
|
|
|
drawLine(lastPos, current);
|
|
|
|
|
} else {
|
|
|
|
|
stampCircle(current.col, current.row);
|
|
|
|
|
}
|
|
|
|
|
lastPos = current;
|
2026-10-22 14:05:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|