|
|
<!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', () => { |
|
|
|
|
|
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'); |
|
|
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'); |
|
|
|
|
|
|
|
|
let inputDim = parseInt(inputDimInput.value); |
|
|
let pixelSize = inputCanvas.width / inputDim; |
|
|
let isDrawing = false; |
|
|
|
|
|
let colors = ['#ffffff', '#000000', '#f44336', '#ffeb3b', '#4caf50', '#2196f3', '#9c27b0', '#ff9800']; |
|
|
let currentColorIndex = 1; |
|
|
let inputGrid = []; |
|
|
|
|
|
|
|
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
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(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
inputDimInput.addEventListener('change', resetInputCanvas); |
|
|
clearBtn.addEventListener('click', resetInputCanvas); |
|
|
|
|
|
|
|
|
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; |
|
|
inputGrid[i][center] = 2; |
|
|
} |
|
|
inputGrid[center][center] = 3; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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> |