123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589 |
- import {
- Component,
- createRef,
- Children,
- isValidElement,
- cloneElement,
- } from "react";
- import PropTypes from "prop-types";
- import { gsap, ScrollTrigger } from "gsap/all";
- import LazyLoad from "react-lazyload";
- import { css } from "@emotion/react";
- // import { Style } from "../style/viewer";
- export default function Viewer(props) {
- const lazyHeight = 0.01 * parseFloat(props.height) * window.innerHeight;
- // offset={1e4}
- var allChildren = Children.map(props.children, function (element) {
- return isValidElement(element)
- ? cloneElement(element, {
- parentHeight: props.height,
- debug: props.debug,
- })
- : element;
- });
- const debug = props.debug || false;
- return (
- <LazyLoad
- height={lazyHeight}
- offset={1e4}
- className={`${debug ? props.name : ""}`}
- >
- <ViewerInner {...props} debug={debug}>
- {allChildren}
- </ViewerInner>
- </LazyLoad>
- );
- }
- Viewer.propTypes = {
- name: PropTypes.string,
- height: PropTypes.string,
- path: PropTypes.string,
- frameCount: PropTypes.number,
- startFrame: PropTypes.number,
- enterTween: PropTypes.object,
- exitTween: PropTypes.object,
- canvasSize: PropTypes.array,
- pause: PropTypes.object,
- children: PropTypes.any,
- debug: PropTypes.bool,
- scrollSpeed: PropTypes.number,
- };
- class ViewerInner extends Component {
- constructor(props) {
- super();
- //ref
- this.containerRef = createRef(null);
- this.viewerRef = createRef(null);
- this.canvasContainerRef = createRef(null);
- this.viewerOffsetRef = createRef(null);
- this.canvasRef = createRef(null);
- this.processBarRef = createRef(null);
- this.processingRef = createRef(null);
- this.preProcessingRef = createRef(null);
- this.processBgRef = createRef(null);
- this.sequence = [];
- this.loadedRenderPool = [];
- this.enterTimeline = false;
- this.exitTimeline = false;
- this.loadComplete = false;
- this.playBarTween = false;
- this.playPreBarTween = false;
- this.loadedCount = 0;
- this.progress = 0;
- this.lastFrame = -1;
- this.floatFrame = 0;
- this.frame = 0;
- this.loadedRenderTimeout = null;
- this.poolAnimateDelay = 40;
- this.context = null;
- this.width = 1552;
- this.height = 873;
- this.justScrolled = false;
- this.lastProgress = false;
- this.isBelow = true;
- this.isAbove = false;
- this.init = this.init.bind(this);
- this.init(props);
- }
- init(props) {
- this.props = props;
- props.debug && console.log("init", props.name);
- this.fullFrameCount = props.frameCount;
- this.notLoadedTween = {
- isActive: false,
- };
- this.frame = props.startFrame || 0;
- if (this.props.pause) {
- Object.keys(this.props.pause).forEach((index) => {
- this.fullFrameCount += this.props.pause[index];
- });
- }
- }
- static propTypes = Viewer.propTypes;
- componentWillUnmount() {
- this.props.debug && console.warn("remove-timeline");
- if (this.timeline) {
- this.timeline.kill(true);
- }
- }
- componentDidUpdate() {
- if (this.props.debug) {
- this.timeline.scrollTrigger.refresh();
- }
- }
- componentDidMount() {
- this.loadAssets();
- this.canvasRef.current && this.initializeCanvas();
- if (!this.timeline) {
- this.initializeTimeline();
- this.setTimeline();
- }
- if (!this.enterTimeline && this.props.enterTween) {
- this.initializeEnterTween();
- }
- if (!this.exitTimeline && this.props.exitTween) {
- this.initializeExitTween();
- }
- ScrollTrigger.refresh();
- }
- loadImage(index) {
- const img = new Image();
- img.retried = 0;
- img.src = this.getSourcePath(index);
- img.ogSrc = img.src;
- if (this.props.pause && index + "" in this.props.pause) {
- for (var r = this.props.pause[index]; r--; ) {
- this.sequence.push(img);
- }
- }
- this.sequence.push(img);
- img.onerror = () => {
- var timeStamp = Math.floor(Date.now() * Math.random())
- .toString()
- .substring(0, 8);
- img.retried < 2
- ? setTimeout(() => {
- img.src = img.ogSrc + "?" + timeStamp;
- }, 80)
- : img.retried < 3 &&
- setTimeout(() => {
- img.src = img.ogSrc.slice(0, -4) + ".jpg?" + timeStamp;
- }, 80),
- img.retried++;
- this.props.debug && console.log("img.retried", img.retried);
- };
- img.onload = () => {
- index === 1 && this.renderImageToCanvas(0);
- if (
- this.frame > index &&
- this.timeline &&
- this.timeline.scrollTrigger.isActive
- ) {
- this.poolNewFrames(index - 1);
- }
- var t = 100 - (this.frame / this.fullFrameCount) * 100 + "%";
- if (this.processBgRef.current) {
- this.processBgRef.current.style.width = t;
- }
- this.loadedCount += 1;
- if (this.loadedCount === parseFloat(this.props.frameCount) - 1) {
- this.loadingComplete();
- }
- };
- }
- getSourcePath(index) {
- const defaultPrefix = import.meta.env.VITE_APP_SOURCE;
- return `${defaultPrefix}${this.props.path}/${"".concat(
- index.toString().padStart(4, "0")
- )}.webp`;
- }
- loadAssets() {
- this.loadImage(1);
- setTimeout(() => {
- for (var t = 2; t <= this.props.frameCount; t += 1) {
- this.loadImage(t);
- }
- }, 60);
- }
- loadingComplete() {
- console.log(this.props.path, "loading complete");
- this.loadComplete = true;
- this.isAbove && this.renderImageToCanvas(this.loadedCount - 1);
- }
- initializeCanvas() {
- this.context = this.canvasRef.current.getContext("2d", {
- alpha: false,
- desynchronized: true,
- powerPreference: "high-performance",
- });
- this.context.imageSmoothingEnabled = true;
- this.context.imageSmoothingQuality = "high";
- }
- initializeTimeline() {
- this.props.debug && console.log(this.props.path, "initializeTimeline");
- this.leaveHideElements();
- let timer = null;
- this.timeline = gsap.timeline({
- scrollTrigger: {
- trigger: this.containerRef.current,
- pin: this.viewerRef.current,
- scrub: 0.66,
- start: "top top",
- end: "bottom bottom",
- ease: "none",
- markers: this.props.debug || false,
- onUpdate: (trigger) => {
- //处理processloading
- if (!this.lastProgress) {
- this.lastProgress = trigger.progress;
- } else {
- if (this.lastProgress !== this.progress) {
- this.justScrolled = true;
- this.lastProgress = trigger.progress;
- }
- }
- clearTimeout(timer);
- timer = setTimeout(() => {
- if (
- this.frame > this.loadedCount &&
- !this.notLoadedTween.isActive
- ) {
- this.props.debug && console.warn(this.props.path, "fast forward");
- this.enterShowElements();
- this.notLoadedTween = gsap.to(this.processBarRef.current, {
- backgroundColor: "#8888a0",
- duration: 0.33,
- repeat: 1,
- yoyo: !0,
- });
- }
- }, 100);
- },
- onScrubComplete: () => {
- this.justScrolled = true;
- },
- onEnter: () => {
- this.isAbove = false;
- this.enterShowElements();
- this.props.debug && console.log(this.props.path, "onEnter");
- },
- onEnterBack: () => {
- this.isBelow = false;
- this.enterShowElements();
- this.props.debug && console.log(this.props.path, "onEnterBack");
- },
- onLeave: () => {
- this.props.debug && console.log(this.props.path, "onLeave");
- this.isAbove = true;
- this.leaveHideElements();
- },
- onLeaveBack: () => {
- this.props.debug && console.log(this.props.path, "onLeaveBack");
- this.isBelow = true;
- this.leaveHideElements();
- },
- },
- });
- }
- setTimeline() {
- this.timeline.to(this, {
- floatFrame: this.fullFrameCount - 1,
- ease: "none",
- onUpdate: () => {
- this.frame = Math.floor(this.floatFrame);
- if (
- this.lastFrame === this.frame ||
- this.loadedRenderPool.length === 0
- ) {
- this.renderImageToCanvas(this.frame);
- }
- },
- });
- }
- initializeEnterTween() {
- const duration = this.props.enterTween.duration || 1;
- let openPin = false;
- // console.warn("this.props.enterTween", duration, this.props.enterTween);
- gsap.set(this.viewerRef.current, {
- yPercent: -100 * duration,
- });
- if (void 0 !== this.props.enterTween.pin) {
- openPin = this.props.enterTween.pin;
- }
- this.enterTimeline = gsap.timeline({
- scrollTrigger: {
- trigger: this.viewerRef.current,
- scrub: true,
- pin: openPin,
- start: function () {
- return "top top";
- },
- end: function () {
- return "top top-=" + window.innerHeight * duration;
- },
- },
- });
- if (this.props.enterTween.to) {
- this.enterTimeline.to(
- this.viewerRef.current,
- Object.assign(
- {
- ease: "none",
- },
- this.props.enterTween.to
- )
- );
- }
- if (this.props.enterTween.from) {
- this.enterTimeline.from(
- this.viewerRef.current,
- Object.assign(
- {
- ease: "none",
- },
- this.props.enterTween.from
- )
- );
- }
- if (this.props.enterTween.fromTo) {
- var form = this.props.enterTween.fromTo[0],
- to = this.props.enterTween.fromTo[1];
- this.enterTimeline.fromTo(
- this.viewerRef.current,
- form,
- Object.assign(
- {
- ease: "none",
- },
- to
- )
- );
- }
- }
- initializeExitTween() {
- this.props.debug &&
- console.log(this.props.path, "initializing exit tween ");
- this.exitTimeline = gsap.timeline({
- scrollTrigger: {
- scrub: true,
- trigger: this.containerRef.current,
- pin: this.viewerRef.current,
- onLeave: this.props.exitTween.onLeave,
- onLeaveBack: this.props.exitTween.onLeaveBack,
- onEnterBack: this.props.exitTween.onEnterBack,
- start: function () {
- return "bottom bottom";
- },
- end: function () {
- return "bottom top";
- },
- },
- });
- if (this.props.exitTween.from) {
- this.exitTimeline.from(
- this.viewerRef.current,
- Object.assign(
- {
- ease: "none",
- },
- this.props.exitTween.from
- )
- );
- }
- if (this.props.exitTween.to) {
- this.exitTimeline.to(
- this.viewerRef.current,
- Object.assign(
- {
- ease: "none",
- },
- this.props.exitTween.to
- )
- );
- }
- if (this.props.exitTween.fromTo) {
- var form = this.props.enterTween.fromTo[0],
- to = this.props.enterTween.fromTo[1];
- this.exitTimeline.fromTo(
- this.viewerRef.current,
- form,
- Object.assign(
- {
- ease: "none",
- },
- to
- )
- );
- }
- }
- poolNewFrames(index) {
- this.loadedRenderPool.unshift(index);
- this.loadedRenderPool.sort(function (a, b) {
- return b - a;
- });
- this.animatePool();
- }
- animatePool() {
- if (!this.loadedRenderTimeout && this.loadedRenderPool.length) {
- this.loadedRenderTimeout = setTimeout(() => {
- this.loadedRenderTimeout = false;
- var poolFrame = this.loadedRenderPool[this.loadedRenderPool.length - 1];
- if (poolFrame <= this.frame) {
- var remainFrame = this.loadedRenderPool.pop();
- this.renderImageToCanvas(remainFrame);
- this.animatePool();
- }
- if (this.frame < poolFrame) {
- this.loadedRenderPool = [];
- }
- }, this.poolAnimateDelay);
- }
- }
- renderImageToCanvas(index) {
- if (this.sequence[index]) {
- if (this.context.drawImage) {
- this.context.drawImage(this.sequence[index], 0, 0);
- this.lastFrame = index;
- this.handleSyncProessBar(index);
- } else {
- this.initializeCanvas();
- }
- }
- }
- handleSyncProessBar(index) {
- const progressingPreload =
- 100 - (this.frame / this.fullFrameCount) * 100 + "%";
- const progressing = 100 - (index / this.fullFrameCount) * 100 + "%";
- // console.log("handleSyncProessBar", this.processingRef.current);
- if (this.preProcessingRef.current) {
- this.playPreBarTween = gsap.to(this.preProcessingRef.current, {
- duration: 0.05,
- right: progressingPreload,
- ease: "none",
- });
- this.playBarTween = gsap.to(this.processingRef.current, {
- duration: 0.05,
- right: progressing,
- ease: "none",
- });
- }
- }
- enterShowElements() {
- this.props.debug && console.log(this.props.path, "show-process-bar");
- gsap.set(this.processBarRef.current, {
- autoAlpha: 1,
- });
- }
- leaveHideElements() {
- this.props.debug && console.log(this.props.path, "hide-process-bar");
- gsap.set(this.processBarRef.current, {
- autoAlpha: 0,
- });
- }
- render() {
- return (
- <>
- <div
- ref={this.processBarRef}
- css={css`
- position: fixed;
- top: calc(100vh - 4px);
- top: calc(var(--vh, 1vh) * 100 - 4px);
- left: 0;
- width: 100vw;
- max-width: 100%;
- border-top: 1px solid rgba(33, 33, 44, 0.6);
- background-color: rgba(17, 17, 34, 0.6);
- height: 4px;
- z-index: 9;
- // visibility: hidden;
- `}
- >
- <div
- css={css`
- position: relative;
- width: 100%;
- height: 4px;
- `}
- >
- <div
- css={css`
- position: absolute;
- width: auto;
- height: 4px;
- left: 0;
- bottom: 0;
- z-index: 10;
- background-color: hsla(0, 0%, 79.2%, 0.5);
- `}
- ref={this.processingRef}
- ></div>
- <div
- css={css`
- position: absolute;
- width: auto;
- height: 4px;
- left: 0;
- bottom: 0;
- z-index: 10;
- margin-left: -1px;
- border-radius: 1px;
- transform: translate3D(-1px, 0, 0);
- background-color: rgba(120, 120, 163, 0.33);
- `}
- ref={this.preProcessingRef}
- ></div>
- </div>
- <div
- ref={this.processBgRef}
- css={css`
- position: absolute;
- left: 0;
- top: 0;
- width: 0;
- height: 6px;
- border-top: 1px solid rgba(33, 33, 44, 0.6);
- background-color: rgba(17, 17, 34, 0.6);
- `}
- ></div>
- </div>
- <div
- css={css`
- position: relative;
- margin: auto;
- text-align: center;
- pointer-events: none;
- max-width: 100vw;
- `}
- ref={this.containerRef}
- style={{ height: this.props.height || "500vh" }}
- >
- <div ref={this.viewerRef}>
- <div style={{ overflow: "hidden" }}>
- <canvas
- css={css`
- width: auto;
- margin-left: 50%;
- transform: translateX(-50%);
- height: 100vh;
- height: calc(var(--vh, 1vh) * 100);
- `}
- ref={this.canvasRef}
- width={this.width}
- height={this.height}
- ></canvas>
- </div>
- </div>
- <>{this.props.children}</>
- </div>
- </>
- );
- }
- }
|