Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | 4x 4x 4x 730x 730x 730x 730x 730x 730x 4x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x 4x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 73x 4x 4x 1x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 13x 1x | /**
* Deterministic parameters for the underwater fish school.
*
* Each fish follows a slow horizontal circular path with a sinusoidal
* vertical bob. Rendering is gated by camera y elsewhere; this module
* only exposes pure helpers.
*/
function mulberry32(seed: number): () => number {
let state = seed >>> 0;
return () => {
state = (state + 0x6d2b79f5) >>> 0;
let t = state;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
export interface FishPath {
/** Centre of the circular path in XZ. */
centerX: number;
centerZ: number;
/** Radius of the circle. */
radius: number;
/** Mean depth (y) the fish swims around. */
baseY: number;
/** Vertical bob amplitude. */
bobAmplitude: number;
/** Angular speed in rad/s. Positive is CCW. */
angularSpeed: number;
/** Starting phase in radians. */
phase: number;
/** Palette index for the fish body colour. */
colorIndex: number;
/** Body length multiplier (per-fish size variation). */
scale: number;
}
export const FISH_COLORS: readonly string[] = Object.freeze([
"#ffb85c",
"#5fb0ff",
"#ff6b6b",
"#f5d65c",
"#b48cff",
"#5fd9a3",
"#ff93c6"
]);
/** Where fish are allowed to swim vertically (world y). */
export const FISH_MIN_Y = -14;
export const FISH_MAX_Y = -2;
/** Horizontal extents for the centre of each fish's orbit. */
export const FISH_ORBIT_MIN_RADIUS = 5;
export const FISH_ORBIT_MAX_RADIUS = 35;
export const FISH_COUNT = 22;
export const FISH_SEED = 0xf15c0111;
export function generateFishPaths(
count: number = FISH_COUNT,
seed: number = FISH_SEED
): FishPath[] {
const rand = mulberry32(seed);
const paths: FishPath[] = [];
for (let i = 0; i < count; i++) {
const centerR = rand() * 25;
const centerTheta = rand() * Math.PI * 2;
paths.push({
centerX: Math.cos(centerTheta) * centerR,
centerZ: Math.sin(centerTheta) * centerR,
radius: FISH_ORBIT_MIN_RADIUS + rand() * (FISH_ORBIT_MAX_RADIUS - FISH_ORBIT_MIN_RADIUS),
baseY: FISH_MIN_Y + rand() * (FISH_MAX_Y - FISH_MIN_Y),
bobAmplitude: 0.3 + rand() * 0.9,
angularSpeed: (rand() < 0.5 ? -1 : 1) * (0.15 + rand() * 0.25),
phase: rand() * Math.PI * 2,
colorIndex: Math.floor(rand() * FISH_COLORS.length),
scale: 0.8 + rand() * 0.8
});
}
return paths;
}
/**
* Pure helper - where is this fish at time `t`, and which direction is
* it heading? Exported so the path math can be unit tested.
*/
export function fishPositionAt(
path: FishPath,
t: number
): { position: [number, number, number]; tangent: [number, number, number] } {
const angle = path.phase + path.angularSpeed * t;
const x = path.centerX + Math.cos(angle) * path.radius;
const z = path.centerZ + Math.sin(angle) * path.radius;
const y = path.baseY + Math.sin(t * 0.7 + path.phase) * path.bobAmplitude;
const sign = Math.sign(path.angularSpeed) || 1;
const tx = -Math.sin(angle) * sign;
const tz = Math.cos(angle) * sign;
return { position: [x, y, z], tangent: [tx, 0, tz] };
}
export const FISH_PATHS: readonly FishPath[] = Object.freeze(generateFishPaths());
|