UNPKG

yarn-spinner-runner-ts

Version:

TypeScript parser, compiler, and runtime for Yarn Spinner 3.x with React adapter [NPM package](https://www.npmjs.com/package/yarn-spinner-runner-ts)

152 lines 7.17 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useState, useEffect, useMemo, useRef } from "react"; /** * Visual scene component that displays background and actor images */ export function DialogueScene({ sceneName, speaker, scenes, className, actorTransitionDuration = 350, }) { const [currentBackground, setCurrentBackground] = useState(null); const [backgroundOpacity, setBackgroundOpacity] = useState(1); const [nextBackground, setNextBackground] = useState(null); const [lastSceneName, setLastSceneName] = useState(undefined); const [lastSpeaker, setLastSpeaker] = useState(undefined); const [activeActor, setActiveActor] = useState(null); const [previousActor, setPreviousActor] = useState(null); const [currentActorVisible, setCurrentActorVisible] = useState(false); const [previousActorVisible, setPreviousActorVisible] = useState(false); const previousActorTimeoutRef = useRef(null); // Get scene config - use last scene if current node has no scene const activeSceneName = sceneName || lastSceneName; const sceneConfig = activeSceneName ? scenes.scenes[activeSceneName] : undefined; const backgroundImage = sceneConfig?.background; const activeSpeakerName = speaker || lastSpeaker; const resolvedActor = useMemo(() => { if (!sceneConfig || !activeSpeakerName) { return null; } const actorEntries = Object.entries(sceneConfig.actors); const matchingActor = actorEntries.find(([actorName]) => actorName.toLowerCase() === activeSpeakerName.toLowerCase()); if (!matchingActor) { return null; } const [actorName, actorConfig] = matchingActor; if (!actorConfig?.image) { return null; } return { name: actorName, image: actorConfig.image }; }, [sceneConfig, activeSpeakerName]); // Track last speaker - update when speaker is provided, keep when undefined useEffect(() => { if (speaker) { setLastSpeaker(speaker); } // Never clear speaker - keep it until a new one is explicitly set }, [speaker]); // Handle background transitions useEffect(() => { if (backgroundImage && backgroundImage !== currentBackground) { if (currentBackground === null) { // First background - set immediately setCurrentBackground(backgroundImage); setBackgroundOpacity(1); if (sceneName) setLastSceneName(sceneName); } else { // Transition: fade out, change, fade in setBackgroundOpacity(0); setTimeout(() => { setNextBackground(backgroundImage); setTimeout(() => { setCurrentBackground(backgroundImage); setNextBackground(null); setBackgroundOpacity(1); if (sceneName) setLastSceneName(sceneName); }, 50); }, 300); // Half of transition duration } } else if (sceneName && sceneName !== lastSceneName) { // New scene name set, update tracking setLastSceneName(sceneName); } // Never clear background - keep it until a new one is explicitly set }, [backgroundImage, currentBackground, sceneName, lastSceneName]); // Handle actor portrait transitions (cross-fade between speakers) useEffect(() => { let fadeOutTimeout = null; setActiveActor((currentActor) => { const currentImage = currentActor?.image ?? null; const currentName = currentActor?.name ?? null; const nextImage = resolvedActor?.image ?? null; const nextName = resolvedActor?.name ?? null; if (currentImage === nextImage && currentName === nextName) { return currentActor; } if (currentActor) { setPreviousActor(currentActor); setPreviousActorVisible(true); fadeOutTimeout = setTimeout(() => { setPreviousActorVisible(false); }, 0); } else { setPreviousActor(null); setPreviousActorVisible(false); } setCurrentActorVisible(false); return resolvedActor; }); return () => { if (fadeOutTimeout !== null) { clearTimeout(fadeOutTimeout); } }; }, [resolvedActor]); useEffect(() => { if (!activeActor) { return; } const fadeInTimeout = setTimeout(() => { setCurrentActorVisible(true); }, 0); return () => { clearTimeout(fadeInTimeout); }; }, [activeActor]); // Remove previous actor once fade-out completes useEffect(() => { if (!previousActor) { return; } if (previousActorTimeoutRef.current) { clearTimeout(previousActorTimeoutRef.current); } previousActorTimeoutRef.current = setTimeout(() => { setPreviousActor(null); previousActorTimeoutRef.current = null; }, actorTransitionDuration); return () => { if (previousActorTimeoutRef.current) { clearTimeout(previousActorTimeoutRef.current); previousActorTimeoutRef.current = null; } }; }, [previousActor, actorTransitionDuration]); // Default background color when no scene const defaultBgColor = "rgba(26, 26, 46, 1)"; // Dark blue-purple const handleActorImageError = (actorName, imageUrl) => () => { console.error(`Failed to load actor image for ${actorName}:`, imageUrl); }; const sceneStyle = { backgroundColor: currentBackground ? undefined : defaultBgColor, backgroundImage: currentBackground ? `url(${currentBackground})` : undefined, opacity: backgroundOpacity, ["--yd-actor-transition"]: `${Math.max(actorTransitionDuration, 0)}ms`, }; return (_jsxs("div", { className: `yd-scene ${className || ""}`, style: sceneStyle, children: [nextBackground && (_jsx("div", { className: "yd-scene-next", style: { backgroundImage: `url(${nextBackground})`, opacity: 1 - backgroundOpacity, } })), previousActor && (_jsx("img", { className: `yd-actor yd-actor--previous ${previousActorVisible ? "yd-actor--visible" : ""}`, src: previousActor.image, alt: previousActor.name, onError: handleActorImageError(previousActor.name, previousActor.image) }, `${previousActor.name}-previous`)), activeActor && (_jsx("img", { className: `yd-actor yd-actor--current ${currentActorVisible ? "yd-actor--visible" : ""}`, src: activeActor.image, alt: activeActor.name, onError: handleActorImageError(activeActor.name, activeActor.image) }, `${activeActor.name}-current`))] })); } //# sourceMappingURL=DialogueScene.js.map