Viewer.jsx 16 KB

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