rearrage_stuff
This commit is contained in:
472
frontend/client.js
Executable file
472
frontend/client.js
Executable file
@@ -0,0 +1,472 @@
|
||||
import { crackdown } from "../utils/crackdown.js";
|
||||
import { parseArgs } from "../utils/parseArgs.js";
|
||||
import { MessageType } from "../utils/messages.js";
|
||||
import { sprintf } from "sprintf-js";
|
||||
|
||||
/** Regex to validate if a :help [topic] command i entered correctly */
|
||||
const helpRegex = /^:help(?:\s+(.*))?$/;
|
||||
|
||||
/** Regex to validate if a :<command> [args] was entered correctly */
|
||||
const colonRegex = /^:([a-z0-9_]+)(?:\s+(.*?)\s*)?$/;
|
||||
|
||||
/**
|
||||
* The client that talks to the MUD Sever
|
||||
*/
|
||||
class MUDClient {
|
||||
//
|
||||
// Constructor
|
||||
constructor() {
|
||||
/** @type {WebSocket} Our WebSocket */
|
||||
this.websocket = null;
|
||||
|
||||
/** @type {boolean} Are we in development mode (decided by the server); */
|
||||
this.isDev = false;
|
||||
|
||||
this.promptOptions = {};
|
||||
this.shouldReply = false;
|
||||
|
||||
/** @type {HTMLElement} The output "monitor" */
|
||||
this.output = document.getElementById("output");
|
||||
|
||||
/** @type {HTMLElement} The input element */
|
||||
this.input = document.getElementById("input");
|
||||
|
||||
/** @type {HTMLElement} Status indicator */
|
||||
this.status = document.getElementById("status");
|
||||
|
||||
// Passwords are crypted and salted before being sent to the server
|
||||
// This means that if ANY of these three parameters below change,
|
||||
// The server can no longer accept the passwords.
|
||||
/** @type {string} Hashing method to use for client-side password hashing */
|
||||
this.digest = "SHA-256";
|
||||
|
||||
/** @type {string} Salt string to use for client-side password hashing */
|
||||
this.salt = "no_salt_no shorts_no_service";
|
||||
|
||||
/** @type {string} Number of times the hashing should be done */
|
||||
this.rounds = 1000;
|
||||
|
||||
/** @type {string} the username also salts the password, so the username must never change. */
|
||||
this.username = "";
|
||||
|
||||
this.setupEventListeners();
|
||||
this.connect();
|
||||
}
|
||||
|
||||
/** @param {string} password the password to be hashed */
|
||||
async hashPassword(password) {
|
||||
const encoder = new TextEncoder();
|
||||
let data = encoder.encode(password + this.salt);
|
||||
|
||||
for (let i = 0; i < this.rounds; i++) {
|
||||
const hashBuffer = await crypto.subtle.digest(this.digest, data);
|
||||
data = new Uint8Array(hashBuffer); // feed hash back in
|
||||
}
|
||||
|
||||
// Convert final hash to hex
|
||||
const rawHash = Array.from(data)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
|
||||
return `KimsKrappyKryptoV1:${this.salt}:${this.rounds}:${this.digest}:${rawHash}`;
|
||||
}
|
||||
|
||||
connect() {
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
// TODO Fix. Port should not be hardcoded
|
||||
const wsUrl = `${protocol}//${window.location.host}`.replace(/:\d+$/, ":3000");
|
||||
|
||||
console.log(wsUrl);
|
||||
|
||||
this.updateStatus("Connecting...", "connecting");
|
||||
|
||||
try {
|
||||
this.websocket = new WebSocket(wsUrl);
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
this.updateStatus("Connected", "connected");
|
||||
this.input.disabled = false;
|
||||
this.input.focus();
|
||||
this.output.innerHTML = "";
|
||||
};
|
||||
|
||||
this.websocket.onmessage = (event) => {
|
||||
console.debug(event);
|
||||
const data = JSON.parse(event.data);
|
||||
this.onMessageReceived(data);
|
||||
this.input.focus();
|
||||
};
|
||||
|
||||
this.websocket.onclose = () => {
|
||||
this.updateStatus("Disconnected", "disconnected");
|
||||
this.input.disabled = true;
|
||||
|
||||
// Attempt to reconnect after 3 seconds
|
||||
setTimeout(() => this.connect(), 3000);
|
||||
};
|
||||
|
||||
this.websocket.onerror = (error) => {
|
||||
this.updateStatus("Connection Error", "error");
|
||||
this.writeToOutput("Connection error occurred. Retrying...", { class: "error" });
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.updateStatus("Connection Failed", "error");
|
||||
setTimeout(() => this.connect(), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
document.addEventListener("keypress", (e) => {
|
||||
if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||
this.input.focus();
|
||||
}
|
||||
});
|
||||
this.input.addEventListener("keypress", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
this.onUserCommand();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a json-encoded message to the server via websocket.
|
||||
*
|
||||
* @param {messageType} string
|
||||
* @param {...any} rest
|
||||
*/
|
||||
send(messageType, ...args) {
|
||||
console.log("sending", messageType, args);
|
||||
|
||||
if (args.length === 0) {
|
||||
this.websocket.send(JSON.stringify([messageType]));
|
||||
return;
|
||||
}
|
||||
|
||||
this.websocket.send(JSON.stringify([messageType, ...args]));
|
||||
}
|
||||
|
||||
/**
|
||||
* User has entered a command
|
||||
*/
|
||||
async onUserCommand() {
|
||||
/** @type {string} */
|
||||
const inputText = this.input.value.trim(); // Trim user's input.
|
||||
|
||||
this.input.value = ""; // Reset the input text field
|
||||
this.input.type = "text"; // Make sure it reverts to being a text input (as opposed to being a password input)
|
||||
|
||||
// -- This is a sneaky command that should not be in production?
|
||||
//
|
||||
// In reality we want to use :clear, nor /clear
|
||||
// :clear would be sent to the server, and we ask if it's okay
|
||||
// to clear the screen right now, and only on a positive answer would we
|
||||
// allow the screen to be cleared. Maybe.....
|
||||
if (inputText === "/clear") {
|
||||
this.output.innerHTML = "";
|
||||
this.input.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Don't allow sending messages (for now)
|
||||
// Later on, prompts may give us the option to simply "press enter";
|
||||
if (!inputText) {
|
||||
console.debug("Cannot send empty message - YET");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Can't send a message without a websocket
|
||||
if (!(this.websocket && this.websocket.readyState === WebSocket.OPEN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// The quit command has its own message type
|
||||
if (inputText === ":quit") {
|
||||
this.send(MessageType.QUIT);
|
||||
this.echo(inputText);
|
||||
return;
|
||||
}
|
||||
|
||||
// _ _
|
||||
// _ | |__ ___| |_ __
|
||||
// (_) | '_ \ / _ \ | '_ \
|
||||
// _ | | | | __/ | |_) |
|
||||
// (_) |_| |_|\___|_| .__/
|
||||
// |_|
|
||||
// ------------------------
|
||||
//
|
||||
// The quit command has its own message type
|
||||
let help = helpRegex.exec(inputText);
|
||||
if (help) {
|
||||
console.log("here");
|
||||
help[1] ? this.send(MshType.HELP, help[1].trim()) : this.send(MshType.HELP);
|
||||
this.echo(inputText);
|
||||
return;
|
||||
}
|
||||
|
||||
// _
|
||||
// _ ___ ___ _ __ ___ _ __ ___ __ _ _ __ __| |
|
||||
// (_) / __/ _ \| '_ ` _ \| '_ ` _ \ / _` | '_ \ / _` |
|
||||
// _ | (_| (_) | | | | | | | | | | | (_| | | | | (_| |
|
||||
// (_) \___\___/|_| |_| |_|_| |_| |_|\__,_|_| |_|\__,_|
|
||||
//------------------------------------------------------
|
||||
const colon = colonRegex.exec(inputText);
|
||||
if (colon) {
|
||||
const args = typeof colon[2] === "string" ? parseArgs(colon[2]) : [];
|
||||
this.send(MessageType.COLON, colon[1], args);
|
||||
this.echo(inputText);
|
||||
return;
|
||||
}
|
||||
|
||||
// _
|
||||
// _ __ ___ _ __ | |_ _
|
||||
// | '__/ _ \ '_ \| | | | |
|
||||
// | | | __/ |_) | | |_| |
|
||||
// |_| \___| .__/|_|\__, |
|
||||
// |_| |___/
|
||||
//-------------------------
|
||||
// We handle replies below
|
||||
//-------------------------
|
||||
|
||||
//
|
||||
if (!this.shouldReply) {
|
||||
// the server is not ready for data!
|
||||
return;
|
||||
}
|
||||
|
||||
// The server wants a password, let's hash it before sending it.
|
||||
if (this.promptOptions.password) {
|
||||
const pwHash = await this.hashPassword(inputText);
|
||||
this.send(MessageType.REPLY, pwHash);
|
||||
this.shouldReply = false;
|
||||
this.promptOptions = {};
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// The server wants a username, let's save it in case we need it.
|
||||
if (this.promptOptions.username) {
|
||||
this.username = inputText;
|
||||
}
|
||||
|
||||
this.send(MessageType.REPLY, inputText);
|
||||
this.shouldReply = false;
|
||||
this.promptOptions = {};
|
||||
|
||||
//
|
||||
// We add our own command to the output stream so the
|
||||
// player can see what they typed.
|
||||
this.echo(inputText);
|
||||
return;
|
||||
}
|
||||
|
||||
// ___ __ __
|
||||
// / _ \ _ __ | \/ | ___ ___ ___ __ _ __ _ ___
|
||||
// | | | | '_ \| |\/| |/ _ \/ __/ __|/ _` |/ _` |/ _ \
|
||||
// | |_| | | | | | | | __/\__ \__ \ (_| | (_| | __/
|
||||
// \___/|_| |_|_| |_|\___||___/___/\__,_|\__, |\___|
|
||||
//
|
||||
/** @param {any[]} data*/
|
||||
onMessageReceived(data) {
|
||||
if (this.isDev) {
|
||||
console.debug(data);
|
||||
}
|
||||
const messageType = data.shift();
|
||||
|
||||
// prompt
|
||||
if (messageType === "P") {
|
||||
return this.handlePromptMessage(data);
|
||||
}
|
||||
|
||||
// text message
|
||||
if (messageType === "T") {
|
||||
return this.handleTextMessages(data);
|
||||
}
|
||||
|
||||
// error
|
||||
if (messageType === "E") {
|
||||
return this.handleErrorMessage(data);
|
||||
}
|
||||
|
||||
// fatal error / calamity
|
||||
if (messageType === "CALAMITY") {
|
||||
return this.handleCalamityMessage(data);
|
||||
}
|
||||
|
||||
// system message
|
||||
if (messageType === "_") {
|
||||
return this.handleSystemMessages(data);
|
||||
}
|
||||
// debug
|
||||
if (messageType === "dbg") {
|
||||
return this.handleDebugMessages(data);
|
||||
}
|
||||
|
||||
if (this.isDev) {
|
||||
this.writeToOutput(`unknown message type: ${messageType}: ${JSON.stringify(data)}`, {
|
||||
class: "debug",
|
||||
verbatim: true,
|
||||
});
|
||||
}
|
||||
console.debug("unknown message type", data);
|
||||
}
|
||||
|
||||
//
|
||||
// "m" => normal/standard message to be displayed to the user
|
||||
handleTextMessages(data) {
|
||||
const options = { ...data[1] }; // coerce options into an object.
|
||||
|
||||
// normal text message to be shown to the player
|
||||
this.writeToOutput(data[0], options);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Debug messages let the server send data to be displayed on the player's screen
|
||||
// and also logged to the players browser's log.
|
||||
handleDebugMessages(data) {
|
||||
if (!this.isDev) {
|
||||
return; // debug messages are thrown away if we're not in dev mode.
|
||||
}
|
||||
this.writeToOutput(data, { class: "debug", verbatim: true });
|
||||
console.debug("DBG", data);
|
||||
}
|
||||
|
||||
//
|
||||
// "_" => system messages, not to be displayed
|
||||
handleSystemMessages(data) {
|
||||
if (data.length < 2) {
|
||||
console.debug("malformed system message", data);
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug("Incoming system message", data);
|
||||
|
||||
/** @type {string} */
|
||||
const systemMessageType = data.shift();
|
||||
|
||||
switch (systemMessageType) {
|
||||
case "username":
|
||||
this.username = data[0];
|
||||
break;
|
||||
case "dev":
|
||||
// This is a message that tells us that the server is in
|
||||
// "dev" mode, and that we should do the same.
|
||||
this.isDev = !!data[0];
|
||||
this.status.textContent = "[DEV] " + this.status.textContent;
|
||||
break;
|
||||
case "salt":
|
||||
this.salt = data[0];
|
||||
console.debug("updating crypto salt", data[0]);
|
||||
break;
|
||||
default:
|
||||
console.debug("unknown system message", systemMessageType, data);
|
||||
}
|
||||
|
||||
// If we're in dev mode, we should output all system messages (in a shaded/faint fashion).
|
||||
if (this.isDev) {
|
||||
this.writeToOutput(`system message: ${systemMessageType} = ${JSON.stringify(data)}`, { class: "debug" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// "calamity" => lethal error. Close connection.
|
||||
// Consider hard refresh of page to reset all variables
|
||||
handleCalamityMessage(data) {
|
||||
//
|
||||
// We assume that calamity errors are pre-formatted, and we do not allow
|
||||
// any of our own formatting-shenanigans to interfere with the error message
|
||||
const options = { ...{ class: "error", verbatim: true }, ...data[1] };
|
||||
this.writeToOutput(data[0], options);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// "e" => non-lethal errors
|
||||
handleErrorMessage(data) {
|
||||
const options = { ...{ class: "error" }, ...data[1] };
|
||||
this.writeToOutput(data[0], options);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// The prompt is the most important message type,
|
||||
// it prompts us send a message back. We should not
|
||||
// send messages back to the server without being
|
||||
// prompted.
|
||||
// In fact, we should ALWAYS be in a state of just-having-been-prompted.
|
||||
handlePromptMessage(data) {
|
||||
let [promptText, options = {}] = data;
|
||||
|
||||
this.shouldReply = true;
|
||||
|
||||
this.promptOptions = { ...{ class: "prompt" }, ...options };
|
||||
|
||||
//
|
||||
this.writeToOutput(promptText, this.promptOptions);
|
||||
|
||||
//
|
||||
// The server has asked for a password, so we set the
|
||||
// input type to password for safety reasons.
|
||||
if (options.password) {
|
||||
this.input.type = "password";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
echo(text) {
|
||||
this.writeToOutput(text, { verbatim: true, echo: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add output to the text.
|
||||
* @param {string} text
|
||||
* @param {object} options
|
||||
*/
|
||||
writeToOutput(text, options = {}) {
|
||||
// tweak the data-formatting so we can iterate and create multiple elements
|
||||
const lines = Array.isArray(text) ? text : [text];
|
||||
|
||||
for (const line of lines) {
|
||||
const element = document.createElement("div");
|
||||
|
||||
if (options.verbatim) {
|
||||
element.textContent = line;
|
||||
element.className = "verbatim";
|
||||
} else {
|
||||
element.innerHTML = crackdown(line);
|
||||
}
|
||||
|
||||
for (const cls of ["calamity", "error", "debug", "prompt", "echo"]) {
|
||||
if (options[cls]) {
|
||||
element.classList.add(cls);
|
||||
}
|
||||
}
|
||||
|
||||
this.output.appendChild(element);
|
||||
this.output.scrollTop = this.output.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status banner.
|
||||
*
|
||||
* @param {string} message
|
||||
* @param {string} className
|
||||
*/
|
||||
updateStatus(message, className) {
|
||||
this.status.textContent = this.isDev ? `[DEV] Status: ${message}` : `Status: ${message}`;
|
||||
this.status.className = className;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the MUD client when the page loads
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
new MUDClient();
|
||||
});
|
||||
BIN
frontend/favicon.ico
Executable file
BIN
frontend/favicon.ico
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
frontend/img/android-chrome-192x192.png
Executable file
BIN
frontend/img/android-chrome-192x192.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
BIN
frontend/img/android-chrome-512x512.png
Executable file
BIN
frontend/img/android-chrome-512x512.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 448 KiB |
BIN
frontend/img/apple-touch-icon.png
Executable file
BIN
frontend/img/apple-touch-icon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
frontend/img/favicon-16x16.png
Executable file
BIN
frontend/img/favicon-16x16.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 783 B |
BIN
frontend/img/favicon-32x32.png
Executable file
BIN
frontend/img/favicon-32x32.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
33
frontend/index.html
Executable file
33
frontend/index.html
Executable file
@@ -0,0 +1,33 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WebSocket MUD</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet" href="style.scss" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<lin rel="icon" href="/favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="status" class="connecting">Connecting...</div>
|
||||
<div id="output"></div>
|
||||
<div id="input-container">
|
||||
<input
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
id="input"
|
||||
placeholder="Enter command..."
|
||||
disabled
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="client.js"></script>
|
||||
//# sourceMappingURL=
|
||||
</body>
|
||||
</html>
|
||||
11
frontend/manifest.json
Executable file
11
frontend/manifest.json
Executable file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
61
frontend/progen.html
Executable file
61
frontend/progen.html
Executable file
@@ -0,0 +1,61 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WebSocket MUD</title>
|
||||
<link rel="stylesheet" href="progen.scss" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="editor-container">
|
||||
<h1>9×9 Pixel Art Editor</h1>
|
||||
<div class="grid-container" id="gridContainer" onmouseleave="painter.updatePreview()">
|
||||
<!-- Pixels will be generated by JavaScript -->
|
||||
</div>
|
||||
<div class="status" id="status">Click a pixel to start drawing</div>
|
||||
</div>
|
||||
|
||||
<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 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tools">
|
||||
<div class="tool-group">
|
||||
<h3>Tools</h3>
|
||||
<button id="drawBtn" class="active" onclick="painter.setTool('draw')">Draw</button>
|
||||
<button id="fillBtn" onclick="painter.setTool('fill')">Fill</button>
|
||||
<button onclick="painter.toggleDrawingMode()" id="drawModeBtn">Toggle Drawing Mode</button>
|
||||
</div>
|
||||
|
||||
<div class="tool-group">
|
||||
<button onclick="painter.clearCanvas()">Clear All</button>
|
||||
<button onclick="painter.randomFill()">Random Fill</button>
|
||||
<button onclick="painter.invertColors()">Invert</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="export-section">
|
||||
<div class="preview" id="preview"></div>
|
||||
<button onclick="painter.exportAsImage()">Export png</button>
|
||||
<button onclick="painter.exportAsData()">Export json</button>
|
||||
<button onclick="painter.importData()">Import json</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="progen.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
393
frontend/progen.js
Executable file
393
frontend/progen.js
Executable file
@@ -0,0 +1,393 @@
|
||||
class PaintProg {
|
||||
/** @type {string} */
|
||||
currentColor = "#000";
|
||||
/** @type {string} */
|
||||
currentTool = "draw";
|
||||
/** @type {boolean} */
|
||||
isDrawing = false;
|
||||
/** @type {boolean} */
|
||||
drawingMode = false;
|
||||
|
||||
/**@param {string[]} pal */
|
||||
constructor(dim, pal, gridElement, paletteElement, currentColorElement, colorPickerElement, previewElement) {
|
||||
/** @type {number} */
|
||||
this.dim = dim;
|
||||
/** @type {string[]} Default color palette */
|
||||
this.palette = pal;
|
||||
/** @type {string[]} */
|
||||
this.samplePixels = new Array(dim ** 2).fill(pal[pal.length - 1]);
|
||||
/** @type {HTMLElement} */
|
||||
this.gridElement = gridElement;
|
||||
/** @type {HTMLElement} */
|
||||
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.createGrid();
|
||||
this.createColorPalette();
|
||||
this.updatePreview();
|
||||
this.setCurrentColor(pal[0]);
|
||||
this.updateAllSubimages();
|
||||
}
|
||||
|
||||
createGrid() {
|
||||
this.gridElement.innerHTML = "";
|
||||
|
||||
for (let i = 0; i < this.dim ** 2; i++) {
|
||||
const pixel = document.createElement("div");
|
||||
pixel.className = "pixel";
|
||||
pixel.setAttribute("data-index", i);
|
||||
pixel.style.backgroundColor = this.samplePixels[i];
|
||||
|
||||
pixel.addEventListener("mousedown", (e) => this.startDrawing(e, i));
|
||||
pixel.addEventListener("mouseenter", (e) => this.continueDrawing(e, i));
|
||||
pixel.addEventListener("mouseup", () => this.stopDrawing());
|
||||
|
||||
this.gridElement.appendChild(pixel);
|
||||
}
|
||||
|
||||
// Prevent context menu and handle mouse events
|
||||
this.gridElement.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||
document.addEventListener("mouseup", () => this.stopDrawing());
|
||||
}
|
||||
|
||||
createColorPalette() {
|
||||
this.paletteElement.innerHTML = "";
|
||||
|
||||
this.palette.forEach((color) => {
|
||||
const swatch = document.createElement("div");
|
||||
swatch.className = "color-swatch";
|
||||
swatch.style.backgroundColor = color;
|
||||
swatch.onclick = () => this.setCurrentColor(color);
|
||||
this.paletteElement.appendChild(swatch);
|
||||
});
|
||||
}
|
||||
|
||||
setCurrentColor(color) {
|
||||
this.currentColor = color;
|
||||
this.currentColorElement.style.backgroundColor = color;
|
||||
this.colorPickerElement.value = color;
|
||||
|
||||
// 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));
|
||||
});
|
||||
}
|
||||
|
||||
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"));
|
||||
document.getElementById(tool + "Btn").classList.add("active");
|
||||
|
||||
const status = document.getElementById("status");
|
||||
switch (tool) {
|
||||
case "draw":
|
||||
status.textContent = "Drawing mode - Click to paint pixels";
|
||||
break;
|
||||
case "fill":
|
||||
status.textContent = "Fill mode - Click to fill area";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
toggleDrawingMode() {
|
||||
this.drawingMode = !this.drawingMode;
|
||||
const btn = document.getElementById("drawModeBtn");
|
||||
if (this.drawingMode) {
|
||||
btn.textContent = "Drawing Mode: ON";
|
||||
btn.classList.add("drawing-mode");
|
||||
document.getElementById("status").textContent = "Drawing mode ON - Click and drag to paint";
|
||||
} else {
|
||||
btn.textContent = "Drawing Mode: OFF";
|
||||
btn.classList.remove("drawing-mode");
|
||||
document.getElementById("status").textContent = "Drawing mode OFF - Click individual pixels";
|
||||
}
|
||||
}
|
||||
|
||||
startDrawing(e, index) {
|
||||
e.preventDefault();
|
||||
this.isDrawing = true;
|
||||
this.applyTool(index);
|
||||
}
|
||||
|
||||
/** @param {MouseEvent} e */
|
||||
continueDrawing(e, index) {
|
||||
if (this.isDrawing && this.drawingMode) {
|
||||
this.applyTool(index);
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateSingleSubImage(index);
|
||||
this.updatePreview(index);
|
||||
}
|
||||
|
||||
stopDrawing() {
|
||||
this.isDrawing = false;
|
||||
}
|
||||
|
||||
applyTool(index) {
|
||||
switch (this.currentTool) {
|
||||
case "draw":
|
||||
this.setPixel(index, this.currentColor);
|
||||
break;
|
||||
case "fill":
|
||||
this.floodFill(index, this.samplePixels[index], this.currentColor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setPixel(index, color) {
|
||||
this.samplePixels[index] = color;
|
||||
const pixel = document.querySelector(`[data-index="${index}"]`);
|
||||
pixel.style.backgroundColor = color;
|
||||
this.updatePreview();
|
||||
}
|
||||
|
||||
floodFill(startIndex, targetColor, fillColor) {
|
||||
if (targetColor === fillColor) return;
|
||||
|
||||
const stack = [startIndex];
|
||||
const visited = new Set();
|
||||
|
||||
while (stack.length > 0) {
|
||||
const index = stack.pop();
|
||||
if (visited.has(index) || this.samplePixels[index] !== targetColor) continue;
|
||||
|
||||
visited.add(index);
|
||||
this.setPixel(index, fillColor);
|
||||
|
||||
// Add neighbors
|
||||
const row = Math.floor(index / this.dim);
|
||||
const col = index % this.dim;
|
||||
|
||||
if (row > 0) stack.push(index - this.dim); // up
|
||||
if (row < 8) stack.push(index + this.dim); // down
|
||||
if (col > 0) stack.push(index - 1); // left
|
||||
if (col < 8) stack.push(index + 1); // right
|
||||
}
|
||||
}
|
||||
|
||||
clearCanvas() {
|
||||
this.samplePixels.fill("#fff");
|
||||
this.createGrid();
|
||||
this.updatePreview();
|
||||
}
|
||||
|
||||
randomFill() {
|
||||
for (let i = 0; i < this.dim ** 2; i++) {
|
||||
const randomColor = this.palette[Math.floor(Math.random() * this.palette.length)];
|
||||
this.setPixel(i, randomColor);
|
||||
}
|
||||
}
|
||||
|
||||
invertColors() {
|
||||
for (let i = 0; i < this.dim ** 2; i++) {
|
||||
const color = this.samplePixels[i];
|
||||
const r = 15 - parseInt(color.substr(1, 1), 16);
|
||||
const g = 15 - parseInt(color.substr(2, 1), 16);
|
||||
const b = 15 - parseInt(color.substr(3, 1), 16);
|
||||
const inverted =
|
||||
"#" +
|
||||
r.toString(16) + // red
|
||||
g.toString(16) + // green
|
||||
b.toString(16); // blue
|
||||
|
||||
this.setPixel(i, inverted);
|
||||
|
||||
if (i % 10 === 0) {
|
||||
console.log("invertion", {
|
||||
color,
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
inverted,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatePreview(subImageIdx = undefined) {
|
||||
const canvas = document.createElement("canvas");
|
||||
if (subImageIdx === undefined) {
|
||||
canvas.width = this.dim;
|
||||
canvas.height = this.dim;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
for (let i = 0; i < this.dim ** 2; i++) {
|
||||
const x = i % this.dim;
|
||||
const y = Math.floor(i / this.dim);
|
||||
ctx.fillStyle = this.samplePixels[i];
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
} else {
|
||||
canvas.width = 3;
|
||||
canvas.height = 3;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
for (let i = 0; i < 3 * 3; i++) {
|
||||
//
|
||||
const x = i % 3;
|
||||
const y = Math.floor(i / 3);
|
||||
ctx.fillStyle = this.subImages[subImageIdx][i];
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.previewElement.style.backgroundImage = `url(${canvas.toDataURL()})`;
|
||||
this.previewElement.style.backgroundSize = "100%";
|
||||
}
|
||||
|
||||
updateAllSubimages() {
|
||||
for (let i = 0; i < this.samplePixels.length; i++) {
|
||||
this.updateSingleSubImage(i);
|
||||
}
|
||||
}
|
||||
|
||||
updateSingleSubImage(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] = [
|
||||
// | neighbour
|
||||
// ---------------------|-----------
|
||||
colorAt(-1, -1), // | northwest
|
||||
colorAt(0, -1), // | north
|
||||
colorAt(1, -1), // | northeast
|
||||
|
||||
colorAt(-1, 0), // | east
|
||||
this.samplePixels[i], //| -- self --
|
||||
colorAt(1, 0), // | west
|
||||
|
||||
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;
|
||||
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;
|
||||
ctx.fillStyle = this.samplePixels[i];
|
||||
ctx.fillRect(x, y, 10, 10);
|
||||
}
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.download = `pixel-art-${this.dim}x${this.dim}.png`;
|
||||
link.href = canvas.toDataURL();
|
||||
link.click();
|
||||
}
|
||||
|
||||
exportAsData() {
|
||||
const data = JSON.stringify(this.samplePixels);
|
||||
const blob = new Blob([data], { type: "application/json" });
|
||||
const link = document.createElement("a");
|
||||
link.download = "pixel-art-data.json";
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.click();
|
||||
}
|
||||
|
||||
importData() {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".json";
|
||||
input.onchange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.target.result);
|
||||
if (Array.isArray(data) && data.length === this.dim ** 2) {
|
||||
this.samplePixels = data;
|
||||
this.createGrid();
|
||||
this.updatePreview();
|
||||
} else {
|
||||
alert("Invalid data format!");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error reading file!");
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
// Initialize the editor
|
||||
}
|
||||
const palette = [
|
||||
"#000",
|
||||
"#008",
|
||||
"#00f",
|
||||
"#080",
|
||||
"#088",
|
||||
"#0f0",
|
||||
"#0ff",
|
||||
"#800",
|
||||
"#808",
|
||||
"#80f",
|
||||
"#880",
|
||||
"#888",
|
||||
"#88f",
|
||||
"#8f8",
|
||||
"#8ff",
|
||||
"#ccc",
|
||||
"#f00",
|
||||
"#f0f",
|
||||
"#f80",
|
||||
"#f88",
|
||||
"#f8f",
|
||||
"#ff0",
|
||||
"#ff8",
|
||||
"#fff",
|
||||
];
|
||||
|
||||
window.painter = new PaintProg(
|
||||
9,
|
||||
palette,
|
||||
document.getElementById("gridContainer"), //
|
||||
document.getElementById("colorPalette"), //
|
||||
document.getElementById("currentColor"), //
|
||||
document.getElementById("colorPicker"), //
|
||||
document.getElementById("preview"), //
|
||||
);
|
||||
|
||||
// share window.dim with the HTML and CSS
|
||||
document.getElementsByTagName("body")[0].style.setProperty("--dim", window.painter.dim);
|
||||
184
frontend/progen.scss
Normal file
184
frontend/progen.scss
Normal file
@@ -0,0 +1,184 @@
|
||||
// number number of cells per side in the source picture
|
||||
$dim: 9;
|
||||
$pixSize: 80px;
|
||||
$gridSize: calc($dim * $pixSize);
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #2c3e50;
|
||||
color: white;
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--dim), 1fr);
|
||||
grid-template-rows: repeat(var(--dim), 1fr);
|
||||
gap: 1px;
|
||||
width: $gridSize;
|
||||
height: $gridSize;
|
||||
background-color: #34495e;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.pixel {
|
||||
background-color: #ffffff;
|
||||
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 {
|
||||
transform: scale(1.1);
|
||||
z-index: 1;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.controls-panel {
|
||||
background: #34495e;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.color-picker-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.current-color {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 3px solid white;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
cursor: pointer;
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
.color-palette {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.color-swatch:hover {
|
||||
transform: scale(1.2);
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.color-swatch.active {
|
||||
border-color: #3498db;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.tools {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.tool-group {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.tool-group h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #ecf0f1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin: 2px;
|
||||
font-size: 12px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
button.active {
|
||||
background: #e74c3c;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
width: 40px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.export-section {
|
||||
margin-top: 5px;
|
||||
padding-top: 5px;
|
||||
border-top: 1px solid #7f8c8d;
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
/* height: 90px; */
|
||||
background: white;
|
||||
border: 2px solid #bdc3c7;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
.grid-lines {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.drawing-mode {
|
||||
font-weight: bold;
|
||||
/* color: #e74c3c; */
|
||||
background: #383;
|
||||
}
|
||||
153
frontend/style.scss
Executable file
153
frontend/style.scss
Executable file
@@ -0,0 +1,153 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=swap");
|
||||
|
||||
body {
|
||||
font-family: "Fira Code", monospace;
|
||||
font-optical-sizing: auto;
|
||||
font-size: 14px;
|
||||
background-color: #1a1a1a;
|
||||
color: #00ff00;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
max-width: 99.9vw;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#output {
|
||||
flex: 1;
|
||||
background-color: #000;
|
||||
border: 2px solid #333;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 20px;
|
||||
font-family: "Fira Code", monospace;
|
||||
font-optical-sizing: auto;
|
||||
font-size: 14px;
|
||||
width: 100ch;
|
||||
}
|
||||
|
||||
#input-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#input {
|
||||
flex: 1;
|
||||
background-color: #222;
|
||||
border: 2px solid #333;
|
||||
color: #00ff00;
|
||||
padding: 10px;
|
||||
font-family: "Fira Code", monospace;
|
||||
font-optical-sizing: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#input:focus {
|
||||
outline: none;
|
||||
border-color: #00ff00;
|
||||
}
|
||||
|
||||
#send {
|
||||
background-color: #333;
|
||||
border: 2px solid #555;
|
||||
color: #00ff00;
|
||||
padding: 10px 20px;
|
||||
font-family: "Fira Code", monospace;
|
||||
font-optical-sizing: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#send:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
#status {
|
||||
background-color: #333;
|
||||
padding: 5px 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
display: inline-block;
|
||||
margin-top: 0.3em;
|
||||
}
|
||||
|
||||
.connected {
|
||||
color: #00ff00;
|
||||
}
|
||||
|
||||
.disconnected {
|
||||
color: #ff4444;
|
||||
}
|
||||
|
||||
.connecting {
|
||||
color: #ffaa00;
|
||||
}
|
||||
|
||||
/*
|
||||
*/
|
||||
.error {
|
||||
color: #ff4444;
|
||||
}
|
||||
|
||||
.calamity {
|
||||
color: #f00;
|
||||
text-decoration: wavy underline rgb(100 0% 00 / 40%);
|
||||
}
|
||||
|
||||
.echo {
|
||||
color: #888;
|
||||
}
|
||||
.echo::before {
|
||||
content: "> ";
|
||||
}
|
||||
|
||||
.debug {
|
||||
opacity: 0.33;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.strike {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.undercurl {
|
||||
text-decoration: wavy underline rgb(00 100% 00 / 40%);
|
||||
}
|
||||
|
||||
.faint {
|
||||
opacity: 0.42;
|
||||
color: #44f;
|
||||
}
|
||||
137
frontend/subpixel.js
Executable file
137
frontend/subpixel.js
Executable file
@@ -0,0 +1,137 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
14
frontend/test.scss
Executable file
14
frontend/test.scss
Executable file
@@ -0,0 +1,14 @@
|
||||
#output {
|
||||
flex: 1;
|
||||
background-color: #000;
|
||||
border: 2px solid #333;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 20px;
|
||||
font-family: "Fira Code", monospace;
|
||||
font-optical-sizing: auto;
|
||||
font-size: 14px;
|
||||
width: 100ch;
|
||||
}
|
||||
12
frontend/vite.config.js
Executable file
12
frontend/vite.config.js
Executable file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "vite";
|
||||
import devtoolsJson from "vite-plugin-devtools-json";
|
||||
|
||||
export default defineConfig({
|
||||
root: ".",
|
||||
plugins: [devtoolsJson()],
|
||||
build: {
|
||||
outDir: "../public/",
|
||||
emptyOutDir: true, // also necessary
|
||||
sourcemap: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user