This commit is contained in:
Andras Schmelczer 2026-05-17 14:12:01 +01:00
commit 80ed37298b
73 changed files with 3619 additions and 1295 deletions

View file

@ -12,19 +12,27 @@ interface IntroTitleAgentOptions {
count: number;
width: number;
height: number;
progress?: number;
seed?: number;
}
type RandomSource = () => number;
const INTRO_TITLE = appConfig.simulation.intro.title;
export const createIntroTitleAgents = ({
count,
width,
height,
progress = 0,
seed,
}: IntroTitleAgentOptions): Float32Array => {
if (count <= 0) {
return new Float32Array();
}
const random = seed === undefined ? Math.random : createSeededRandom(seed);
const introProgress = clamp(progress, 0, 1);
const safeWidth = Math.max(1, width);
const safeHeight = Math.max(1, height);
const points = createIntroTitlePoints(safeWidth, safeHeight);
@ -62,14 +70,14 @@ export const createIntroTitleAgents = ({
);
for (let i = 0; i < count; i++) {
const point = points[Math.floor(Math.random() * points.length)];
const point = points[Math.floor(random() * points.length)];
const targetX = Math.max(
0,
Math.min(safeWidth - 1, point.x + (Math.random() - 0.5) * targetJitter)
Math.min(safeWidth - 1, point.x + (random() - 0.5) * targetJitter)
);
const targetY = Math.max(
0,
Math.min(safeHeight - 1, point.y + (Math.random() - 0.5) * targetJitter)
Math.min(safeHeight - 1, point.y + (random() - 0.5) * targetJitter)
);
const [startX, startY] = getIntroRadialStart(
targetX,
@ -77,7 +85,8 @@ export const createIntroTitleAgents = ({
safeWidth,
safeHeight,
introCircleRadius,
entryJitter
entryJitter,
random
);
const approachAngle = Math.atan2(targetY - startY, targetX - startX);
let targetAngle = point.tangent ?? approachAngle;
@ -87,21 +96,32 @@ export const createIntroTitleAgents = ({
const distanceFraction =
Math.hypot(targetX - startX, targetY - startY) / Math.hypot(safeWidth, safeHeight);
const introDelay = Math.min(
appConfig.simulation.intro.targetDelayMax,
distanceFraction * appConfig.simulation.intro.targetDelayDistanceMultiplier +
random() * appConfig.simulation.intro.targetDelayRandomMultiplier
);
const pathProgress = getIntroAgentPathProgress(introProgress, introDelay);
const initialAngle =
approachAngle + (random() - 0.5) * appConfig.simulation.intro.angleJitterRadians;
const currentAngle = mixAngle(
initialAngle,
targetAngle,
smoothstep(
appConfig.simulation.intro.angleEaseStart,
appConfig.simulation.intro.angleEaseEnd,
pathProgress
)
);
const base = i * AGENT_FLOAT_COUNT;
data[base] = startX;
data[base + 1] = startY;
data[base + 2] =
approachAngle +
(Math.random() - 0.5) * appConfig.simulation.intro.angleJitterRadians;
data[base] = mix(startX, targetX, pathProgress);
data[base + 1] = mix(startY, targetY, pathProgress);
data[base + 2] = currentAngle;
data[base + 3] = point.colorIndex;
data[base + 4] = targetX;
data[base + 5] = targetY;
data[base + 6] = targetAngle;
data[base + 7] = Math.min(
appConfig.simulation.intro.targetDelayMax,
distanceFraction * appConfig.simulation.intro.targetDelayDistanceMultiplier +
Math.random() * appConfig.simulation.intro.targetDelayRandomMultiplier
);
data[base + 7] = introDelay;
}
return data;
@ -113,7 +133,8 @@ const getIntroRadialStart = (
width: number,
height: number,
radius: number,
jitter: number
jitter: number,
random: RandomSource
): [number, number] => {
const centerX = width / 2;
const centerY = height * appConfig.simulation.intro.verticalAnchor;
@ -121,14 +142,16 @@ const getIntroRadialStart = (
const offsetY = targetY - centerY;
const length = Math.hypot(offsetX, offsetY);
const angle =
length > 0.001 ? Math.atan2(offsetY, offsetX) : Math.random() * Math.PI * 2;
length > appConfig.simulation.intro.radialStartEpsilon
? Math.atan2(offsetY, offsetX)
: random() * Math.PI * 2;
const directionX = Math.cos(angle);
const directionY = Math.sin(angle);
const tangentX = -directionY;
const tangentY = directionX;
const tangentJitter = (Math.random() - 0.5) * jitter;
const tangentJitter = (random() - 0.5) * jitter;
const radialJitter =
(Math.random() - 0.5) * jitter * appConfig.simulation.intro.radialJitterRatio;
(random() - 0.5) * jitter * appConfig.simulation.intro.radialJitterRatio;
const startX =
centerX + directionX * (radius + radialJitter) + tangentX * tangentJitter;
const startY =
@ -154,7 +177,7 @@ const createIntroTitlePoints = (
const fontSize = getIntroTitleFontSize(context, width, height);
context.clearRect(0, 0, width, height);
context.font = `${fontSize}px "Open Sans", sans-serif`;
context.font = `${fontSize}px ${appConfig.simulation.intro.fontFamily}`;
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillStyle = '#fff';
@ -302,7 +325,7 @@ const getIntroTitleFontSize = (
);
while (fontSize > appConfig.simulation.intro.minFontSizePx) {
context.font = `${fontSize}px "Open Sans", sans-serif`;
context.font = `${fontSize}px ${appConfig.simulation.intro.fontFamily}`;
const metrics = context.measureText(INTRO_TITLE);
const measuredHeight =
metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent || fontSize;
@ -352,3 +375,53 @@ const getMaskAlpha = (
const clampedY = Math.max(0, Math.min(height - 1, Math.round(y)));
return data[(clampedY * width + clampedX) * 4 + 3];
};
const getIntroAgentPathProgress = (introProgress: number, introDelay: number): number => {
if (introProgress <= introDelay) {
return 0;
}
const activeProgress =
(introProgress - introDelay) /
Math.max(appConfig.simulation.intro.pathProgressEpsilon, 1 - introDelay);
return easePathProgress(clamp(activeProgress, 0, 1));
};
const createSeededRandom = (seed: number): RandomSource => {
let state = seed >>> 0;
return () => {
let value = (state += 0x6d2b79f5);
value = Math.imul(value ^ (value >>> 15), value | 1);
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
};
};
const mix = (from: number, to: number, amount: number): number =>
from + (to - from) * amount;
const mixAngle = (from: number, to: number, amount: number): number => {
const delta = Math.atan2(Math.sin(to - from), Math.cos(to - from));
return from + delta * amount;
};
const smoothstep = (edge0: number, edge1: number, value: number): number => {
const t = clamp((value - edge0) / (edge1 - edge0), 0, 1);
return t * t * (3 - 2 * t);
};
const easePathProgress = (amount: number): number => {
if (appConfig.simulation.intro.pathEasing === 'linear') {
return amount;
}
return easeOutQuad(amount);
};
const easeOutQuad = (value: number): number => value * (2 - value);
const clamp = (value: number, min: number, max: number): number => {
const safeValue = Number.isFinite(value) ? value : min;
return Math.min(max, Math.max(min, safeValue));
};