syffy
This commit is contained in:
@@ -2,7 +2,6 @@ import { crackdown } from "../utils/crackdown.js";
|
|||||||
import { parseArgs } from "../utils/parseArgs.js";
|
import { parseArgs } from "../utils/parseArgs.js";
|
||||||
import { MessageType } from "../utils/messages.js";
|
import { MessageType } from "../utils/messages.js";
|
||||||
import { sprintf } from "sprintf-js";
|
import { sprintf } from "sprintf-js";
|
||||||
import { Config } from "../config.js";
|
|
||||||
|
|
||||||
/** Regex to validate if a :help [topic] command i entered correctly */
|
/** Regex to validate if a :help [topic] command i entered correctly */
|
||||||
const helpRegex = /^:help(?:\s+(.*))?$/;
|
const helpRegex = /^:help(?:\s+(.*))?$/;
|
||||||
@@ -75,9 +74,10 @@ class MUDClient {
|
|||||||
connect() {
|
connect() {
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}`.replace(/:\d+$/, Config.port);
|
// TODO Fix. Port should not be hardcoded
|
||||||
|
const wsUrl = `${protocol}//${window.location.host}`.replace(/:\d+$/, ":3000");
|
||||||
|
|
||||||
console.log(window.location);
|
console.log(wsUrl);
|
||||||
|
|
||||||
this.updateStatus("Connecting...", "connecting");
|
this.updateStatus("Connecting...", "connecting");
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,10 @@
|
|||||||
"name": "",
|
"name": "",
|
||||||
"short_name": "",
|
"short_name": "",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{ "src": "./img/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
|
||||||
"src": "./img/android-chrome-192x192.png",
|
{ "src": "./img/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "./img/android-chrome-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"theme_color": "#ffffff",
|
"theme_color": "#ffffff",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
"display": "standalone"
|
"display": "standalone"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
61
server/frontend/progen.html
Executable file
61
server/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
server/frontend/progen.js
Executable file
393
server/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
server/frontend/progen.scss
Normal file
184
server/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;
|
||||||
|
}
|
||||||
137
server/frontend/subpixel.js
Executable file
137
server/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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,12 +83,19 @@ Each group chat has a name.
|
|||||||
- `Donjons`
|
- `Donjons`
|
||||||
- GameMode = _Crawling_: Lady Gurthie Firefoot is Crawling the Donjons of Speematoforr.
|
- GameMode = _Crawling_: Lady Gurthie Firefoot is Crawling the Donjons of Speematoforr.
|
||||||
- Played like `Knights of Pen and Paper`
|
- Played like `Knights of Pen and Paper`
|
||||||
|
- WebGL: Actual rendered 3d, but black and white.
|
||||||
|
- Texture pack is just ascii text (TEXTures).
|
||||||
|
- Possibly Procedurally generated
|
||||||
|
- Most likely like this: https://www.youtube.com/watch?v=D1jLK4TG6O4&list=LL
|
||||||
|
- Animations like `Stonekeep` - only showing animation when the player moves.
|
||||||
|
- Monsters are just outlines of monster shapes, shaded with variably-sized letters
|
||||||
|
that spell out the monster's name.
|
||||||
- Procedurally (pre-)generated dungeons
|
- Procedurally (pre-)generated dungeons
|
||||||
- Very simple square dungeon layout (like KoPaP).
|
|
||||||
- Every time you enter a non-explored space, you roll a die, and see what happens.
|
- Every time you enter a non-explored space, you roll a die, and see what happens.
|
||||||
- Combat is like `Dark Queen of Krynn`
|
- Combat is like `Dark Queen of Krynn` (i.e. third person semi-iso)
|
||||||
- 1 Location == 1 donjon room/area
|
- 1 Location == 1 donjon room/area
|
||||||
- BSP (binary space partition) https://www.youtube.com/watch?v=TlLIOgWYVpI&t=374s
|
- BSP (binary space partition) https://www.youtube.com/watch?v=TlLIOgWYVpI&t=374s
|
||||||
|
|
||||||
- `Overland`
|
- `Overland`
|
||||||
- GameMode = _Traveling_: Swift Dangledonk the Slow is Traveling the Marshes of Moohfaahsaah
|
- GameMode = _Traveling_: Swift Dangledonk the Slow is Traveling the Marshes of Moohfaahsaah
|
||||||
- Travel is like `Rogue`
|
- Travel is like `Rogue`
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"frontend:build": "vite build frontend",
|
"frontend:build": "vite build frontend",
|
||||||
"frontend:dev": "vite frontend",
|
"frontend:dev": "vite frontend",
|
||||||
"dev": "concurrently 'npm run server:dev' 'npm run frontend:dev'",
|
"dev": "concurrently 'npm run server:dev' 'npm run frontend:dev'",
|
||||||
"dev:color": "concurrently -n 'API,UI' -c 'blue,green' 'npm run server:dev' 'npm run frontend:dev'"
|
"dev:color": "concurrently -n 'Server,Client' -c 'blue,green' 'npm run server:dev' 'npm run frontend:dev'"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"mud",
|
"mud",
|
||||||
|
|||||||
Reference in New Issue
Block a user