Support scenic-only scenes and blackout fades
This commit is contained in:
@@ -909,30 +909,6 @@ const createTextStrip = (
|
||||
return { mesh, texture };
|
||||
};
|
||||
|
||||
const fallbackLoadedAssets = (definition: SceneDefinition, count: number): LoadedPhotoAsset[] => {
|
||||
const palette = ["#98a8c0", "#d8b28f", "#b5c8d8", "#bea5d6"];
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
asset: {
|
||||
id: `${definition.sceneKey}-fallback-${index}`,
|
||||
submissionId: definition.id,
|
||||
originalKey: "",
|
||||
mimeType: "placeholder",
|
||||
processingStatus: "ready",
|
||||
moderationStatus: "approved",
|
||||
createdAt: new Date(0).toISOString(),
|
||||
width: 1200,
|
||||
height: 900
|
||||
},
|
||||
texture: null,
|
||||
aspect: 4 / 3,
|
||||
dominantColor: palette[index % palette.length] ?? "#8ea0b4",
|
||||
sourceUrl: null
|
||||
}));
|
||||
};
|
||||
|
||||
const withAssets = (input: SceneActivationInput, minCount: number) =>
|
||||
input.loadedAssets.length > 0 ? input.loadedAssets : fallbackLoadedAssets(input.definition, minCount);
|
||||
|
||||
const combineInstances = (...instances: Array<SceneInstance | null | undefined>): SceneInstance => {
|
||||
const active = instances.filter((instance): instance is SceneInstance => Boolean(instance));
|
||||
if (active.length === 1) {
|
||||
@@ -1034,7 +1010,7 @@ const buildTextOverlay = (input: SceneActivationInput): SceneInstance | null =>
|
||||
return null;
|
||||
}
|
||||
|
||||
const palette = paletteFromAssets(withAssets(input, 1), input.params.scenicTreatment);
|
||||
const palette = paletteFromAssets(input.loadedAssets, input.params.scenicTreatment);
|
||||
const root = new THREE.Group();
|
||||
const textures: THREE.Texture[] = [];
|
||||
const animated: Array<{
|
||||
@@ -1179,7 +1155,7 @@ const buildWitnessFloat = (input: SceneActivationInput): SceneInstance => {
|
||||
const { composition, photoTreatment } = input.params;
|
||||
const mode = input.modeKey ?? "near_witness";
|
||||
const count = clamp(1 + Math.round(composition.supportCount), 1, 3);
|
||||
const assets = withAssets(input, count).slice(0, count);
|
||||
const assets = input.loadedAssets.slice(0, count);
|
||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||
const backdrop = buildBackdropSystem(input, palette);
|
||||
const root = new THREE.Group();
|
||||
@@ -1240,7 +1216,7 @@ const buildPortalFrame = (input: SceneActivationInput): SceneInstance => {
|
||||
const { composition, photoTreatment } = input.params;
|
||||
const mode = input.modeKey ?? "soft_gate";
|
||||
const count = clamp(1 + Math.round(composition.supportCount), 1, 2);
|
||||
const assets = withAssets(input, count).slice(0, count);
|
||||
const assets = input.loadedAssets.slice(0, count);
|
||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||
const backdrop = buildBackdropSystem(input, palette);
|
||||
const root = new THREE.Group();
|
||||
@@ -1313,7 +1289,7 @@ const buildOrbitGallery = (input: SceneActivationInput): SceneInstance => {
|
||||
const { composition, photoTreatment } = input.params;
|
||||
const mode = input.modeKey ?? "halo_arc";
|
||||
const count = clamp(1 + Math.round(composition.supportCount), 1, 3);
|
||||
const assets = withAssets(input, count).slice(0, count);
|
||||
const assets = input.loadedAssets.slice(0, count);
|
||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||
const backdrop = buildBackdropSystem(input, palette);
|
||||
const root = new THREE.Group();
|
||||
@@ -1374,7 +1350,7 @@ const buildSuspensionField = (input: SceneActivationInput): SceneInstance => {
|
||||
const { composition, photoTreatment } = input.params;
|
||||
const mode = input.modeKey ?? "hover_shelf";
|
||||
const count = clamp(1 + Math.round(composition.supportCount), 2, 4);
|
||||
const assets = withAssets(input, count).slice(0, count);
|
||||
const assets = input.loadedAssets.slice(0, count);
|
||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||
const backdrop = buildBackdropSystem(input, palette);
|
||||
const root = new THREE.Group();
|
||||
@@ -1440,7 +1416,7 @@ const buildChorusArray = (input: SceneActivationInput): SceneInstance => {
|
||||
const { composition, photoTreatment } = input.params;
|
||||
const mode = input.modeKey ?? "grid_choir";
|
||||
const count = clamp(1 + Math.round(composition.supportCount), 3, 4);
|
||||
const assets = withAssets(input, count).slice(0, count);
|
||||
const assets = input.loadedAssets.slice(0, count);
|
||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||
const backdrop = buildBackdropSystem(input, palette);
|
||||
const root = new THREE.Group();
|
||||
@@ -1501,7 +1477,7 @@ const buildEqualCollage = (input: SceneActivationInput): SceneInstance => {
|
||||
const { composition, photoTreatment } = input.params;
|
||||
const mode = input.modeKey ?? "quadrant";
|
||||
const count = clamp(1 + Math.round(composition.supportCount), 2, 4);
|
||||
const assets = withAssets(input, count).slice(0, count);
|
||||
const assets = input.loadedAssets.slice(0, count);
|
||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||
const backdrop = buildBackdropSystem(input, palette);
|
||||
const root = new THREE.Group();
|
||||
@@ -1560,7 +1536,7 @@ const buildArrivalRelay = (input: SceneActivationInput): SceneInstance => {
|
||||
const { composition, photoTreatment } = input.params;
|
||||
const mode = input.modeKey ?? "edge_queue";
|
||||
const count = clamp(1 + Math.round(composition.supportCount), 1, 4);
|
||||
const assets = withAssets(input, count).slice(0, count);
|
||||
const assets = input.loadedAssets.slice(0, count);
|
||||
const palette = paletteFromAssets(assets, input.params.scenicTreatment);
|
||||
const backdrop = buildBackdropSystem(input, palette);
|
||||
const root = new THREE.Group();
|
||||
@@ -1759,6 +1735,14 @@ interface TransitionRuntime {
|
||||
startedAtMs: number;
|
||||
}
|
||||
|
||||
interface BlackoutRuntime {
|
||||
fromLevel: number;
|
||||
toLevel: number;
|
||||
style: CueTransition["style"];
|
||||
durationMs: number;
|
||||
startedAtMs: number;
|
||||
}
|
||||
|
||||
const transitionStyleToValue = (style: CueTransition["style"]) => {
|
||||
switch (style) {
|
||||
case "dissolve":
|
||||
@@ -1879,6 +1863,18 @@ export class RenderSurface {
|
||||
});
|
||||
private readonly compositeFromQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.compositeFromMaterial);
|
||||
private readonly compositeToQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.compositeToMaterial);
|
||||
private readonly blackoutQuad = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(2, 2),
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: "#000000",
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
side: THREE.DoubleSide,
|
||||
toneMapped: false
|
||||
})
|
||||
);
|
||||
private readonly shutterBars = Array.from({ length: 8 }, () =>
|
||||
new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(2.4, 0.18),
|
||||
@@ -1922,8 +1918,9 @@ export class RenderSurface {
|
||||
};
|
||||
private currentRuntime: SceneRuntime | null = null;
|
||||
private transitionRuntime: TransitionRuntime | null = null;
|
||||
private blackoutRuntime: BlackoutRuntime | null = null;
|
||||
private lastFrameMs = 0;
|
||||
private blackout = false;
|
||||
private blackoutLevel = 0;
|
||||
private activationToken = 0;
|
||||
private activePresentationKey: string | undefined;
|
||||
private qualityProfile: SurfaceQualityProfile = "program";
|
||||
@@ -1950,8 +1947,9 @@ export class RenderSurface {
|
||||
|
||||
this.compositeFromQuad.position.z = 0;
|
||||
this.compositeToQuad.position.z = 0.01;
|
||||
this.blackoutQuad.position.z = 0.019;
|
||||
this.veilOverlay.position.z = 0.02;
|
||||
this.compositeScene.add(this.compositeFromQuad, this.compositeToQuad, this.veilOverlay);
|
||||
this.compositeScene.add(this.compositeFromQuad, this.compositeToQuad, this.blackoutQuad, this.veilOverlay);
|
||||
this.shutterBars.forEach((bar, index) => {
|
||||
bar.position.set(0, 0.86 - index * 0.24, 0.03 + index * 0.001);
|
||||
this.compositeScene.add(bar);
|
||||
@@ -1968,13 +1966,6 @@ export class RenderSurface {
|
||||
const deltaMs = this.lastFrameMs === 0 ? 16.6 : timestamp - this.lastFrameMs;
|
||||
this.lastFrameMs = timestamp;
|
||||
|
||||
if (this.blackout) {
|
||||
this.renderer.setClearColor("#000000", 1);
|
||||
this.renderer.setRenderTarget(null);
|
||||
this.renderer.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderer.setClearColor("#040508", 1);
|
||||
const context = {
|
||||
elapsedMs: timestamp,
|
||||
@@ -1989,6 +1980,7 @@ export class RenderSurface {
|
||||
this.renderRuntimeToTarget(this.transitionRuntime.from, this.fromTarget);
|
||||
this.renderRuntimeToTarget(this.transitionRuntime.to, this.toTarget);
|
||||
this.renderCompositeTransition(progress, this.transitionRuntime.style);
|
||||
this.renderBlackoutOverlay(timestamp);
|
||||
if (progress >= 1) {
|
||||
this.finishTransition();
|
||||
}
|
||||
@@ -1998,12 +1990,14 @@ export class RenderSurface {
|
||||
if (!this.currentRuntime) {
|
||||
this.renderer.setRenderTarget(null);
|
||||
this.renderer.clear();
|
||||
this.renderBlackoutOverlay(timestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
updateSceneRuntime(this.currentRuntime, context);
|
||||
this.renderRuntimeToTarget(this.currentRuntime, this.toTarget);
|
||||
this.renderTargetToScreen(this.toTarget);
|
||||
this.renderBlackoutOverlay(timestamp);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2061,8 +2055,33 @@ export class RenderSurface {
|
||||
}
|
||||
}
|
||||
|
||||
setBlackout(blackout: boolean) {
|
||||
this.blackout = blackout;
|
||||
setBlackout(blackout: boolean, transition?: CueTransition | null, immediate = false) {
|
||||
const nextLevel = blackout ? 1 : 0;
|
||||
const now = this.lastFrameMs || (typeof performance !== "undefined" ? performance.now() : 0);
|
||||
|
||||
if (this.blackoutRuntime) {
|
||||
const progress = clamp((now - this.blackoutRuntime.startedAtMs) / this.blackoutRuntime.durationMs, 0, 1);
|
||||
const eased = THREE.MathUtils.smoothstep(progress, 0, 1);
|
||||
this.blackoutLevel = THREE.MathUtils.lerp(this.blackoutRuntime.fromLevel, this.blackoutRuntime.toLevel, eased);
|
||||
this.blackoutRuntime = null;
|
||||
}
|
||||
|
||||
if (Math.abs(nextLevel - this.blackoutLevel) < 0.001) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (immediate || !transition || transition.style === "cut" || transition.durationMs <= 0) {
|
||||
this.blackoutLevel = nextLevel;
|
||||
return;
|
||||
}
|
||||
|
||||
this.blackoutRuntime = {
|
||||
fromLevel: this.blackoutLevel,
|
||||
toLevel: nextLevel,
|
||||
style: transition.style,
|
||||
durationMs: transition.durationMs,
|
||||
startedAtMs: now
|
||||
};
|
||||
}
|
||||
|
||||
updatePresentation(presentation: SurfacePresentation | null, activationKey?: string) {
|
||||
@@ -2131,6 +2150,7 @@ export class RenderSurface {
|
||||
this.toTarget.dispose();
|
||||
this.compositeFromMaterial.dispose();
|
||||
this.compositeToMaterial.dispose();
|
||||
(this.blackoutQuad.material as THREE.Material).dispose();
|
||||
(this.veilOverlay.material as THREE.Material).dispose();
|
||||
this.shutterBars.forEach((bar) => (bar.material as THREE.Material).dispose());
|
||||
this.renderer.dispose();
|
||||
@@ -2220,6 +2240,7 @@ export class RenderSurface {
|
||||
this.compositeToQuad.scale.set(1, 1, 1);
|
||||
this.compositeFromQuad.rotation.z = 0;
|
||||
this.compositeToQuad.rotation.z = 0;
|
||||
(this.blackoutQuad.material as THREE.MeshBasicMaterial).opacity = 0;
|
||||
|
||||
const veilMaterial = this.veilOverlay.material as THREE.MeshBasicMaterial;
|
||||
veilMaterial.opacity = 0;
|
||||
@@ -2266,6 +2287,7 @@ export class RenderSurface {
|
||||
this.compositeToQuad.scale.set(1, 1, 1);
|
||||
this.compositeFromQuad.rotation.z = 0;
|
||||
this.compositeToQuad.rotation.z = 0;
|
||||
(this.blackoutQuad.material as THREE.MeshBasicMaterial).opacity = 0;
|
||||
(this.veilOverlay.material as THREE.MeshBasicMaterial).opacity = 0;
|
||||
this.shutterBars.forEach((bar, index) => {
|
||||
const material = bar.material as THREE.MeshBasicMaterial;
|
||||
@@ -2277,6 +2299,77 @@ export class RenderSurface {
|
||||
this.renderer.render(this.compositeScene, this.compositeCamera);
|
||||
}
|
||||
|
||||
private renderBlackoutOverlay(timestamp: number) {
|
||||
let style: CueTransition["style"] = "dissolve";
|
||||
let progress = this.blackoutLevel > 0 ? 1 : 0;
|
||||
let toBlack = this.blackoutLevel >= 1;
|
||||
|
||||
if (this.blackoutRuntime) {
|
||||
const rawProgress = clamp((timestamp - this.blackoutRuntime.startedAtMs) / this.blackoutRuntime.durationMs, 0, 1);
|
||||
const eased = THREE.MathUtils.smoothstep(rawProgress, 0, 1);
|
||||
this.blackoutLevel = THREE.MathUtils.lerp(this.blackoutRuntime.fromLevel, this.blackoutRuntime.toLevel, eased);
|
||||
style = this.blackoutRuntime.style;
|
||||
progress = eased;
|
||||
toBlack = this.blackoutRuntime.toLevel > this.blackoutRuntime.fromLevel;
|
||||
if (rawProgress >= 1) {
|
||||
this.blackoutLevel = this.blackoutRuntime.toLevel;
|
||||
this.blackoutRuntime = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.blackoutLevel <= 0.001) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.compositeFromMaterial.opacity = 0;
|
||||
this.compositeToMaterial.opacity = 0;
|
||||
this.compositeFromQuad.position.set(0, 0, 0);
|
||||
this.compositeToQuad.position.set(0, 0, 0.01);
|
||||
this.compositeFromQuad.scale.set(1, 1, 1);
|
||||
this.compositeToQuad.scale.set(1, 1, 1);
|
||||
this.compositeFromQuad.rotation.z = 0;
|
||||
this.compositeToQuad.rotation.z = 0;
|
||||
|
||||
const blackoutMaterial = this.blackoutQuad.material as THREE.MeshBasicMaterial;
|
||||
blackoutMaterial.opacity = clamp(this.blackoutLevel, 0, 1);
|
||||
this.blackoutQuad.position.set(0, 0, 0.019);
|
||||
this.blackoutQuad.scale.set(1, 1, 1);
|
||||
this.blackoutQuad.rotation.z = 0;
|
||||
|
||||
const veilMaterial = this.veilOverlay.material as THREE.MeshBasicMaterial;
|
||||
veilMaterial.opacity = 0;
|
||||
|
||||
this.shutterBars.forEach((bar, index) => {
|
||||
const material = bar.material as THREE.MeshBasicMaterial;
|
||||
material.opacity = 0;
|
||||
bar.position.set(0, 0.86 - index * 0.24, 0.03 + index * 0.001);
|
||||
});
|
||||
|
||||
if (style === "mist_reveal" && this.blackoutRuntime) {
|
||||
veilMaterial.opacity = (0.02 + this.blackoutLevel * 0.08) * Math.sin(progress * Math.PI);
|
||||
blackoutMaterial.opacity = clamp(this.blackoutLevel * 1.02, 0, 1);
|
||||
} else if (style === "depth_drift" && this.blackoutRuntime) {
|
||||
const offset = (toBlack ? 1 - progress : progress - 1) * 0.06;
|
||||
this.blackoutQuad.position.x = offset;
|
||||
this.blackoutQuad.scale.set(1.03, 1, 1);
|
||||
veilMaterial.opacity = Math.sin(progress * Math.PI) * 0.05;
|
||||
} else if (style === "shutter_reveal" && this.blackoutRuntime) {
|
||||
blackoutMaterial.opacity = clamp(Math.max(this.blackoutLevel, toBlack ? progress * 0.82 : this.blackoutLevel), 0, 1);
|
||||
this.shutterBars.forEach((bar, index) => {
|
||||
const material = bar.material as THREE.MeshBasicMaterial;
|
||||
const bandProgress = THREE.MathUtils.clamp((progress - index * 0.05) / 0.45, 0, 1);
|
||||
material.opacity = (1 - bandProgress) * 0.3;
|
||||
bar.position.x = toBlack ? -1.2 + bandProgress * 2.4 : 1.2 - bandProgress * 2.4;
|
||||
});
|
||||
}
|
||||
|
||||
const previousAutoClear = this.renderer.autoClear;
|
||||
this.renderer.autoClear = false;
|
||||
this.renderer.setRenderTarget(null);
|
||||
this.renderer.render(this.compositeScene, this.compositeCamera);
|
||||
this.renderer.autoClear = previousAutoClear;
|
||||
}
|
||||
|
||||
private finishTransition() {
|
||||
if (!this.transitionRuntime) {
|
||||
return;
|
||||
@@ -2303,6 +2396,7 @@ export class RenderSurface {
|
||||
disposeSceneRuntime(this.transitionRuntime.to);
|
||||
this.transitionRuntime = null;
|
||||
}
|
||||
this.blackoutRuntime = null;
|
||||
this.activePresentationKey = undefined;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user