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="controls-panel">
|
||||||
<div class="color-picker-section">
|
<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>
|
<h3>Color Palette</h3>
|
||||||
<div class="color-palette" id="colorPalette">
|
<div class="color-palette" id="colorPalette">
|
||||||
<!-- Color swatches will be generated -->
|
<!-- Color swatches will be generated -->
|
||||||
@@ -53,6 +43,7 @@
|
|||||||
<button onclick="painter.exportAsImage()">Export png</button>
|
<button onclick="painter.exportAsImage()">Export png</button>
|
||||||
<button onclick="painter.exportAsData()">Export json</button>
|
<button onclick="painter.exportAsData()">Export json</button>
|
||||||
<button onclick="painter.importData()">Import json</button>
|
<button onclick="painter.importData()">Import json</button>
|
||||||
|
<button onclick="painter.waveFunction()">WaifuCollapse</button>
|
||||||
</div>
|
</div>
|
||||||
</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} */
|
/** @type {string} */
|
||||||
currentColor = "#000";
|
activeColor = "#000";
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
currentTool = "draw";
|
currentTool = "draw";
|
||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
@@ -9,7 +15,7 @@ class PaintProg {
|
|||||||
drawingMode = false;
|
drawingMode = false;
|
||||||
|
|
||||||
/**@param {string[]} pal */
|
/**@param {string[]} pal */
|
||||||
constructor(dim, pal, gridElement, paletteElement, currentColorElement, colorPickerElement, previewElement) {
|
constructor(dim, pal, gridElement, paletteElement, previewElement) {
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
this.dim = dim;
|
this.dim = dim;
|
||||||
/** @type {string[]} Default color palette */
|
/** @type {string[]} Default color palette */
|
||||||
@@ -22,18 +28,19 @@ class PaintProg {
|
|||||||
this.previewElement = previewElement;
|
this.previewElement = previewElement;
|
||||||
/** @type {HTMLElement} */
|
/** @type {HTMLElement} */
|
||||||
this.paletteElement = paletteElement;
|
this.paletteElement = paletteElement;
|
||||||
/** @type {HTMLElement} */
|
|
||||||
this.currentColorElement = currentColorElement;
|
|
||||||
/** @type {HTMLInputElement} */
|
/** @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.createGrid();
|
||||||
this.createColorPalette();
|
this.createColorPalette();
|
||||||
this.updatePreview();
|
this.updatePreview();
|
||||||
this.setCurrentColor(pal[0]);
|
this.setActiveColor(pal[0]);
|
||||||
this.updateAllSubimages();
|
this.updateTrainingGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
createGrid() {
|
createGrid() {
|
||||||
@@ -60,38 +67,28 @@ class PaintProg {
|
|||||||
createColorPalette() {
|
createColorPalette() {
|
||||||
this.paletteElement.innerHTML = "";
|
this.paletteElement.innerHTML = "";
|
||||||
|
|
||||||
this.palette.forEach((color) => {
|
this.palette.forEach((color, paletteIndex) => {
|
||||||
const swatch = document.createElement("div");
|
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.style.backgroundColor = color;
|
||||||
swatch.onclick = () => this.setCurrentColor(color);
|
swatch.onclick = () => this.setActiveColor(paletteIndex);
|
||||||
this.paletteElement.appendChild(swatch);
|
this.paletteElement.appendChild(swatch);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentColor(color) {
|
setActiveColor(paletteIndex) {
|
||||||
this.currentColor = color;
|
//
|
||||||
this.currentColorElement.style.backgroundColor = color;
|
this.activeColor = this.palette[paletteIndex];
|
||||||
this.colorPickerElement.value = color;
|
|
||||||
|
|
||||||
// Update active swatch
|
const colorSwatches = this.paletteElement.querySelectorAll(".color-swatch");
|
||||||
// NOTE: this was "document.querySelectorAll "
|
colorSwatches.forEach((swatch) => {
|
||||||
this.paletteElement.querySelectorAll(".color-swatch").forEach((swatch) => {
|
const isActive = swatch.classList.contains(`pal-idx-${paletteIndex}`);
|
||||||
swatch.classList.toggle("active", swatch.style.backgroundColor === this.colorToRgb(color));
|
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) {
|
setTool(tool) {
|
||||||
this.currentTool = tool;
|
this.currentTool = tool;
|
||||||
document.querySelectorAll(".tools button").forEach((btn) => btn.classList.remove("active"));
|
document.querySelectorAll(".tools button").forEach((btn) => btn.classList.remove("active"));
|
||||||
@@ -135,7 +132,7 @@ class PaintProg {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateSingleSubImage(index);
|
this.updateTrainingCell(index);
|
||||||
this.updatePreview(index);
|
this.updatePreview(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,10 +143,10 @@ class PaintProg {
|
|||||||
applyTool(index) {
|
applyTool(index) {
|
||||||
switch (this.currentTool) {
|
switch (this.currentTool) {
|
||||||
case "draw":
|
case "draw":
|
||||||
this.setPixel(index, this.currentColor);
|
this.setPixel(index, this.activeColor);
|
||||||
break;
|
break;
|
||||||
case "fill":
|
case "fill":
|
||||||
this.floodFill(index, this.samplePixels[index], this.currentColor);
|
this.floodFill(index, this.samplePixels[index], this.activeColor);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,7 +243,7 @@ class PaintProg {
|
|||||||
//
|
//
|
||||||
const x = i % 3;
|
const x = i % 3;
|
||||||
const y = Math.floor(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);
|
ctx.fillRect(x, y, 1, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,28 +252,24 @@ class PaintProg {
|
|||||||
this.previewElement.style.backgroundSize = "100%";
|
this.previewElement.style.backgroundSize = "100%";
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAllSubimages() {
|
updateTrainingGrid() {
|
||||||
for (let i = 0; i < this.samplePixels.length; i++) {
|
for (let i = 0; i < this.samplePixels.length; i++) {
|
||||||
this.updateSingleSubImage(i);
|
this.updateTrainingCell(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSingleSubImage(i) {
|
updateTrainingCell(i) {
|
||||||
const dim = this.dim;
|
const dim = this.dim;
|
||||||
const len = dim ** 2;
|
|
||||||
const x = i % dim;
|
const x = i % dim;
|
||||||
const y = Math.floor(i / dim);
|
const y = Math.floor(i / dim);
|
||||||
|
|
||||||
const colorAt = (dX, dY) => {
|
const colorAt = (dX, dY) => {
|
||||||
const _x = (x + dim + dX) % dim; // add dim before modulo because JS modulo allows negative results
|
const _x = (x + dim + dX) % dim; // add dim before modulo because JS modulo allows negative results
|
||||||
const _y = (y + dim + dY) % dim;
|
const _y = (y + dim + dY) % dim;
|
||||||
if (y == 0 && dY < 0) {
|
|
||||||
console.log(_x, _y);
|
|
||||||
}
|
|
||||||
return this.samplePixels[_y * dim + _x];
|
return this.samplePixels[_y * dim + _x];
|
||||||
};
|
};
|
||||||
|
|
||||||
this.subImages[i] = [
|
this.trainingImage.pixels[i] = new TrainingCell([
|
||||||
// | neighbour
|
// | neighbour
|
||||||
// ---------------------|-----------
|
// ---------------------|-----------
|
||||||
colorAt(-1, -1), // | northwest
|
colorAt(-1, -1), // | northwest
|
||||||
@@ -290,21 +283,21 @@ class PaintProg {
|
|||||||
colorAt(-1, 1), // | southwest
|
colorAt(-1, 1), // | southwest
|
||||||
colorAt(0, 1), // | south
|
colorAt(0, 1), // | south
|
||||||
colorAt(1, 1), // | southeast
|
colorAt(1, 1), // | southeast
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
exportAsImage() {
|
exportAsImage() {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = 90; // 9x upscale
|
canvas.width = this.dim; // 9x upscale
|
||||||
canvas.height = 90;
|
canvas.height = this.dim;
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
for (let i = 0; i < this.dim ** 2; i++) {
|
for (let i = 0; i < this.dim ** 2; i++) {
|
||||||
const x = (i % this.dim) * 10;
|
const x = i % this.dim;
|
||||||
const y = Math.floor(i / this.dim) * 10;
|
const y = Math.floor(i / this.dim);
|
||||||
ctx.fillStyle = this.samplePixels[i];
|
ctx.fillStyle = this.samplePixels[i];
|
||||||
ctx.fillRect(x, y, 10, 10);
|
ctx.fillRect(x, y, 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
@@ -341,7 +334,7 @@ class PaintProg {
|
|||||||
alert("Invalid data format!");
|
alert("Invalid data format!");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert("Error reading file!");
|
alert("Error reading file!" + error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
@@ -350,42 +343,86 @@ class PaintProg {
|
|||||||
input.click();
|
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",
|
"#000",
|
||||||
"#008",
|
"#007",
|
||||||
"#00f",
|
"#00F",
|
||||||
"#080",
|
"#070",
|
||||||
"#088",
|
"#077",
|
||||||
|
"#0F0",
|
||||||
|
"#0FF",
|
||||||
"#0f0",
|
"#0f0",
|
||||||
"#0ff",
|
"#700",
|
||||||
"#800",
|
"#707",
|
||||||
"#808",
|
"#770",
|
||||||
"#80f",
|
"#F00",
|
||||||
"#880",
|
"#F0F",
|
||||||
"#888",
|
"#FF0",
|
||||||
"#88f",
|
|
||||||
"#8f8",
|
|
||||||
"#8ff",
|
|
||||||
"#ccc",
|
|
||||||
"#f00",
|
|
||||||
"#f0f",
|
|
||||||
"#f80",
|
|
||||||
"#f88",
|
|
||||||
"#f8f",
|
|
||||||
"#ff0",
|
|
||||||
"#ff8",
|
|
||||||
"#fff",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
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,
|
9,
|
||||||
palette,
|
palette,
|
||||||
document.getElementById("gridContainer"), //
|
document.getElementById("gridContainer"), //
|
||||||
document.getElementById("colorPalette"), //
|
document.getElementById("colorPalette"), //
|
||||||
document.getElementById("currentColor"), //
|
|
||||||
document.getElementById("colorPicker"), //
|
|
||||||
document.getElementById("preview"), //
|
document.getElementById("preview"), //
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// number number of cells per side in the source picture
|
// number number of cells per side in the source picture
|
||||||
$dim: 9;
|
$dim: 9;
|
||||||
$pixSize: 80px;
|
$pixSize: 40px;
|
||||||
$gridSize: calc($dim * $pixSize);
|
$gridSize: calc($dim * $pixSize);
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -35,21 +35,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pixel {
|
.pixel {
|
||||||
background-color: #ffffff;
|
background-color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid #7f8c8d;
|
border: 1px solid #7f8c8d;
|
||||||
transition: transform 0.1s ease;
|
transition: transform 0.1s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
grid-template-rows: repeat(3, 1fr);
|
|
||||||
|
|
||||||
.subpixel {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pixel:hover {
|
.pixel:hover {
|
||||||
@@ -82,14 +74,14 @@ body {
|
|||||||
|
|
||||||
.color-palette {
|
.color-palette {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(8, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-swatch {
|
.color-swatch {
|
||||||
width: 25px;
|
width: 28px;
|
||||||
height: 25px;
|
height: 28px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -102,8 +94,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.color-swatch.active {
|
.color-swatch.active {
|
||||||
border-color: #3498db;
|
outline: 4px solid #fff;
|
||||||
transform: scale(1.1);
|
border: 1px solid #000;
|
||||||
|
/* transform: scale(1.1); */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tools {
|
.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"
|
"ws": "^8.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.35.0",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
|
"eslint": "^9.35.0",
|
||||||
|
"globals": "^16.4.0",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"sass-embedded": "^1.92.1",
|
"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 = Math.floor(Math.random() * (maxInt32 - 1)) + 1;
|
||||||
}
|
}
|
||||||
seed = seed | 0;
|
seed = seed | 0;
|
||||||
console.info("RNG Initial Seed %d", seed);
|
|
||||||
this.state = Uint32Array.of(seed);
|
this.state = Uint32Array.of(seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @protected Shuffle the internal state. */
|
/** @protected Shuffle the internal state. */
|
||||||
shuffle() {
|
shuffle() {
|
||||||
console.log("RNG Shuffle: Initial State: %d", this.state);
|
|
||||||
this.state[0] ^= this.state[0] << 13;
|
this.state[0] ^= this.state[0] << 13;
|
||||||
this.state[0] ^= this.state[0] >>> 17;
|
this.state[0] ^= this.state[0] >>> 17;
|
||||||
this.state[0] ^= this.state[0] << 5;
|
this.state[0] ^= this.state[0] << 5;
|
||||||
@@ -39,9 +37,6 @@ export class Xorshift32 {
|
|||||||
// return x;
|
// return x;
|
||||||
// But we'd have to xor the x with 2^32 after every op,
|
// But we'd have to xor the x with 2^32 after every op,
|
||||||
// we get that "for free" by using the uint32array
|
// 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() {
|
get() {
|
||||||
this.shuffle();
|
this.shuffle();
|
||||||
return this.state;
|
return this.state[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {number} x @returns {number} a positive integer lower than x */
|
/** @param {number} x @returns {number} a positive integer lower than x */
|
||||||
@@ -58,15 +53,15 @@ export class Xorshift32 {
|
|||||||
return this.get() % x;
|
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) {
|
lowerThanOrEqual(x) {
|
||||||
return this.get() % (x + 1);
|
return this.get() % (x + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {<T>[]} arr
|
* @template T
|
||||||
*
|
* @param {T[]} arr - The array to pick from.
|
||||||
* @return {<T>}
|
* @returns {T} One element from the array. * @return {<T>}
|
||||||
*/
|
*/
|
||||||
randomElement(arr) {
|
randomElement(arr) {
|
||||||
const idx = this.lowerThan(arr.length);
|
const idx = this.lowerThan(arr.length);
|
||||||
@@ -75,7 +70,7 @@ export class Xorshift32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {...<T>} args
|
* @param {...<T>} ... pick random function argument
|
||||||
* @returns {<T>}
|
* @returns {<T>}
|
||||||
*/
|
*/
|
||||||
oneOf(...args) {
|
oneOf(...args) {
|
||||||
@@ -97,3 +92,6 @@ export class Xorshift32 {
|
|||||||
return num + greaterThanOrEqual;
|
return num + greaterThanOrEqual;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rng = new Xorshift32();
|
||||||
|
console.log(rng.get());
|
||||||
|
|||||||
Reference in New Issue
Block a user