import type { CurveFactory } from "d3"; import * as d3 from "d3"; import { color, line, range } from "d3"; import { round } from "../utils"; declare global { var drawHeightmap: () => void; } const heightmapRenderer = (): void => { TIME && console.time("drawHeightmap"); const ocean = terrs.select("#oceanHeights"); const land = terrs.select("#landHeights"); ocean.selectAll("*").remove(); land.selectAll("*").remove(); const paths: (string | undefined)[] = new Array(101); const { cells, vertices } = grid; const used = new Uint8Array(cells.i.length); const heights = Array.from(cells.i as number[]).sort( (a, b) => cells.h[a] - cells.h[b], ); // ocean cells const renderOceanCells = Boolean(+ocean.attr("data-render")); if (renderOceanCells) { const skip = +ocean.attr("skip") + 1 || 1; const relax = +ocean.attr("relax") || 0; // TODO: Improve for treeshaking const curveType: keyof typeof d3 = (ocean.attr("curve") || "curveBasisClosed") as keyof typeof d3; const lineGen = line().curve(d3[curveType] as CurveFactory); let currentLayer = 0; for (const i of heights) { const h = cells.h[i]; if (h > currentLayer) currentLayer += skip; if (h < currentLayer) continue; if (currentLayer >= 20) break; if (used[i]) continue; // already marked const onborder = cells.c[i].some((n: number) => cells.h[n] < h); if (!onborder) continue; const vertex = cells.v[i].find((v: number) => vertices.c[v].some((i: number) => cells.h[i] < h), ); const chain = connectVertices(cells, vertices, vertex, h, used); if (chain.length < 3) continue; const points = simplifyLine(chain, relax).map( (v: number) => vertices.p[v], ); if (!paths[h]) paths[h] = ""; paths[h] += round(lineGen(points) || ""); } } // land cells { const skip = +land.attr("skip") + 1 || 1; const relax = +land.attr("relax") || 0; const curveType: keyof typeof d3 = (land.attr("curve") || "curveBasisClosed") as keyof typeof d3; const lineGen = line().curve(d3[curveType] as CurveFactory); let currentLayer = 20; for (const i of heights) { const h = cells.h[i]; if (h > currentLayer) currentLayer += skip; if (h < currentLayer) continue; if (currentLayer > 100) break; // no layers possible with height > 100 if (used[i]) continue; // already marked const onborder = cells.c[i].some((n: number) => cells.h[n] < h); if (!onborder) continue; const startVertex = cells.v[i].find((v: number) => vertices.c[v].some((i: number) => cells.h[i] < h), ); const chain = connectVertices(cells, vertices, startVertex, h, used); if (chain.length < 3) continue; const points = simplifyLine(chain, relax).map( (v: number) => vertices.p[v], ); if (!paths[h]) paths[h] = ""; paths[h] += round(lineGen(points) || ""); } } // render paths for (const height of range(0, 101)) { const group = height < 20 ? ocean : land; const scheme = getColorScheme(group.attr("scheme")); if (height === 0 && renderOceanCells) { // draw base ocean layer group .append("rect") .attr("x", 0) .attr("y", 0) .attr("width", graphWidth) .attr("height", graphHeight) .attr("fill", scheme(1)); } if (height === 20) { // draw base land layer group .append("rect") .attr("x", 0) .attr("y", 0) .attr("width", graphWidth) .attr("height", graphHeight) .attr("fill", scheme(0.8)); } if (paths[height] && paths[height]!.length >= 10) { const terracing = +group.attr("terracing") / 10 || 0; const fillColor = getColor(height, scheme); if (terracing) { group .append("path") .attr("d", paths[height]!) .attr("transform", "translate(.7,1.4)") .attr("fill", color(fillColor)!.darker(terracing).toString()) .attr("data-height", height); } group .append("path") .attr("d", paths[height]!) .attr("fill", fillColor) .attr("data-height", height); } } // connect vertices to chain: specific case for heightmap function connectVertices( cells: any, vertices: any, start: number, h: number, used: Uint8Array, ): number[] { const MAX_ITERATIONS = vertices.c.length; const n = cells.i.length; const chain: number[] = []; // vertices chain to form a path for ( let i = 0, current = start; i === 0 || (current !== start && i < MAX_ITERATIONS); i++ ) { const prev = chain[chain.length - 1]; // previous vertex in chain chain.push(current); // add current vertex to sequence const c = vertices.c[current]; // cells adjacent to vertex c.filter((c: number) => cells.h[c] === h).forEach((c: number) => { used[c] = 1; }); const c0 = c[0] >= n || cells.h[c[0]] < h; const c1 = c[1] >= n || cells.h[c[1]] < h; const c2 = c[2] >= n || cells.h[c[2]] < h; const v = vertices.v[current]; // neighboring vertices if (v[0] !== prev && c0 !== c1) current = v[0]; else if (v[1] !== prev && c1 !== c2) current = v[1]; else if (v[2] !== prev && c0 !== c2) current = v[2]; if (current === chain[chain.length - 1]) { ERROR && console.error("Next vertex is not found"); break; } } return chain; } function simplifyLine(chain: number[], simplification: number): number[] { if (!simplification) return chain; const n = simplification + 1; // filter each nth element return chain.filter((_d, i) => i % n === 0); } TIME && console.timeEnd("drawHeightmap"); }; window.drawHeightmap = heightmapRenderer;