mirror of
https://github.com/sbuffkin/hexmaker.git
synced 2026-07-22 06:14:01 +00:00
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:
parent
06bb93c91d
commit
84bcd76bd5
8 changed files with 94 additions and 27 deletions
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
11
styles.css
11
styles.css
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
Loading…
Reference in a new issue