Remove resize dither buttons and show frontend palette grid

This commit is contained in:
Enrique Calot
2026-04-08 13:30:29 +10:00
parent 54ff507857
commit b09a14c7eb
+57 -139
View File
@@ -3,7 +3,7 @@
*
* Responsibilities:
* - Render before/after previews directly inside the node.
* - Provide clickable controls for dither mode and output sizing.
* - Render a palette swatch grid from the frontend-computed active palette.
* - Compute local preview with the same nearest + dither logic as backend.
*
* Event contract:
@@ -17,19 +17,19 @@ const NODE_HEADER_HEIGHT = 28;
const NODE_PANEL_HEIGHT = 130;
const NODE_PADDING = 6;
const PREVIEW_GAP = 6;
const BUTTON_GAP = 6;
const BUTTON_ROW_HEIGHT = 24;
const MAX_DIMENSION = 8192;
const NODE_WIDGET_ROW_HEIGHT = 24;
const NODE_MIN_TOP_RESERVED = 74;
const PALETTE_GRID_GAP = 1;
const PALETTE_GRID_TOP_INSET = 20;
const PALETTE_GRID_BOTTOM_INSET = 4;
const PALETTE_GRID_MAX_COLUMNS = 32;
const TEXT_COLOR_DEFAULT = "#e6e6e6";
const BG_PANEL_COLOR = "#2d2d2d";
const BG_CONTROL_COLOR = "#2a2a2a";
const BUTTON_BG_COLOR = "#3d3d3d";
const BUTTON_ACTIVE_BG_COLOR = "#8e5321";
const BUTTON_BORDER_COLOR = "#9a9a9a";
const PREVIEW_PLACEHOLDER_TEXT_COLOR = "#ebebeb";
const PALETTE_SWATCH_BORDER_COLOR = "rgba(0,0,0,0.35)";
const DITHER_MODES = ["positional", "floyd"];
const PALETTE_MODES = ["vga_256", "optimized_x_colors"];
@@ -122,9 +122,6 @@ const normalizePalette = (rawPalette) => {
return null;
};
const isPointInRect = (x, y, rect) =>
!!rect && x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
app.registerExtension({
name: "resize and dither",
@@ -145,7 +142,6 @@ app.registerExtension({
localPreviewCanvas: null,
localPreviewCtx: null,
hasUserResizedNode: false,
controlLayout: null,
vgaPalette: [...FALLBACK_PALETTE],
paletteFloat: null,
paletteInt: null,
@@ -429,56 +425,61 @@ app.registerExtension({
return area;
},
buildButtonRow: function (area, y, labels, ids) {
const width = (area.width - BUTTON_GAP * (labels.length - 1)) / labels.length;
return labels.map((label, i) => ({
id: ids[i],
label,
x: area.x + i * (width + BUTTON_GAP),
y,
width,
height: BUTTON_ROW_HEIGHT,
}));
},
updateControlLayout: function () {
const controlsArea = this.getControlsArea();
if (!controlsArea) {
this.controlLayout = null;
drawPaletteGrid: function (ctx, controlsArea) {
const palette = this.paletteInt?.length ? this.paletteInt : FALLBACK_PALETTE;
const colorCount = palette.length;
if (colorCount === 0) {
return;
}
const row1Y = controlsArea.y;
const row2Y = row1Y + BUTTON_ROW_HEIGHT + BUTTON_GAP;
const row3Y = row2Y + BUTTON_ROW_HEIGHT + BUTTON_GAP;
this.controlLayout = {
modeButtons: this.buildButtonRow(
controlsArea,
row1Y,
["Positional", "Floyd"],
["mode:positional", "mode:floyd"],
),
scaleButtons: this.buildButtonRow(controlsArea, row2Y, ["1x", "2x", "4x"], ["scale:1", "scale:2", "scale:4"]),
adjustButtons: this.buildButtonRow(
controlsArea,
row3Y,
["W-", "W+", "H-", "H+", "Reset"],
["w:-16", "w:+16", "h:-16", "h:+16", "reset"],
),
};
},
drawButton: function (ctx, button, isActive = false) {
ctx.save();
ctx.fillStyle = isActive ? BUTTON_ACTIVE_BG_COLOR : BUTTON_BG_COLOR;
ctx.fillRect(button.x, button.y, button.width, button.height);
ctx.strokeStyle = BUTTON_BORDER_COLOR;
ctx.lineWidth = 1;
ctx.strokeRect(button.x + 0.5, button.y + 0.5, button.width - 1, button.height - 1);
drawStyledText(ctx, button.label, button.x + button.width / 2, button.y + button.height / 2, {
const labelY = controlsArea.y + PALETTE_GRID_TOP_INSET - 8;
drawStyledText(ctx, `Palette (${colorCount} colors)`, controlsArea.x + controlsArea.width / 2, labelY, {
font: "12px Arial",
fillStyle: "rgba(235,235,235,0.95)",
});
const gridX = controlsArea.x;
const gridY = controlsArea.y + PALETTE_GRID_TOP_INSET;
const gridWidth = controlsArea.width;
const gridHeight = controlsArea.height - PALETTE_GRID_TOP_INSET - PALETTE_GRID_BOTTOM_INSET;
if (gridWidth <= 0 || gridHeight <= 0) {
return;
}
const minColumns = Math.min(4, colorCount);
const maxColumns = Math.min(PALETTE_GRID_MAX_COLUMNS, colorCount);
let bestColumns = minColumns;
let bestRows = Math.ceil(colorCount / bestColumns);
let bestScore = -1;
for (let columns = minColumns; columns <= maxColumns; columns++) {
const rows = Math.ceil(colorCount / columns);
const cellWidth = (gridWidth - PALETTE_GRID_GAP * (columns - 1)) / columns;
const cellHeight = (gridHeight - PALETTE_GRID_GAP * (rows - 1)) / rows;
const score = Math.min(cellWidth, cellHeight);
if (score > bestScore) {
bestScore = score;
bestColumns = columns;
bestRows = rows;
}
}
const cellWidth = (gridWidth - PALETTE_GRID_GAP * (bestColumns - 1)) / bestColumns;
const cellHeight = (gridHeight - PALETTE_GRID_GAP * (bestRows - 1)) / bestRows;
ctx.save();
for (let i = 0; i < colorCount; i++) {
const col = i % bestColumns;
const row = Math.floor(i / bestColumns);
const color = palette[i];
const x = gridX + col * (cellWidth + PALETTE_GRID_GAP);
const y = gridY + row * (cellHeight + PALETTE_GRID_GAP);
ctx.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`;
ctx.fillRect(x, y, cellWidth, cellHeight);
ctx.strokeStyle = PALETTE_SWATCH_BORDER_COLOR;
ctx.strokeRect(x + 0.5, y + 0.5, Math.max(0, cellWidth - 1), Math.max(0, cellHeight - 1));
}
ctx.restore();
},
@@ -693,27 +694,6 @@ app.registerExtension({
this.setDirtyCanvas(true, true);
},
applyScalePreset: function (scale) {
const baseWidth = this.sourcePixelWidth > 0 ? this.sourcePixelWidth : this.getIntWidgetValue("width", 256);
const baseHeight = this.sourcePixelHeight > 0 ? this.sourcePixelHeight : this.getIntWidgetValue("height", 256);
this.setWidgetValue("width", clamp(Math.round(baseWidth * scale), 1, MAX_DIMENSION));
this.setWidgetValue("height", clamp(Math.round(baseHeight * scale), 1, MAX_DIMENSION));
this.updateBackend();
},
adjustDimension: function (axis, delta) {
const widgetName = axis === "w" ? "width" : "height";
const current = this.getIntWidgetValue(widgetName, 0);
this.setWidgetValue(widgetName, clamp(current + delta, 0, MAX_DIMENSION));
this.updateBackend();
},
setMode: function (mode) {
this.setWidgetValue("dither_mode", mode);
this.updateBackend();
},
onDrawForeground: function (ctx) {
if (this.flags?.collapsed) return;
@@ -722,8 +702,6 @@ app.registerExtension({
const controlsArea = this.getControlsArea();
if (!topPreviewArea || !bottomPreviewArea || !controlsArea) return;
this.updateControlLayout();
ctx.save();
ctx.fillStyle = BG_PANEL_COLOR;
ctx.fillRect(topPreviewArea.x, topPreviewArea.y, topPreviewArea.width, topPreviewArea.height);
@@ -792,17 +770,6 @@ app.registerExtension({
ctx.fillRect(controlsArea.x, controlsArea.y, controlsArea.width, controlsArea.height);
ctx.restore();
if (this.controlLayout) {
const mode = this.getModeValue();
this.controlLayout.modeButtons.forEach((button) => {
const isActive = button.id === `mode:${mode}`;
this.drawButton(ctx, button, isActive);
});
this.controlLayout.scaleButtons.forEach((button) => this.drawButton(ctx, button, false));
this.controlLayout.adjustButtons.forEach((button) => this.drawButton(ctx, button, false));
}
const requestedWidth = this.getIntWidgetValue("width", 0);
const requestedHeight = this.getIntWidgetValue("height", 0);
const resolved = this.resolveTargetSize(requestedWidth, requestedHeight);
@@ -818,52 +785,8 @@ app.registerExtension({
fillStyle: "rgba(235,235,235,0.95)",
},
);
},
onMouseDown: function (event) {
if (event.button !== 0) return false;
if (!this.controlLayout) return false;
const localPos = app.canvas.convertEventToCanvasOffset(event);
const mouseX = localPos[0] - this.pos[0];
const mouseY = localPos[1] - this.pos[1];
const hit = (buttons) => buttons.find((button) => isPointInRect(mouseX, mouseY, button));
const modeButton = hit(this.controlLayout.modeButtons);
if (modeButton) {
const mode = modeButton.id.split(":")[1];
this.setMode(mode);
return true;
}
const scaleButton = hit(this.controlLayout.scaleButtons);
if (scaleButton) {
const scale = Number.parseFloat(scaleButton.id.split(":")[1]);
this.applyScalePreset(scale);
return true;
}
const adjustButton = hit(this.controlLayout.adjustButtons);
if (adjustButton) {
if (adjustButton.id === "reset") {
const width = this.sourcePixelWidth > 0 ? this.sourcePixelWidth : 256;
const height = this.sourcePixelHeight > 0 ? this.sourcePixelHeight : 256;
this.setWidgetValue("width", width);
this.setWidgetValue("height", height);
this.setWidgetValue("dither_mode", "positional");
this.setWidgetValue("palette_mode", "vga_256");
this.setWidgetValue("palette_colors", 24);
this.updateBackend();
return true;
}
const [axis, deltaText] = adjustButton.id.split(":");
this.adjustDimension(axis, toInt(deltaText, 0));
return true;
}
return false;
this.drawPaletteGrid(ctx, controlsArea);
},
onResize: function () {
@@ -919,11 +842,6 @@ api.addEventListener("game_dev.resize_dither.initialise", ({ detail }) => {
);
}
const incomingPalette = normalizePalette(detail.vga_palette);
if (incomingPalette) {
node.setPalette(incomingPalette);
}
const onBeforeLoad = () => {
if (revision !== node.bootstrapRevision) {
return;