aboutsummaryrefslogtreecommitdiff
path: root/src/renderer.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/renderer.js')
-rw-r--r--src/renderer.js776
1 files changed, 776 insertions, 0 deletions
diff --git a/src/renderer.js b/src/renderer.js
new file mode 100644
index 0000000..f2001f0
--- /dev/null
+++ b/src/renderer.js
@@ -0,0 +1,776 @@
+/**
+ * @typedef {import('./configuration.js').tConfiguration} tConfiguration
+ * @typedef {import('./stage_type.js').StageType} StageType
+ * @typedef {import('wmap-parser').Map} ParsedMap
+ */
+
+import { createPattern, patterns } from "./patterns.js";
+import { createComponentMap, percentToPixel, getStageValues } from "./utils.js";
+import {
+ calculateOptimalLabelPosition,
+ collectAllLines,
+ collectNoteRects,
+ collectInertiaRects,
+} from "./smart-label-positioning.js";
+import concave from "@turf/concave";
+import { featureCollection, point } from "@turf/helpers";
+
+// Pattern cache to avoid recreating patterns on every render
+const patternCache = new WeakMap();
+
+// Offscreen canvas cache for groups
+let offscreenCanvasCache = null;
+let offscreenCanvasContext = null;
+
+/**
+ * Map rendering constants and utilities
+ */
+
+const internals = {
+ // Padding around the axes
+ kAxisLabelPadding: 5,
+
+ // The default axis labels
+ kAxisLabels: {
+ xAxis: {
+ leading: "Uncharted",
+ trailing: "Industrialised",
+ },
+ yAxis: {
+ top: "Visible",
+ bottom: "Invisible",
+ },
+ },
+};
+
+/**
+ * Draw the map background with patterns for evolution stages
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
+ * @param {Array<number>} stages - Stage values
+ * @param {tConfiguration} configuration - Configuration object
+ */
+async function drawBackground(ctx, stages, config) {
+ const { mapWidth, mapHeight } = config.theme.sizes;
+ const { stageForeground, stageBackground } = config.theme.colors;
+ const width = mapWidth;
+ const height = mapHeight;
+
+ // Check cache first
+ let cachedPatterns = patternCache.get(ctx);
+ if (
+ !cachedPatterns ||
+ cachedPatterns.fg !== stageForeground ||
+ cachedPatterns.bg !== stageBackground
+ ) {
+ // Create patterns and cache them
+ cachedPatterns = {
+ fg: stageForeground,
+ bg: stageBackground,
+ stitch: await createPattern(
+ ctx,
+ patterns.stitch,
+ 1,
+ stageForeground,
+ stageBackground,
+ ),
+ shingles: await createPattern(
+ ctx,
+ patterns.shingles,
+ 1,
+ stageForeground,
+ stageBackground,
+ ),
+ shadowGrid: await createPattern(
+ ctx,
+ patterns.shadowGrid,
+ 1,
+ stageForeground,
+ stageBackground,
+ ),
+ wicker: await createPattern(
+ ctx,
+ patterns.wicker,
+ 1,
+ stageForeground,
+ stageBackground,
+ ),
+ };
+ patternCache.set(ctx, cachedPatterns);
+ }
+
+ const w = (dim) => percentToPixel(dim, width);
+
+ ctx.fillStyle = cachedPatterns.stitch;
+ ctx.fillRect(0, 0, w(stages[0]), height);
+
+ ctx.fillStyle = cachedPatterns.shingles;
+ ctx.fillRect(w(stages[0]), 0, w(stages[1]) - w(stages[0]), height);
+
+ ctx.fillStyle = cachedPatterns.shadowGrid;
+ ctx.fillRect(w(stages[1]), 0, w(stages[2]) - w(stages[1]), height);
+
+ ctx.fillStyle = cachedPatterns.wicker;
+ ctx.fillRect(w(stages[2]), 0, width - w(stages[2]), height);
+}
+
+/**
+ * Draw the map axes (X and Y)
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
+ * @param {Array<number>} stages - Stage values
+ * @param {StageType} [stageType=StageType.ACTIVITIES] - The labels to use for each stage in the map.
+ * @param {tConfiguration} configuration - Configuration object
+ */
+function drawAxes(ctx, stages, stageType, config) {
+ const { mapWidth, mapHeight, lineWidth } = config.theme.sizes;
+ const { axis, label } = config.theme.colors;
+ const { family: fontFamily, axisLabel } = config.theme.fonts;
+ const width = mapWidth;
+ const height = mapHeight;
+ const xLabels = internals.kAxisLabels.xAxis;
+ const yLabels = internals.kAxisLabels.yAxis;
+
+ const w = (dim) => percentToPixel(dim, width);
+
+ ctx.strokeStyle = axis;
+ ctx.lineWidth = lineWidth * 2;
+ ctx.beginPath();
+ ctx.moveTo(0, 0);
+ ctx.lineTo(0, height);
+ ctx.lineTo(width, height);
+ ctx.stroke();
+
+ ctx.fillStyle = label;
+ ctx.font = `${axisLabel}px ${fontFamily}`;
+ ctx.textBaseline = "top";
+
+ ctx.save();
+ ctx.translate(-20, 45);
+ ctx.rotate(-Math.PI / 2);
+ ctx.fillText(yLabels.top, 0, 0);
+ ctx.restore();
+
+ ctx.save();
+ ctx.translate(-20, height);
+ ctx.rotate(-Math.PI / 2);
+ ctx.fillText(yLabels.bottom, 0, 0);
+ ctx.restore();
+
+ const stageHeight = config.theme.sizes.stageHeight;
+
+ ctx.textAlign = "left";
+ ctx.fillText(xLabels.leading, 0, -stageHeight / 4);
+ ctx.textAlign = "right";
+ ctx.fillText(xLabels.trailing, width, -stageHeight / 4);
+
+ ctx.textAlign = "left";
+ ctx.fillText(stageType.stages[0], 0, height + internals.kAxisLabelPadding);
+ ctx.fillText(
+ stageType.stages[1],
+ w(stages[0]),
+ height + internals.kAxisLabelPadding,
+ );
+ ctx.fillText(
+ stageType.stages[2],
+ w(stages[1]),
+ height + internals.kAxisLabelPadding,
+ );
+ ctx.fillText(
+ stageType.stages[3],
+ w(stages[2]),
+ height + internals.kAxisLabelPadding,
+ );
+}
+
+/**
+ * Draw vertical stage lines
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
+ * @param {Array<number>} stages - Stage values
+ * @param {tConfiguration} configuration - Configuration object
+ */
+function drawStages(ctx, stages, config) {
+ const { mapWidth, mapHeight, lineWidth } = config.theme.sizes;
+ const { axis } = config.theme.colors;
+ const width = mapWidth;
+ const height = mapHeight;
+ const w = (dim) => percentToPixel(dim, width);
+
+ ctx.strokeStyle = axis;
+ ctx.lineWidth = lineWidth * 2;
+ ctx.setLineDash([10, 18]);
+ ctx.globalAlpha = 1.0;
+
+ ctx.beginPath();
+ ctx.moveTo(w(stages[0]), 0);
+ ctx.lineTo(w(stages[0]), height);
+ ctx.moveTo(w(stages[1]), 0);
+ ctx.lineTo(w(stages[1]), height);
+ ctx.moveTo(w(stages[2]), 0);
+ ctx.lineTo(w(stages[2]), height);
+ ctx.stroke();
+
+ ctx.setLineDash([]);
+}
+
+/**
+ * Draw a vertex shape
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
+ * @param {number} x - X position in pixels
+ * @param {number} y - Y position in pixels
+ * @param {string} shape - Shape type (circle, square, triangle, x)
+ * @param {tConfiguration} configuration - Configuration object
+ */
+function drawVertexShape(ctx, x, y, shape, config) {
+ const { vertexWidth, vertexHeight } = config.theme.sizes;
+ const { vertex } = config.theme.colors;
+ const width = vertexWidth;
+ const height = vertexHeight;
+
+ ctx.fillStyle = vertex;
+
+ switch (shape) {
+ case "circle":
+ ctx.beginPath();
+ ctx.ellipse(
+ x + width / 2,
+ y + height / 2,
+ width / 2,
+ height / 2,
+ 0,
+ 0,
+ Math.PI * 2,
+ );
+ ctx.fill();
+ break;
+
+ case "square":
+ ctx.fillRect(x, y, width, height);
+ break;
+
+ case "triangle":
+ ctx.beginPath();
+ ctx.moveTo(x, y + height);
+ ctx.lineTo(x + width, y + height);
+ ctx.lineTo(x + width / 2, y);
+ ctx.closePath();
+ ctx.fill();
+ break;
+
+ case "x":
+ ctx.strokeStyle = vertex;
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(x, y);
+ ctx.lineTo(x + width, y + height);
+ ctx.moveTo(x + width, y);
+ ctx.lineTo(x, y + height);
+ ctx.stroke();
+ break;
+
+ default:
+ ctx.beginPath();
+ ctx.ellipse(
+ x + width / 2,
+ y + height / 2,
+ width / 2,
+ height / 2,
+ 0,
+ 0,
+ Math.PI * 2,
+ );
+ ctx.fill();
+ }
+}
+
+/**
+ * Draw map components (vertices)
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
+ * @param {Array} components - Array of component objects
+ * @param {Object} map - Full map object (for smart label positioning)
+ * @param {Map} componentMap - Map of component labels to coordinates
+ * @param {tConfiguration} configuration - Configuration object
+ */
+function drawVertices(ctx, components, map, componentMap, configuration) {
+ const {
+ options: { smartLabelPositioning },
+ theme: {
+ sizes: { mapWidth, mapHeight, vertexWidth },
+ colors: { label },
+ fonts: { family: fontFamily, vertexLabel: fontSize },
+ },
+ } = configuration;
+
+ const width = mapWidth;
+ const height = mapHeight;
+ const w = (dim) => percentToPixel(dim, width);
+ const h = (dim) => percentToPixel(dim, height);
+
+ ctx.font = `${fontSize}px ${fontFamily}`;
+ ctx.fillStyle = label;
+ ctx.textBaseline = "middle";
+ ctx.textAlign = "left";
+
+ // Collect lines, notes, and inertias once if using smart positioning
+ let allLines, noteRects, inertiaRects;
+ if (smartLabelPositioning) {
+ allLines = collectAllLines(map, configuration, componentMap);
+ noteRects = collectNoteRects(map, configuration, ctx);
+ inertiaRects = collectInertiaRects(map, configuration, componentMap);
+ }
+
+ for (const component of components) {
+ const [xPercent, yPercent] = component.coordinates;
+ const x = w(xPercent);
+ const y = h(yPercent);
+
+ drawVertexShape(ctx, x, y, component.shape, configuration);
+
+ let labelX, labelY;
+
+ if (smartLabelPositioning) {
+ const position = calculateOptimalLabelPosition(
+ component,
+ map,
+ configuration,
+ ctx,
+ allLines,
+ noteRects,
+ inertiaRects,
+ );
+ labelX = position.x;
+ labelY = position.y;
+ ctx.textBaseline = "top";
+ } else {
+ // Use default positioning (to the right)
+ labelX = x + vertexWidth + internals.kAxisLabelPadding;
+ labelY = y + 7;
+ ctx.textBaseline = "middle";
+ }
+
+ ctx.fillText(component.label, labelX, labelY);
+ }
+}
+
+/**
+ * Draw dependencies between components
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
+ * @param {Array} dependencies - Array of dependency objects
+ * @param {Map} componentMap - Map of component labels to coordinates
+ * @param {tConfiguration} configuration - Configuration object
+ */
+function drawDependencies(ctx, dependencies, componentMap, configuration) {
+ const {
+ theme: {
+ sizes: { vertexWidth, vertexHeight, lineWidth, arrowheadSize },
+ colors: { vertex },
+ },
+ } = configuration;
+
+ ctx.strokeStyle = vertex;
+ ctx.lineWidth = lineWidth;
+
+ const halfWidth = vertexWidth / 2;
+ const halfHeight = vertexHeight / 2;
+ const arrowAngle = Math.PI / 4;
+
+ for (const dependency of dependencies) {
+ const fromCoordinates = componentMap[dependency.from.toLowerCase()];
+ const toCoordinates = componentMap[dependency.to.toLowerCase()];
+
+ if (!fromCoordinates || !toCoordinates) continue;
+
+ const originX = fromCoordinates[0] + halfWidth;
+ const originY = fromCoordinates[1] + halfHeight;
+ const destX = toCoordinates[0] + halfWidth;
+ const destY = toCoordinates[1] + halfHeight;
+
+ const angle = Math.atan2(destY - originY, destX - originX);
+ const cosine = Math.cos(angle);
+ const sine = Math.sin(angle);
+
+ const offsetOriginX = originX + halfWidth * cosine;
+ const offsetOriginY = originY + halfHeight * sine;
+ const offsetDestX = destX - halfWidth * cosine;
+ const offsetDestY = destY - halfHeight * sine;
+
+ ctx.beginPath();
+ ctx.moveTo(offsetOriginX, offsetOriginY);
+ ctx.lineTo(offsetDestX, offsetDestY);
+ ctx.stroke();
+
+ if (dependency.isDirected) {
+ const upperAngle = angle - arrowAngle;
+ const lowerAngle = angle + arrowAngle;
+
+ ctx.beginPath();
+ ctx.moveTo(
+ offsetDestX - arrowheadSize * Math.cos(upperAngle),
+ offsetDestY - arrowheadSize * Math.sin(upperAngle),
+ );
+ ctx.lineTo(offsetDestX, offsetDestY);
+ ctx.lineTo(
+ offsetDestX - arrowheadSize * Math.cos(lowerAngle),
+ offsetDestY - arrowheadSize * Math.sin(lowerAngle),
+ );
+ ctx.stroke();
+ }
+ }
+}
+
+/**
+ * Draw notes on the map
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
+ * @param {Array} notes - Array of note objects
+ * @param {tConfiguration} configuration - Configuration object
+ */
+function drawNotes(ctx, notes, configuration) {
+ const {
+ theme: {
+ sizes: { mapWidth, mapHeight, lineWidth },
+ colors: { background, vertex, label },
+ fonts: { family: fontFamily, note: fontSize },
+ lineHeights: { note: lineHeight },
+ },
+ } = configuration;
+
+ const w = (dim) => percentToPixel(dim, mapWidth);
+ const h = (dim) => percentToPixel(dim, mapHeight);
+
+ const maxWidth = 400;
+ const padding = internals.kAxisLabelPadding;
+
+ ctx.font = `${fontSize}px ${fontFamily}`;
+ ctx.textBaseline = "top";
+ ctx.textAlign = "left";
+ ctx.lineWidth = lineWidth;
+
+ // Batch all background fills first
+ ctx.fillStyle = background;
+ const noteData = [];
+
+ for (const note of notes) {
+ const x = w(note.coordinates[0]);
+ const y = h(note.coordinates[1]);
+ const lines = note.text.replace(/\\n/g, "\n").split("\n");
+
+ let widestLine = 0;
+ for (let i = 0; i < lines.length; i++) {
+ const lineWidth = ctx.measureText(lines[i]).width;
+ if (lineWidth > widestLine) widestLine = lineWidth;
+ }
+
+ const boxWidth = Math.min(maxWidth, widestLine + padding * 2);
+ const boxHeight = lines.length * lineHeight + padding * 2;
+
+ ctx.fillRect(x, y, boxWidth, boxHeight);
+ noteData.push({ x, y, boxWidth, boxHeight, lines });
+ }
+
+ // Batch all strokes
+ ctx.strokeStyle = vertex;
+ for (const { x, y, boxWidth, boxHeight } of noteData) {
+ ctx.strokeRect(x, y, boxWidth, boxHeight);
+ }
+
+ // Batch all text
+ ctx.fillStyle = label;
+ for (const { x, y, lines } of noteData) {
+ for (let i = 0; i < lines.length; i++) {
+ ctx.fillText(lines[i], x + padding, y + padding + i * lineHeight);
+ }
+ }
+}
+
+/**
+ * Draw inertia indicators
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
+ * @param {Array} inertias - Array of inertia objects
+ * @param {Map} componentMap - Map of component labels to coordinates
+ * @param {tConfiguration} configuration - Configuration object
+ */
+function drawInertias(ctx, inertias, componentMap, configuration) {
+ const {
+ theme: {
+ sizes: { vertexWidth, vertexHeight },
+ colors: { inertia },
+ },
+ } = configuration;
+
+ const cornerRadius = 2;
+
+ ctx.fillStyle = inertia;
+
+ for (const inertia of inertias) {
+ if (!inertia.component) return;
+ const coordinates = componentMap[inertia.component.toLowerCase()];
+ if (!coordinates) return;
+
+ const x = coordinates[0] + 3 * vertexWidth;
+ const y = coordinates[1] - (vertexHeight * 2) / 3;
+ const rectWidth = vertexWidth / 2;
+ const rectHeight = vertexHeight * 2;
+
+ ctx.beginPath();
+ ctx.roundRect(x, y, rectWidth, rectHeight, cornerRadius);
+ ctx.fill();
+ }
+}
+
+/**
+ * Draw evolution arrows (opportunities)
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
+ * @param {Array} evolutions - Array of evolution objects
+ * @param {Map} componentMap - Map of component labels to coordinates
+ * @param {tConfiguration} configuration - Configuration object
+ */
+function drawEvolutions(ctx, evolutions, componentMap, configuration) {
+ const {
+ theme: {
+ sizes: {
+ mapWidth,
+ vertexWidth,
+ vertexHeight,
+ lineWidth,
+ arrowheadSize,
+ },
+ colors: { evolution },
+ },
+ } = configuration;
+
+ ctx.strokeStyle = evolution;
+ ctx.lineWidth = lineWidth * 2;
+ ctx.setLineDash([10]);
+ ctx.globalAlpha = 1.0;
+
+ evolutions.forEach((evolutionItem) => {
+ const coords = componentMap[evolutionItem.component.toLowerCase()];
+ if (!coords) return;
+
+ const originX = coords[0];
+ const originY = coords[1];
+ const destX = Math.max(
+ 0,
+ Math.min(
+ mapWidth,
+ originX + percentToPixel(evolutionItem.value, mapWidth),
+ ),
+ );
+ const destY = originY;
+
+ const multiplier = destX > originX ? 1 : -1;
+ const upperAngle = -Math.PI / 4;
+ const lowerAngle = Math.PI / 4;
+
+ const offsetOriginX =
+ originX + multiplier * (vertexWidth / 2) + vertexWidth / 2;
+ const offsetOriginY = originY + vertexHeight / 2;
+ const offsetDestX =
+ destX - multiplier * (vertexWidth / 2) + vertexWidth / 2;
+ const offsetDestY = destY + vertexHeight / 2;
+
+ ctx.beginPath();
+ ctx.moveTo(offsetOriginX, offsetOriginY);
+ ctx.lineTo(offsetDestX, offsetDestY);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(offsetDestX, offsetDestY);
+ ctx.lineTo(
+ offsetDestX - multiplier * arrowheadSize * Math.cos(upperAngle),
+ offsetDestY - multiplier * arrowheadSize * Math.sin(upperAngle),
+ );
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(offsetDestX, offsetDestY);
+ ctx.lineTo(
+ offsetDestX - multiplier * arrowheadSize * Math.cos(lowerAngle),
+ offsetDestY - multiplier * arrowheadSize * Math.sin(lowerAngle),
+ );
+ ctx.stroke();
+ });
+
+ ctx.setLineDash([]);
+ ctx.globalAlpha = 1.0;
+}
+
+/**
+ * Draw groups with concave hulls
+ * @param {CanvasRenderingContext2D} ctx - Canvas context
+ * @param {Array} groups - Array of group objects
+ * @param {Map} componentMap - Map of component labels to coordinates
+ * @param {tConfiguration} configuration - Configuration object
+ */
+async function drawGroups(ctx, groups, componentMap, configuration) {
+ const {
+ theme: {
+ colors: { groups: groupColors },
+ sizes: { mapWidth, mapHeight, vertexWidth, vertexHeight },
+ opacity: { groups: groupOpacity },
+ },
+ } = configuration;
+
+ const width = mapWidth;
+ const height = mapHeight;
+
+ // Create/reuse offscreen canvas for groups to flatten fill + stroke before applying opacity
+ const isNode = typeof window === "undefined";
+
+ if (
+ !offscreenCanvasCache ||
+ offscreenCanvasCache.width !== width + vertexWidth ||
+ offscreenCanvasCache.height !== height + vertexHeight
+ ) {
+ if (isNode) {
+ const { createCanvas } = await import("canvas");
+ offscreenCanvasCache = createCanvas(
+ width + vertexWidth,
+ height + vertexHeight,
+ );
+ } else {
+ offscreenCanvasCache = document.createElement("canvas");
+ offscreenCanvasCache.width = width + vertexWidth;
+ offscreenCanvasCache.height = height + vertexHeight;
+ }
+ offscreenCanvasContext = offscreenCanvasCache.getContext("2d");
+ }
+
+ groups.forEach((group, groupIndex) => {
+ const componentCoords = group.components
+ .map((label) => componentMap[label.toLowerCase()])
+ .filter(Boolean);
+
+ if (componentCoords.length === 0) return;
+
+ const color = groupColors[groupIndex % groupColors.length];
+ let coords;
+
+ if (componentCoords.length === 1) {
+ // For 1 component, create a small circle around it
+ const [x, y] = componentCoords[0];
+ const radius = 3; // Small radius in percentage units
+ const segments = 16;
+ coords = [];
+ for (let i = 0; i <= segments; i++) {
+ const angle = (i / segments) * 2 * Math.PI;
+ coords.push([
+ x + radius * Math.cos(angle),
+ y + radius * Math.sin(angle),
+ ]);
+ }
+ } else if (componentCoords.length === 2) {
+ // For 2 components, create a simple line between them
+ coords = [componentCoords[0], componentCoords[1]];
+ } else {
+ // For 3+ components, use concave hull
+ const points = componentCoords.map(([x, y]) => point([x, y]));
+ const fc = featureCollection(points);
+ const hull = concave(fc);
+
+ if (!hull) return;
+ coords = hull.geometry.coordinates[0];
+ }
+
+ // Clear the offscreen canvas
+ offscreenCanvasContext.clearRect(
+ 0,
+ 0,
+ offscreenCanvasCache.width,
+ offscreenCanvasCache.height,
+ );
+
+ // Reset transform and then translate
+ offscreenCanvasContext.setTransform(1, 0, 0, 1, 0, 0);
+ offscreenCanvasContext.translate(vertexWidth / 2, vertexHeight / 2);
+
+ // Create path
+ offscreenCanvasContext.beginPath();
+ coords.forEach(([x, y], i) => {
+ const px = x;
+ const py = y;
+ if (i === 0) {
+ offscreenCanvasContext.moveTo(px, py);
+ } else {
+ offscreenCanvasContext.lineTo(px, py);
+ }
+ });
+
+ // For lines (2 components), don't close the path or fill
+ const isLine = componentCoords.length === 2;
+ if (!isLine) {
+ offscreenCanvasContext.closePath();
+ offscreenCanvasContext.fillStyle = color;
+ offscreenCanvasContext.fill();
+ }
+
+ // Stroke with thick line
+ offscreenCanvasContext.strokeStyle = color;
+ offscreenCanvasContext.lineWidth = 1.75 * vertexWidth;
+ offscreenCanvasContext.lineCap = "round";
+ offscreenCanvasContext.lineJoin = "round";
+ offscreenCanvasContext.stroke();
+
+ // Draw the offscreen canvas to the main canvas with opacity
+ ctx.save();
+ ctx.globalAlpha = groupOpacity;
+ ctx.drawImage(offscreenCanvasCache, 0, 0);
+ ctx.restore();
+ });
+}
+
+/**
+ * Render a parsed Wardley map to a canvas context
+ * @param {ParsedMap} map - Parsed map object from wmap-parser
+ * @param {CanvasRenderingContext2D} ctx - Canvas context to render to
+ * @param {StageType} stageType - The labels to use for each stage in the map.
+ * @param {tConfiguration} configuration - Configuration object
+ * @returns {Promise<void>}
+ */
+export async function render(map, ctx, stageType, configuration) {
+ const {
+ options: { showBackground },
+ theme: {
+ colors: { background },
+ sizes: { mapWidth, mapHeight, padding },
+ },
+ } = configuration;
+
+ const stages = getStageValues(map.stages);
+
+ // Create component map once for all functions to use
+ const componentMap = createComponentMap(
+ map.components,
+ mapWidth,
+ mapHeight,
+ );
+
+ // Fill entire canvas with white background BEFORE translate
+ ctx.fillStyle = background;
+ const totalWidth = mapWidth + padding * 2;
+ const totalHeight = mapHeight + padding * 2;
+ ctx.fillRect(0, 0, totalWidth, totalHeight);
+
+ ctx.save();
+ ctx.translate(padding, padding);
+
+ // Fill map area with background color (in case patterns don't fill completely)
+ ctx.fillStyle = background;
+ ctx.fillRect(0, 0, mapWidth, mapHeight);
+
+ if (showBackground) {
+ await drawBackground(ctx, stages, configuration);
+ }
+
+ drawAxes(ctx, stages, stageType, configuration);
+ drawStages(ctx, stages, configuration);
+ drawDependencies(ctx, map.dependencies, componentMap, configuration);
+
+ // Draw groups first (with low opacity)
+ await drawGroups(ctx, map.groups, componentMap, configuration);
+
+ drawVertices(ctx, map.components, map, componentMap, configuration);
+ drawInertias(ctx, map.inertias, componentMap, configuration);
+ drawEvolutions(ctx, map.evolutions, componentMap, configuration);
+ drawNotes(ctx, map.notes, configuration);
+
+ ctx.restore();
+}