EnovinxSchool's picture
undefined - Initial Deployment
f11c2b5 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wave Function Collapse (WFC) Demo</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
background-color: #f0f2f5;
color: #333;
margin: 0;
padding: 1rem;
}
h1 {
color: #111;
}
.main-container {
display: flex;
flex-wrap: wrap;
gap: 2rem;
justify-content: center;
width: 100%;
max-width: 1400px;
}
.canvas-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
canvas {
border: 1px solid #ccc;
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
background-color: #e9e9e9;
}
h2 {
margin-top: 0;
margin-bottom: 1rem;
color: #444;
}
.controls {
padding: 1rem;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 280px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.control-group label {
font-weight: bold;
}
.control-group input[type="number"], .control-group input[type="checkbox"] {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
width: 100%;
}
.control-group input[type="checkbox"] {
width: 20px;
height: 20px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
}
.palette {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
align-items: center;
}
.color-swatch {
width: 30px;
height: 30px;
border: 2px solid #ccc;
border-radius: 4px;
cursor: pointer;
transition: transform 0.1s;
}
.color-swatch.selected {
border-color: #007bff;
transform: scale(1.1);
box-shadow: 0 0 5px #007bff;
}
input[type="color"] {
width: 40px;
height: 40px;
border: none;
padding: 0;
background: none;
cursor: pointer;
}
button {
padding: 0.75rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: background-color 0.2s;
}
.btn-generate {
background-color: #28a745;
color: white;
}
.btn-generate:hover {
background-color: #218838;
}
.btn-clear {
background-color: #dc3545;
color: white;
}
.btn-clear:hover {
background-color: #c82333;
}
#status {
margin-top: 1rem;
font-weight: bold;
font-size: 1.1rem;
min-height: 1.5em;
}
</style>
</head>
<body>
<h1>Wave Function Collapse</h1>
<p>Draw on the left canvas, set your parameters, and click "Generate"!</p>
<div class="main-container">
<div class="controls">
<h2>Controls</h2>
<div class="control-group">
<label for="n-size">Pattern Size (N)</label>
<input type="number" id="n-size" value="3" min="2" max="5">
</div>
<div class="control-group">
<label for="input-dim">Input Grid Size</label>
<input type="number" id="input-dim" value="20" min="10" max="40">
</div>
<div class="control-group">
<label for="output-width">Output Width</label>
<input type="number" id="output-width" value="48" min="10" max="256">
</div>
<div class="control-group">
<label for="output-height">Output Height</label>
<input type="number" id="output-height" value="48" min="10" max="256">
</div>
<div class="control-group">
<label class="checkbox-label">
<input type="checkbox" id="augment" checked>
Use Augmentations (Rotations/Reflections)
</label>
</div>
<div class="control-group">
<label class="checkbox-label">
<input type="checkbox" id="visualize">
Visualize Generation
</label>
</div>
<div class="control-group">
<label>Color Palette</label>
<div class="palette">
<div id="palette-container"></div>
<input type="color" id="color-picker" value="#ff0000">
</div>
</div>
<button class="btn-generate" id="generate-btn">Generate</button>
<button class="btn-clear" id="clear-btn">Clear Input</button>
<div id="status">Ready.</div>
</div>
<div class="canvas-container">
<h2>Input Canvas</h2>
<canvas id="input-canvas" width="300" height="300"></canvas>
</div>
<div class="canvas-container">
<h2>Output Canvas</h2>
<canvas id="output-canvas" width="480" height="480"></canvas>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- DOM ELEMENTS ---
const inputCanvas = document.getElementById('input-canvas');
const inputCtx = inputCanvas.getContext('2d');
const outputCanvas = document.getElementById('output-canvas');
const outputCtx = outputCanvas.getContext('2d');
const nSizeInput = document.getElementById('n-size');
const inputDimInput = document.getElementById('input-dim'); // New
const outputWidthInput = document.getElementById('output-width');
const outputHeightInput = document.getElementById('output-height');
const augmentCheckbox = document.getElementById('augment');
const visualizeCheckbox = document.getElementById('visualize');
const generateBtn = document.getElementById('generate-btn');
const clearBtn = document.getElementById('clear-btn');
const statusDiv = document.getElementById('status');
const paletteContainer = document.getElementById('palette-container');
const colorPicker = document.getElementById('color-picker');
// --- DRAWING STATE ---
let inputDim = parseInt(inputDimInput.value);
let pixelSize = inputCanvas.width / inputDim;
let isDrawing = false;
// Expanded color palette
let colors = ['#ffffff', '#000000', '#f44336', '#ffeb3b', '#4caf50', '#2196f3', '#9c27b0', '#ff9800'];
let currentColorIndex = 1;
let inputGrid = [];
// --- HELPER FUNCTIONS ---
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// --- PALETTE & DRAWING LOGIC ---
function updatePalette() {
paletteContainer.innerHTML = '';
colors.forEach((color, index) => {
const swatch = document.createElement('div');
swatch.className = 'color-swatch';
swatch.style.backgroundColor = color;
if (index === currentColorIndex) {
swatch.classList.add('selected');
}
swatch.addEventListener('click', () => {
currentColorIndex = index;
updatePalette();
});
paletteContainer.appendChild(swatch);
});
}
colorPicker.addEventListener('change', (e) => {
const newColor = e.target.value;
if (!colors.includes(newColor)) {
colors.push(newColor);
currentColorIndex = colors.length - 1;
updatePalette();
}
});
function drawInputGrid() {
inputCtx.clearRect(0, 0, inputCanvas.width, inputCanvas.height);
for (let y = 0; y < inputDim; y++) {
for (let x = 0; x < inputDim; x++) {
inputCtx.fillStyle = colors[inputGrid[y][x]];
inputCtx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
}
}
// Draw grid lines for clarity
inputCtx.strokeStyle = '#ddd';
inputCtx.lineWidth = 0.5;
for (let i = 0; i <= inputDim; i++) {
inputCtx.beginPath();
inputCtx.moveTo(i * pixelSize, 0);
inputCtx.lineTo(i * pixelSize, inputCanvas.height);
inputCtx.stroke();
inputCtx.beginPath();
inputCtx.moveTo(0, i * pixelSize);
inputCtx.lineTo(inputCanvas.width, i * pixelSize);
inputCtx.stroke();
}
}
// NEW: Function to reset the input canvas based on new dimensions
function resetInputCanvas() {
inputDim = parseInt(inputDimInput.value);
pixelSize = inputCanvas.width / inputDim;
inputGrid = Array.from({ length: inputDim }, () => Array(inputDim).fill(0));
drawInputGrid();
}
function handleDraw(event) {
if (!isDrawing) return;
const rect = inputCanvas.getBoundingClientRect();
const x = Math.floor((event.clientX - rect.left) / pixelSize);
const y = Math.floor((event.clientY - rect.top) / pixelSize);
if (x >= 0 && x < inputDim && y >= 0 && y < inputDim) {
if (inputGrid[y][x] !== currentColorIndex) {
inputGrid[y][x] = currentColorIndex;
drawInputGrid();
}
}
}
inputCanvas.addEventListener('mousedown', (e) => { isDrawing = true; handleDraw(e); });
inputCanvas.addEventListener('mouseup', () => { isDrawing = false; });
inputCanvas.addEventListener('mouseleave', () => { isDrawing = false; });
inputCanvas.addEventListener('mousemove', handleDraw);
// NEW: Event listener for changing input grid size
inputDimInput.addEventListener('change', resetInputCanvas);
clearBtn.addEventListener('click', resetInputCanvas); // Clear button now also uses the reset function
// Default pattern, now scales with inputDim
function createDefaultPattern() {
inputGrid = Array.from({ length: inputDim }, () => Array(inputDim).fill(0));
const center = Math.floor(inputDim / 2);
const start = Math.floor(inputDim / 4);
const end = inputDim - start;
for(let i = start; i < end; i++) {
inputGrid[center][i] = 1; // Black line
inputGrid[i][center] = 2; // Red line
}
inputGrid[center][center] = 3; // Yellow center
}
// --- WFC ALGORITHM (No changes needed in this class) ---
class WFC {
constructor(inputGrid, N, outputWidth, outputHeight, useAugmentations, visualizeCallback) {
this.N = N;
this.outputWidth = outputWidth;
this.outputHeight = outputHeight;
this.useAugmentations = useAugmentations;
this.visualizeCallback = visualizeCallback;
this.patterns = [];
this.patternWeights = {};
this.parseInput(inputGrid);
this.buildAdjacency();
this.wave = [];
this.entropy = [];
this.allPatternIndices = Array.from({ length: this.patterns.length }, (_, i) => i);
for (let y = 0; y < outputHeight; y++) {
this.wave[y] = [];
this.entropy[y] = [];
for (let x = 0; x < outputWidth; x++) {
this.wave[y][x] = [...this.allPatternIndices];
this.entropy[y][x] = this.patterns.length;
}
}
}
parseInput(grid) {
const patternMap = new Map();
const gridHeight = grid.length;
const gridWidth = grid[0].length;
for (let y = 0; y <= gridHeight - this.N; y++) {
for (let x = 0; x <= gridWidth - this.N; x++) {
const pattern = [];
for (let dy = 0; dy < this.N; dy++) {
pattern.push(...grid[y + dy].slice(x, x + this.N));
}
if (this.useAugmentations) {
const augmentations = this.getAugmentations(pattern);
augmentations.forEach(p => {
const hash = p.join(',');
patternMap.set(hash, (patternMap.get(hash) || 0) + 1);
});
} else {
const hash = pattern.join(',');
patternMap.set(hash, (patternMap.get(hash) || 0) + 1);
}
}
}
patternMap.forEach((weight, hash) => {
const pattern = hash.split(',').map(Number);
this.patterns.push(pattern);
this.patternWeights[this.patterns.length - 1] = weight;
});
if (this.patterns.length === 0) {
throw new Error("No patterns found. Try a larger or more varied input, or a smaller N.");
}
}
getAugmentations(pattern) {
const augmentations = new Set();
let current = pattern;
for (let i = 0; i < 4; i++) {
augmentations.add(current.join(','));
augmentations.add(this.reflect(current).join(','));
current = this.rotate(current);
}
return Array.from(augmentations).map(hash => hash.split(',').map(Number));
}
rotate(p) {
const newPattern = Array(this.N * this.N);
for (let y = 0; y < this.N; y++) {
for (let x = 0; x < this.N; x++) {
newPattern[x * this.N + (this.N - 1 - y)] = p[y * this.N + x];
}
}
return newPattern;
}
reflect(p) {
const newPattern = Array(this.N * this.N);
for (let y = 0; y < this.N; y++) {
for (let x = 0; x < this.N; x++) {
newPattern[y * this.N + (this.N - 1 - x)] = p[y * this.N + x];
}
}
return newPattern;
}
buildAdjacency() {
this.adjacency = {};
for (let i = 0; i < this.patterns.length; i++) {
this.adjacency[i] = { '1,0': [], '-1,0': [], '0,1': [], '0,-1': [] };
}
for (let i = 0; i < this.patterns.length; i++) {
for (let j = 0; j < this.patterns.length; j++) {
if (this.checkOverlap(this.patterns[i], this.patterns[j], 1, 0)) this.adjacency[i]['1,0'].push(j);
if (this.checkOverlap(this.patterns[i], this.patterns[j], -1, 0)) this.adjacency[i]['-1,0'].push(j);
if (this.checkOverlap(this.patterns[i], this.patterns[j], 0, 1)) this.adjacency[i]['0,1'].push(j);
if (this.checkOverlap(this.patterns[i], this.patterns[j], 0, -1)) this.adjacency[i]['0,-1'].push(j);
}
}
}
checkOverlap(p1, p2, dx, dy) {
for (let y = 0; y < this.N; y++) {
for (let x = 0; x < this.N; x++) {
const nx = x - dx;
const ny = y - dy;
if (nx >= 0 && nx < this.N && ny >= 0 && ny < this.N) {
if (p1[y * this.N + x] !== p2[ny * this.N + nx]) return false;
}
}
}
return true;
}
async run() {
let iterations = this.outputWidth * this.outputHeight;
while(iterations > 0) {
const collapsedCoords = this.observe();
if (collapsedCoords === null) return this.generateOutput();
const contradiction = this.propagate(collapsedCoords);
if (contradiction) throw new Error("Contradiction reached. Cannot continue.");
iterations--;
if (this.visualizeCallback) {
await this.visualizeCallback(this.wave, this.patterns.length);
await sleep(0);
}
}
return this.generateOutput();
}
observe() {
let minEntropy = Infinity;
let minCoords = null;
for (let y = 0; y < this.outputHeight; y++) {
for (let x = 0; x < this.outputWidth; x++) {
const possibilities = this.wave[y][x].length;
if (possibilities > 1 && possibilities < minEntropy) {
minEntropy = possibilities;
minCoords = [{x, y}];
} else if (possibilities > 1 && possibilities === minEntropy) {
minCoords.push({x, y});
}
}
}
if (minCoords === null) return null;
const choice = minCoords[Math.floor(Math.random() * minCoords.length)];
const {x, y} = choice;
const possiblePatterns = this.wave[y][x];
const totalWeight = possiblePatterns.reduce((sum, pIndex) => sum + this.patternWeights[pIndex], 0);
let rnd = Math.random() * totalWeight;
let chosenPattern;
for(const pIndex of possiblePatterns) {
rnd -= this.patternWeights[pIndex];
if(rnd <= 0) {
chosenPattern = pIndex;
break;
}
}
this.wave[y][x] = [chosenPattern];
return {x, y};
}
propagate(startCoords) {
const stack = [startCoords];
while(stack.length > 0) {
const {x, y} = stack.pop();
const currentPatterns = this.wave[y][x];
for (const [dx, dy] of [[0,1], [0,-1], [1,0], [-1,0]]) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || nx >= this.outputWidth || ny < 0 || ny >= this.outputHeight) continue;
let neighborPatterns = this.wave[ny][nx];
if(neighborPatterns.length <= 1) continue;
const validNeighboringPatterns = new Set();
const directionKey = `${-dx},${-dy}`;
for (const pIndex of currentPatterns) {
this.adjacency[pIndex][directionKey].forEach(validIndex => validNeighboringPatterns.add(validIndex));
}
const originalLength = neighborPatterns.length;
const newNeighborPatterns = neighborPatterns.filter(pIndex => validNeighboringPatterns.has(pIndex));
if (newNeighborPatterns.length === 0) return true;
if (newNeighborPatterns.length < originalLength) {
this.wave[ny][nx] = newNeighborPatterns;
stack.push({x: nx, y: ny});
}
}
}
return false;
}
generateOutput() {
const outputGrid = Array.from({ length: this.outputHeight }, () => Array(this.outputWidth));
for (let y = 0; y < this.outputHeight; y++) {
for (let x = 0; x < this.outputWidth; x++) {
if (this.wave[y][x].length > 0) {
const pattern = this.patterns[this.wave[y][x][0]];
outputGrid[y][x] = pattern[0];
} else {
outputGrid[y][x] = 0;
}
}
}
return outputGrid;
}
}
// --- MAIN EXECUTION ---
async function handleGenerate() {
generateBtn.disabled = true;
statusDiv.textContent = 'Starting...';
statusDiv.style.color = '#333';
const outputWidth = parseInt(outputWidthInput.value);
const outputHeight = parseInt(outputHeightInput.value);
outputCanvas.width = outputWidth * 10;
outputCanvas.height = outputHeight * 10;
outputCtx.clearRect(0, 0, outputCanvas.width, outputCanvas.height);
await sleep(10);
try {
statusDiv.textContent = '1. Parsing input patterns...';
const N = parseInt(nSizeInput.value);
const useAugmentations = augmentCheckbox.checked;
const visualize = visualizeCheckbox.checked;
let visualizeCallback = null;
if (visualize) {
visualizeCallback = (wave, maxEntropy) => {
const pixelW = outputCanvas.width / outputWidth;
const pixelH = outputCanvas.height / outputHeight;
outputCtx.clearRect(0, 0, outputCanvas.width, outputCanvas.height);
for(let y = 0; y < outputHeight; y++) {
for (let x = 0; x < outputWidth; x++) {
const entropy = wave[y][x].length;
if(entropy === 1) {
const colorIndex = wfc.patterns[wave[y][x][0]][0];
outputCtx.fillStyle = colors[colorIndex];
} else {
const grayscale = Math.floor(255 * (1 - (entropy / maxEntropy)));
outputCtx.fillStyle = `rgb(${grayscale}, ${grayscale}, ${grayscale})`;
}
outputCtx.fillRect(x * pixelW, y * pixelH, pixelW, pixelH);
}
}
};
}
const wfc = new WFC(inputGrid, N, outputWidth, outputHeight, useAugmentations, visualizeCallback);
statusDiv.textContent = '2. Running WFC algorithm...';
await sleep(10);
const resultGrid = await wfc.run();
statusDiv.textContent = '3. Rendering output...';
await sleep(10);
const pixelW = outputCanvas.width / outputWidth;
const pixelH = outputCanvas.height / outputHeight;
outputCtx.clearRect(0, 0, outputCanvas.width, outputCanvas.height);
for(let y = 0; y < outputHeight; y++) {
for (let x = 0; x < outputWidth; x++) {
const colorIndex = resultGrid[y][x];
outputCtx.fillStyle = colors[colorIndex] || '#ff00ff';
outputCtx.fillRect(x * pixelW, y * pixelH, pixelW, pixelH);
}
}
statusDiv.textContent = 'Finished successfully!';
statusDiv.style.color = 'green';
} catch (error) {
console.error(error);
statusDiv.textContent = `Error: ${error.message}`;
statusDiv.style.color = 'red';
} finally {
generateBtn.disabled = false;
}
}
generateBtn.addEventListener('click', handleGenerate);
// --- INITIALIZATION ---
function initialize() {
updatePalette();
createDefaultPattern();
drawInputGrid();
}
initialize();
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=EnovinxSchool/wave-collapse-function-not-designed-by-me" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>