Conjurations grow into the world, wrapped in glowing swirls
Nothing appears fully-formed anymore. Summoned creatures and conjured terrain volumes scale up from a sliver, bottom-anchored, as if growing out of the floor. Conjured SOLIDS — walls, firewalls, stone, and yes, illusion walls for whoever they fool — RISE: while growing, the rays pass over them so the room behind stays visible, and the renderer draws the young face climbing bottom-up with violet swirls running across it, solidifying as it tops out. Every creation also blooms the shimmer at its cell. The growth is driven through the fx channel, so the director's slate applies: things grow only once the camera faces them. Two camera notes squared away en route: the slate-setter read back the state it had just written, registering itself as its own dependency (an update-depth ping-pong under fast stepping) — locals now; and a reel whose FINAL step strides into a corner gets one more turn after the walk lands, to a view worth ending on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
cd9ce9fddf
commit
285c91b2c8
@@ -5,7 +5,7 @@
|
||||
// by the same depth buffer the walls wrote.
|
||||
import { castRay, billboards } from "./raycast";
|
||||
import { materialTextures } from "./textures";
|
||||
import { doorOpenness, fxFallback, type FpFx } from "./fx3d";
|
||||
import { doorOpenness, fxFallback, growProgress, type FpFx } from "./fx3d";
|
||||
import { terrainFallback, TERRAIN3D } from "./terrain3d";
|
||||
import { tokenArt } from "../art";
|
||||
import { PLAYER_COLORS } from "../colors";
|
||||
@@ -150,12 +150,21 @@
|
||||
ey += f.mag * (1 - p) * Math.cos(time * 0.087);
|
||||
}
|
||||
|
||||
// Doors mid-swing this frame, from the animated fx.
|
||||
// Doors mid-swing and conjurations mid-growth this frame.
|
||||
let doors: Record<string, number> | undefined;
|
||||
let growing: Record<string, number> | undefined;
|
||||
const spawn: Record<string, number> = {};
|
||||
for (const f of fx) {
|
||||
if (f.kind !== "door") continue;
|
||||
const a = doorOpenness((time - f.t0) / f.dur);
|
||||
if (a > 0) (doors ??= {})[f.edge] = Math.max(doors?.[f.edge] ?? 0, a);
|
||||
if (f.kind === "door") {
|
||||
const a = doorOpenness((time - f.t0) / f.dur);
|
||||
if (a > 0) (doors ??= {})[f.edge] = Math.max(doors?.[f.edge] ?? 0, a);
|
||||
} else if (f.kind === "grow") {
|
||||
const p = (time - f.t0) / f.dur;
|
||||
if (p < 0 || p >= 1) continue;
|
||||
const g = growProgress(p);
|
||||
if (f.slot === "solid") (growing ??= {})[f.key] = g;
|
||||
else spawn[f.key] = g;
|
||||
}
|
||||
}
|
||||
|
||||
// The ground and the vault overhead, cast per pixel: each screen row
|
||||
@@ -248,11 +257,13 @@
|
||||
// stands at the crossing — drawn after the sprites so bodies show
|
||||
// through it, shimmering so nobody mistakes it for stone.
|
||||
const ghosts: { col: number; depth: number; u: number; worldU: number }[] = [];
|
||||
// Open doorways collect their lintels, hung after the sprites.
|
||||
// Open doorways collect their lintels, hung after the sprites; and
|
||||
// conjurations still rising collect their growing columns.
|
||||
const lintels: { col: number; depth: number; u: number }[] = [];
|
||||
const risings: { col: number; depth: number; u: number; worldU: number; kind: string; g: number }[] = [];
|
||||
for (let col = 0; col < W; col++) {
|
||||
const rayAngle = facing + Math.atan((col / W - 0.5) * 2 * Math.tan(FOV / 2));
|
||||
const hit = castRay(view, ex, ey, rayAngle, doors);
|
||||
const hit = castRay(view, ex, ey, rayAngle, doors, growing);
|
||||
const depth = hit.dist * Math.cos(rayAngle - facing); // no fisheye
|
||||
zbuf[col] = depth;
|
||||
if (hit.warpId !== undefined) {
|
||||
@@ -268,6 +279,12 @@
|
||||
if (hit.doorway) {
|
||||
lintels.push({ col, depth: hit.doorway.dist * Math.cos(rayAngle - facing), u: hit.doorway.u });
|
||||
}
|
||||
if (hit.rising) {
|
||||
risings.push({
|
||||
col, depth: hit.rising.dist * Math.cos(rayAngle - facing),
|
||||
u: hit.rising.u, worldU: hit.rising.worldU, kind: hit.rising.kind, g: hit.rising.g,
|
||||
});
|
||||
}
|
||||
const wallH = Math.min(H * 2.5, H / Math.max(depth, 0.05));
|
||||
const top = half - wallH / 2;
|
||||
const tex = hit.frame ? textures.doorframe! : textures[hit.kind] ?? textures.wall!;
|
||||
@@ -331,7 +348,10 @@
|
||||
|
||||
// Sprites, far to near, sliced against the depth buffer.
|
||||
const sprites = billboards(view, povId, tokenArt, posOverride)
|
||||
.map((b) => project(b, ex, ey))
|
||||
.map((b) => {
|
||||
const g = b.key !== undefined ? spawn[b.key] : undefined;
|
||||
return project(g !== undefined ? { ...b, scale: b.scale * g } : b, ex, ey);
|
||||
})
|
||||
.filter((s): s is Projected => s !== null);
|
||||
for (const spot of rubble) {
|
||||
const s = project({ x: spot.x, y: spot.y, src: "/fx3d/rubble.png", scale: 0.5, aspect: 2, rise: 0 }, ex, ey);
|
||||
@@ -392,6 +412,22 @@
|
||||
if (s.alpha !== undefined || s.glow) ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// Conjurations rising: the wall (or stone) grows bottom-up where it
|
||||
// will stand, glowing swirls running over the young face.
|
||||
for (const ri of risings) {
|
||||
const full = Math.min(H * 2.5, H / Math.max(ri.depth, 0.05));
|
||||
const top0 = half + full / 2 - full * ri.g;
|
||||
const tex = textures[ri.kind] ?? textures.wall!;
|
||||
ctx.globalAlpha = 0.55 + 0.45 * ri.g;
|
||||
ctx.drawImage(tex, Math.min(tex.width - 1, ri.u * tex.width), 0,
|
||||
Math.max(1, tex.width / 96), tex.height, ri.col, top0, 1, full * ri.g);
|
||||
ctx.globalAlpha = 1;
|
||||
const swirl = (0.35 * (1 - ri.g) + 0.08) *
|
||||
(0.7 + 0.3 * Math.sin(time / 90 + ri.worldU * 21));
|
||||
ctx.fillStyle = `rgba(190,150,255,${Math.max(0, swirl)})`;
|
||||
ctx.fillRect(ri.col, top0, 1, full * ri.g);
|
||||
}
|
||||
|
||||
// Doorway lintels: the frame's top rows hung across each opening.
|
||||
for (const li of lintels) {
|
||||
const lh = Math.min(H * 2.5, H / Math.max(li.depth, 0.05));
|
||||
|
||||
Reference in New Issue
Block a user