vyyy
This commit is contained in:
11
WfcConstants.js
Normal file
11
WfcConstants.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export const Direction = Object.freeze({
|
||||
NW: 0,
|
||||
N: 1,
|
||||
NE: 2,
|
||||
E: 3,
|
||||
C: 4,
|
||||
W: 5,
|
||||
SW: 6,
|
||||
S: 7,
|
||||
SE: 8,
|
||||
});
|
||||
7
eslint.config.js
Normal file
7
eslint.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import { defineConfig } from "eslint/config";
|
||||
|
||||
export default defineConfig([
|
||||
{ files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
|
||||
]);
|
||||
88
frontend/TrainingCell.js
Executable file
88
frontend/TrainingCell.js
Executable file
@@ -0,0 +1,88 @@
|
||||
import { Direction } from "./WfcConstants.js";
|
||||
|
||||
/**
|
||||
* Represents a 3x3 grid of values (sub-cells), that are used as building blocks for procedurally
|
||||
* generated grids. In reality, only the center value will be included in the outputted WfcGrid;
|
||||
* the 8 surrounding colors are there to establish which TrainingCells can live next to each other.
|
||||
*/
|
||||
|
||||
export class TrainingCell {
|
||||
/** @param {Uint8Array} values The 9 sub cells that make up this TrainingCell */
|
||||
constructor() {
|
||||
this.values = new Uint8Array(9);
|
||||
}
|
||||
|
||||
/** @returns {string} The actual value of this Trainin gCell is represented by its center value */
|
||||
get value() {
|
||||
return this.values[Direction.C];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TrainingCell} other
|
||||
* @param {number} direction
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
potentialNeighbours(other, direction) {
|
||||
// sadly, we're not allowed to be friends with ourselves.
|
||||
if (this === other) {
|
||||
console.log("WTF were checking to be friends with ourselves!", this, other, direction);
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
//
|
||||
// if they want to live to my east,
|
||||
// their two westmost columns must match
|
||||
// my two eastmost columns
|
||||
(direction == Direction.E &&
|
||||
// My center col must match their west col
|
||||
this.values[Direction.N] === other.values[Direction.NW] &&
|
||||
this.values[Direction.C] === other.values[Direction.W] &&
|
||||
this.values[Direction.S] === other.values[Direction.SW] &&
|
||||
// My east col must match their center col
|
||||
this.values[Direction.NE] === other.values[Direction.N] &&
|
||||
this.values[Direction.E] === other.values[Direction.C] &&
|
||||
this.values[Direction.SE] === other.values[Direction.S]) ||
|
||||
//
|
||||
// if they want to live to my west,
|
||||
// their two eastmost columns must match
|
||||
// my two westmost columns
|
||||
(direction == Direction.W &&
|
||||
// My center col must match their east col
|
||||
this.values[Direction.N] === other.values[Direction.NE] &&
|
||||
this.values[Direction.C] === other.values[Direction.E] &&
|
||||
this.values[Direction.S] === other.values[Direction.SE] &&
|
||||
// My west col must match their center col
|
||||
this.values[Direction.NW] === other.values[Direction.N] &&
|
||||
this.values[Direction.W] === other.values[Direction.C] &&
|
||||
this.values[Direction.SW] === other.values[Direction.S]) ||
|
||||
//
|
||||
// if they want to live to my north,
|
||||
// their two souther rows must match
|
||||
// my two northern rows
|
||||
(direction == Direction.N &&
|
||||
// my middle row must match their south row
|
||||
this.values[Direction.W] === other.values[Direction.SW] &&
|
||||
this.values[Direction.C] === other.values[Direction.S] &&
|
||||
this.values[Direction.E] === other.values[Direction.SE] &&
|
||||
// my north row must match their middle row
|
||||
this.values[Direction.NW] === other.values[Direction.W] &&
|
||||
this.values[Direction.N] === other.values[Direction.C] &&
|
||||
this.values[Direction.Direction.NE] === other.values[Direction.E]) ||
|
||||
//
|
||||
// if they want to live to my south,
|
||||
// their two northern rows must match
|
||||
// my two southern rows
|
||||
(direction == Direction.S &&
|
||||
// my middle row must match their north row
|
||||
this.values[Direction.W] === other.values[Direction.NW] &&
|
||||
this.values[Direction.C] === other.values[Direction.N] &&
|
||||
this.values[Direction.E] === other.values[Direction.SE] &&
|
||||
// my south row must match their middle row
|
||||
this.values[Direction.SW] === other.values[Direction.W] &&
|
||||
this.values[Direction.S] === other.values[Direction.C] &&
|
||||
this.values[Direction.SE] === other.values[Direction.E])
|
||||
);
|
||||
}
|
||||
}
|
||||
30
frontend/TrainingGrid.js
Normal file
30
frontend/TrainingGrid.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { TrainingCell } from "./TrainingCell.js";
|
||||
|
||||
export class TrainingGrid {
|
||||
/**
|
||||
* @param {TrainingCell[]} cells
|
||||
*/
|
||||
constructor(cells) {
|
||||
if (cells[0] === undefined) {
|
||||
throw new Error("cells must be a non empty array");
|
||||
}
|
||||
|
||||
if (!(cells[0] instanceof TrainingCell)) {
|
||||
throw new Error("cells arg must be an array of TrainingCell, but it isn't");
|
||||
}
|
||||
|
||||
/** @type {TrainingCell[]} cells*/
|
||||
this.cells = cells;
|
||||
|
||||
/** @type {number} the width and/or height of the training grid */
|
||||
this.dim = Math.round(Math.sqrt(cells.length));
|
||||
|
||||
if (this.dim ** 2 !== cells.length) {
|
||||
throw new Error("Training grid must be quadratic (height === width), but it isn't");
|
||||
}
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new TrainingGrid(this.cells.slice());
|
||||
}
|
||||
}
|
||||
50
frontend/WfcCell.js
Executable file
50
frontend/WfcCell.js
Executable file
@@ -0,0 +1,50 @@
|
||||
import { TrainingCell } from "./TrainingCell";
|
||||
|
||||
/**
|
||||
* Represents a single cell in a WfcGrid
|
||||
*/
|
||||
export class WfcCell {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {number} i index in the cell-array of this cell
|
||||
* @param {number} x x-coordinate of cell
|
||||
* @param {number} y y-coordinate of cell
|
||||
* @param {TrainingCell[]} options - A list of training cells that could potentially live here.
|
||||
*/
|
||||
constructor(i, x, y, options) {
|
||||
if (!options.length) {
|
||||
console.log("Bad >>options<< arg in WfcCell constructor. Must not be empty.", options);
|
||||
throw Error("Bad >>options<< arg in WfcCell constructor. Must not be empty.", options);
|
||||
}
|
||||
|
||||
if (!(options[0] instanceof TrainingCell)) {
|
||||
console.log("Bad >>options<< arg in WfcCell constructor. Must be array of WfcCells, but wasn't.", options);
|
||||
throw Error("Bad >>options<< arg in WfcCell constructor. Must be array of WfcCells, but wasn't.", options);
|
||||
}
|
||||
|
||||
this.i = i;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
getEntropy() {
|
||||
const result = this.options.length;
|
||||
return result;
|
||||
}
|
||||
|
||||
get lockedIn() {
|
||||
return this.getEntropy() === 1;
|
||||
}
|
||||
|
||||
get valid() {
|
||||
return this.options.length > 0;
|
||||
}
|
||||
|
||||
get value() {
|
||||
if (this.options[0] === undefined) {
|
||||
throw new Error("Bad! I do not have any options, and therefore no color");
|
||||
}
|
||||
return this.options[0].value;
|
||||
}
|
||||
}
|
||||
186
frontend/WfcGrid.js
Executable file
186
frontend/WfcGrid.js
Executable file
@@ -0,0 +1,186 @@
|
||||
import { Direction } from "./WfcConstants.js";
|
||||
import { WfcCell } from "./WfcCell.js";
|
||||
|
||||
/**
|
||||
* A WfcGrid represents the output of a Wave Function Collapse operation.
|
||||
*/
|
||||
export class WfcGrid {
|
||||
/**
|
||||
* @param {number} w width (in cells)
|
||||
* @param {number} h height (in cells)
|
||||
* @param {TrainingGrid} trainingGrid the training grid that will be the source from which we populate this grid.
|
||||
* @type {Xorshift32} pre-seeded pseudo random number generator
|
||||
*/
|
||||
constructor(w, h, trainingGrid, rng) {
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
this.trainingGrid = trainingGrid;
|
||||
this.rng = rng;
|
||||
|
||||
//
|
||||
// Populate the cells so each has all available options
|
||||
// For now, this means *copying* all TrainingCell options into each cell
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
console.log("Resetting Cells");
|
||||
const [w, h] = [this.width, this.height];
|
||||
const len = w * h;
|
||||
this.cells = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
const x = i % w;
|
||||
const y = Math.floor(i / w);
|
||||
|
||||
this.cells[i] = new WfcCell(i, x, y, this.trainingGrid.clone().pixels);
|
||||
}
|
||||
console.log("Done");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cells that currently have the lowest entropy
|
||||
* @returns {number[]}
|
||||
*/
|
||||
cellsIdsWithLowestEntropy() {
|
||||
console.log("Finding cells with lowest entopy");
|
||||
let result = [];
|
||||
|
||||
// set lowestEntropy to the highest possible entropy,
|
||||
// and let's search for lower entropy in the cells
|
||||
let lowestEntropy = this.trainingGrid.dim ** 2;
|
||||
|
||||
this.cells.forEach((cell, idx) => {
|
||||
console.log("\t checking cell %d (entropy: %d)", idx, cell.getEntropy());
|
||||
//
|
||||
// Have we found cells with low entropy?
|
||||
if (cell.getEntropy() < lowestEntropy) {
|
||||
// we've found a cell with lower entropy that the ones we've been looking
|
||||
// at so far Clear the search results and start over with this cell's
|
||||
// entropy as our target
|
||||
result = [idx];
|
||||
lowestEntropy = cell.getEntropy();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Cell matches current entropy level, add it to search results.
|
||||
if (cell.getEntropy() === lowestEntropy) {
|
||||
// Cell matches our current level of entropy, so we add it to our search results.
|
||||
// at so far! Clear the results and start over.
|
||||
result.push(idx);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (result.length <= 0) {
|
||||
console.log("Found zero lowest-entropy cells.", { lowestEntropy });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
collapse() {
|
||||
console.log("Starting collaps()");
|
||||
let count = this.cells.length;
|
||||
while (count > 0) {
|
||||
count--;
|
||||
// Get a list of possible target cells
|
||||
const lowEntropyCellIds = this.cellIdsWithLowestEntropy();
|
||||
|
||||
//
|
||||
// We've hit a dead end
|
||||
// No appropriate target cells found.
|
||||
if (lowEntropyCellIds.length === 0) {
|
||||
console.log("Found no lowest-entropy cells. This should not happen");
|
||||
return count;
|
||||
}
|
||||
|
||||
const rCellId = this.rng.randomElement(lowEntropyCellIds);
|
||||
const rCell = this.cells[rCellId];
|
||||
|
||||
/** @type {TrainingCell} a randomly chosen option that was available to rCell */
|
||||
const rOption = this.rng.randomElement(rCell.options);
|
||||
|
||||
// Lock in the choice for this cell
|
||||
rCell.options = [rOption];
|
||||
|
||||
// _____ ____ _ _
|
||||
// | ____|_ __ _ __ ___ _ __| __ ) ___| | _____ _| |
|
||||
// | _| | '__| '__/ _ \| '__| _ \ / _ \ |/ _ \ \ /\ / / |
|
||||
// | |___| | | | | (_) | | | |_) | __/ | (_) \ V V /|_|
|
||||
// |_____|_| |_| \___/|_| |____/ \___|_|\___/ \_/\_/ (_)
|
||||
// Locking in this cell has changed the grid.
|
||||
// We must look at the cell's cardinal neighbours and update their options.
|
||||
for (let nArr of this.getNeighboursFor(rCell)) {
|
||||
/** @type {number} direction of the neighbour */
|
||||
const neighbourDirection = nArr[0];
|
||||
|
||||
/** @type {WfcCell} the neighbouring cell */
|
||||
const neighbourCell = nArr[1];
|
||||
|
||||
// Clear the neighbour's options, and
|
||||
// repopulate with valid options.
|
||||
const newOptions = [];
|
||||
|
||||
for (let neighbourOption of neighbourCell.options) {
|
||||
if (neighbourOption.potentialNeighbours(rOption, neighbourDirection)) {
|
||||
newOptions.push(neighbourOption);
|
||||
}
|
||||
}
|
||||
|
||||
// We've collapsed too deep.
|
||||
if (newOptions.length === 0) {
|
||||
console.error("We've removed all options from a neighbour!", {
|
||||
rCell,
|
||||
rOption,
|
||||
neighbourCell,
|
||||
neighbourDirection,
|
||||
newOptions,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
neighbourCell.options = newOptions;
|
||||
}
|
||||
}
|
||||
console.log("Done");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the neighbours of a cell.
|
||||
*/
|
||||
getNeighboursFor(cell) {
|
||||
const result = [];
|
||||
|
||||
const yNorth = cell.y - 1;
|
||||
if (yNorth >= 0) {
|
||||
const xNorth = cell.x;
|
||||
const idx = this.width * yNorth + xNorth;
|
||||
result.push([Direction.N, this.cells[idx]]);
|
||||
}
|
||||
|
||||
const ySouth = cell.y + 1;
|
||||
if (ySouth < this.height) {
|
||||
const xSouth = cell.x;
|
||||
const idx = this.width * ySouth + xSouth;
|
||||
result.push([Direction.S, this.cells[idx]]);
|
||||
}
|
||||
|
||||
const xEast = cell.x + 1;
|
||||
if (xEast < this.width) {
|
||||
const yEast = cell.y;
|
||||
const idx = this.width * yEast + xEast;
|
||||
result.push([Direction.E, this.cells[idx]]);
|
||||
}
|
||||
|
||||
const xWest = cell.x - 1;
|
||||
if (xWest >= 0) {
|
||||
const yWest = cell.y;
|
||||
const idx = this.width * yWest + xWest;
|
||||
result.push([Direction.W, this.cells[idx]]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
0
frontend/WfcImage.1.js
Normal file
0
frontend/WfcImage.1.js
Normal file
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{ "src": "./img/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "./img/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
@@ -17,16 +17,6 @@
|
||||
|
||||
<div class="controls-panel">
|
||||
<div class="color-picker-section">
|
||||
<h3>Current Color</h3>
|
||||
<div class="current-color" id="currentColor" onclick="painter.openColorPicker()"></div>
|
||||
<input
|
||||
type="color"
|
||||
id="colorPicker"
|
||||
value="#000000"
|
||||
onchange="setCurrentColor(this.value)"
|
||||
style="display: none"
|
||||
/>
|
||||
|
||||
<h3>Color Palette</h3>
|
||||
<div class="color-palette" id="colorPalette">
|
||||
<!-- Color swatches will be generated -->
|
||||
@@ -53,6 +43,7 @@
|
||||
<button onclick="painter.exportAsImage()">Export png</button>
|
||||
<button onclick="painter.exportAsData()">Export json</button>
|
||||
<button onclick="painter.importData()">Import json</button>
|
||||
<button onclick="painter.waveFunction()">WaifuCollapse</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
class PaintProg {
|
||||
import { sprintf } from "sprintf-js";
|
||||
import { Xorshift32 } from "../utils/random.js";
|
||||
import { WfcGrid } from "./WfcGrid.js";
|
||||
import { TrainingCell } from "./TrainingCell.js";
|
||||
import { TrainingGrid } from "./TrainingGrid.js";
|
||||
|
||||
class PainApp {
|
||||
/** @type {string} */
|
||||
currentColor = "#000";
|
||||
activeColor = "#000";
|
||||
/** @type {string} */
|
||||
currentTool = "draw";
|
||||
/** @type {boolean} */
|
||||
@@ -9,7 +15,7 @@ class PaintProg {
|
||||
drawingMode = false;
|
||||
|
||||
/**@param {string[]} pal */
|
||||
constructor(dim, pal, gridElement, paletteElement, currentColorElement, colorPickerElement, previewElement) {
|
||||
constructor(dim, pal, gridElement, paletteElement, previewElement) {
|
||||
/** @type {number} */
|
||||
this.dim = dim;
|
||||
/** @type {string[]} Default color palette */
|
||||
@@ -22,18 +28,19 @@ class PaintProg {
|
||||
this.previewElement = previewElement;
|
||||
/** @type {HTMLElement} */
|
||||
this.paletteElement = paletteElement;
|
||||
/** @type {HTMLElement} */
|
||||
this.currentColorElement = currentColorElement;
|
||||
/** @type {HTMLInputElement} */
|
||||
this.colorPickerElement = colorPickerElement;
|
||||
|
||||
this.subImages = Array.from({ length: this.samplePixels.length }, () => [...this.samplePixels]);
|
||||
this.trainingImage = new TrainingGrid(
|
||||
this.samplePixels.map(() => {
|
||||
return new TrainingCell();
|
||||
}),
|
||||
);
|
||||
|
||||
this.createGrid();
|
||||
this.createColorPalette();
|
||||
this.updatePreview();
|
||||
this.setCurrentColor(pal[0]);
|
||||
this.updateAllSubimages();
|
||||
this.setActiveColor(pal[0]);
|
||||
this.updateTrainingGrid();
|
||||
}
|
||||
|
||||
createGrid() {
|
||||
@@ -60,38 +67,28 @@ class PaintProg {
|
||||
createColorPalette() {
|
||||
this.paletteElement.innerHTML = "";
|
||||
|
||||
this.palette.forEach((color) => {
|
||||
this.palette.forEach((color, paletteIndex) => {
|
||||
const swatch = document.createElement("div");
|
||||
swatch.className = "color-swatch";
|
||||
swatch.classList.add("color-swatch");
|
||||
swatch.classList.add(`pal-idx-${paletteIndex}`);
|
||||
swatch.classList.add(`pal-color-${color}`);
|
||||
swatch.style.backgroundColor = color;
|
||||
swatch.onclick = () => this.setCurrentColor(color);
|
||||
swatch.onclick = () => this.setActiveColor(paletteIndex);
|
||||
this.paletteElement.appendChild(swatch);
|
||||
});
|
||||
}
|
||||
|
||||
setCurrentColor(color) {
|
||||
this.currentColor = color;
|
||||
this.currentColorElement.style.backgroundColor = color;
|
||||
this.colorPickerElement.value = color;
|
||||
setActiveColor(paletteIndex) {
|
||||
//
|
||||
this.activeColor = this.palette[paletteIndex];
|
||||
|
||||
// Update active swatch
|
||||
// NOTE: this was "document.querySelectorAll "
|
||||
this.paletteElement.querySelectorAll(".color-swatch").forEach((swatch) => {
|
||||
swatch.classList.toggle("active", swatch.style.backgroundColor === this.colorToRgb(color));
|
||||
const colorSwatches = this.paletteElement.querySelectorAll(".color-swatch");
|
||||
colorSwatches.forEach((swatch) => {
|
||||
const isActive = swatch.classList.contains(`pal-idx-${paletteIndex}`);
|
||||
swatch.classList.toggle("active", isActive);
|
||||
});
|
||||
}
|
||||
|
||||
colorToRgb(hex) {
|
||||
const r = parseInt(hex.substr(1, 2), 16);
|
||||
const g = parseInt(hex.substr(3, 2), 16);
|
||||
const b = parseInt(hex.substr(5, 2), 16);
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
openColorPicker() {
|
||||
this.colorPickerElement.click();
|
||||
}
|
||||
|
||||
setTool(tool) {
|
||||
this.currentTool = tool;
|
||||
document.querySelectorAll(".tools button").forEach((btn) => btn.classList.remove("active"));
|
||||
@@ -135,7 +132,7 @@ class PaintProg {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateSingleSubImage(index);
|
||||
this.updateTrainingCell(index);
|
||||
this.updatePreview(index);
|
||||
}
|
||||
|
||||
@@ -146,10 +143,10 @@ class PaintProg {
|
||||
applyTool(index) {
|
||||
switch (this.currentTool) {
|
||||
case "draw":
|
||||
this.setPixel(index, this.currentColor);
|
||||
this.setPixel(index, this.activeColor);
|
||||
break;
|
||||
case "fill":
|
||||
this.floodFill(index, this.samplePixels[index], this.currentColor);
|
||||
this.floodFill(index, this.samplePixels[index], this.activeColor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -246,7 +243,7 @@ class PaintProg {
|
||||
//
|
||||
const x = i % 3;
|
||||
const y = Math.floor(i / 3);
|
||||
ctx.fillStyle = this.subImages[subImageIdx][i];
|
||||
ctx.fillStyle = this.trainingImage.pixels[subImageIdx].subPixels[i];
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
@@ -255,28 +252,24 @@ class PaintProg {
|
||||
this.previewElement.style.backgroundSize = "100%";
|
||||
}
|
||||
|
||||
updateAllSubimages() {
|
||||
updateTrainingGrid() {
|
||||
for (let i = 0; i < this.samplePixels.length; i++) {
|
||||
this.updateSingleSubImage(i);
|
||||
this.updateTrainingCell(i);
|
||||
}
|
||||
}
|
||||
|
||||
updateSingleSubImage(i) {
|
||||
updateTrainingCell(i) {
|
||||
const dim = this.dim;
|
||||
const len = dim ** 2;
|
||||
const x = i % dim;
|
||||
const y = Math.floor(i / dim);
|
||||
|
||||
const colorAt = (dX, dY) => {
|
||||
const _x = (x + dim + dX) % dim; // add dim before modulo because JS modulo allows negative results
|
||||
const _y = (y + dim + dY) % dim;
|
||||
if (y == 0 && dY < 0) {
|
||||
console.log(_x, _y);
|
||||
}
|
||||
return this.samplePixels[_y * dim + _x];
|
||||
};
|
||||
|
||||
this.subImages[i] = [
|
||||
this.trainingImage.pixels[i] = new TrainingCell([
|
||||
// | neighbour
|
||||
// ---------------------|-----------
|
||||
colorAt(-1, -1), // | northwest
|
||||
@@ -290,21 +283,21 @@ class PaintProg {
|
||||
colorAt(-1, 1), // | southwest
|
||||
colorAt(0, 1), // | south
|
||||
colorAt(1, 1), // | southeast
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
exportAsImage() {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 90; // 9x upscale
|
||||
canvas.height = 90;
|
||||
canvas.width = this.dim; // 9x upscale
|
||||
canvas.height = this.dim;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
|
||||
for (let i = 0; i < this.dim ** 2; i++) {
|
||||
const x = (i % this.dim) * 10;
|
||||
const y = Math.floor(i / this.dim) * 10;
|
||||
const x = i % this.dim;
|
||||
const y = Math.floor(i / this.dim);
|
||||
ctx.fillStyle = this.samplePixels[i];
|
||||
ctx.fillRect(x, y, 10, 10);
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
|
||||
const link = document.createElement("a");
|
||||
@@ -341,7 +334,7 @@ class PaintProg {
|
||||
alert("Invalid data format!");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error reading file!");
|
||||
alert("Error reading file!" + error);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
@@ -350,42 +343,86 @@ class PaintProg {
|
||||
input.click();
|
||||
}
|
||||
|
||||
// Initialize the editor
|
||||
waveFunction() {
|
||||
this.updateTrainingGrid();
|
||||
const wfcImg = new WfcGrid(
|
||||
// this.previewElement.clientWidth,
|
||||
// this.previewElement.clientHeight,
|
||||
30,
|
||||
30,
|
||||
this.trainingImage.clone(),
|
||||
new Xorshift32(Date.now()),
|
||||
);
|
||||
|
||||
// Could not "collapse" the image.
|
||||
// We should reset and try again?
|
||||
let its = wfcImg.collapse();
|
||||
|
||||
if (its > 0) {
|
||||
throw new Error(`Function Collapse failed with ${its} iterations left to go`);
|
||||
}
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = wfcImg.width;
|
||||
canvas.height = wfcImg.height;
|
||||
|
||||
// debug values
|
||||
canvas.width = 30;
|
||||
canvas.height = 30;
|
||||
//
|
||||
const ctx = canvas.getContext("2d");
|
||||
let i = 0;
|
||||
for (let y = 0; y < canvas.height; y++) {
|
||||
for (let x = 0; x < canvas.width; x++) {
|
||||
console.log("pix");
|
||||
const cell = wfcImg.cells[i++];
|
||||
if (cell.valid) {
|
||||
ctx.fillStyle = "magenta";
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.previewElement.style.backgroundImage = `url(${canvas.toDataURL()})`;
|
||||
this.previewElement.style.backgroundSize = "100%";
|
||||
}
|
||||
}
|
||||
const palette = [
|
||||
const base_palette = [
|
||||
"#000",
|
||||
"#008",
|
||||
"#00f",
|
||||
"#080",
|
||||
"#088",
|
||||
"#007",
|
||||
"#00F",
|
||||
"#070",
|
||||
"#077",
|
||||
"#0F0",
|
||||
"#0FF",
|
||||
"#0f0",
|
||||
"#0ff",
|
||||
"#800",
|
||||
"#808",
|
||||
"#80f",
|
||||
"#880",
|
||||
"#888",
|
||||
"#88f",
|
||||
"#8f8",
|
||||
"#8ff",
|
||||
"#ccc",
|
||||
"#f00",
|
||||
"#f0f",
|
||||
"#f80",
|
||||
"#f88",
|
||||
"#f8f",
|
||||
"#ff0",
|
||||
"#ff8",
|
||||
"#fff",
|
||||
"#700",
|
||||
"#707",
|
||||
"#770",
|
||||
"#F00",
|
||||
"#F0F",
|
||||
"#FF0",
|
||||
];
|
||||
|
||||
window.painter = new PaintProg(
|
||||
const palette = new Array(base_palette.length * 2);
|
||||
|
||||
base_palette.forEach((color, idx) => {
|
||||
//
|
||||
// Calc inverted color
|
||||
const invR = 15 - Number.parseInt(color.substr(1, 1), 16);
|
||||
const invG = 15 - Number.parseInt(color.substr(2, 1), 16);
|
||||
const invB = 15 - Number.parseInt(color.substr(3, 1), 16);
|
||||
const invColor = sprintf("#%x%x%x", invR, invG, invB);
|
||||
|
||||
// populate the palette
|
||||
palette[idx] = color;
|
||||
palette[7 * 4 - 1 - idx] = invColor;
|
||||
});
|
||||
|
||||
window.painter = new PainApp(
|
||||
9,
|
||||
palette,
|
||||
document.getElementById("gridContainer"), //
|
||||
document.getElementById("colorPalette"), //
|
||||
document.getElementById("currentColor"), //
|
||||
document.getElementById("colorPicker"), //
|
||||
document.getElementById("preview"), //
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// number number of cells per side in the source picture
|
||||
$dim: 9;
|
||||
$pixSize: 80px;
|
||||
$pixSize: 40px;
|
||||
$gridSize: calc($dim * $pixSize);
|
||||
|
||||
body {
|
||||
@@ -35,21 +35,13 @@ body {
|
||||
}
|
||||
|
||||
.pixel {
|
||||
background-color: #ffffff;
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
border: 1px solid #7f8c8d;
|
||||
transition: transform 0.1s ease;
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
|
||||
.subpixel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.pixel:hover {
|
||||
@@ -82,14 +74,14 @@ body {
|
||||
|
||||
.color-palette {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
@@ -102,8 +94,9 @@ body {
|
||||
}
|
||||
|
||||
.color-swatch.active {
|
||||
border-color: #3498db;
|
||||
transform: scale(1.1);
|
||||
outline: 4px solid #fff;
|
||||
border: 1px solid #000;
|
||||
/* transform: scale(1.1); */
|
||||
}
|
||||
|
||||
.tools {
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
const NW = 0;
|
||||
const N = 1;
|
||||
const NE = 2;
|
||||
const E = 3;
|
||||
const C = 4;
|
||||
const W = 5;
|
||||
const SW = 6;
|
||||
const S = 7;
|
||||
const SE = 8;
|
||||
|
||||
export class TrainingImage {
|
||||
/** @param {number} w Width (in pixels) of the training image */
|
||||
/** @param {number} h Height (in pixels) of the training image */
|
||||
/** @param {Array<TrainingPixel>} pixels
|
||||
*
|
||||
*/
|
||||
constructor(w, h, pixels) {
|
||||
this.pixels = pixels;
|
||||
|
||||
this.w = w;
|
||||
|
||||
this.h = h;
|
||||
}
|
||||
|
||||
establishFriendships() {
|
||||
//
|
||||
// This can be optimized a helluvalot much
|
||||
this.pixels.forEach((pix1) => {
|
||||
this.pixels.forEach((pix2) => {
|
||||
pix1.addFriend(pix2);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a 3x3 grid of trianing-pixels, that are used as building blocks for procedurally generated
|
||||
* images. In reality, only the center color will be included in the output image. The 8 surrounding
|
||||
* colors are there to establish restrictions/options for the WFC algorithm.
|
||||
*/
|
||||
export class TrainingPixel {
|
||||
/** @type {string[9]} The 9 sub pixels that make up this TrainingPixel */
|
||||
subPixels;
|
||||
|
||||
/** @type {TrainingPixel[]} The other TrainingPixels that we can share eastern borders with */
|
||||
friendsEast = new Set();
|
||||
|
||||
/** @type {TrainingPixel[]} The other TrainingPixels that we can share western borders with */
|
||||
friendsWest = new Set();
|
||||
|
||||
/** @type {TrainingPixel[]} The other TrainingPixels that we can share northern borders with */
|
||||
friendsNorth = new Set();
|
||||
|
||||
/** @type {TrainingPixel[]} The other TrainingPixels that we can share southern borders with */
|
||||
friendsSouth = new Set();
|
||||
|
||||
/** @type {TrainingPixel[]} Superset of all the friends this TrainingPixel has */
|
||||
friendsTotal = new Set();
|
||||
|
||||
constructor(subPixels) {
|
||||
this.subPixels = subPixels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TrainingPixel} other
|
||||
*
|
||||
* @returns {N,S,E,W,false}
|
||||
*/
|
||||
addFriend(other) {
|
||||
// sadly, we're not allowed to be friends with ourselves.
|
||||
if (this === other) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if other can be placed to the east of me
|
||||
if (
|
||||
// My center col must match their west col
|
||||
this.subPixels[N] === other.subPixels[NW] &&
|
||||
this.subPixels[C] === other.subPixels[W] &&
|
||||
this.subPixels[S] === other.subPixels[SW] &&
|
||||
// My east col must match their center col
|
||||
this.subPixels[NE] === other.subPixels[N] &&
|
||||
this.subPixels[E] === other.subPixels[C] &&
|
||||
this.subPixels[SE] === other.subPixels[S]
|
||||
) {
|
||||
this.friendsEast.add(other);
|
||||
other.friendsWest.add(this);
|
||||
}
|
||||
|
||||
// check if other can be placed west of me
|
||||
if (
|
||||
// My center col must match their east col
|
||||
this.subPixels[N] === other.subPixels[NE] &&
|
||||
this.subPixels[C] === other.subPixels[E] &&
|
||||
this.subPixels[S] === other.subPixels[SE] &&
|
||||
// My west col must match their center col
|
||||
this.subPixels[NW] === other.subPixels[N] &&
|
||||
this.subPixels[W] === other.subPixels[C] &&
|
||||
this.subPixels[SW] === other.subPixels[S]
|
||||
) {
|
||||
this.friendsWest.add(other);
|
||||
other.friendsEast.add(this);
|
||||
}
|
||||
|
||||
// check if other can be placed to my north
|
||||
if (
|
||||
// my middle row must match their south row
|
||||
this.subPixels[W] === other.subPixels[SW] &&
|
||||
this.subPixels[C] === other.subPixels[S] &&
|
||||
this.subPixels[E] === other.subPixels[SE] &&
|
||||
// my north row must match their middle row
|
||||
this.subPixels[NW] === other.subPixels[W] &&
|
||||
this.subPixels[NC] === other.subPixels[C] &&
|
||||
this.subPixels[NE] === other.subPixels[E]
|
||||
) {
|
||||
this.friendsNorth.add(other);
|
||||
other.friendsSouth.add(this);
|
||||
}
|
||||
|
||||
// check if other can be placed to my south
|
||||
if (
|
||||
// my middle row must match their north row
|
||||
this.subPixels[W] === other.subPixels[NW] &&
|
||||
this.subPixels[C] === other.subPixels[N] &&
|
||||
this.subPixels[E] === other.subPixels[SE] &&
|
||||
// my south row must match their middle row
|
||||
this.subPixels[SW] === other.subPixels[W] &&
|
||||
this.subPixels[SC] === other.subPixels[c] &&
|
||||
this.subPixels[SE] === other.subPixels[E]
|
||||
) {
|
||||
this.friendsSouth.add(other);
|
||||
other.friendsNorth.add(this);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
901
node_modules/.package-lock.json
generated
vendored
901
node_modules/.package-lock.json
generated
vendored
File diff suppressed because it is too large
Load Diff
904
package-lock.json
generated
904
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,10 @@
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.35.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9.35.0",
|
||||
"globals": "^16.4.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"prettier": "3.6.2",
|
||||
"sass-embedded": "^1.92.1",
|
||||
|
||||
0
resources/inspiration/castle_ascii_art.txt
Normal file → Executable file
0
resources/inspiration/castle_ascii_art.txt
Normal file → Executable file
0
resources/randomization/random_name_adjectives.txt
Normal file → Executable file
0
resources/randomization/random_name_adjectives.txt
Normal file → Executable file
0
resources/randomization/random_stuff_generator_package.json
Normal file → Executable file
0
resources/randomization/random_stuff_generator_package.json
Normal file → Executable file
@@ -21,13 +21,11 @@ export class Xorshift32 {
|
||||
seed = Math.floor(Math.random() * (maxInt32 - 1)) + 1;
|
||||
}
|
||||
seed = seed | 0;
|
||||
console.info("RNG Initial Seed %d", seed);
|
||||
this.state = Uint32Array.of(seed);
|
||||
}
|
||||
|
||||
/** @protected Shuffle the internal state. */
|
||||
shuffle() {
|
||||
console.log("RNG Shuffle: Initial State: %d", this.state);
|
||||
this.state[0] ^= this.state[0] << 13;
|
||||
this.state[0] ^= this.state[0] >>> 17;
|
||||
this.state[0] ^= this.state[0] << 5;
|
||||
@@ -39,9 +37,6 @@ export class Xorshift32 {
|
||||
// return x;
|
||||
// But we'd have to xor the x with 2^32 after every op,
|
||||
// we get that "for free" by using the uint32array
|
||||
|
||||
console.log("RNG Shuffle: Exit State: %d", this.state);
|
||||
return this.state[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +45,7 @@ export class Xorshift32 {
|
||||
*/
|
||||
get() {
|
||||
this.shuffle();
|
||||
return this.state;
|
||||
return this.state[0];
|
||||
}
|
||||
|
||||
/** @param {number} x @returns {number} a positive integer lower than x */
|
||||
@@ -58,15 +53,15 @@ export class Xorshift32 {
|
||||
return this.get() % x;
|
||||
}
|
||||
|
||||
/** @param {number} x @reurns {number} a positive integer lower than or equal to x */
|
||||
/** @param {number} x @returns {number} a positive integer lower than or equal to x */
|
||||
lowerThanOrEqual(x) {
|
||||
return this.get() % (x + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {<T>[]} arr
|
||||
*
|
||||
* @return {<T>}
|
||||
* @template T
|
||||
* @param {T[]} arr - The array to pick from.
|
||||
* @returns {T} One element from the array. * @return {<T>}
|
||||
*/
|
||||
randomElement(arr) {
|
||||
const idx = this.lowerThan(arr.length);
|
||||
@@ -75,7 +70,7 @@ export class Xorshift32 {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {...<T>} args
|
||||
* @param {...<T>} ... pick random function argument
|
||||
* @returns {<T>}
|
||||
*/
|
||||
oneOf(...args) {
|
||||
@@ -97,3 +92,6 @@ export class Xorshift32 {
|
||||
return num + greaterThanOrEqual;
|
||||
}
|
||||
}
|
||||
|
||||
const rng = new Xorshift32();
|
||||
console.log(rng.get());
|
||||
|
||||
Reference in New Issue
Block a user