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
JavaScript
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