fix: make nightdrive signage reliable and legible
This commit is contained in:
@@ -7,7 +7,7 @@ import { createRoad } from '@/scene/world/road';
|
||||
import { createTerrain } from '@/scene/world/terrain';
|
||||
import { createSun } from '@/scene/world/sun';
|
||||
import { createCar, loadGrandTourer } from '@/scene/world/car';
|
||||
import { createSign } from '@/scene/world/sign';
|
||||
import { createSign, prepareSignTypography } from '@/scene/world/sign';
|
||||
import { createAtmosphere } from '@/scene/world/atmosphere';
|
||||
import { createPostProcessor } from '@/scene/post-processing';
|
||||
import { createDriveDynamics } from '@/scene/drive-dynamics';
|
||||
@@ -97,7 +97,10 @@ export async function createProceduralSceneGeneration(
|
||||
const authoredCar = await loadGrandTourer(context.signal);
|
||||
const car = createCar(context.resources, authoredCar);
|
||||
car.setDetail(mobileOrTablet ? 'reduced' : 'full');
|
||||
const sign = createSign(context.resources);
|
||||
await prepareSignTypography(context.signal);
|
||||
const sign = createSign(context.resources, {
|
||||
maximumAnisotropy: renderer.capabilities.getMaxAnisotropy(),
|
||||
});
|
||||
const atmosphere = createAtmosphere(context.resources, quality);
|
||||
scene.add(
|
||||
atmosphere.group,
|
||||
@@ -152,7 +155,7 @@ export async function createProceduralSceneGeneration(
|
||||
}
|
||||
car.setDetail(width < 1_100 || height < 620 ? 'reduced' : 'full');
|
||||
car.group.visible = height >= 340;
|
||||
sign.setViewportAspect(camera.aspect);
|
||||
sign.setViewportSize(width, height);
|
||||
atmosphere.setViewportAspect(camera.aspect);
|
||||
postProcessor?.resize(width, height, quality.pixelRatio * quality.postScale);
|
||||
if (TEST_BUILD && context.quality !== 'medium') {
|
||||
@@ -296,6 +299,21 @@ export async function createProceduralSceneGeneration(
|
||||
camera.fov = cameraState.fov;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
if (TEST_BUILD && snapshot.sign !== null) {
|
||||
const presentation = sign.inspect(camera, width, height);
|
||||
if (presentation !== null) {
|
||||
window.dispatchEvent(new CustomEvent('portfolio-sign-frame', {
|
||||
detail: {
|
||||
now,
|
||||
...snapshot.sign,
|
||||
activeSectionId: snapshot.activeSectionId,
|
||||
direction: snapshot.direction,
|
||||
...presentation,
|
||||
rendererPixelRatio: renderer.getPixelRatio(),
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (postProcessor) {
|
||||
postProcessor.setDynamics(response.bloomEnergy);
|
||||
postProcessor.render(scene, camera);
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import * as THREE from 'three';
|
||||
import type { SectionId } from '@/content/types';
|
||||
import type { SignSnapshot } from '@/journey/types';
|
||||
import {
|
||||
signRouteFor,
|
||||
signViewportProfileFor,
|
||||
type SignCorridor,
|
||||
type SignRouteSpec,
|
||||
type SignViewportProfile,
|
||||
} from '@/journey/sign-routes';
|
||||
import type { SceneResourceBag } from '@/scene/resources';
|
||||
|
||||
const routeLabels: Readonly<Record<SectionId, string>> = {
|
||||
start: 'Start',
|
||||
profile: 'Profile',
|
||||
experience: 'Experience',
|
||||
infrastructure: 'Infrastructure',
|
||||
'selected-builds': 'Selected builds',
|
||||
'additional-work': 'Additional work',
|
||||
toolkit: 'Toolkit',
|
||||
contact: 'Contact',
|
||||
};
|
||||
|
||||
interface SignPose {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -21,22 +17,111 @@ interface SignPose {
|
||||
scale: number;
|
||||
}
|
||||
|
||||
const NARROW_SIGN_POSES = {
|
||||
horizon: { x: 5.2, y: 6.8, z: -142, scale: 0.15 },
|
||||
dwell: { x: 5.8, y: 4.86, z: -20, scale: 0.82 },
|
||||
} as const;
|
||||
interface SignPoseSet {
|
||||
horizon: SignPose;
|
||||
dwell: SignPose;
|
||||
}
|
||||
|
||||
const WIDE_SIGN_POSES = {
|
||||
horizon: { x: 9.1, y: 7.4, z: -160, scale: 0.18 },
|
||||
dwell: { x: 10.7, y: 6.64, z: -14, scale: 1.12 },
|
||||
} as const;
|
||||
interface ProjectingCamera {
|
||||
updateMatrixWorld(force: boolean): void;
|
||||
}
|
||||
|
||||
const SIGN_MARKER_FONT = '560 46px "Geist Mono Variable", ui-monospace, monospace';
|
||||
const SIGN_LABEL_FONT = '660 164px "Geist Mono Variable", ui-monospace, monospace';
|
||||
|
||||
const SIGN_POSES: Readonly<Record<SignViewportProfile, Readonly<Record<SignCorridor, SignPoseSet>>>> = {
|
||||
wide: {
|
||||
left: {
|
||||
horizon: { x: -9.6, y: 7.2, z: -156, scale: 0.2 },
|
||||
dwell: { x: -11.6, y: 7.9, z: -16, scale: 1.25 },
|
||||
},
|
||||
right: {
|
||||
horizon: { x: 10.2, y: 7.2, z: -156, scale: 0.2 },
|
||||
dwell: { x: 12.4, y: 7.9, z: -16, scale: 1.25 },
|
||||
},
|
||||
'outer-left': {
|
||||
horizon: { x: -17.5, y: 7.4, z: -168, scale: 0.2 },
|
||||
dwell: { x: -21.5, y: 8.55, z: -24, scale: 1.35 },
|
||||
},
|
||||
'outer-right': {
|
||||
horizon: { x: 18.5, y: 7.4, z: -168, scale: 0.2 },
|
||||
dwell: { x: 22.5, y: 8.55, z: -24, scale: 1.35 },
|
||||
},
|
||||
},
|
||||
tall: {
|
||||
left: {
|
||||
horizon: { x: -4.2, y: 6.4, z: -136, scale: 0.16 },
|
||||
dwell: { x: -5.2, y: 8.1, z: -24, scale: 1.3 },
|
||||
},
|
||||
right: {
|
||||
horizon: { x: 4.2, y: 6.4, z: -136, scale: 0.16 },
|
||||
dwell: { x: 5.2, y: 8.1, z: -24, scale: 1.3 },
|
||||
},
|
||||
'outer-left': {
|
||||
horizon: { x: -5.8, y: 6.7, z: -148, scale: 0.16 },
|
||||
dwell: { x: -7.2, y: 8.72, z: -30, scale: 1.4 },
|
||||
},
|
||||
'outer-right': {
|
||||
horizon: { x: 5.8, y: 6.7, z: -148, scale: 0.16 },
|
||||
dwell: { x: 7.2, y: 8.72, z: -30, scale: 1.4 },
|
||||
},
|
||||
},
|
||||
short: {
|
||||
left: {
|
||||
horizon: { x: -7.8, y: 6.6, z: -144, scale: 0.18 },
|
||||
dwell: { x: -8.8, y: 5.45, z: -26, scale: 2.55 },
|
||||
},
|
||||
right: {
|
||||
horizon: { x: 7.8, y: 6.6, z: -144, scale: 0.18 },
|
||||
dwell: { x: 8.8, y: 5.45, z: -26, scale: 2.55 },
|
||||
},
|
||||
'outer-left': {
|
||||
horizon: { x: -11.2, y: 6.8, z: -154, scale: 0.18 },
|
||||
dwell: { x: -12.6, y: 5.8, z: -32, scale: 2.25 },
|
||||
},
|
||||
'outer-right': {
|
||||
horizon: { x: 11.2, y: 6.8, z: -154, scale: 0.18 },
|
||||
dwell: { x: 12.6, y: 5.8, z: -32, scale: 2.25 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export interface SignWorld {
|
||||
group: SignGroup;
|
||||
inspect(
|
||||
camera: ProjectingCamera,
|
||||
width: number,
|
||||
height: number,
|
||||
): SignPresentation | null;
|
||||
setViewportAspect(aspect: number): void;
|
||||
setViewportSize(width: number, height: number): void;
|
||||
update(sign: SignSnapshot | null): void;
|
||||
}
|
||||
|
||||
export interface SignPresentation {
|
||||
visible: boolean;
|
||||
corridor: SignCorridor;
|
||||
viewportProfile: SignViewportProfile;
|
||||
bounds: {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
texture: {
|
||||
width: number;
|
||||
height: number;
|
||||
anisotropy: number;
|
||||
fontReadyWhenDrawn: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SignRenderOptions {
|
||||
maximumAnisotropy?: number;
|
||||
}
|
||||
|
||||
interface SignGroup {
|
||||
visible: boolean;
|
||||
position: {
|
||||
@@ -51,13 +136,28 @@ interface SignGroup {
|
||||
scale: { x: number; setScalar(scale: number): void };
|
||||
add(...objects: unknown[]): void;
|
||||
getObjectByName(name: string): unknown;
|
||||
updateMatrixWorld(force: boolean): void;
|
||||
}
|
||||
|
||||
export function routeLabelFor(id: SectionId): string {
|
||||
return routeLabels[id];
|
||||
return signRouteFor(id).label;
|
||||
}
|
||||
|
||||
export function createSign(resources: SceneResourceBag): SignWorld {
|
||||
export async function prepareSignTypography(signal: AbortSignal): Promise<void> {
|
||||
const fonts = document.fonts;
|
||||
if (fonts === undefined) return;
|
||||
await Promise.all([
|
||||
fonts.load(SIGN_MARKER_FONT),
|
||||
fonts.load(SIGN_LABEL_FONT),
|
||||
fonts.ready,
|
||||
]);
|
||||
if (signal.aborted) throw new DOMException('Scene build aborted', 'AbortError');
|
||||
}
|
||||
|
||||
export function createSign(
|
||||
resources: SceneResourceBag,
|
||||
options: SignRenderOptions = {},
|
||||
): SignWorld {
|
||||
const group = new THREE.Group();
|
||||
group.name = 'roadside-sign';
|
||||
group.visible = false;
|
||||
@@ -103,7 +203,7 @@ export function createSign(resources: SceneResourceBag): SignWorld {
|
||||
}));
|
||||
|
||||
const housing = new THREE.Mesh(
|
||||
resources.own('geometry', createSignBody(7.5, 2.3, 0.34, 0.2)),
|
||||
resources.own('geometry', createSignBody(8.55, 2.65, 0.42, 0.22)),
|
||||
housingMaterial,
|
||||
);
|
||||
housing.name = 'sign-housing';
|
||||
@@ -111,7 +211,7 @@ export function createSign(resources: SceneResourceBag): SignWorld {
|
||||
group.add(housing);
|
||||
|
||||
const backplate = new THREE.Mesh(
|
||||
resources.own('geometry', createSignBody(7.2, 2.04, 0.18, 0.16)),
|
||||
resources.own('geometry', createSignBody(8.24, 2.34, 0.2, 0.18)),
|
||||
backplateMaterial,
|
||||
);
|
||||
backplate.name = 'sign-backplate';
|
||||
@@ -119,15 +219,18 @@ export function createSign(resources: SceneResourceBag): SignWorld {
|
||||
group.add(backplate);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1_024;
|
||||
canvas.height = 288;
|
||||
canvas.width = 2_048;
|
||||
canvas.height = 512;
|
||||
const context = canvas.getContext('2d');
|
||||
const texture = resources.own('texture', new THREE.CanvasTexture(canvas));
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
texture.generateMipmaps = true;
|
||||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.anisotropy = 4;
|
||||
texture.anisotropy = Math.min(
|
||||
8,
|
||||
Math.max(1, Math.floor(options.maximumAnisotropy ?? 8)),
|
||||
);
|
||||
const faceMaterial = resources.own('material', new THREE.MeshBasicMaterial({
|
||||
color: 0xffffff,
|
||||
transparent: true,
|
||||
@@ -140,55 +243,56 @@ export function createSign(resources: SceneResourceBag): SignWorld {
|
||||
toneMapped: false,
|
||||
}));
|
||||
const face = new THREE.Mesh(
|
||||
resources.own('geometry', new THREE.PlaneGeometry(6.92, 1.76)),
|
||||
resources.own('geometry', new THREE.PlaneGeometry(8, 2)),
|
||||
faceMaterial,
|
||||
);
|
||||
face.name = 'sign-face';
|
||||
face.position.z = 0.545;
|
||||
face.position.z = 0.625;
|
||||
face.userData.textureUpdateCount = 0;
|
||||
face.userData.fontReadyWhenDrawn = false;
|
||||
group.add(face);
|
||||
|
||||
const edgeLight = new THREE.Group();
|
||||
edgeLight.name = 'sign-edge-light';
|
||||
const edgeGeometry = resources.own('geometry', new THREE.BoxGeometry(0.055, 1.42, 0.055));
|
||||
const upperEdgeGeometry = resources.own('geometry', new THREE.BoxGeometry(1.16, 0.045, 0.055));
|
||||
const edgeGeometry = resources.own('geometry', new THREE.BoxGeometry(0.055, 1.62, 0.055));
|
||||
const upperEdgeGeometry = resources.own('geometry', new THREE.BoxGeometry(1.32, 0.045, 0.055));
|
||||
const leftEdge = new THREE.Mesh(edgeGeometry, edgeMaterial);
|
||||
leftEdge.position.set(-3.36, 0, 0.585);
|
||||
leftEdge.position.set(-3.9, 0, 0.665);
|
||||
edgeLight.add(leftEdge);
|
||||
const upperEdge = new THREE.Mesh(upperEdgeGeometry, edgeMaterial);
|
||||
upperEdge.position.set(-2.81, 0.88, 0.585);
|
||||
upperEdge.position.set(-3.28, 1, 0.665);
|
||||
edgeLight.add(upperEdge);
|
||||
group.add(edgeLight);
|
||||
|
||||
const routeAccent = new THREE.Mesh(
|
||||
resources.own('geometry', new THREE.BoxGeometry(1.05, 0.055, 0.065)),
|
||||
resources.own('geometry', new THREE.BoxGeometry(1.18, 0.055, 0.065)),
|
||||
routeMaterial,
|
||||
);
|
||||
routeAccent.name = 'sign-route-accent';
|
||||
routeAccent.position.set(2.77, -0.88, 0.585);
|
||||
routeAccent.position.set(3.35, -1, 0.665);
|
||||
group.add(routeAccent);
|
||||
|
||||
const post = new THREE.Mesh(
|
||||
resources.own('geometry', new THREE.CylinderGeometry(0.095, 0.14, 4.85, 8)),
|
||||
resources.own('geometry', new THREE.CylinderGeometry(0.095, 0.145, 5.05, 8)),
|
||||
supportMaterial,
|
||||
);
|
||||
post.name = 'sign-post';
|
||||
post.position.set(2.35, -3.32, -0.14);
|
||||
post.position.set(2.75, -3.58, -0.16);
|
||||
group.add(post);
|
||||
|
||||
const bracket = new THREE.Group();
|
||||
bracket.name = 'sign-bracket';
|
||||
const arm = new THREE.Mesh(
|
||||
resources.own('geometry', new THREE.BoxGeometry(2.5, 0.11, 0.15)),
|
||||
resources.own('geometry', new THREE.BoxGeometry(2.95, 0.11, 0.15)),
|
||||
supportMaterial,
|
||||
);
|
||||
arm.position.set(1.18, -1.08, -0.13);
|
||||
arm.position.set(1.4, -1.24, -0.15);
|
||||
bracket.add(arm);
|
||||
const brace = new THREE.Mesh(
|
||||
resources.own('geometry', new THREE.BoxGeometry(0.1, 1.25, 0.12)),
|
||||
supportMaterial,
|
||||
);
|
||||
brace.position.set(1.72, -1.56, -0.13);
|
||||
brace.position.set(2.05, -1.74, -0.15);
|
||||
brace.rotation.z = -0.72;
|
||||
bracket.add(brace);
|
||||
group.add(bracket);
|
||||
@@ -198,7 +302,7 @@ export function createSign(resources: SceneResourceBag): SignWorld {
|
||||
supportMaterial,
|
||||
);
|
||||
footing.name = 'sign-footing';
|
||||
footing.position.set(2.35, -5.76, -0.14);
|
||||
footing.position.set(2.75, -6.18, -0.16);
|
||||
group.add(footing);
|
||||
|
||||
const fadeMaterials = [
|
||||
@@ -212,15 +316,24 @@ export function createSign(resources: SceneResourceBag): SignWorld {
|
||||
const departureOrigin = new THREE.Vector3();
|
||||
let departureScale = 1;
|
||||
let target: SectionId | null = null;
|
||||
let pendingNarrowViewport = false;
|
||||
let activeNarrowViewport = false;
|
||||
let pendingViewport: SignViewportProfile = 'wide';
|
||||
let activeViewport: SignViewportProfile = 'wide';
|
||||
let activeCorridor: SignCorridor = 'right';
|
||||
let previousPhase: SignSnapshot['phase'] | null = null;
|
||||
const faceCornerCoordinates = [
|
||||
[-4, -1, 0],
|
||||
[4, -1, 0],
|
||||
[4, 1, 0],
|
||||
[-4, 1, 0],
|
||||
] as const;
|
||||
const projectedCorner = new THREE.Vector3();
|
||||
|
||||
const updateTexture = (id: SectionId): void => {
|
||||
if (target === id) return;
|
||||
target = id;
|
||||
face.userData.textureUpdateCount += 1;
|
||||
if (context) drawLabel(context, routeLabelFor(id), canvas.width, canvas.height);
|
||||
face.userData.fontReadyWhenDrawn = document.fonts?.check(SIGN_LABEL_FONT) ?? true;
|
||||
if (context) drawLabel(context, signRouteFor(id), canvas.width, canvas.height);
|
||||
texture.needsUpdate = true;
|
||||
};
|
||||
|
||||
@@ -231,34 +344,92 @@ export function createSign(resources: SceneResourceBag): SignWorld {
|
||||
const applyPose = (pose: SignPose): void => {
|
||||
group.position.set(pose.x, pose.y, pose.z);
|
||||
group.scale.setScalar(pose.scale);
|
||||
plantMounting(pose.y, pose.scale);
|
||||
};
|
||||
|
||||
const plantMounting = (groupY: number, groupScale: number): void => {
|
||||
const safeScale = Math.max(0.001, groupScale);
|
||||
const footingY = -groupY / safeScale;
|
||||
const postTopY = -1.36;
|
||||
const postHeight = Math.max(0.36, postTopY - footingY);
|
||||
post.position.y = (postTopY + footingY) * 0.5;
|
||||
post.scale.set(1 / safeScale, postHeight / 5.05, 1 / safeScale);
|
||||
footing.position.y = footingY;
|
||||
footing.scale.setScalar(1 / Math.max(1, safeScale));
|
||||
};
|
||||
|
||||
return {
|
||||
group,
|
||||
inspect(camera, width, height) {
|
||||
if (target === null || width <= 0 || height <= 0) return null;
|
||||
group.updateMatrixWorld(true);
|
||||
camera.updateMatrixWorld(true);
|
||||
let left = Number.POSITIVE_INFINITY;
|
||||
let top = Number.POSITIVE_INFINITY;
|
||||
let right = Number.NEGATIVE_INFINITY;
|
||||
let bottom = Number.NEGATIVE_INFINITY;
|
||||
for (const [xPosition, yPosition, zPosition] of faceCornerCoordinates) {
|
||||
projectedCorner
|
||||
.set(xPosition, yPosition, zPosition)
|
||||
.applyMatrix4(face.matrixWorld)
|
||||
.project(camera);
|
||||
const x = (projectedCorner.x + 1) * 0.5 * width;
|
||||
const y = (1 - projectedCorner.y) * 0.5 * height;
|
||||
left = Math.min(left, x);
|
||||
top = Math.min(top, y);
|
||||
right = Math.max(right, x);
|
||||
bottom = Math.max(bottom, y);
|
||||
}
|
||||
return {
|
||||
visible: group.visible,
|
||||
corridor: activeCorridor,
|
||||
viewportProfile: activeViewport,
|
||||
bounds: {
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
width: right - left,
|
||||
height: bottom - top,
|
||||
},
|
||||
texture: {
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
anisotropy: texture.anisotropy,
|
||||
fontReadyWhenDrawn: face.userData.fontReadyWhenDrawn === true,
|
||||
},
|
||||
};
|
||||
},
|
||||
setViewportAspect(aspect) {
|
||||
pendingNarrowViewport = !Number.isFinite(aspect) || aspect < 0.85;
|
||||
if (!group.visible) activeNarrowViewport = pendingNarrowViewport;
|
||||
pendingViewport = !Number.isFinite(aspect) || aspect < 0.85 ? 'tall' : 'wide';
|
||||
if (previousPhase === null) activeViewport = pendingViewport;
|
||||
},
|
||||
setViewportSize(width, height) {
|
||||
pendingViewport = signViewportProfileFor(width, height);
|
||||
if (previousPhase === null) activeViewport = pendingViewport;
|
||||
},
|
||||
update(sign) {
|
||||
if (!sign) {
|
||||
group.visible = false;
|
||||
applyOpacity(0);
|
||||
previousPhase = null;
|
||||
activeNarrowViewport = pendingNarrowViewport;
|
||||
activeViewport = pendingViewport;
|
||||
return;
|
||||
}
|
||||
|
||||
if (target !== null && target !== sign.targetId && group.visible) return;
|
||||
if (target !== sign.targetId) {
|
||||
activeNarrowViewport = pendingNarrowViewport;
|
||||
activeViewport = sign.viewportProfile ?? pendingViewport;
|
||||
updateTexture(sign.targetId);
|
||||
}
|
||||
|
||||
const opacity = clamp01(sign.opacity);
|
||||
const progress = smoothstep(clamp01(sign.progress));
|
||||
const { horizon, dwell } = activeNarrowViewport
|
||||
? NARROW_SIGN_POSES
|
||||
: WIDE_SIGN_POSES;
|
||||
const route = signRouteFor(sign.targetId);
|
||||
const corridor = route.corridor ?? 'right';
|
||||
activeCorridor = corridor;
|
||||
const { horizon, dwell } = SIGN_POSES[activeViewport][corridor];
|
||||
const side = corridor.includes('left') ? -1 : 1;
|
||||
|
||||
if (sign.phase === 'anticipate') {
|
||||
applyPose(horizon);
|
||||
@@ -269,6 +440,7 @@ export function createSign(resources: SceneResourceBag): SignWorld {
|
||||
THREE.MathUtils.lerp(horizon.z, dwell.z, progress),
|
||||
);
|
||||
group.scale.setScalar(THREE.MathUtils.lerp(horizon.scale, dwell.scale, progress));
|
||||
plantMounting(group.position.y, group.scale.x);
|
||||
} else if (sign.phase === 'dwell') {
|
||||
applyPose(dwell);
|
||||
} else {
|
||||
@@ -277,15 +449,18 @@ export function createSign(resources: SceneResourceBag): SignWorld {
|
||||
departureOrigin.copy(group.position);
|
||||
departureScale = group.scale.x;
|
||||
}
|
||||
const lateralExit = activeViewport === 'wide'
|
||||
? 14
|
||||
: activeViewport === 'short' ? 10 : 8;
|
||||
group.position.set(
|
||||
departureOrigin.x + progress * (activeNarrowViewport ? 2.2 : 4.4),
|
||||
departureOrigin.y + progress * 0.62,
|
||||
THREE.MathUtils.lerp(departureOrigin.z, 28, progress),
|
||||
departureOrigin.x + progress * side * lateralExit,
|
||||
departureOrigin.y + progress * 0.72,
|
||||
departureOrigin.z - progress * 32,
|
||||
);
|
||||
group.scale.setScalar(departureScale * THREE.MathUtils.lerp(1, 1.06, progress));
|
||||
group.scale.setScalar(departureScale * THREE.MathUtils.lerp(1, 0.82, progress));
|
||||
}
|
||||
|
||||
group.rotation.y = activeNarrowViewport ? -0.14 : -0.23;
|
||||
group.rotation.y = -side * (activeViewport === 'wide' ? 0.2 : 0.12);
|
||||
applyOpacity(opacity);
|
||||
group.visible = opacity > 0;
|
||||
previousPhase = sign.phase;
|
||||
@@ -325,32 +500,32 @@ function createClippedShape(width: number, height: number, cut: number) {
|
||||
|
||||
function drawLabel(
|
||||
context: CanvasRenderingContext2D,
|
||||
label: string,
|
||||
route: SignRouteSpec,
|
||||
width: number,
|
||||
height: number,
|
||||
): void {
|
||||
context.clearRect(0, 0, width, height);
|
||||
context.fillStyle = '#07111a';
|
||||
context.fillRect(0, 0, width, height);
|
||||
context.fillStyle = '#55d4df';
|
||||
context.fillRect(42, 36, 8, height - 72);
|
||||
context.fillStyle = '#91aab3';
|
||||
context.font = '500 23px "Geist Mono Variable", ui-monospace, monospace';
|
||||
context.fillStyle = '#4cbac4';
|
||||
context.fillRect(86, 72, 12, height - 144);
|
||||
context.fillStyle = '#879da4';
|
||||
context.font = SIGN_MARKER_FONT;
|
||||
context.textAlign = 'left';
|
||||
context.textBaseline = 'middle';
|
||||
context.fillText('NEXT / ROUTE', 82, 72);
|
||||
context.fillStyle = '#eef7f8';
|
||||
context.font = '650 74px "Geist Mono Variable", ui-monospace, monospace';
|
||||
context.fillText(label, 82, 170, width - 210);
|
||||
context.strokeStyle = '#d99758';
|
||||
context.lineWidth = 7;
|
||||
context.fillText(`ROUTE ${route.marker} / NEXT`, 150, 126);
|
||||
context.fillStyle = '#c8d8da';
|
||||
context.font = SIGN_LABEL_FONT;
|
||||
context.fillText(route.label, 150, 326, width - 420);
|
||||
context.strokeStyle = '#cf8e54';
|
||||
context.lineWidth = 12;
|
||||
context.lineCap = 'square';
|
||||
context.beginPath();
|
||||
context.moveTo(width - 116, 156);
|
||||
context.lineTo(width - 74, 156);
|
||||
context.lineTo(width - 92, 138);
|
||||
context.moveTo(width - 74, 156);
|
||||
context.lineTo(width - 92, 174);
|
||||
context.moveTo(width - 250, 304);
|
||||
context.lineTo(width - 158, 304);
|
||||
context.lineTo(width - 198, 264);
|
||||
context.moveTo(width - 158, 304);
|
||||
context.lineTo(width - 198, 344);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user