@arcblock/terminal 3.1.12 → 3.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lib/Player.js +178 -101
  2. package/package.json +4 -4
  3. package/src/Player.jsx +227 -107
package/lib/Player.js CHANGED
@@ -1,129 +1,206 @@
1
- import { jsxs as f, jsx as n } from "react/jsx-runtime";
2
- import { useRef as b, useState as A, useReducer as O, useEffect as x } from "react";
1
+ import { jsxs as F, jsx as a } from "react/jsx-runtime";
2
+ import { useRef as P, useState as _, useReducer as U, useEffect as q, useCallback as g } from "react";
3
3
  import o from "prop-types";
4
- import { useSize as W, useInterval as H } from "ahooks";
5
- import y from "lodash/isUndefined";
6
- import h from "lodash/noop";
7
- import I from "./Terminal.js";
8
- import { PlayerRoot as L } from "./styles.js";
9
- import { defaultOptions as _, formatFrames as U, defaultState as Y, isFrameAt as N, findFrameAt as F, getPlayerClass as X, getFrameClass as $, formatTime as G } from "./util.js";
10
- const K = 8;
11
- function Q({ ...P }) {
12
- const i = Object.assign({}, P);
13
- y(i.onComplete) && (i.onComplete = h), y(i.onStart) && (i.onStart = h), y(i.onStop) && (i.onStop = h), y(i.onPause) && (i.onPause = h), y(i.onTick) && (i.onTick = h), y(i.onJump) && (i.onJump = h);
14
- const t = Object.assign({}, _, i.options), { frames: u, totalDuration: m } = U(i.frames, t), R = {
15
- cols: t.cols,
16
- rows: t.rows,
17
- cursorStyle: t.cursorStyle,
18
- fontFamily: t.fontFamily,
19
- fontSize: t.fontSize,
20
- lineHeight: t.lineHeight,
21
- letterSpacing: t.letterSpacing,
4
+ import { useSize as Y } from "ahooks";
5
+ import x from "lodash/isUndefined";
6
+ import b from "lodash/noop";
7
+ import X from "./Terminal.js";
8
+ import { PlayerRoot as $ } from "./styles.js";
9
+ import { defaultOptions as G, formatFrames as K, defaultState as Q, findFrameAt as M, getPlayerClass as V, getFrameClass as Z, formatTime as ee } from "./util.js";
10
+ const me = 24;
11
+ function te({ ...j }) {
12
+ const s = Object.assign({}, j);
13
+ x(s.onComplete) && (s.onComplete = b), x(s.onStart) && (s.onStart = b), x(s.onStop) && (s.onStop = b), x(s.onPause) && (s.onPause = b), x(s.onTick) && (s.onTick = b), x(s.onJump) && (s.onJump = b);
14
+ const e = Object.assign({}, G, s.options), { frames: c, totalDuration: w } = K(s.frames, e), D = {
15
+ cols: e.cols,
16
+ rows: e.rows,
17
+ cursorStyle: e.cursorStyle,
18
+ fontFamily: e.fontFamily,
19
+ fontSize: e.fontSize,
20
+ lineHeight: e.lineHeight,
21
+ letterSpacing: e.letterSpacing,
22
22
  allowTransparency: !0,
23
23
  scrollback: 0,
24
- theme: t.theme || {}
25
- }, C = (a, e) => {
26
- switch (e.type) {
24
+ theme: e.theme || {}
25
+ }, E = (t, r) => {
26
+ switch (r.type) {
27
27
  case "jump":
28
- return { ...a, isPlaying: !1, ...e.payload };
28
+ return { ...t, ...r.payload };
29
29
  case "start":
30
- return { ...a, isStarted: !0, lastTickTime: Date.now() };
30
+ return { ...t, isStarted: !0, lastTickTime: Date.now() };
31
31
  case "play":
32
- return { ...a, isPlaying: !0, lastTickTime: Date.now(), ...e.payload };
32
+ return { ...t, isPlaying: !0, lastTickTime: Date.now(), ...r.payload };
33
33
  case "pause":
34
- return { ...a, isPlaying: !1, ...e.payload };
34
+ return { ...t, isPlaying: !1, ...r.payload };
35
35
  case "tickStart":
36
- return { ...a, isRendering: !0, lastTickTime: Date.now(), ...e.payload };
36
+ return { ...t, isRendering: !0, lastTickTime: Date.now(), ...r.payload };
37
37
  case "tickEnd":
38
- return { ...a, isRendering: !1, lastTickTime: Date.now(), ...e.payload };
38
+ return { ...t, isRendering: !1, lastTickTime: Date.now(), ...r.payload };
39
39
  case "reset":
40
- return { ...a, currentFrame: 0, currentTime: 0, ...e.payload };
40
+ return { ...t, currentFrame: -1, currentTime: 0, ...r.payload };
41
41
  default:
42
- return { ...a, lastTickTime: Date.now(), ...e.payload };
42
+ return { ...t, lastTickTime: Date.now(), ...r.payload };
43
43
  }
44
- }, c = b(null), k = b(null), d = b(null), [B, D] = A(0), M = W(document.body), [r, l] = O(C, Y), j = M?.width || 0;
45
- x(() => {
46
- if (c.current)
44
+ }, l = P(null), R = P(null), S = P(null), p = P(null), d = P(null), [z, J] = _(0), L = Y(document.body), [n, u] = U(E, Q), O = L?.width || 0;
45
+ q(() => {
46
+ if (l.current)
47
47
  try {
48
- const a = 8.03305785123967, e = d.current.getBoundingClientRect();
49
- let s = e.x < 0 ? e.width + e.x : e.width;
50
- if (d.current.parentNode) {
51
- const v = d.current.parentNode.getBoundingClientRect();
52
- e.width > v.width && (s = v.width);
48
+ const t = 8.03305785123967, r = S.current.getBoundingClientRect();
49
+ let i = r.x < 0 ? r.width + r.x : r.width;
50
+ if (S.current.parentNode) {
51
+ const T = S.current.parentNode.getBoundingClientRect();
52
+ r.width > T.width && (i = T.width);
53
53
  }
54
- t.controls && (s -= 12);
55
- const T = Math.ceil(s / a), J = Math.min(Math.max(T, 40), t.cols);
56
- c.current.resize(J, t.rows);
54
+ e.controls && (i -= 12);
55
+ const f = Math.ceil(i / t), y = Math.min(Math.max(f, 40), e.cols);
56
+ l.current.resize(y, e.rows);
57
57
  } catch {
58
58
  }
59
- }, [j, B]);
60
- const w = (a, e) => {
61
- const s = u[a];
62
- s && s.content && (r.requireReset && c.current.reset(), c.current.write(s.content, () => {
63
- typeof e == "function" && e();
64
- }));
65
- }, p = (a) => {
66
- typeof i[a] == "function" && i[a]({ state: r, frames: u, options: t });
67
- }, S = (a) => {
68
- c.current.reset();
69
- const e = F(u, a);
70
- for (let s = 0; s < e; s++)
71
- w(s);
72
- }, E = (a) => {
73
- if (!k.current || !c.current || !r.isStarted)
74
- return !1;
75
- const e = k.current.getBoundingClientRect().width, s = a.nativeEvent.offsetX, T = Math.floor(m * s / e);
76
- return l({ type: "jump", payload: { currentTime: T } }), S(T), p("onJump"), !1;
77
- }, g = () => (r.isStarted === !1 && (l({ type: "start" }), c.current.reset()), l({ type: "play" }), p("onStart"), !1), z = () => (l({ type: "pause" }), p("onPause"), !1), q = () => (r.currentFrame === u.length - 1 && r.currentTime === m && (l({ type: "reset" }), c.current.reset()), p("onPlay"), g());
78
- return x(() => {
79
- if (c.current) {
80
- if (d.current)
59
+ }, [O, z]);
60
+ const k = g(
61
+ (t) => new Promise((r) => {
62
+ const i = c[t];
63
+ i && i.content && l.current ? (n.requireReset && l.current.reset(), l.current.write(i.content, () => {
64
+ r();
65
+ })) : r();
66
+ }),
67
+ [c, n.requireReset]
68
+ ), m = g(
69
+ (t) => {
70
+ typeof s[t] == "function" && s[t]({ state: n, frames: c, options: e });
71
+ },
72
+ [s, n, c, e]
73
+ ), C = g(
74
+ async (t) => {
75
+ if (!l.current) return;
76
+ l.current.reset();
77
+ const r = M(c, t);
78
+ if (r >= 0)
79
+ for (let i = 0; i <= r; i++)
80
+ await k(i);
81
+ },
82
+ [c, k]
83
+ ), I = g(
84
+ (t) => {
85
+ if (!R.current || !l.current || !n.isStarted)
86
+ return !1;
87
+ const r = R.current.getBoundingClientRect().width, i = t.nativeEvent.offsetX, f = Math.floor(w * i / r), y = M(c, f), T = n.isPlaying;
88
+ return u({
89
+ type: "jump",
90
+ payload: {
91
+ currentTime: f,
92
+ currentFrame: y,
93
+ isPlaying: T
94
+ // Keep the same playing state
95
+ }
96
+ }), d.current = null, C(f), m("onJump"), !1;
97
+ },
98
+ [n.isStarted, n.isPlaying, w, c, C, m]
99
+ ), N = g(() => (n.isStarted === !1 && (u({ type: "start" }), l.current && l.current.reset()), d.current = null, u({ type: "play", payload: { currentFrame: -1, currentTime: 0 } }), m("onStart"), !1), [n.isStarted, m]), W = g(() => (u({ type: "pause" }), m("onPause"), !1), [m]), H = g(() => (n.currentFrame >= c.length - 1 ? (u({ type: "reset", payload: { currentFrame: -1, currentTime: 0 } }), l.current && l.current.reset(), d.current = null, u({ type: "play", payload: { currentFrame: -1, currentTime: 0 } })) : (d.current = null, u({ type: "play" })), m("onPlay"), !1), [n.currentFrame, c.length, m]);
100
+ q(() => {
101
+ if (l.current) {
102
+ if (S.current)
81
103
  try {
82
- D(d.current.getBoundingClientRect().width);
104
+ J(S.current.getBoundingClientRect().width);
83
105
  } catch {
84
106
  }
85
- t.autoplay ? g() : S(Math.min(Math.abs(t.thumbnailTime), m));
107
+ e.autoplay ? N() : C(Math.min(Math.abs(e.thumbnailTime), w));
86
108
  }
87
- }, []), H(
88
- () => {
89
- if (r.isRendering)
90
- return !1;
91
- const a = Date.now() - r.lastTickTime, e = {};
92
- r.currentTime < m && (e.currentTime = r.currentTime + a), r.currentTime > m && (e.currentTime = m);
93
- const s = N(u, e.currentTime, r.currentFrame);
94
- return r.currentFrame !== -1 && s ? l({ type: "tick", payload: e }) : r.currentFrame === u.length - 1 ? (p("onComplete"), t.repeat ? l({ type: "reset", payload: e }) : (e.currentTime = 0, e.currentFrame = 0, e.requireReset = !0, e.isStarted = !1, l({ type: "pause", payload: e }))) : (e?.currentTime != null && r?.currentFrame != null && N(e.currentTime, r.currentFrame + 1) ? e.currentFrame = r.currentFrame + 1 : e.currentFrame = F(u, e.currentTime), l({ type: "tickStart", payload: e }), w(e.currentFrame, () => (r.requireReset && (e.requireReset = !1), l({ type: "tickEnd", payload: e }), p("onTick"))));
95
- },
96
- r.isPlaying ? K : null
97
- ), t.controls && (t.frameBox.title = null, t.frameBox.type = null, t.frameBox.style = {}, t.theme?.background === "transparent" ? t.frameBox.style.background = "black" : t.theme?.background && (t.frameBox.style.background = t.theme.background), t.frameBox.style.padding = "10px", t.frameBox.style.paddingBottom = "40px"), /* @__PURE__ */ f(L, { className: X(t, r), ref: d, children: [
98
- /* @__PURE__ */ n("div", { className: "cover", onClick: g }),
99
- /* @__PURE__ */ n("div", { className: "start", onClick: g, children: /* @__PURE__ */ f("svg", { style: { enableBackground: "new 0 0 30 30" }, viewBox: "0 0 30 30", children: [
100
- /* @__PURE__ */ n("polygon", { points: "6.583,3.186 5,4.004 5,15 26,15 26.483,14.128 " }),
101
- /* @__PURE__ */ n("polygon", { points: "6.583,26.814 5,25.996 5,15 26,15 26.483,15.872 " }),
102
- /* @__PURE__ */ n("circle", { cx: "26", cy: "15", r: "1" }),
103
- /* @__PURE__ */ n("circle", { cx: "6", cy: "4", r: "1" }),
104
- /* @__PURE__ */ n("circle", { cx: "6", cy: "26", r: "1" })
109
+ }, []);
110
+ const v = P(n);
111
+ v.current = n;
112
+ const B = g(async () => {
113
+ const t = v.current;
114
+ if (!t.isPlaying || t.isRendering)
115
+ return;
116
+ const r = performance.now();
117
+ if (!d.current) {
118
+ const h = t.currentFrame >= 0 && c[t.currentFrame]?.startTime || 0;
119
+ d.current = r - h;
120
+ }
121
+ const i = r - d.current, { currentFrame: f } = t, y = f + 1;
122
+ if (f >= c.length - 1) {
123
+ if (m("onComplete"), e.repeat) {
124
+ d.current = r, u({ type: "reset", payload: { currentTime: 0, currentFrame: -1 } });
125
+ return;
126
+ }
127
+ const h = {
128
+ currentTime: w,
129
+ currentFrame: c.length - 1,
130
+ requireReset: !0,
131
+ isStarted: !1
132
+ };
133
+ u({ type: "pause", payload: h }), d.current = null;
134
+ return;
135
+ }
136
+ const T = c[y];
137
+ if (!T) {
138
+ n.isPlaying && (p.current = requestAnimationFrame(B));
139
+ return;
140
+ }
141
+ const A = T.startTime || 0;
142
+ if (i >= A) {
143
+ u({
144
+ type: "tickStart",
145
+ payload: {
146
+ currentTime: A,
147
+ currentFrame: y
148
+ }
149
+ });
150
+ try {
151
+ await k(y);
152
+ const h = {
153
+ currentTime: A,
154
+ currentFrame: y
155
+ };
156
+ t.requireReset && (h.requireReset = !1), u({ type: "tickEnd", payload: h }), m("onTick");
157
+ } catch (h) {
158
+ console.error("Frame rendering error:", h);
159
+ }
160
+ } else
161
+ u({ type: "tick", payload: { currentTime: i } });
162
+ }, [c, k, m, e.repeat, w]);
163
+ return q(() => {
164
+ let t = !0;
165
+ const r = async () => {
166
+ for (; t && v.current.isPlaying; )
167
+ await new Promise((i) => {
168
+ p.current = requestAnimationFrame(i);
169
+ }), t && v.current.isPlaying && await B();
170
+ };
171
+ return n.isPlaying ? r() : (p.current && (cancelAnimationFrame(p.current), p.current = null), d.current = null), () => {
172
+ t = !1, p.current && (cancelAnimationFrame(p.current), p.current = null);
173
+ };
174
+ }, [n.isPlaying, B]), e.controls && (e.frameBox.title = null, e.frameBox.type = null, e.frameBox.style = {}, e.theme?.background === "transparent" ? e.frameBox.style.background = "black" : e.theme?.background && (e.frameBox.style.background = e.theme.background), e.frameBox.style.padding = "10px", e.frameBox.style.paddingBottom = "40px"), /* @__PURE__ */ F($, { className: V(e, n), ref: S, children: [
175
+ /* @__PURE__ */ a("div", { className: "cover", onClick: N }),
176
+ /* @__PURE__ */ a("div", { className: "start", onClick: N, children: /* @__PURE__ */ F("svg", { style: { enableBackground: "new 0 0 30 30" }, viewBox: "0 0 30 30", children: [
177
+ /* @__PURE__ */ a("polygon", { points: "6.583,3.186 5,4.004 5,15 26,15 26.483,14.128 " }),
178
+ /* @__PURE__ */ a("polygon", { points: "6.583,26.814 5,25.996 5,15 26,15 26.483,15.872 " }),
179
+ /* @__PURE__ */ a("circle", { cx: "26", cy: "15", r: "1" }),
180
+ /* @__PURE__ */ a("circle", { cx: "6", cy: "4", r: "1" }),
181
+ /* @__PURE__ */ a("circle", { cx: "6", cy: "26", r: "1" })
105
182
  ] }) }),
106
- /* @__PURE__ */ n("div", { className: "terminal", children: /* @__PURE__ */ f("div", { className: $(t), style: t.frameBox.style || {}, children: [
107
- /* @__PURE__ */ f("div", { className: "terminal-titlebar", children: [
108
- /* @__PURE__ */ f("div", { className: "buttons", children: [
109
- /* @__PURE__ */ n("div", { className: "close-button" }),
110
- /* @__PURE__ */ n("div", { className: "minimize-button" }),
111
- /* @__PURE__ */ n("div", { className: "maximize-button" })
183
+ /* @__PURE__ */ a("div", { className: "terminal", children: /* @__PURE__ */ F("div", { className: Z(e), style: e.frameBox.style || {}, children: [
184
+ /* @__PURE__ */ F("div", { className: "terminal-titlebar", children: [
185
+ /* @__PURE__ */ F("div", { className: "buttons", children: [
186
+ /* @__PURE__ */ a("div", { className: "close-button" }),
187
+ /* @__PURE__ */ a("div", { className: "minimize-button" }),
188
+ /* @__PURE__ */ a("div", { className: "maximize-button" })
112
189
  ] }),
113
- /* @__PURE__ */ n("div", { className: "title", children: t.frameBox.title || "" })
190
+ /* @__PURE__ */ a("div", { className: "title", children: e.frameBox.title || "" })
114
191
  ] }),
115
- /* @__PURE__ */ n("div", { className: "terminal-body", children: /* @__PURE__ */ n(I, { ref: c, options: R }) })
192
+ /* @__PURE__ */ a("div", { className: "terminal-body", children: /* @__PURE__ */ a(X, { ref: l, options: D }) })
116
193
  ] }) }),
117
- /* @__PURE__ */ f("div", { className: "controller", children: [
118
- r.isPlaying && /* @__PURE__ */ n("div", { className: "pause", onClick: z, title: "Pause", children: /* @__PURE__ */ n("span", { className: "icon" }) }),
119
- !r.isPlaying && r.isStarted && /* @__PURE__ */ n("div", { className: "play", onClick: q, title: "Play", children: /* @__PURE__ */ n("span", { className: "icon" }) }),
120
- !r.isPlaying && !r.isStarted && /* @__PURE__ */ n("div", { className: "play", onClick: g, title: "Start", children: /* @__PURE__ */ n("span", { className: "icon" }) }),
121
- /* @__PURE__ */ n("div", { className: "timer", children: G(r.currentTime) }),
122
- /* @__PURE__ */ n("div", { className: "progressbar-wrapper", children: /* @__PURE__ */ n("div", { className: "progressbar", ref: k, onClick: E, children: /* @__PURE__ */ n("div", { className: "progress", style: { width: `${r.currentTime / m * 100}%` } }) }) })
194
+ /* @__PURE__ */ F("div", { className: "controller", children: [
195
+ n.isPlaying && /* @__PURE__ */ a("div", { className: "pause", onClick: W, title: "Pause", children: /* @__PURE__ */ a("span", { className: "icon" }) }),
196
+ !n.isPlaying && n.isStarted && /* @__PURE__ */ a("div", { className: "play", onClick: H, title: "Play", children: /* @__PURE__ */ a("span", { className: "icon" }) }),
197
+ !n.isPlaying && !n.isStarted && /* @__PURE__ */ a("div", { className: "play", onClick: N, title: "Start", children: /* @__PURE__ */ a("span", { className: "icon" }) }),
198
+ /* @__PURE__ */ a("div", { className: "timer", children: ee(n.currentTime) }),
199
+ /* @__PURE__ */ a("div", { className: "progressbar-wrapper", children: /* @__PURE__ */ a("div", { className: "progressbar", ref: R, onClick: I, children: /* @__PURE__ */ a("div", { className: "progress", style: { width: `${n.currentTime / w * 100}%` } }) }) })
123
200
  ] })
124
201
  ] });
125
202
  }
126
- Q.propTypes = {
203
+ te.propTypes = {
127
204
  frames: o.array.isRequired,
128
205
  options: o.shape({
129
206
  autoplay: o.bool,
@@ -142,6 +219,6 @@ Q.propTypes = {
142
219
  onJump: o.func
143
220
  };
144
221
  export {
145
- K as PLAYER_FRAME_DELAY,
146
- Q as default
222
+ me as PLAYER_FRAME_DELAY,
223
+ te as default
147
224
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/terminal",
3
- "version": "3.1.12",
3
+ "version": "3.1.14",
4
4
  "description": "A react wrapper for xterm allowing you to easily render a terminal in the browser",
5
5
  "keywords": [
6
6
  "react",
@@ -40,10 +40,10 @@
40
40
  "peerDependencies": {
41
41
  "react": "^19.0.0"
42
42
  },
43
- "gitHead": "0cb1a641693d3f3c1ac105d8117365f289d9e5c2",
43
+ "gitHead": "8dce20e08f2dc02559df0a0abb27c63ca25e2a73",
44
44
  "dependencies": {
45
- "@arcblock/react-hooks": "3.1.12",
46
- "@arcblock/ux": "3.1.12",
45
+ "@arcblock/react-hooks": "3.1.14",
46
+ "@arcblock/ux": "3.1.14",
47
47
  "@emotion/react": "^11.14.0",
48
48
  "@emotion/styled": "^11.14.0",
49
49
  "ahooks": "^3.8.5",
package/src/Player.jsx CHANGED
@@ -1,7 +1,7 @@
1
- /* eslint-disable react/no-unused-prop-types */
2
- import { useReducer, useState, useRef, useEffect } from 'react';
1
+ /* eslint-disable react/no-unused-prop-types, no-console */
2
+ import { useReducer, useState, useRef, useEffect, useCallback } from 'react';
3
3
  import PropTypes from 'prop-types';
4
- import { useSize, useInterval } from 'ahooks';
4
+ import { useSize } from 'ahooks';
5
5
  import isUndefined from 'lodash/isUndefined';
6
6
  import noop from 'lodash/noop';
7
7
 
@@ -13,14 +13,13 @@ import {
13
13
  formatFrames,
14
14
  formatTime,
15
15
  findFrameAt,
16
- isFrameAt,
17
16
  getFrameClass,
18
17
  getPlayerClass,
19
18
  defaultOptions,
20
19
  defaultState,
21
20
  } from './util';
22
21
 
23
- export const PLAYER_FRAME_DELAY = 8;
22
+ export const PLAYER_FRAME_DELAY = 24;
24
23
 
25
24
  export default function Player({ ...rawProps }) {
26
25
  const props = Object.assign({}, rawProps);
@@ -63,7 +62,7 @@ export default function Player({ ...rawProps }) {
63
62
  // console.log(`dispatch.${action.type}`, action.payload);
64
63
  switch (action.type) {
65
64
  case 'jump':
66
- return { ...state, isPlaying: false, ...action.payload };
65
+ return { ...state, ...action.payload };
67
66
  case 'start':
68
67
  return { ...state, isStarted: true, lastTickTime: Date.now() };
69
68
  case 'play':
@@ -75,7 +74,7 @@ export default function Player({ ...rawProps }) {
75
74
  case 'tickEnd':
76
75
  return { ...state, isRendering: false, lastTickTime: Date.now(), ...action.payload };
77
76
  case 'reset':
78
- return { ...state, currentFrame: 0, currentTime: 0, ...action.payload };
77
+ return { ...state, currentFrame: -1, currentTime: 0, ...action.payload };
79
78
  default:
80
79
  return { ...state, lastTickTime: Date.now(), ...action.payload };
81
80
  }
@@ -84,6 +83,8 @@ export default function Player({ ...rawProps }) {
84
83
  const terminal = useRef(null);
85
84
  const progress = useRef(null);
86
85
  const container = useRef(null);
86
+ const animationRef = useRef(null);
87
+ const startTimeRef = useRef(null);
87
88
  const [maxWidth, setMaxWidth] = useState(0);
88
89
  const size = useSize(document.body);
89
90
  const [state, dispatch] = useReducer(stateReducer, defaultState);
@@ -118,83 +119,133 @@ export default function Player({ ...rawProps }) {
118
119
  // eslint-disable-next-line react-hooks/exhaustive-deps
119
120
  }, [width, maxWidth]);
120
121
 
121
- // console.log('main.render', state, { totalFrame: frames.length, totalDuration });
122
- // Render a frame
123
- const renderFrame = (frameIndex, done) => {
124
- const frame = frames[frameIndex];
125
- if (frame && frame.content) {
126
- if (state.requireReset) {
127
- terminal.current.reset();
128
- }
129
-
130
- terminal.current.write(frame.content, () => {
131
- if (typeof done === 'function') {
132
- done();
122
+ // Render a frame with Promise-based approach
123
+ const renderFrame = useCallback(
124
+ (frameIndex) => {
125
+ return new Promise((resolve) => {
126
+ const frame = frames[frameIndex];
127
+ if (frame && frame.content && terminal.current) {
128
+ if (state.requireReset) {
129
+ terminal.current.reset();
130
+ }
131
+
132
+ terminal.current.write(frame.content, () => {
133
+ resolve();
134
+ });
135
+ } else {
136
+ resolve();
133
137
  }
134
138
  });
135
- }
136
- };
139
+ },
140
+ [frames, state.requireReset]
141
+ );
137
142
 
138
143
  // Emit a event
139
- const emitEvent = (name) => {
140
- if (typeof props[name] === 'function') {
141
- props[name]({ state, frames, options });
142
- }
143
- };
144
+ const emitEvent = useCallback(
145
+ (name) => {
146
+ if (typeof props[name] === 'function') {
147
+ props[name]({ state, frames, options });
148
+ }
149
+ },
150
+ [props, state, frames, options]
151
+ );
144
152
 
145
- const doJump = (time) => {
146
- terminal.current.reset();
153
+ const doJump = useCallback(
154
+ async (time) => {
155
+ if (!terminal.current) return;
147
156
 
148
- const toFrameIndex = findFrameAt(frames, time);
149
- for (let i = 0; i < toFrameIndex; i++) {
150
- renderFrame(i);
151
- }
152
- };
157
+ terminal.current.reset();
158
+ const toFrameIndex = findFrameAt(frames, time);
153
159
 
154
- const onJump = (e) => {
155
- if (!progress.current || !terminal.current || !state.isStarted) {
156
- return false;
157
- }
160
+ if (toFrameIndex >= 0) {
161
+ // Render all frames up to the target frame sequentially
162
+ for (let i = 0; i <= toFrameIndex; i++) {
163
+ // eslint-disable-next-line no-await-in-loop
164
+ await renderFrame(i);
165
+ }
166
+ }
167
+ },
168
+ [frames, renderFrame]
169
+ );
158
170
 
159
- const length = progress.current.getBoundingClientRect().width;
160
- const position = e.nativeEvent.offsetX;
161
- // console.log('onJump', { length, position, e });
171
+ const onJump = useCallback(
172
+ (e) => {
173
+ if (!progress.current || !terminal.current || !state.isStarted) {
174
+ return false;
175
+ }
162
176
 
163
- const currentTime = Math.floor((totalDuration * position) / length);
164
- dispatch({ type: 'jump', payload: { currentTime } });
165
- doJump(currentTime);
166
- emitEvent('onJump');
177
+ const length = progress.current.getBoundingClientRect().width;
178
+ const position = e.nativeEvent.offsetX;
167
179
 
168
- return false;
169
- };
180
+ const currentTime = Math.floor((totalDuration * position) / length);
181
+ const targetFrameIndex = findFrameAt(frames, currentTime);
182
+
183
+ // Preserve the current playing state
184
+ const isCurrentlyPlaying = state.isPlaying;
185
+
186
+ // Update state to reflect the jump while preserving play state
187
+ dispatch({
188
+ type: 'jump',
189
+ payload: {
190
+ currentTime,
191
+ currentFrame: targetFrameIndex,
192
+ isPlaying: isCurrentlyPlaying, // Keep the same playing state
193
+ },
194
+ });
195
+
196
+ // Reset the timing for accurate playback after jump
197
+ startTimeRef.current = null;
170
198
 
171
- const onStart = () => {
199
+ // Perform the jump
200
+ doJump(currentTime);
201
+ emitEvent('onJump');
202
+
203
+ return false;
204
+ },
205
+ [state.isStarted, state.isPlaying, totalDuration, frames, doJump, emitEvent]
206
+ );
207
+
208
+ const onStart = useCallback(() => {
172
209
  if (state.isStarted === false) {
173
210
  dispatch({ type: 'start' });
174
- terminal.current.reset();
211
+ if (terminal.current) {
212
+ terminal.current.reset();
213
+ }
175
214
  }
176
215
 
177
- dispatch({ type: 'play' });
216
+ // Reset start time and frame for accurate timing
217
+ startTimeRef.current = null;
218
+ dispatch({ type: 'play', payload: { currentFrame: -1, currentTime: 0 } });
178
219
  emitEvent('onStart');
179
220
 
180
221
  return false;
181
- };
222
+ }, [state.isStarted, emitEvent]);
182
223
 
183
- const onPause = () => {
224
+ const onPause = useCallback(() => {
184
225
  dispatch({ type: 'pause' });
185
226
  emitEvent('onPause');
186
227
  return false;
187
- };
228
+ }, [emitEvent]);
188
229
 
189
- const onPlay = () => {
190
- if (state.currentFrame === frames.length - 1 && state.currentTime === totalDuration) {
191
- dispatch({ type: 'reset' });
192
- terminal.current.reset();
230
+ const onPlay = useCallback(() => {
231
+ if (state.currentFrame >= frames.length - 1) {
232
+ // Reset to beginning if at the end
233
+ dispatch({ type: 'reset', payload: { currentFrame: -1, currentTime: 0 } });
234
+ if (terminal.current) {
235
+ terminal.current.reset();
236
+ }
237
+ startTimeRef.current = null;
238
+ // Start from beginning
239
+ dispatch({ type: 'play', payload: { currentFrame: -1, currentTime: 0 } });
240
+ } else {
241
+ // Continue from current position
242
+ startTimeRef.current = null; // Reset timing for accurate playback
243
+ dispatch({ type: 'play' });
193
244
  }
194
245
 
195
246
  emitEvent('onPlay');
196
- return onStart();
197
- };
247
+ return false;
248
+ }, [state.currentFrame, frames.length, emitEvent]);
198
249
 
199
250
  // Render thumbnailTime
200
251
  useEffect(() => {
@@ -216,68 +267,137 @@ export default function Player({ ...rawProps }) {
216
267
  // eslint-disable-next-line react-hooks/exhaustive-deps
217
268
  }, []);
218
269
 
219
- // Tick intervals
220
- useInterval(
221
- () => {
222
- if (state.isRendering) {
223
- return false;
224
- }
270
+ // Use ref to store the latest state and avoid recreating the loop function
271
+ const stateRef = useRef(state);
272
+ stateRef.current = state;
225
273
 
226
- const tickDelay = Date.now() - state.lastTickTime;
227
- const newState = {};
274
+ // Animation loop - frame-based sequential rendering
275
+ const animationLoop = useCallback(async () => {
276
+ const currentState = stateRef.current;
228
277
 
229
- if (state.currentTime < totalDuration) {
230
- newState.currentTime = state.currentTime + tickDelay;
231
- }
232
- if (state.currentTime > totalDuration) {
233
- newState.currentTime = totalDuration;
278
+ if (!currentState.isPlaying || currentState.isRendering) {
279
+ return;
280
+ }
281
+
282
+ const now = performance.now();
283
+ if (!startTimeRef.current) {
284
+ // Calculate start time based on current frame position
285
+ const currentFrameTime = currentState.currentFrame >= 0 ? frames[currentState.currentFrame]?.startTime || 0 : 0;
286
+ startTimeRef.current = now - currentFrameTime;
287
+ }
288
+
289
+ const elapsed = now - startTimeRef.current;
290
+
291
+ // Check if it's time to render the next frame
292
+ const { currentFrame } = currentState;
293
+ const nextFrameIndex = currentFrame + 1;
294
+
295
+ // If we've rendered all frames, we're done
296
+ if (currentFrame >= frames.length - 1) {
297
+ emitEvent('onComplete');
298
+
299
+ if (options.repeat) {
300
+ // Reset for repeat
301
+ startTimeRef.current = now;
302
+ dispatch({ type: 'reset', payload: { currentTime: 0, currentFrame: -1 } });
303
+ return;
234
304
  }
305
+ // Stop playing
306
+ const endState = {
307
+ currentTime: totalDuration,
308
+ currentFrame: frames.length - 1,
309
+ requireReset: true,
310
+ isStarted: false,
311
+ };
312
+ dispatch({ type: 'pause', payload: endState });
313
+ startTimeRef.current = null;
314
+ return;
315
+ }
235
316
 
236
- const alreadyRendered = isFrameAt(frames, newState.currentTime, state.currentFrame);
237
- if (state.currentFrame !== -1 && alreadyRendered) {
238
- return dispatch({ type: 'tick', payload: newState });
317
+ // Calculate when the next frame should be rendered
318
+ const nextFrame = frames[nextFrameIndex];
319
+ if (!nextFrame) {
320
+ // No more frames, schedule next animation frame
321
+ if (state.isPlaying) {
322
+ animationRef.current = requestAnimationFrame(animationLoop);
239
323
  }
324
+ return;
325
+ }
240
326
 
241
- // Reached the end
242
- if (state.currentFrame === frames.length - 1) {
243
- emitEvent('onComplete');
327
+ // Check if enough time has passed for the next frame
328
+ const frameStartTime = nextFrame.startTime || 0;
329
+ if (elapsed >= frameStartTime) {
330
+ // Time to render the next frame
331
+ dispatch({
332
+ type: 'tickStart',
333
+ payload: {
334
+ currentTime: frameStartTime,
335
+ currentFrame: nextFrameIndex,
336
+ },
337
+ });
244
338
 
245
- if (options.repeat) {
246
- // console.log('tick.restart', newState, state);
247
- return dispatch({ type: 'reset', payload: newState });
339
+ try {
340
+ await renderFrame(nextFrameIndex);
341
+
342
+ const finalState = {
343
+ currentTime: frameStartTime,
344
+ currentFrame: nextFrameIndex,
345
+ };
346
+ if (currentState.requireReset) {
347
+ finalState.requireReset = false;
248
348
  }
249
349
 
250
- // console.log('tick.end', newState, state);
251
- newState.currentTime = 0;
252
- newState.currentFrame = 0;
253
- newState.requireReset = true;
254
- newState.isStarted = false;
255
- return dispatch({ type: 'pause', payload: newState });
350
+ dispatch({ type: 'tickEnd', payload: finalState });
351
+ emitEvent('onTick');
352
+ } catch (error) {
353
+ console.error('Frame rendering error:', error);
256
354
  }
355
+ } else {
356
+ // Just update time without rendering
357
+ dispatch({ type: 'tick', payload: { currentTime: elapsed } });
358
+ }
257
359
 
258
- // Check if current time belongs to the next frame's duration
259
- if (
260
- newState?.currentTime != null &&
261
- state?.currentFrame != null &&
262
- isFrameAt(newState.currentTime, state.currentFrame + 1)
263
- ) {
264
- newState.currentFrame = state.currentFrame + 1;
265
- } else {
266
- newState.currentFrame = findFrameAt(frames, newState.currentTime);
267
- }
360
+ // Don't schedule here - let useEffect handle it
361
+ // eslint-disable-next-line react-hooks/exhaustive-deps
362
+ }, [frames, renderFrame, emitEvent, options.repeat, totalDuration]);
268
363
 
269
- // console.log('tick.tick', newState, state);
270
- dispatch({ type: 'tickStart', payload: newState });
271
- return renderFrame(newState.currentFrame, () => {
272
- if (state.requireReset) {
273
- newState.requireReset = false;
364
+ // Start/stop animation loop based on playing state
365
+ useEffect(() => {
366
+ let isActive = true;
367
+
368
+ const runAnimationLoop = async () => {
369
+ // eslint-disable-next-line no-await-in-loop
370
+ while (isActive && stateRef.current.isPlaying) {
371
+ // eslint-disable-next-line no-await-in-loop
372
+ await new Promise((resolve) => {
373
+ animationRef.current = requestAnimationFrame(resolve);
374
+ });
375
+
376
+ if (isActive && stateRef.current.isPlaying) {
377
+ // eslint-disable-next-line no-await-in-loop
378
+ await animationLoop();
274
379
  }
275
- dispatch({ type: 'tickEnd', payload: newState });
276
- return emitEvent('onTick');
277
- });
278
- },
279
- state.isPlaying ? PLAYER_FRAME_DELAY : null
280
- );
380
+ }
381
+ };
382
+
383
+ if (state.isPlaying) {
384
+ runAnimationLoop();
385
+ } else {
386
+ if (animationRef.current) {
387
+ cancelAnimationFrame(animationRef.current);
388
+ animationRef.current = null;
389
+ }
390
+ startTimeRef.current = null;
391
+ }
392
+
393
+ return () => {
394
+ isActive = false;
395
+ if (animationRef.current) {
396
+ cancelAnimationFrame(animationRef.current);
397
+ animationRef.current = null;
398
+ }
399
+ };
400
+ }, [state.isPlaying, animationLoop]);
281
401
 
282
402
  // If controls are enabled, we need to disable frameBox
283
403
  if (options.controls) {