Skip to content

Commit

Permalink
fix: Sync nameplate state
Browse files Browse the repository at this point in the history
  • Loading branch information
cma2819 committed Dec 28, 2023
1 parent 096cecc commit 813c322
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 69 deletions.
14 changes: 2 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"klona": "^2.0.6",
"moment": "^2.29.4",
"obs-websocket-js": "^5.0.2",
"react-transition-group": "^4.4.5",
"tslib": "^2.5.0",
"uuid": "^9.0.0",
"ws": "^8.13.0"
Expand Down
119 changes: 63 additions & 56 deletions src/browser/graphics/components/nameplate/index.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
import gsap from "gsap";
import {ThinText, TimerText} from "../lib/text";
import {useCurrentRun, useTimer} from "../lib/hooks";
import iconTwitter from "../../images/icon/icon_twitter.svg";
import iconTwitch from "../../images/icon/icon_twitch.svg";
import iconNico from "../../images/icon/icon_nico.svg";
import iconRunner from "../../images/icon/icon_runner.svg";
import iconCommentator from "../../images/icon/icon_commentary.svg";
import {CSSProperties, HTMLAttributes, useEffect, useRef} from "react";
import {CSSProperties, HTMLAttributes, useContext, useRef} from "react";
import {background, border, text} from "../../styles/colors";
import {filterNonNullable} from "../../../../extension/lib/array";
import {Commentator, Runner, Timer} from "../../../../nodecg/replicants";
import {SyncDisplayContext} from "./sync-display";
import styled from "styled-components";
import {
SwitchTransition,
Transition,
TransitionStatus,
} from "react-transition-group";

const textPlacement = {
gridColumn: "1 / 2",
gridRow: "1 / 2",
};

const useSocial = (icon: string, text?: string) => {
const ref = useRef<HTMLDivElement>(null);
if (!text) {
return [null, null] as const;
}
return [
const FadeContainer = styled.div<{state: TransitionStatus}>`
transition: all 0.5s;
opacity: 0;
opacity: ${(props) =>
["entered", "existing"].includes(props.state) ? "1" : "0"};
`;

const SocialText = ({icon, text}: {icon: string; text?: string}) => {
return text ? (
<ThinText
ref={ref}
style={{
...textPlacement,
fontSize: "24px",
Expand All @@ -32,14 +39,12 @@ const useSocial = (icon: string, text?: string) => {
gridTemplateColumns: "24px auto",
placeContent: "center",
placeItems: "center",
opacity: 0,
}}
>
<img src={icon} height={24} width={24}></img>
<div> {text}</div>
</ThinText>,
ref,
] as const;
<div>{text}</div>
</ThinText>
) : null;
};

const NamePlateContent = ({
Expand All @@ -51,35 +56,10 @@ const NamePlateContent = ({
style?: CSSProperties;
isRaceRunner?: boolean;
}) => {
const nameRef = useRef<HTMLDivElement>(null);
const emptyRef = useRef<HTMLDivElement>(null);
const [twitter, twitterRef] = useSocial(iconTwitter, runner?.twitter);
const [twitch, twitchRef] = useSocial(iconTwitch, runner?.twitch);
const [nico, nicoRef] = useSocial(iconNico, runner?.nico);

useEffect(() => {
const refs = filterNonNullable(
!isRaceRunner
? [nameRef, twitterRef, twitchRef, nicoRef].map(
(ref) => ref?.current ?? nameRef.current,
)
: [emptyRef, twitterRef, twitchRef, nicoRef].map(
(ref) => ref?.current ?? emptyRef.current,
),
);
if (!refs[0]) {
return;
}
const contextDisplay = useContext(SyncDisplayContext);
const display = runner?.[contextDisplay] ? contextDisplay : "name";

const tl = gsap.timeline({repeat: -1});
for (const ref of refs) {
tl.fromTo(ref, {opacity: 0}, {opacity: 1, duration: 0.5});
tl.to(refs, {opacity: 0, duration: 0.5}, "+=30");
}
return () => {
tl.kill();
};
}, [nicoRef, twitterRef, twitchRef, isRaceRunner]);
const fadeNodeRef = useRef(null);

return !isRaceRunner ? (
<div
Expand All @@ -90,15 +70,28 @@ const NamePlateContent = ({
...style,
}}
>
<ThinText
ref={nameRef}
style={{fontSize: "26px", opacity: 0, ...textPlacement}}
>
{runner?.name}
</ThinText>
{twitter}
{twitch}
{nico}
<SwitchTransition>
<Transition ref={fadeNodeRef} key={display} timeout={500}>
{(state) => (
<FadeContainer state={state}>
{display === "name" && (
<ThinText style={{fontSize: "26px", ...textPlacement}}>
{runner?.name}
</ThinText>
)}
{display === "twitter" && (
<SocialText icon={iconTwitter} text={runner?.twitter} />
)}
{display === "twitch" && (
<SocialText icon={iconTwitch} text={runner?.twitch} />
)}
{display === "nico" && (
<SocialText icon={iconNico} text={runner?.nico} />
)}
</FadeContainer>
)}
</Transition>
</SwitchTransition>
</div>
) : (
<div
Expand All @@ -122,10 +115,24 @@ const NamePlateContent = ({
...style,
}}
>
<div ref={emptyRef}></div>
{twitter}
{twitch}
{nico}
<SwitchTransition>
<Transition ref={fadeNodeRef} key={display} timeout={500}>
{(state) => (
<FadeContainer state={state}>
{display === "name" && <div></div>}
{display === "twitter" && (
<SocialText icon={iconTwitter} text={runner?.twitter} />
)}
{display === "twitch" && (
<SocialText icon={iconTwitch} text={runner?.twitch} />
)}
{display === "nico" && (
<SocialText icon={iconNico} text={runner?.nico} />
)}
</FadeContainer>
)}
</Transition>
</SwitchTransition>
</div>
</div>
);
Expand Down
68 changes: 68 additions & 0 deletions src/browser/graphics/components/nameplate/sync-display.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import gsap from "gsap";
import {ReactNode, createContext, useEffect, useState} from "react";
import {useReplicant} from "../../../use-replicant";

type DisplayLabel = "name" | "twitter" | "twitch" | "nico";

export const SyncDisplayContext = createContext<DisplayLabel>("name");

export const SyncDisplayProvider = ({children}: {children: ReactNode}) => {
const [display, setDisplay] = useState<DisplayLabel>("name");

const currentRun = useReplicant("current-run");

const participantSocials = [
...(currentRun?.runners.map((runner) =>
[
runner.twitter ? "twitter" : null,
runner.twitch ? "twitch" : null,
runner.nico ? "nico" : null,
].filter((v): v is DisplayLabel => v !== null),
) ?? []),
...(currentRun?.commentators.map((commentator) =>
[
commentator?.twitter ? "twitter" : null,
commentator?.twitch ? "twitch" : null,
commentator?.nico ? "nico" : null,
].filter((v): v is DisplayLabel => v !== null),
) ?? []),
];

const displayTwitter = participantSocials.some((socials) =>
socials.includes("twitter"),
);
const displayTwitch = participantSocials.some((socials) =>
socials.includes("twitch"),
);
const displayNico = participantSocials.some((socials) =>
socials.includes("nico"),
);

useEffect(() => {
const tl = gsap.timeline({repeat: -1});
const displays: DisplayLabel[] = [
displayTwitter ? "twitter" : null,
displayTwitch ? "twitch" : null,
displayNico ? "nico" : null,
"name",
].filter((v): v is DisplayLabel => v !== null);
for (const social of displays) {
tl.call(
(s) => {
setDisplay(s);
},
[social],
"+=31", // 表示時間の30秒と切り替え時間の1秒
);
}
return () => {
tl.kill();
};
}, [displayTwitter, displayTwitch, displayNico]);

return (
<SyncDisplayContext.Provider value={display}>
{children}
</SyncDisplayContext.Provider>
);
};
7 changes: 6 additions & 1 deletion src/browser/graphics/views/game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import "modern-normalize";
import "../styles/adobe-fonts.js";

import {render} from "../../render.js";
import {SyncDisplayProvider} from "../components/nameplate/sync-display.js";

const params = new URLSearchParams(location.search);
const layout = params.get("layout") ?? "4x3-1";

(async () => {
const {default: App} = await import(`./game-scene/${layout}.tsx`);
render(<App />);
render(
<SyncDisplayProvider>
<App />
</SyncDisplayProvider>,
);
})();

0 comments on commit 813c322

Please sign in to comment.