]> git.localhorst.tv Git - alttp.git/blob - resources/js/components/common/PngPlayer.js
better png player
[alttp.git] / resources / js / components / common / PngPlayer.js
1 import parseApng from 'apng-js';
2 import axios from 'axios';
3 import PropTypes from 'prop-types';
4 import React from 'react';
5 import { Button } from 'react-bootstrap';
6 import { useTranslation } from 'react-i18next';
7
8 import Icon from './Icon';
9
10 const createPlayer = async (apng, canvas) => {
11         const context = canvas.getContext('2d', { willReadFrequently: true });
12         const player = await apng.getPlayer(context);
13         player.stop();
14         return player;
15 };
16
17 const PngPlayer = ({ src }) => {
18         const canvas = React.useRef();
19         const { t } = useTranslation();
20
21         const [apng, setApng] = React.useState(null);
22         const [error, setError] = React.useState(null);
23         const [frameInfo, setFrameInfo] = React.useState('');
24         const [loading, setLoading] = React.useState(true);
25         const [player, setPlayer] = React.useState(null);
26
27         React.useEffect(() => {
28                 if (!src) return;
29                 setError(null);
30                 setLoading(true);
31                 const ctrl = new AbortController();
32                 const fetchPng = async () => {
33                         try {
34                                 const response = await axios.get(src, {
35                                         responseType: 'arraybuffer',
36                                         signal: ctrl.signal,
37                                 });
38                                 const png = parseApng(response.data);
39                                 await png.createImages();
40                                 setApng(png);
41                                 setLoading(false);
42                         } catch (e) {
43                                 if (!axios.isCancel(e)) {
44                                         setError(e);
45                                         console.log(e);
46                                 }
47                         }
48                 };
49                 fetchPng();
50                 return () => {
51                         ctrl.abort();
52                 };
53         }, [src]);
54
55         React.useEffect(async () => {
56                 if (loading || !canvas.current) return;
57                 setFrameInfo(`1/${apng.frames.length}`);
58                 const p = await createPlayer(apng, canvas.current);
59                 setPlayer(p);
60                 const updateFrame = (number) => {
61                         setFrameInfo(`${number + 1}/${apng.frames.length}`);
62                 };
63                 p.on('frame', updateFrame);
64                 return () => {
65                         p.off('frame', updateFrame);
66                 };
67         }, [apng, canvas.current, loading]);
68
69         const stop = React.useCallback(() => {
70                 if (player) player.stop();
71         }, [player]);
72
73         const toggle = React.useCallback(() => {
74                 if (!player) return;
75                 if (player.paused) {
76                         player.play();
77                 } else {
78                         player.pause();
79                 }
80         }, [player]);
81
82         const nextFrame = React.useCallback(() => {
83                 if (player) player.renderNextFrame();
84         }, [player]);
85
86         if (error) {
87                 return <div>Error</div>;
88         }
89         if (loading) {
90                 return <div>Loading</div>;
91         }
92
93         return <div className="png-player">
94                 <div className="screen">
95                         <canvas ref={canvas} width={apng.width} height={apng.height} />
96                 </div>
97                 <span className="ms-auto">{frameInfo}</span>
98                 <div className="button-bar controls">
99                         <Button
100                                 onClick={stop}
101                                 title={t('button.stop')}
102                                 variant="outline-secondary"
103                         >
104                                 <Icon.STOP title="" />
105                         </Button>
106                         <Button
107                                 onClick={toggle}
108                                 title={t('button.playPause')}
109                                 variant="outline-secondary"
110                         >
111                                 <Icon.PLAY title="" />
112                                 {' '}
113                                 <Icon.PAUSE title="" />
114                         </Button>
115                         <Button
116                                 onClick={nextFrame}
117                                 title={t('button.nextFrame')}
118                                 variant="outline-secondary"
119                         >
120                                 <Icon.STEP_FORWARD title="" />
121                         </Button>
122                 </div>
123         </div>;
124 };
125
126 PngPlayer.propTypes = {
127         src: PropTypes.string,
128 };
129
130 export default PngPlayer;