mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
refactor: control character from scene (#58551)
This commit is contained in:
committed by
GitHub
parent
1948c3ef73
commit
25c964abc0
@@ -3,13 +3,14 @@ import { Characters, CharacterPosition } from '../../../../redux/prop-types';
|
||||
import { characterAssets } from './scene-assets';
|
||||
|
||||
import './character.css';
|
||||
import { SceneSubject } from './scene-subject';
|
||||
|
||||
interface CharacterProps {
|
||||
position: CharacterPosition;
|
||||
opacity: number;
|
||||
name: Characters;
|
||||
isBlinking: boolean;
|
||||
isTalking: boolean;
|
||||
sceneSubject: SceneSubject;
|
||||
}
|
||||
|
||||
interface CharacterStyles {
|
||||
@@ -27,29 +28,43 @@ export function Character({
|
||||
position,
|
||||
opacity,
|
||||
name,
|
||||
isBlinking,
|
||||
isTalking
|
||||
isTalking,
|
||||
sceneSubject
|
||||
}: CharacterProps): JSX.Element {
|
||||
const [eyesAreOpen, setEyesAreOpen] = useState(true);
|
||||
const [mouthIsOpen, setMouthIsOpen] = useState(false);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
const onNotify = (eventType: 'play' | 'stop') => {
|
||||
if (eventType === 'play') {
|
||||
setIsPlaying(true);
|
||||
} else {
|
||||
setIsPlaying(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let blinkIntervalId: NodeJS.Timeout;
|
||||
sceneSubject.attach(onNotify);
|
||||
return () => {
|
||||
sceneSubject.detach(onNotify);
|
||||
};
|
||||
}, [sceneSubject]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying) return;
|
||||
let blinkTimeoutId: NodeJS.Timeout;
|
||||
|
||||
if (isBlinking) {
|
||||
const blinkPeriod = getRandomInt(2000, 5000);
|
||||
blinkIntervalId = setInterval(() => {
|
||||
const blinkJitter = getRandomInt(0, 1000);
|
||||
blinkTimeoutId = setTimeout(() => {
|
||||
setEyesAreOpen(false);
|
||||
const blinkPeriod = getRandomInt(2000, 5000);
|
||||
const blinkIntervalId = setInterval(() => {
|
||||
const blinkJitter = getRandomInt(0, 1000);
|
||||
blinkTimeoutId = setTimeout(() => {
|
||||
setEyesAreOpen(false);
|
||||
|
||||
blinkTimeoutId = setTimeout(() => {
|
||||
setEyesAreOpen(true);
|
||||
}, 30); // always unblink after 30ms
|
||||
}, blinkJitter);
|
||||
}, blinkPeriod);
|
||||
}
|
||||
blinkTimeoutId = setTimeout(() => {
|
||||
setEyesAreOpen(true);
|
||||
}, 30); // always unblink after 30ms
|
||||
}, blinkJitter);
|
||||
}, blinkPeriod);
|
||||
|
||||
// Clear intervals when component is unmounted or conditions change
|
||||
return () => {
|
||||
@@ -57,9 +72,10 @@ export function Character({
|
||||
clearInterval(blinkIntervalId);
|
||||
clearTimeout(blinkTimeoutId);
|
||||
};
|
||||
}, [isBlinking]);
|
||||
}, [isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying) return;
|
||||
let talkIntervalId: NodeJS.Timeout;
|
||||
let mouthOpenTimeoutId: NodeJS.Timeout;
|
||||
let mouthCloseTimeoutId: NodeJS.Timeout;
|
||||
@@ -91,7 +107,7 @@ export function Character({
|
||||
clearTimeout(mouthOpenTimeoutId);
|
||||
clearTimeout(mouthCloseTimeoutId);
|
||||
};
|
||||
}, [isTalking]);
|
||||
}, [isTalking, isPlaying]);
|
||||
|
||||
const characterWrapStyles: CharacterStyles = {
|
||||
opacity
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type Observer = () => void;
|
||||
type Observer = (eventType: 'play' | 'stop') => void;
|
||||
|
||||
export class SceneSubject {
|
||||
#observers: Observer[];
|
||||
@@ -16,7 +16,7 @@ export class SceneSubject {
|
||||
|
||||
// For now, we don't need to pass any data to the observers, so notify()
|
||||
// doesn't take any arguments.
|
||||
notify() {
|
||||
this.#observers.forEach(observer => observer());
|
||||
notify(eventType: 'play' | 'stop') {
|
||||
this.#observers.forEach(observer => observer(eventType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export function Scene({
|
||||
canPauseRef.current = false;
|
||||
};
|
||||
|
||||
const playScene = useCallback(() => {
|
||||
const handlePlay = useCallback(() => {
|
||||
const updateCurrentTime = () => {
|
||||
const time = Date.now() - startRef.current;
|
||||
setCurrentTime(time);
|
||||
@@ -207,9 +207,9 @@ export function Scene({
|
||||
},
|
||||
duration + sToMs(audio.startTime)
|
||||
);
|
||||
}, [isPlaying, sceneIsReady, audio, duration]);
|
||||
}, [audio, duration, isPlaying, sceneIsReady]);
|
||||
|
||||
const resetScene = useCallback(() => {
|
||||
const handleStop = useCallback(() => {
|
||||
usedCommandsRef.current.clear();
|
||||
pause();
|
||||
audioRef.current.currentTime = audio.startTimestamp || 0;
|
||||
@@ -222,12 +222,27 @@ export function Scene({
|
||||
setBackground(initBackground);
|
||||
}, [audio, initCharacters, initBackground]);
|
||||
|
||||
const onNotify = useCallback(
|
||||
(eventType: 'play' | 'stop') => {
|
||||
if (eventType === 'play') {
|
||||
handlePlay();
|
||||
} else {
|
||||
handleStop();
|
||||
}
|
||||
},
|
||||
[handlePlay, handleStop]
|
||||
);
|
||||
|
||||
const resetScene = useCallback(() => {
|
||||
sceneSubject.notify('stop');
|
||||
}, [sceneSubject]);
|
||||
|
||||
useEffect(() => {
|
||||
sceneSubject.attach(playScene);
|
||||
sceneSubject.attach(onNotify);
|
||||
return () => {
|
||||
sceneSubject.detach(playScene);
|
||||
sceneSubject.detach(onNotify);
|
||||
};
|
||||
}, [playScene, sceneSubject]);
|
||||
}, [onNotify, sceneSubject]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmpty(sortedCommands)) return;
|
||||
@@ -306,8 +321,8 @@ export function Scene({
|
||||
name={character}
|
||||
position={position}
|
||||
opacity={opacity}
|
||||
sceneSubject={sceneSubject}
|
||||
isTalking={isTalking}
|
||||
isBlinking={isPlaying}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -328,7 +343,7 @@ export function Scene({
|
||||
<div className='scene-start-screen'>
|
||||
<button
|
||||
className='scene-start-btn scene-play-btn'
|
||||
onClick={() => sceneSubject.notify()}
|
||||
onClick={() => sceneSubject.notify('play')}
|
||||
>
|
||||
<img
|
||||
src={`${images}/play-button.png`}
|
||||
|
||||
@@ -198,7 +198,7 @@ const ShowGeneric = ({
|
||||
<Hotkeys
|
||||
executeChallenge={handleSubmit}
|
||||
containerRef={container}
|
||||
playScene={scene ? () => sceneSubject.notify() : undefined}
|
||||
playScene={scene ? () => sceneSubject.notify('play') : undefined}
|
||||
>
|
||||
<LearnLayout>
|
||||
<Helmet
|
||||
|
||||
Reference in New Issue
Block a user