aboutsummaryrefslogtreecommitdiffstats
path: root/app/map.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/map.js')
-rw-r--r--app/map.js549
1 files changed, 294 insertions, 255 deletions
diff --git a/app/map.js b/app/map.js
index 65b3a5a..e4d4331 100644
--- a/app/map.js
+++ b/app/map.js
@@ -1,6 +1,6 @@
-import { Bounds, Collection, LineString, MultiLineString, Point, Polygon } from "geom";
+import { Bounds, Collection, Feature, LineString, MultiLineString, Point } from "geom";
import { District, House, TrainStation, TrainTracks } from "models";
-import { Svg } from "svg";
+import { Svg, SvgOptions } from "svg";
/**
* Color parameters for house markers
@@ -14,6 +14,54 @@ export const ColorParameter = {
};
/**
+ * Math utility functions
+ */
+export class MapMath {
+ /**
+ * Clamp a value between min and max
+ * @param {number} value
+ * @param {number} min
+ * @param {number} max
+ * @returns {number}
+ */
+ static clamp(value, min, max) {
+ return Math.max(min, Math.min(value, max));
+ }
+
+ /**
+ * Calculate linear interpolation between two values
+ * @param {number} a
+ * @param {number} b
+ * @param {number} t
+ * @returns {number}
+ */
+ static lerp(a, b, t) {
+ return a + (b - a) * t;
+ }
+
+ /**
+ * Normalize a value from one range to another
+ * @param {number} value
+ * @param {number} min
+ * @param {number} max
+ * @returns {number}
+ */
+ static normalize(value, min, max) {
+ return Math.max(0, Math.min(1, (value - min) / (max - min)));
+ }
+}
+
+/**
+ * Panning and inertia configuration
+ */
+export class PanningConfig {
+ static DEFAULT_FRICTION = 0.995;
+ static DEFAULT_SPEED_THRESHOLD = 0.001;
+ static DEFAULT_BOUNCE_FACTOR = 0.5;
+ static DEFAULT_VIEWBOX_SCALE = 1;
+}
+
+/**
* Map component for rendering houses, districts, and train infrastructure
*/
export class MapEl {
@@ -37,8 +85,8 @@ export class MapEl {
#onHouseClick = null;
/** @type {Function|null} */
#onHouseHover = null;
- /** @type {number|null} */
- #modalTimer = null;
+ /** @type {number|undefined} */
+ #modalTimer = undefined;
/** @type {boolean} */
#persistentModal = false;
/** @type {Bounds|null} */
@@ -50,28 +98,38 @@ export class MapEl {
* @param {Function} [options.onHouseHover]
*/
constructor(options = {}) {
- const svg = Svg.svg({
- attributes: {
- preserveAspectRatio: "xMidYMid meet",
- viewBox: "2400 6000 200 200", // longitude 24-26, latitude 60-62
- },
- });
- Object.assign(svg.style, {
- background: "#e0e7ff",
- cursor: "grab",
- display: "block",
- flex: "1",
- minHeight: "0",
- });
+ const svg = Svg.svg(
+ new SvgOptions({
+ attributes: {
+ preserveAspectRatio: "xMidYMid meet",
+ viewBox: "24 60 2 2", // longitude 24-26, latitude 60-62
+ },
+ styles: {
+ cursor: "grab",
+ display: "block",
+ flex: "1",
+ minHeight: "0",
+ },
+ }),
+ );
- this.#background = Svg.g({ id: "background" });
- this.#trainTracksGroup = Svg.g({ id: "train-tracks" });
- this.#trainStationsGroup = Svg.g({ id: "train-stations" });
- this.#districtsGroup = Svg.g({ id: "districts" });
- this.#housesGroup = Svg.g({
- id: "houses",
- styles: { pointerEvents: "auto" },
- });
+ this.#housesGroup = Svg.g(
+ new SvgOptions({
+ id: "houses",
+ }),
+ );
+ this.#trainTracksGroup = Svg.g(
+ new SvgOptions({
+ attributes: {
+ stroke: "rgba(255, 68, 68, 1)",
+ "stroke-width": "0.001",
+ },
+ id: "train-tracks",
+ }),
+ );
+ this.#trainStationsGroup = Svg.g(new SvgOptions({ id: "train-stations" }));
+ this.#districtsGroup = Svg.g(new SvgOptions({ id: "districts" }));
+ this.#background = Svg.g(new SvgOptions({ id: "background" }));
this.#svg = svg;
this.#onHouseClick = options.onHouseClick || null;
@@ -83,24 +141,19 @@ export class MapEl {
* @returns {SVGSVGElement}
*/
initializeMap() {
- Svg.clear(this.#svg);
-
- // Apply W3C recommended transform for WGS84 coordinates
- const transformGroup = Svg.g({
- attributes: {
- transform: "scale(1, -1)",
- },
- id: "map-transform",
- });
-
- // Create and store group handles
+ const transformGroup = Svg.g(
+ new SvgOptions({
+ attributes: { transform: "scale(1, -1)" },
+ id: "map-transform",
+ }),
+ );
transformGroup.append(
+ this.#background,
+ this.#districtsGroup,
this.#trainTracksGroup,
this.#trainStationsGroup,
- this.#districtsGroup,
this.#housesGroup,
- this.#background,
);
this.#svg.append(transformGroup);
this.#enablePanning(this.#svg);
@@ -109,10 +162,18 @@ export class MapEl {
}
/**
- * @param {number} initVx
- * @param {number} initVy
+ * Start inertia animation for panning
+ * @param {number} initVx - Initial velocity X
+ * @param {number} initVy - Initial velocity Y
+ * @param {number} [friction=PanningConfig.DEFAULT_FRICTION]
+ * @param {number} [speedThreshold=PanningConfig.DEFAULT_SPEED_THRESHOLD]
*/
- #startInertia(initVx, initVy) {
+ #startInertia(
+ initVx,
+ initVy,
+ friction = PanningConfig.DEFAULT_FRICTION,
+ speedThreshold = PanningConfig.DEFAULT_SPEED_THRESHOLD,
+ ) {
let lastTime = performance.now();
let currVx = initVx;
let currVy = initVy;
@@ -122,27 +183,23 @@ export class MapEl {
const dt = now - lastTime;
lastTime = now;
- // Apply friction (exponential decay, frame-independent)
- const friction = 0.995 ** dt; // Adjust 0.995 for faster/slower stop (lower = more friction)
- currVx *= friction;
- currVy *= friction;
+ const actualFriction = friction ** dt;
+ currVx *= actualFriction;
+ currVy *= actualFriction;
const speed = Math.hypot(currVx, currVy);
- if (speed < 0.001) return; // Stop threshold
+ if (speed < speedThreshold) return;
- // Compute delta
const deltaX = currVx * dt;
const deltaY = currVy * dt;
- // Update viewBox
const vb = this.#svg.viewBox.baseVal;
vb.x -= deltaX;
vb.y -= deltaY;
- // Clamp and bounce on edge
const { clampedX, clampedY } = this.#clampViewBox();
- if (clampedX) currVx = -currVx * 0.5; // Bounce (adjust 0.5 for elasticity)
- if (clampedY) currVy = -currVy * 0.5;
+ if (clampedX) currVx = -currVx * PanningConfig.DEFAULT_BOUNCE_FACTOR;
+ if (clampedY) currVy = -currVy * PanningConfig.DEFAULT_BOUNCE_FACTOR;
requestAnimationFrame(anim);
};
@@ -151,17 +208,18 @@ export class MapEl {
}
/**
- * @param {number} bounce
+ * Clamp viewBox to stay within bounds
+ * @returns {{clampedX: boolean, clampedY: boolean}}
*/
- #clampViewBox(bounce = 0) {
+ #clampViewBox() {
if (!this.#fullBounds) return { clampedX: false, clampedY: false };
const vb = this.#svg.viewBox.baseVal;
const oldX = vb.x;
const oldY = vb.y;
- vb.x = Math.max(this.#fullBounds.minX, Math.min(vb.x, this.#fullBounds.maxX - vb.width));
- vb.y = Math.max(-this.#fullBounds.maxY, Math.min(vb.y, -this.#fullBounds.minY - vb.height));
+ vb.x = MapMath.clamp(vb.x, this.#fullBounds.minX, this.#fullBounds.maxX - vb.width);
+ vb.y = MapMath.clamp(vb.y, -this.#fullBounds.maxY, -this.#fullBounds.minY - vb.height);
return {
clampedX: vb.x !== oldX,
@@ -170,20 +228,66 @@ export class MapEl {
}
/**
+ * Render a LineString or MultiLineString feature as SVG path
+ * @param {Feature} feature - GeoJSON feature with geometry
+ * @param {Object} style - Style attributes
+ * @param {string} [style.stroke] - Stroke color
+ * @param {string} [style.strokeWidth] - Stroke width
+ * @param {string} [style.fill] - Fill color
+ * @returns {SVGPathElement|null}
+ */
+ static #renderLineFeature(feature, style = {}) {
+ const defaultStyle = {
+ fill: "none",
+ stroke: "rgba(0, 0, 0, 1)",
+ "stroke-width": "0.0005",
+ ...style,
+ };
+
+ if (feature.geometry instanceof LineString) {
+ return Svg.path(
+ feature.geometry,
+ new SvgOptions({
+ attributes: defaultStyle,
+ }),
+ );
+ } else if (feature.geometry instanceof MultiLineString) {
+ return Svg.path(
+ new LineString(feature.geometry.coordinates.flat()),
+ new SvgOptions({
+ attributes: defaultStyle,
+ }),
+ );
+ }
+ return null;
+ }
+
+ /**
* Create an SVG element
* @param {SVGSVGElement} svg
*/
#enablePanning(svg) {
let isDragging = false;
+ /** @type {number|null} */
let pointerId = null;
- let startX, startY;
- let lastX, lastY, lastTime;
+ /** @type {number} */
+ let startX;
+ /** @type {number} */
+ let startY;
+ /** @type {number} */
+ let lastX;
+ /** @type {number} */
+ let lastY;
+ /** @type {number} */
+ let lastTime;
+
let vx = 0,
- vy = 0; // Velocity in SVG units per ms
+ vy = 0;
+ /** @type {SVGRect} */
let startViewBox;
svg.addEventListener("pointerdown", (e) => {
- if (e.pointerType === "touch" && e.touches?.length > 1) return;
+ if (e.pointerType === "touch") return;
isDragging = true;
pointerId = e.pointerId;
svg.setPointerCapture(pointerId);
@@ -192,7 +296,7 @@ export class MapEl {
lastTime = performance.now();
vx = vy = 0;
startViewBox = svg.viewBox.baseVal;
- svg.style.cursor = "grabbing";
+ svg.setAttribute("style", "cursor: grabbing;");
e.preventDefault();
});
@@ -203,19 +307,25 @@ export class MapEl {
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
- // Update velocity (for inertia)
if (dt > 0) {
- const ctm = svg.getScreenCTM().inverse();
+ const ctm = svg.getScreenCTM()?.inverse();
+ if (ctm === undefined) {
+ throw new Error("Unexpected");
+ }
+
const svgDx = dx * ctm.a + dy * ctm.c;
const svgDy = dx * ctm.b + dy * ctm.d;
vx = svgDx / dt;
vy = svgDy / dt;
}
- // Total drag for precise panning
const totalDx = e.clientX - startX;
const totalDy = e.clientY - startY;
- const ctm = svg.getScreenCTM().inverse();
+ const ctm = svg.getScreenCTM()?.inverse();
+ if (ctm === undefined) {
+ throw new Error("Unexpected");
+ }
+
const svgTotalDx = totalDx * ctm.a + totalDy * ctm.c;
const svgTotalDy = totalDx * ctm.b + totalDy * ctm.d;
@@ -226,7 +336,6 @@ export class MapEl {
`${newMinX} ${newMinY} ${startViewBox.width} ${startViewBox.height}`,
);
- // Clamp to bounds
this.#clampViewBox();
lastX = e.clientX;
@@ -240,12 +349,10 @@ export class MapEl {
isDragging = false;
pointerId = null;
this.#svg.releasePointerCapture(e.pointerId);
- this.#svg.style.cursor = "grab";
+ this.#svg.setAttribute("style", "cursor: grab;");
- // Start inertia if velocity sufficient
const speed = Math.hypot(vx, vy);
- if (speed > 0.001) {
- // Threshold (adjust as needed)
+ if (speed > PanningConfig.DEFAULT_SPEED_THRESHOLD) {
this.#startInertia(vx, vy);
}
});
@@ -255,7 +362,7 @@ export class MapEl {
isDragging = false;
pointerId = null;
this.#svg.releasePointerCapture(e.pointerId);
- this.#svg.style.cursor = "grab";
+ this.#svg.setAttribute("style", "cursor: grab;");
});
}
@@ -269,31 +376,55 @@ export class MapEl {
this.#colorParameter = colorParameter;
const houseElements = houses.map((house) => {
- const circle = Svg.circle(house.coordinates, {
- attributes: {
- "data-id": house.id,
- r: 0.002,
- },
- classes: ["house-marker"],
- styles: {
- cursor: "pointer",
- fill: this.#getHouseColor(house),
- stroke: "#333",
- "stroke-width": "0.001",
- },
- });
+ const circle = Svg.circle(
+ house.coordinates,
+ new SvgOptions({
+ attributes: {
+ "data-id": house.id,
+ fill: this.#getHouseColor(house),
+ "pointer-events": "visiblePainted",
+ r: "0.003",
+ stroke: "rgba(51, 51, 51, 1)",
+ "stroke-linecap": "butt",
+ "stroke-width": "0.001",
+ },
+ classes: ["house-marker"],
+ styles: { cursor: "pointer" },
+ }),
+ );
- // Add tooltip
const tooltipText = `${house.address}, ${house.district}\n€${house.price.toLocaleString()}`;
const title = Svg.title(tooltipText);
circle.appendChild(title);
- // Attach event listeners
- circle.addEventListener("mouseenter", () => this.#onHouseMouseEnter(circle, house.id));
- circle.addEventListener("mouseleave", () => this.#onHouseMouseLeave(circle, house.id));
+ circle.addEventListener("mouseenter", () => {
+ circle.setAttribute("r", "0.005");
+ clearTimeout(this.#modalTimer);
+ if (this.#onHouseHover) {
+ this.#onHouseHover(house.id, false);
+ }
+ });
+
+ circle.addEventListener("mouseleave", () => {
+ circle.setAttribute("r", "0.003");
+ circle.setAttribute("stroke", "rgba(51, 51, 51, 1)");
+ circle.setAttribute("stroke-width", "0.001");
+
+ if (!this.#persistentModal && this.#onHouseHover) {
+ this.#modalTimer = setTimeout(() => {
+ if (this.#onHouseHover) {
+ this.#onHouseHover(house.id, true);
+ }
+ }, 200);
+ }
+ });
+
circle.addEventListener("click", (e) => {
e.stopPropagation();
- this.#onHouseClickCallback(circle, house.id);
+ if (this.#onHouseClick) {
+ this.#onHouseClick(house.id, true);
+ this.#persistentModal = true;
+ }
});
return circle;
@@ -303,75 +434,59 @@ export class MapEl {
}
/**
- * Set houses data and render markers
- * @param {District[]} districts
- */
- static #calculateBounds(districts) {
- const bounds = new Bounds(Infinity, Infinity, -Infinity, -Infinity);
-
- // Include districts in bounds
- for (const district of districts) {
- const districtBounds = district.polygon.bounds();
- bounds.minX = Math.min(districtBounds.minX, bounds.minX);
- bounds.minY = Math.min(districtBounds.minY, bounds.minY);
- bounds.maxX = Math.max(districtBounds.maxX, bounds.maxX);
- bounds.maxY = Math.max(districtBounds.maxY, bounds.maxY);
- }
- return bounds;
- }
-
- /**
* Set districts data and render polygons
* @param {District[]} districts
*/
setDistricts(districts) {
const polygonElements = districts.map((district) => {
- const poly = Svg.polygon(district.polygon, {
- attributes: {
- "data-id": district.name,
- fill: "rgba(100,150,255,0.2)",
- stroke: "#555",
- "stroke-width": 0.001,
- },
- classes: ["district"],
- });
+ const poly = Svg.polygon(
+ district.polygon,
+ new SvgOptions({
+ attributes: {
+ "data-id": district.name,
+ fill: "rgba(100, 150, 255, 0.2)",
+ stroke: "rgba(85, 85, 85, 1)",
+ "stroke-width": "0.001",
+ },
+ classes: ["district"],
+ }),
+ );
poly.addEventListener("mouseenter", () => {
- poly.style.fill = "rgba(100,150,255,0.4)";
- poly.style.stroke = "#333";
+ poly.setAttribute("fill", "rgba(100, 150, 255, 0.4)");
+ poly.setAttribute("stroke", "rgba(51, 51, 51, 1)");
poly.setAttribute("stroke-width", "0.002");
});
poly.addEventListener("mouseleave", () => {
- poly.style.fill = "rgba(100,150,255,0.2)";
- poly.style.stroke = "#555";
+ poly.setAttribute("fill", "rgba(100, 150, 255, 0.2)");
+ poly.setAttribute("stroke", "rgba(85, 85, 85, 1)");
poly.setAttribute("stroke-width", "0.001");
});
return poly;
});
- // Render district labels
const labelElements = districts.map((district) => {
const center = district.polygon.centroid();
- return Svg.text(center, district.name, {
- attributes: {
- "data-id": district.name,
- transform: `translate(${center.lng}, ${center.lat}) scale(1, -1) translate(${-center.lng}, ${-center.lat})`,
- },
- classes: ["district-label"],
- styles: {
- dominantBaseline: "middle",
- fill: "#333",
- fontSize: "0.005",
- pointerEvents: "none",
- textAnchor: "middle",
- },
- });
+ return Svg.text(
+ center,
+ district.name,
+ new SvgOptions({
+ attributes: {
+ "data-id": district.name,
+ "dominant-baseline": "middle",
+ "font-size": "0.005",
+ "pointer-events": "none",
+ "text-anchor": "middle",
+ transform: `translate(${center.lng}, ${center.lat}) scale(1, -1) translate(${-center.lng}, ${-center.lat})`,
+ },
+ classes: ["district-label"],
+ }),
+ );
});
- const bounds = MapEl.#calculateBounds(districts);
- this.#fullBounds = bounds;
+ const bounds = District.bounds(districts);
this.#updateViewBox(bounds);
this.#replaceGroupContent(this.#districtsGroup, [...polygonElements, ...labelElements]);
}
@@ -382,51 +497,28 @@ export class MapEl {
*/
setMapData(coastline, mainRoads) {
const coastLinePaths = coastline.features
- .map((feature) => {
- if (feature.geometry instanceof LineString) {
- return Svg.path(feature.geometry, {
- attributes: {
- stroke: "#191970",
- "stroke-width": 0.001,
- },
- });
- } else if (feature.geometry instanceof MultiLineString) {
- return Svg.path(new LineString(feature.geometry.coordinates.flat()), {
- attributes: {
- stroke: "#191970",
- "stroke-width": 0.001,
- },
- });
- } else {
- return null;
- }
- })
+ .map((feature) =>
+ MapEl.#renderLineFeature(feature, {
+ stroke: "rgba(25, 25, 112, 1)",
+ strokeWidth: "0.0005",
+ }),
+ )
.filter((x) => x !== null);
const mainRoadPaths = mainRoads.features
- .map((feature) => {
- if (feature.geometry instanceof LineString) {
- return Svg.path(feature.geometry, {
- attributes: {
- stroke: "#000000",
- "stroke-width": 0.0005,
- },
- });
- } else if (feature.geometry instanceof MultiLineString) {
- return Svg.path(new LineString(feature.geometry.coordinates.flat()), {
- attributes: {
- stroke: "#000000",
- "stroke-width": 0.0005,
- },
- });
- } else {
- return null;
- }
- })
+ .map((feature) =>
+ MapEl.#renderLineFeature(feature, {
+ stroke: "rgba(0, 0, 0, 1)",
+ strokeWidth: "0.0005",
+ }),
+ )
.filter((x) => x !== null);
- this.#background.setAttribute("fill", "none");
this.#replaceGroupContent(this.#background, [...coastLinePaths, ...mainRoadPaths]);
+
+ const coastBounds = Bounds.union(coastline.features.map((f) => f.geometry.bounds()));
+ const roadBounds = Bounds.union(mainRoads.features.map((f) => f.geometry.bounds()));
+ this.#fullBounds = Bounds.union([coastBounds, roadBounds]);
}
/**
@@ -436,33 +528,27 @@ export class MapEl {
*/
setTrainData(stations, tracks) {
const trackElements = tracks.map((track) => {
- return Svg.path(track.lineString, {
- attributes: {
- stroke: "#ff4444",
- "stroke-width": 0.001,
- },
- classes: ["train-track"],
- });
+ return Svg.path(track.lineString, new SvgOptions({}));
});
- this.#trainTracksGroup.setAttribute("fill", "none");
this.#replaceGroupContent(this.#trainTracksGroup, trackElements);
const stationElements = stations.map((station) => {
const exterior = station.polygon.getExterior();
const point = new Point(exterior[0][0], exterior[0][1]);
- return Svg.circle(point, {
- attributes: {
- r: 0.003,
- },
- classes: ["train-station"],
- styles: {
- fill: "#ff4444",
- stroke: "#cc0000",
- "stroke-width": "0.001",
- },
- });
+ return Svg.circle(
+ point,
+ new SvgOptions({
+ attributes: {
+ fill: "rgba(255, 68, 68, 1)",
+ r: "0.003",
+ stroke: "rgba(204, 0, 0, 1)",
+ "stroke-width": "0.001",
+ },
+ classes: ["train-station"],
+ }),
+ );
});
this.#replaceGroupContent(this.#trainStationsGroup, stationElements);
@@ -477,11 +563,11 @@ export class MapEl {
const markers = this.#housesGroup.querySelectorAll(".house-marker");
markers.forEach((marker) => {
- const houseId = marker.dataset.id;
+ const houseId = marker.id;
const house = this.#houses.find((h) => h.id === houseId);
if (house) {
const color = this.#getHouseColor(house);
- marker.style.fill = color;
+ marker.setAttribute("fill", color);
}
});
}
@@ -495,8 +581,8 @@ export class MapEl {
const markers = this.#housesGroup.querySelectorAll(".house-marker");
markers.forEach((marker) => {
- const houseId = marker.dataset.id;
- marker.style.display = filteredSet.has(houseId) ? "" : "none";
+ const houseId = marker.id;
+ marker.setAttribute("display", filteredSet.has(houseId) ? "" : "none");
});
}
@@ -538,53 +624,6 @@ export class MapEl {
}
/**
- * House mouse enter handler
- * @param {SVGCircleElement} marker
- * @param {string} houseId
- */
- #onHouseMouseEnter(marker, houseId) {
- marker.setAttribute("r", "0.01");
- marker.style.stroke = "#000";
- marker.setAttribute("stroke-width", "0.01");
-
- clearTimeout(this.#modalTimer);
- if (this.#onHouseHover) {
- this.#onHouseHover(houseId, false);
- }
- }
-
- /**
- * House mouse leave handler
- * @param {SVGCircleElement} marker
- * @param {string} houseId
- */
- #onHouseMouseLeave(marker, houseId) {
- marker.setAttribute("r", "0.003");
- marker.style.stroke = "#333";
- marker.setAttribute("stroke-width", "0.001");
-
- if (!this.#persistentModal && this.#onHouseHover) {
- this.#modalTimer = setTimeout(() => {
- if (this.#onHouseHover) {
- this.#onHouseHover(houseId, true);
- }
- }, 200);
- }
- }
-
- /**
- * House click handler
- * @param {SVGCircleElement} marker
- * @param {string} houseId
- */
- #onHouseClickCallback(marker, houseId) {
- if (this.#onHouseClick) {
- this.#onHouseClick(houseId, true);
- this.#persistentModal = true;
- }
- }
-
- /**
* Get color for house based on parameter value
* @param {House} house
* @returns {string}
@@ -614,10 +653,10 @@ export class MapEl {
max = 200;
break;
default:
- return "#4caf50";
+ return "rgba(76, 175, 80, 1)";
}
- const normalized = Math.max(0, Math.min(1, (value - min) / (max - min)));
+ const normalized = MapMath.normalize(value, min, max);
return MapEl.#gradientColor(normalized);
}
@@ -629,16 +668,16 @@ export class MapEl {
static #gradientColor(normalized) {
if (normalized < 0.5) {
const t = normalized * 2;
- const r = Math.round(42 + (87 - 42) * t);
- const g = Math.round(123 + (199 - 123) * t);
- const b = Math.round(155 + (133 - 155) * t);
- return `rgb(${r}, ${g}, ${b})`;
+ const r = Math.round(MapMath.lerp(42, 87, t));
+ const g = Math.round(MapMath.lerp(123, 199, t));
+ const b = Math.round(MapMath.lerp(155, 133, t));
+ return `rgba(${r}, ${g}, ${b}, 1)`;
} else {
const t = (normalized - 0.5) * 2;
- const r = Math.round(87 + (237 - 87) * t);
- const g = Math.round(199 + (221 - 199) * t);
- const b = Math.round(133 + (83 - 133) * t);
- return `rgb(${r}, ${g}, ${b})`;
+ const r = Math.round(MapMath.lerp(87, 237, t));
+ const g = Math.round(MapMath.lerp(199, 221, t));
+ const b = Math.round(MapMath.lerp(133, 83, t));
+ return `rgba(${r}, ${g}, ${b}, 1)`;
}
}
}