fix: legend swatch shows pattern at readable density (1.3.1)

Bumps the faction-legend swatch and shrinks the pattern tile so the
pattern is legible at swatch-size rather than showing just 1-2 tile
repeats.

- On-screen legend: hex 28 → 32 px, pattern tile scaled by swatch/96
  with a 4 px floor — yields ~6 repeats across the swatch.
- PNG legend: swatch fontSize*1.1 → fontSize*1.7, lineHeight floored
  at swatch+4 so rows don't overlap; pattern shrunk by the same ratio.
- renderHexPreview gains an optional patternScaleMultiplier param so
  callers (legend, palette, future small-swatch contexts) can shrink
  the pattern without touching the user's scale slider value.

Also bundles the prior 1.3.0 follow-up fixes (hex-shaped on-map-scale
preview, palette tile refresh-on-save, thin outline default + slider)
since those were committed locally on top of 1.3.0.
This commit is contained in:
isaprettycoolguy@protonmail.com 2026-06-17 12:33:00 -04:00
parent 06bb93c91d
commit 84bcd76bd5
8 changed files with 94 additions and 27 deletions

View file

@ -1,7 +1,7 @@
{
"id": "hexmaker",
"name": "Hexmap World Creator",
"version": "1.3.0",
"version": "1.3.1",
"minAppVersion": "1.12.0",
"description": "Create a hex map and a guidebook level wiki for your setting with minimal fuss. Hex crawl worldbuilding toolbox. System agnostic, ready for any TRPG you care to run in it.",
"author": "morkdev",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "hexmaker-plugin",
"version": "1.3.0",
"version": "1.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hexmaker-plugin",
"version": "1.3.0",
"version": "1.3.1",
"license": "MIT",
"dependencies": {
"minisearch": "^7.2.0"

View file

@ -1,6 +1,6 @@
{
"name": "hexmaker-plugin",
"version": "1.3.0",
"version": "1.3.1",
"description": "A plugin to power hex crawl worldbuilding and running.",
"main": "main.js",
"scripts": {

View file

@ -176,7 +176,8 @@ export async function renderMapToPngBlob(
}
const hexes: HexState[] = [];
const iconsNeeded = new Set<string>();
const factionsUsed = new Map<string, string>(); // name → color
interface LegendEntry { color: string; style: OverlayStyle }
const factionsUsed = new Map<string, LegendEntry>(); // name → entry
const regionsUsed = new Map<string, string>(); // name → color
// Per-region hex centres, used to position the on-map region label at the
// cluster centroid (with PCA-derived rotation). Same approach as the
@ -206,7 +207,7 @@ export async function renderMapToPngBlob(
const s = factionStyleMap.get(fName);
if (c && s) {
factions.push({ color: c, style: s });
factionsUsed.set(fName, c);
factionsUsed.set(fName, { color: c, style: s });
}
}
}
@ -340,6 +341,7 @@ export async function renderMapToPngBlob(
fontSize,
legendPadding,
true, // measure pass first to anchor from the bottom
orientation,
);
drawLegend(
ctx,
@ -350,6 +352,7 @@ export async function renderMapToPngBlob(
fontSize,
legendPadding,
false,
orientation,
);
}
@ -483,7 +486,7 @@ function drawRegionLabel(
ctx.restore();
}
function sortedEntries(m: Map<string, string>): Array<[string, string]> {
function sortedEntries<V>(m: Map<string, V>): Array<[string, V]> {
return Array.from(m.entries()).sort((a, b) =>
a[0].localeCompare(b[0], undefined, { sensitivity: "base" }),
);
@ -499,15 +502,16 @@ function drawLegend(
x: number,
y: number,
title: string,
entries: Array<[string, string]>,
entries: Array<[string, { color: string; style: OverlayStyle }]>,
fontSize: number,
pad: number,
measureOnly: boolean,
orientation: "flat" | "pointy" = "flat",
): { width: number; height: number } {
if (entries.length === 0) return { width: 0, height: 0 };
const lineHeight = Math.round(fontSize * 1.45);
const swatch = Math.round(fontSize * 1.1);
const swatch = Math.round(fontSize * 1.7);
const lineHeight = Math.max(Math.round(fontSize * 1.45), swatch + 4);
const gap = Math.round(fontSize * 0.5);
// Measure widths. We need to set the font for accurate measurements.
@ -550,13 +554,49 @@ function drawLegend(
// Entry rows.
ctx.font = `${fontSize}px sans-serif`;
let entryY = y + pad + lineHeight;
for (const [name, color] of entries) {
// Swatch — vertically centred on the text baseline.
// Shrink pattern tile so more repeats are visible in the small swatch
// (user's scale slider is sized for on-map hexes ~96 px wide).
const LEGEND_PATTERN_MULT = swatch / 96;
const MIN_EFFECTIVE_SCALE = 4;
for (const [name, entry] of entries) {
const { color, style } = entry;
const swatchY = entryY + Math.round((lineHeight - swatch) / 2);
ctx.fillStyle = color;
ctx.fillRect(x + pad, swatchY, swatch, swatch);
ctx.strokeStyle = "#888";
ctx.strokeRect(x + pad + 0.5, swatchY + 0.5, swatch - 1, swatch - 1);
// Build pattern fill if non-solid
let fill: string | CanvasPattern = color;
if (style.pattern !== "solid") {
const effectiveScale = Math.max(
MIN_EFFECTIVE_SCALE,
style.scale * LEGEND_PATTERN_MULT,
);
const tile = new OffscreenCanvas(effectiveScale, effectiveScale);
const tctx = tile.getContext("2d");
if (tctx) {
drawPatternTile(tctx, {
pattern: style.pattern,
color,
scale: effectiveScale,
});
const pat = ctx.createPattern(tile, "repeat");
if (pat) fill = pat;
}
}
// Hex polygon swatch
const cx = x + pad + swatch / 2;
const cy = swatchY + swatch / 2;
const r = swatch / 2 - 1;
const pts = hexPolygonPoints(cx, cy, orientation, r);
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
ctx.closePath();
const prevAlpha = ctx.globalAlpha;
ctx.globalAlpha = style.opacity;
ctx.fillStyle = fill;
ctx.fill();
ctx.globalAlpha = prevAlpha;
ctx.strokeStyle = color;
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = "#fff";
ctx.fillText(name, x + pad + swatch + gap, entryY);

View file

@ -16,6 +16,7 @@ import { IconPickerModal } from "./IconPickerModal";
import { addLinkToSection, getLinksInSection, removeLinkFromSection } from "../sections";
import { getFactionColorFromFile, getRegionColorFromFile, getFactionStyleFromFile, getRegionStyleFromFile, setHexRegionInFile, getSubmapFromFile, setSubmapInFile, type OverlayStyle } from "../frontmatter";
import { buildSvgPattern, colorToIdToken, type OverlayPatternKey } from "../overlayPatterns";
import { renderHexPreview } from "./overlayPatternControls";
import {
VIEW_TYPE_HEX_MAP,
VIEW_TYPE_HEX_TABLE,
@ -3396,7 +3397,7 @@ export class HexMapView extends ItemView {
return k1 < k2 ? `${k1}|${k2}` : `${k2}|${k1}`;
};
const activeFactions: { name: string; color: string }[] = [];
const activeFactions: { name: string; color: string; style: OverlayStyle }[] = [];
let hasElements = false;
// ── Shared <defs> for SVG patterns (deduped by key|color|scale) ───────────
@ -3516,7 +3517,7 @@ export class HexMapView extends ItemView {
g.appendChild(path);
}
activeFactions.push({ name: factionName, color });
activeFactions.push({ name: factionName, color, style });
hasElements = true;
}
@ -3534,7 +3535,7 @@ export class HexMapView extends ItemView {
}
private renderFactionLegend(
factions: { name: string; color: string }[],
factions: { name: string; color: string; style: OverlayStyle }[],
gridContainer: HTMLElement,
): void {
if (!this.viewportEl) return;
@ -3543,10 +3544,21 @@ export class HexMapView extends ItemView {
const gridRight = gridContainer.offsetLeft + gridContainer.offsetWidth;
legend.style.left = `${gridRight + 24}px`;
legend.style.top = `${gridContainer.offsetTop}px`;
for (const { name, color } of [...factions].sort((a, b) => a.name.localeCompare(b.name))) {
const orientation = this.plugin.settings.hexOrientation;
const LEGEND_HEX_PX = 32;
// Shrink the pattern so more repeats fit in the small swatch (the user's
// scale slider is sized for on-map hexes, ~96 px wide).
const LEGEND_PATTERN_MULT = LEGEND_HEX_PX / 96;
for (const { name, color, style } of [...factions].sort((a, b) => a.name.localeCompare(b.name))) {
const row = legend.createDiv({ cls: "duckmage-faction-legend-row" });
const swatch = row.createDiv({ cls: "duckmage-faction-legend-swatch" });
swatch.style.backgroundColor = color;
renderHexPreview(swatch, {
color,
style,
orientation,
hexSizePx: LEGEND_HEX_PX,
patternScaleMultiplier: LEGEND_PATTERN_MULT,
});
row.createSpan({ text: name, cls: "duckmage-faction-legend-name" });
}
}

View file

@ -29,6 +29,11 @@ export function getOnMapHexSizePx(): number {
/**
* Render a hex-shaped preview into `container` showing what the overlay will
* look like on the map. Replaces any existing children.
*
* `patternScaleMultiplier` shrinks the pattern tile so more repeats fit in a
* small swatch. Useful for the legend/palette where the swatch is much
* smaller than an on-map hex and the user-chosen scale would otherwise show
* just 1-2 tile repeats.
*/
export function renderHexPreview(
container: HTMLElement,
@ -37,10 +42,16 @@ export function renderHexPreview(
style: OverlayStyle;
orientation: "flat" | "pointy";
hexSizePx: number;
patternScaleMultiplier?: number;
},
): void {
container.replaceChildren();
const { color, style, orientation, hexSizePx } = opts;
const MIN_EFFECTIVE_SCALE = 4;
const effectiveScale = Math.max(
MIN_EFFECTIVE_SCALE,
style.scale * (opts.patternScaleMultiplier ?? 1),
);
const W = hexSizePx;
let radius: number;
@ -73,7 +84,7 @@ export function renderHexPreview(
id,
pattern: style.pattern,
color,
scale: style.scale,
scale: effectiveScale,
});
if (pat) {
defs.appendChild(pat);

View file

@ -3902,13 +3902,16 @@ tr:hover .duckmage-rt-entry-copy-btn:hover {
}
.duckmage-faction-legend-swatch {
width: 12px;
height: 12px;
border-radius: 3px;
border: 1px solid var(--background-modifier-border);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.duckmage-faction-legend-swatch svg {
display: block;
}
.duckmage-faction-legend-name {
font-size: 0.8em;
color: var(--text-normal);

View file

@ -39,5 +39,6 @@
"1.2.1": "1.12.0",
"1.2.2": "1.12.0",
"1.2.3": "1.12.0",
"1.3.0": "1.12.0"
"1.3.0": "1.12.0",
"1.3.1": "1.12.0"
}