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