feat: redesign roadside sign faces as exit signs with chapter accents
This commit is contained in:
@@ -2,6 +2,7 @@ import * as THREE from 'three';
|
||||
import type { SectionId } from '@/content/types';
|
||||
import type { SignSnapshot } from '@/journey/types';
|
||||
import {
|
||||
SIGN_ROUTE_ORDER,
|
||||
signRouteFor,
|
||||
signViewportProfileFor,
|
||||
type SignCorridor,
|
||||
@@ -18,7 +19,7 @@ interface ProjectingCamera {
|
||||
}
|
||||
|
||||
const SIGN_MARKER_FONT =
|
||||
'560 46px "Geist Mono Variable", ui-monospace, monospace';
|
||||
'600 60px "Geist Mono Variable", ui-monospace, monospace';
|
||||
const SIGN_LABEL_FONT = '650 180px "Geist Variable", sans-serif';
|
||||
export interface SignWorld {
|
||||
group: SignGroup;
|
||||
@@ -516,6 +517,7 @@ export function createSign(
|
||||
face.userData.textureUpdateCount += 1;
|
||||
face.userData.fontReadyWhenDrawn =
|
||||
document.fonts?.check(SIGN_LABEL_FONT) ?? true;
|
||||
edgeMaterial.emissive.set(SIGN_ACCENTS[sign.targetId]);
|
||||
if (context)
|
||||
drawLabel(
|
||||
context,
|
||||
@@ -693,60 +695,116 @@ function createClippedShape(width: number, height: number, cut: number) {
|
||||
return shape;
|
||||
}
|
||||
|
||||
/** Chapter accents, matching the CSS --chapter-accent of each panel. */
|
||||
const SIGN_ACCENTS: Readonly<Record<SectionId, string>> = {
|
||||
start: '#ff83c2',
|
||||
profile: '#ff83c2',
|
||||
experience: '#c4a2ff',
|
||||
infrastructure: '#8be9ed',
|
||||
'selected-builds': '#ffa68d',
|
||||
'additional-work': '#ff83c2',
|
||||
toolkit: '#c4a2ff',
|
||||
contact: '#ffa68d',
|
||||
};
|
||||
|
||||
export function signAccentFor(id: SectionId): string {
|
||||
return SIGN_ACCENTS[id];
|
||||
}
|
||||
|
||||
function nextRouteLabel(id: SectionId): string | null {
|
||||
const index = SIGN_ROUTE_ORDER.indexOf(id);
|
||||
const next = SIGN_ROUTE_ORDER[index + 1];
|
||||
return next === undefined ? null : signRouteFor(next).label;
|
||||
}
|
||||
|
||||
function chamferedRect(
|
||||
context: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
cut: number,
|
||||
): void {
|
||||
context.beginPath();
|
||||
context.moveTo(x + cut, y);
|
||||
context.lineTo(x + width - cut, y);
|
||||
context.lineTo(x + width, y + cut);
|
||||
context.lineTo(x + width, y + height);
|
||||
context.lineTo(x, y + height);
|
||||
context.lineTo(x, y + cut);
|
||||
context.closePath();
|
||||
}
|
||||
|
||||
/** Face layout shared with the DOM plates: exit tab, "then" line, chapter
|
||||
* label and an exit arrow that points toward the sign's side of the road. */
|
||||
function drawLabel(
|
||||
context: CanvasRenderingContext2D,
|
||||
route: SignRouteSpec,
|
||||
width: number,
|
||||
height: number,
|
||||
): void {
|
||||
const accent = SIGN_ACCENTS[route.id];
|
||||
context.clearRect(0, 0, width, height);
|
||||
context.save();
|
||||
context.scale(width / 2_048, height / 512);
|
||||
context.imageSmoothingEnabled = true;
|
||||
context.imageSmoothingQuality = 'high';
|
||||
|
||||
context.globalAlpha = 0.16;
|
||||
context.fillStyle = '#cfabcd';
|
||||
for (const [x, y, scratchWidth] of [
|
||||
[312, 84, 142],
|
||||
[742, 424, 96],
|
||||
[1_116, 66, 188],
|
||||
[1_488, 446, 112],
|
||||
] as const) {
|
||||
context.fillRect(x, y, scratchWidth, 2);
|
||||
}
|
||||
|
||||
context.globalAlpha = 1;
|
||||
context.fillStyle = '#ff95cd';
|
||||
context.fillRect(82, 72, 10, 368);
|
||||
context.fillStyle = '#bbaae0';
|
||||
context.font = SIGN_MARKER_FONT;
|
||||
context.textAlign = 'left';
|
||||
context.textBaseline = 'middle';
|
||||
context.fillText(`ROUTE ${route.marker} / NEXT`, 142, 120);
|
||||
|
||||
context.globalAlpha = 0.6;
|
||||
context.fillStyle = '#61416d';
|
||||
context.fillRect(142, 174, 1_610, 2);
|
||||
// Exit tab.
|
||||
context.font = '720 66px "Geist Mono Variable", ui-monospace, monospace';
|
||||
const tabText = `EXIT ${route.marker}`;
|
||||
const tabWidth = context.measureText(tabText).width + 72;
|
||||
context.fillStyle = accent;
|
||||
chamferedRect(context, 96, 50, tabWidth, 116, 18);
|
||||
context.fill();
|
||||
context.fillStyle = '#160b22';
|
||||
context.fillText(tabText, 132, 110);
|
||||
|
||||
context.globalAlpha = 1;
|
||||
// "Then" line: the next exit on the route.
|
||||
const next = nextRouteLabel(route.id);
|
||||
context.font = SIGN_MARKER_FONT;
|
||||
context.fillStyle = '#d8c9ea';
|
||||
context.fillText(
|
||||
next === null ? 'END OF ROUTE' : `THEN \u2192 ${next.toUpperCase()}`,
|
||||
96 + tabWidth + 40,
|
||||
110,
|
||||
);
|
||||
|
||||
// Chapter label.
|
||||
context.fillStyle = '#fff2df';
|
||||
const labelSize = fittedLabelSize(context, route.label, 1_520);
|
||||
const labelSize = fittedLabelSize(context, route.label, 1_480);
|
||||
context.font = `650 ${labelSize}px "Geist Variable", sans-serif`;
|
||||
context.fillText(route.label, 142, 326);
|
||||
context.fillText(route.label, 96, 322);
|
||||
|
||||
context.strokeStyle = '#ffb879';
|
||||
context.lineWidth = 10;
|
||||
// Lane rule under the label.
|
||||
context.globalAlpha = 0.7;
|
||||
context.fillStyle = accent;
|
||||
context.fillRect(96, 448, 1_380, 6);
|
||||
context.globalAlpha = 0.35;
|
||||
for (let x = 1_500; x < 1_940; x += 64) context.fillRect(x, 448, 34, 6);
|
||||
context.globalAlpha = 1;
|
||||
|
||||
// Exit arrow: leans toward the roadside the sign stands on.
|
||||
const leansLeft = route.corridor === 'left' || route.corridor === 'outer-left';
|
||||
context.save();
|
||||
context.translate(1_800, 270);
|
||||
if (leansLeft) context.scale(-1, 1);
|
||||
context.rotate(-Math.PI / 4);
|
||||
context.strokeStyle = accent;
|
||||
context.lineWidth = 22;
|
||||
context.lineCap = 'square';
|
||||
context.lineJoin = 'miter';
|
||||
context.beginPath();
|
||||
context.moveTo(1_776, 304);
|
||||
context.lineTo(1_890, 304);
|
||||
context.lineTo(1_846, 260);
|
||||
context.moveTo(1_890, 304);
|
||||
context.lineTo(1_846, 348);
|
||||
context.moveTo(-110, 0);
|
||||
context.lineTo(100, 0);
|
||||
context.moveTo(30, -70);
|
||||
context.lineTo(100, 0);
|
||||
context.lineTo(30, 70);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function fittedLabelSize(
|
||||
|
||||
Reference in New Issue
Block a user