Viewer.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. import { Component, createRef } from "react";
  2. import PropTypes from "prop-types";
  3. import { gsap, ScrollTrigger } from "gsap/all";
  4. import LazyLoad from "react-lazyload";
  5. import { css } from "@emotion/react";
  6. const isDebug = Number(import.meta.env.VITE_APP_DEBUG) === 1;
  7. console.log("isDebug", isDebug);
  8. export default function Viewer({ height, ...props }) {
  9. return (
  10. <LazyLoad height={height || "500vh"}>
  11. <ViewerInner height={height} {...props} />
  12. </LazyLoad>
  13. );
  14. }
  15. Viewer.propTypes = {
  16. name: PropTypes.string,
  17. height: PropTypes.string,
  18. path: PropTypes.string,
  19. frameCount: PropTypes.number,
  20. startFrame: PropTypes.number,
  21. enterTween: PropTypes.object,
  22. exitTween: PropTypes.object,
  23. canvasSize: PropTypes.array,
  24. pause: PropTypes.object,
  25. children: PropTypes.object,
  26. debug: PropTypes.bool,
  27. };
  28. class ViewerInner extends Component {
  29. constructor() {
  30. super();
  31. console.log("hello constructor");
  32. //ref
  33. this.containerRef = createRef(null);
  34. this.viewerRef = createRef(null);
  35. this.canvasContainerRef = createRef(null);
  36. this.loadingWrap = createRef(null);
  37. this.viewerOffsetRef = createRef(null);
  38. this.canvasRef = createRef(null);
  39. this.processingRef = createRef(null);
  40. this.preProcessingRef = createRef(null);
  41. this.processBarRef = createRef(null);
  42. this.sequence = [];
  43. this.loadedRenderPool = [];
  44. this.enterTimeline = false;
  45. this.exitTimeline = false;
  46. this.loadComplete = false;
  47. this.playBarTween = false;
  48. this.playPreBarTween = false;
  49. this.loadedCount = 0;
  50. this.progress = 0;
  51. this.lastFrame = -1;
  52. this.floatFrame = 0;
  53. this.frame = 0;
  54. this.loadedRenderTimeout = null;
  55. this.poolAnimateDelay = 40;
  56. this.context = null;
  57. this.width = 1552;
  58. this.height = 873;
  59. this.justScrolled = false;
  60. this.lastProgress = false;
  61. this.isBelow = true;
  62. this.isAbove = false;
  63. }
  64. static propTypes = Viewer.propTypes;
  65. componentWillUnmount() {
  66. console.error("remove-timeline");
  67. if (this.timeline) {
  68. this.timeline.kill(true);
  69. }
  70. }
  71. componentDidMount() {
  72. this.fullFrameCount = this.props.frameCount;
  73. this.frame = this.props.startFrame || 0;
  74. if (this.props.pause) {
  75. Object.keys(this.props.pause).forEach((index) => {
  76. this.fullFrameCount += this.props.pause[index];
  77. });
  78. }
  79. this.loadAssets();
  80. this.canvasRef.current && this.initializeCanvas();
  81. if (!this.timeline) {
  82. this.initializeTimeline();
  83. this.setTimeline();
  84. }
  85. if (!this.enterTimeline && this.props.enterTween) {
  86. this.initializeEnterTween();
  87. }
  88. if (!this.exitTimeline && this.props.exitTween) {
  89. this.initializeExitTween();
  90. }
  91. ScrollTrigger.refresh();
  92. }
  93. loadImage(index) {
  94. const img = new Image();
  95. img.retried = 0;
  96. img.src = this.getSourcePath(index);
  97. img.ogSrc = img.src;
  98. if (this.props.pause && index + "" in this.props.pause) {
  99. for (var r = this.props.pause[index]; r--; ) {
  100. this.sequence.push(img);
  101. }
  102. }
  103. this.sequence.push(img);
  104. img.onload = () => {
  105. index === 1 && this.renderImageToCanvas(0);
  106. if (
  107. this.frame > index &&
  108. this.timeline &&
  109. this.timeline.scrollTrigger.isActive
  110. ) {
  111. this.poolNewFrames(index - 1);
  112. }
  113. // var t = 100 - (this.frame / this.fullFrameCount) * 100 + "%";
  114. // this.loadingProgress.current.style.width
  115. this.loadedCount += 1;
  116. if (this.loadedCount === parseFloat(this.props.frameCount) - 1) {
  117. this.loadingComplete();
  118. }
  119. };
  120. }
  121. getSourcePath(index) {
  122. const defaultPrefix = import.meta.env.VITE_APP_SOURCE;
  123. return `${defaultPrefix}${this.props.path}/${"".concat(
  124. index.toString().padStart(4, "0")
  125. )}.webp`;
  126. }
  127. loadAssets() {
  128. this.loadImage(1);
  129. setTimeout(() => {
  130. for (var t = 2; t <= this.props.frameCount; t += 1) {
  131. this.loadImage(t);
  132. }
  133. }, 60);
  134. }
  135. loadingComplete() {
  136. console.log(this.props.path, "loading complete");
  137. this.loadComplete = true;
  138. this.isAbove && this.renderImageToCanvas(this.loadedCount - 1);
  139. }
  140. initializeCanvas() {
  141. this.context = this.canvasRef.current.getContext("2d", {
  142. alpha: false,
  143. desynchronized: true,
  144. powerPreference: "high-performance",
  145. });
  146. this.context.imageSmoothingEnabled = true;
  147. this.context.imageSmoothingQuality = "high";
  148. }
  149. initializeTimeline() {
  150. // const openLoading = () => {
  151. // gsap.to(this.loadingWrap.current, {
  152. // autoAlpha: 1,
  153. // });
  154. // };
  155. // const closeLoading = () => {
  156. // gsap.to(this.loadingWrap.current, {
  157. // autoAlpha: 0,
  158. // });
  159. // };
  160. // closeLoading();
  161. this.timeline = gsap.timeline({
  162. scrollTrigger: {
  163. trigger: this.containerRef.current,
  164. pin: this.viewerRef.current,
  165. scrub: 0.66,
  166. start: "top top",
  167. end: "bottom bottom",
  168. ease: "none",
  169. markers: isDebug,
  170. onUpdate: (trigger) => {
  171. //处理processloading
  172. if (!this.lastProgress) {
  173. this.lastProgress = trigger.progress;
  174. } else {
  175. if (this.lastProgress !== this.progress) {
  176. this.justScrolled = true;
  177. this.lastProgress = trigger.progress;
  178. }
  179. }
  180. },
  181. onScrubComplete: () => {
  182. this.justScrolled = true;
  183. },
  184. onEnter: () => {
  185. this.isAbove = false;
  186. console.log(this.props.path, "onEnter");
  187. this.enterShowElements();
  188. },
  189. onEnterBack: () => {
  190. // openLoading();
  191. console.log(this.props.path, "onEnterBack");
  192. this.isBelow = false;
  193. this.enterShowElements();
  194. },
  195. onLeave: () => {
  196. console.log(this.props.path, "onLeave");
  197. this.isAbove = true;
  198. this.leaveHideElements();
  199. },
  200. onLeaveBack: () => {
  201. console.log(this.props.path, "onLeaveBack");
  202. this.isBelow = true;
  203. this.leaveHideElements();
  204. },
  205. },
  206. });
  207. }
  208. setTimeline() {
  209. this.timeline.to(this, {
  210. floatFrame: this.fullFrameCount - 1,
  211. ease: "none",
  212. onUpdate: () => {
  213. this.frame = Math.floor(this.floatFrame);
  214. if (
  215. this.lastFrame === this.frame ||
  216. this.loadedRenderPool.length === 0
  217. ) {
  218. this.renderImageToCanvas(this.frame);
  219. }
  220. },
  221. });
  222. }
  223. initializeEnterTween() {
  224. const duration = this.props.enterTween.duration || 1;
  225. let openPin = false;
  226. console.warn("this.props.enterTween", duration, this.props.enterTween);
  227. gsap.set(this.viewerRef.current, {
  228. yPercent: -100 * duration,
  229. });
  230. if (void 0 !== this.props.enterTween.pin) {
  231. openPin = this.props.enterTween.pin;
  232. }
  233. this.enterTimeline = gsap.timeline({
  234. scrollTrigger: {
  235. trigger: this.viewerRef.current,
  236. scrub: true,
  237. pin: openPin,
  238. start: function () {
  239. return "top top";
  240. },
  241. end: function () {
  242. return "top top-=" + window.innerHeight * duration;
  243. },
  244. },
  245. });
  246. if (this.props.enterTween.to) {
  247. this.enterTimeline.to(
  248. this.viewerRef.current,
  249. Object.assign(
  250. {
  251. ease: "none",
  252. },
  253. this.props.enterTween.to
  254. )
  255. );
  256. }
  257. if (this.props.enterTween.from) {
  258. this.enterTimeline.from(
  259. this.viewerRef.current,
  260. Object.assign(
  261. {
  262. ease: "none",
  263. },
  264. this.props.enterTween.from
  265. )
  266. );
  267. }
  268. }
  269. initializeExitTween() {
  270. console.log(this.props.path, "initializing exit tween ");
  271. this.exitTimeline = gsap.timeline({
  272. scrollTrigger: {
  273. scrub: true,
  274. trigger: this.containerRef.current,
  275. pin: this.viewerRef.current,
  276. onLeave: this.props.exitTween.onLeave,
  277. onLeaveBack: this.props.exitTween.onLeaveBack,
  278. onEnterBack: this.props.exitTween.onEnterBack,
  279. start: function () {
  280. return "bottom bottom";
  281. },
  282. end: function () {
  283. return "bottom top";
  284. },
  285. },
  286. });
  287. if (this.props.exitTween.from) {
  288. this.exitTimeline.from(
  289. this.viewerRef.current,
  290. Object.assign(
  291. {
  292. ease: "none",
  293. },
  294. this.props.exitTween.from
  295. )
  296. );
  297. }
  298. if (this.props.exitTween.to) {
  299. this.exitTimeline.to(
  300. this.viewerRef.current,
  301. Object.assign(
  302. {
  303. ease: "none",
  304. },
  305. this.props.exitTween.to
  306. )
  307. );
  308. }
  309. }
  310. poolNewFrames(index) {
  311. this.loadedRenderPool.unshift(index);
  312. this.loadedRenderPool.sort(function (a, b) {
  313. return b - a;
  314. });
  315. this.animatePool();
  316. }
  317. animatePool() {
  318. if (!this.loadedRenderTimeout && this.loadedRenderPool.length) {
  319. this.loadedRenderTimeout = setTimeout(() => {
  320. this.loadedRenderTimeout = false;
  321. var poolFrame = this.loadedRenderPool[this.loadedRenderPool.length - 1];
  322. if (poolFrame <= this.frame) {
  323. var remainFrame = this.loadedRenderPool.pop();
  324. this.renderImageToCanvas(remainFrame);
  325. this.animatePool();
  326. }
  327. if (this.frame < poolFrame) {
  328. this.loadedRenderPool = [];
  329. }
  330. }, this.poolAnimateDelay);
  331. }
  332. }
  333. renderImageToCanvas(index) {
  334. if (this.sequence[index]) {
  335. if (this.context.drawImage) {
  336. this.context.drawImage(this.sequence[index], 0, 0);
  337. this.lastFrame = index;
  338. this.handleSyncProessBar(index);
  339. } else {
  340. this.initializeCanvas();
  341. }
  342. }
  343. }
  344. handleSyncProessBar(index) {
  345. const progressingPreload =
  346. 100 - (this.frame / this.fullFrameCount) * 100 + "%";
  347. const progressing = 100 - (index / this.fullFrameCount) * 100 + "%";
  348. // console.log("handleSyncProessBar", this.processingRef.current);
  349. if (this.preProcessingRef.current) {
  350. this.playPreBarTween = gsap.to(this.preProcessingRef.current, {
  351. duration: 0.05,
  352. right: progressingPreload,
  353. ease: "none",
  354. });
  355. this.playBarTween = gsap.to(this.processingRef.current, {
  356. duration: 0.05,
  357. right: progressing,
  358. ease: "none",
  359. });
  360. }
  361. }
  362. enterShowElements() {
  363. gsap.set(this.processBarRef.current, {
  364. autoAlpha: 1,
  365. });
  366. }
  367. leaveHideElements() {
  368. gsap.set(this.processBarRef.current, {
  369. autoAlpha: 0,
  370. });
  371. }
  372. render() {
  373. return (
  374. <>
  375. <div
  376. className={`processBar ${this.props.name}`}
  377. ref={this.processBarRef}
  378. css={css`
  379. position: fixed;
  380. top: calc(100vh - 4px);
  381. // top: calc(var(--vh, 1vh) * 100 - 4px);
  382. left: 0;
  383. width: 100vw;
  384. max-width: 100%;
  385. border-top: 1px solid rgba(33, 33, 44, 0.6);
  386. background-color: rgba(17, 17, 34, 0.6);
  387. height: 4px;
  388. z-index: 9;
  389. `}
  390. >
  391. <div
  392. css={css`
  393. position: absolute;
  394. width: auto;
  395. height: 4px;
  396. left: 0;
  397. bottom: 0;
  398. z-index: 10;
  399. background-color: rgba(120, 120, 163, 0.33);
  400. `}
  401. ref={this.preProcessingRef}
  402. ></div>
  403. <div
  404. css={css`
  405. position: absolute;
  406. width: auto;
  407. height: 4px;
  408. left: 0;
  409. bottom: 0;
  410. z-index: 10;
  411. background-color: hsla(0, 0%, 79.2%, 0.5);
  412. `}
  413. ref={this.processingRef}
  414. ></div>
  415. </div>
  416. <div
  417. css={css`
  418. position: relative;
  419. margin: auto;
  420. text-align: center;
  421. pointer-events: none;
  422. max-width: 100vw;
  423. `}
  424. ref={this.containerRef}
  425. style={{ height: this.props.height || "500vh" }}
  426. >
  427. <div ref={this.viewerRef}>
  428. <div style={{ overflow: "hidden" }}>
  429. <canvas
  430. css={css`
  431. width: auto;
  432. margin-left: 50%;
  433. transform: translateX(-50%);
  434. height: 100vh;
  435. height: calc(var(--vh, 1vh) * 100);
  436. `}
  437. ref={this.canvasRef}
  438. width={this.width}
  439. height={this.height}
  440. ></canvas>
  441. </div>
  442. </div>
  443. <>{this.props.children}</>
  444. </div>
  445. </>
  446. );
  447. }
  448. }