Files
nodecg-venue-gfx/src/graphics/Break.tsx
T

139 lines
4.3 KiB
TypeScript

import React from "react";
import type NodeCG from "@nodecg/types";
import type { AssetFile, BreakSlide } from "../types";
import { useReplicantValue } from "./lib";
const breakSlideReplicant = nodecg.Replicant(
"breakSlide",
) as unknown as NodeCG.ServerReplicant<BreakSlide>;
// Optional background image (dashboard Assets → "Pauza — pozadinska slika");
// when present it replaces the drifting blobs.
const backgroundReplicant = nodecg.Replicant(
"assets:break-bg",
) as unknown as NodeCG.ServerReplicant<AssetFile[]>;
// Optional band logo (dashboard Assets → "Pauza — logo benda"), shown between
// the heading and the act name.
const logoReplicant = nodecg.Replicant(
"assets:break-logo",
) as unknown as NodeCG.ServerReplicant<AssetFile[]>;
interface Blob {
size: number;
left: string;
top: string;
color: string;
animation: string;
}
// Huge blurred radial blobs on slow drift loops — GPU-cheap ambient motion.
// Colors come from the THEME LAB block in index.css.
const BLOBS: Blob[] = [
{
size: 900,
left: "-10%",
top: "-20%",
color: "var(--color-break-blob-1)",
animation: "drift-a 90s ease-in-out infinite alternate",
},
{
size: 800,
left: "55%",
top: "40%",
color: "var(--color-break-blob-2)",
animation: "drift-b 110s ease-in-out infinite alternate",
},
{
size: 700,
left: "25%",
top: "55%",
color: "var(--color-break-blob-3)",
animation: "drift-c 70s ease-in-out infinite alternate",
},
];
export function Break() {
const slide = useReplicantValue<BreakSlide>(breakSlideReplicant);
const backgroundUrl =
useReplicantValue<AssetFile[]>(backgroundReplicant)?.[0]?.url ?? null;
// The slide names which uploaded file to use; fall back to the first one if
// nothing is picked or the picked file was deleted.
const logoFiles = useReplicantValue<AssetFile[]>(logoReplicant);
const logoUrl =
(logoFiles?.find((f) => f.base === slide?.logoName) ?? logoFiles?.[0])
?.url ?? null;
return (
<div
className="relative h-[1080px] w-[1920px] overflow-hidden"
style={{
background:
"linear-gradient(135deg, var(--color-break-bg-a) 0%, var(--color-break-bg-b) 55%, var(--color-break-bg-a) 100%)",
}}
>
{backgroundUrl ? (
<>
<img
src={backgroundUrl}
alt=""
className="absolute left-0 top-0 h-full w-full object-cover"
style={{ animation: "break-bounce 30s ease-in-out infinite alternate" }}
/>
{/* scrim keeps the announcement text readable — tune to taste */}
<div className="absolute inset-0 bg-black/40" />
</>
) : (
BLOBS.map((blob, i) => (
<div
key={i}
className="absolute rounded-full"
style={{
width: blob.size,
height: blob.size,
left: blob.left,
top: blob.top,
background: blob.color,
filter: "blur(120px)",
animation: blob.animation,
}}
/>
))
)}
{/*
Text stays well inside the edges: this same source is shown shrunk to
70% in the BREAK_AD_* scenes, so extremes would crowd the ad cutout.
*/}
<div
className="font-display absolute inset-0 flex flex-col items-center justify-center gap-10 px-[15%] text-center"
style={{ animation: "breathe 8s ease-in-out infinite" }}
>
<div className="text-6xl font-medium tracking-wide text-white/80">
{slide?.heading}
</div>
{(slide?.logoVisible ?? true) && logoUrl && (
<img
src={logoUrl}
alt=""
className="max-w-[45%] object-contain"
style={{
height: slide?.logoHeightPx ?? 240,
// invert must come before the shadow so the shadow itself
// doesn't flip to white
filter: `${slide?.logoInverted ? "invert(1) " : ""}drop-shadow(0 4px 24px rgba(0,0,0,0.6))`,
}}
/>
)}
<div className="text-9xl font-extrabold leading-tight text-white drop-shadow-[0_4px_24px_rgba(0,0,0,0.6)]">
{slide?.nextActName}
</div>
<div className="text-5xl font-light tracking-wider text-break-accent">
{slide?.nextActFrom}
</div>
</div>
</div>
);
}