-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 图片不需要全部加载完才渲染;修正图片加载过快时首屏不渲染的问题 #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,7 @@ interface IImageSequenceAnimatorProps { | |
|
|
||
| interface IAnimationState { | ||
| /** all image loaded */ | ||
| isAllLoaded: boolean; | ||
| // isAllLoaded: boolean; | ||
| /** container to viewport top */ | ||
| containerTop: number; | ||
| /** animation container width */ | ||
|
|
@@ -35,7 +35,7 @@ interface IAnimationState { | |
| /** canvas ctx ref */ | ||
| ctx?: CanvasRenderingContext2D; | ||
| /** force react render */ | ||
| forceRefresh: () => void; | ||
| // forceRefresh: () => void; | ||
|
|
||
| currentCanvas?: HTMLCanvasElement; | ||
| offscreenCanvases?: HTMLCanvasElement[]; | ||
|
|
@@ -47,15 +47,14 @@ interface IAnimationState { | |
| * @param props | ||
| * @param canvasRef | ||
| * @param state | ||
| * @param dep | ||
| */ | ||
| const useUpdateState = ( | ||
| props: IImageSequenceAnimatorProps, | ||
| canvasRef: React.RefObject<HTMLCanvasElement>, | ||
| state: React.MutableRefObject<IAnimationState>, | ||
| dep: any | ||
| setState: React.Dispatch<React.SetStateAction<IAnimationState>> | ||
| ) => { | ||
| React.useEffect(() => { | ||
| // console.log('effect called'); | ||
| const resizeHandler = () => { | ||
| if (!canvasRef.current) { | ||
| console.error('canvas not ready'); | ||
|
|
@@ -82,30 +81,39 @@ const useUpdateState = ( | |
| canvasRef.current.width = canvasRectBox.width * dpr; | ||
| canvasRef.current.height = canvasRectBox.height * dpr; | ||
|
|
||
| state.current.containerWidth = canvasRectBox.width; | ||
| state.current.containerHeight = canvasRectBox.height; | ||
| setState(prevState => { | ||
| prevState.containerWidth = canvasRectBox.width; | ||
| prevState.containerHeight = canvasRectBox.height; | ||
|
|
||
| state.current.animationDistance = | ||
| stickyContainerBox.height - | ||
| props.framePaddingEnd - | ||
| props.framePaddingStart; | ||
| prevState.animationDistance = | ||
| stickyContainerBox.height - | ||
| props.framePaddingEnd - | ||
| props.framePaddingStart; | ||
|
|
||
| if (canvasRef.current) { | ||
| state.current.ctx = canvasRef.current.getContext( | ||
| '2d' | ||
| ) as CanvasRenderingContext2D; | ||
| state.current.ctx.scale(dpr, dpr); | ||
| } | ||
| if (canvasRef.current) { | ||
| prevState.ctx = canvasRef.current.getContext( | ||
| '2d' | ||
| ) as CanvasRenderingContext2D; | ||
| prevState.ctx.scale(dpr, dpr); | ||
| } | ||
|
|
||
| // 这里有些脏 | ||
| // 假设要重构,那么应该考虑渲染所需内容不放在入参而放在作用域上,比如用useCallback | ||
| // 如果觉得useCallback会导致重渲判定那么可以配合useRef进行state缓存,参考: | ||
| // https://ahooks.js.org/hooks/advanced/use-persist-fn | ||
| drawImageSequence(props, canvasRef, prevState); | ||
|
|
||
| return prevState | ||
| }) | ||
|
|
||
| drawImageSequence(props, canvasRef, state); | ||
| }; | ||
|
|
||
| resizeHandler(); | ||
| window.addEventListener('resize', resizeHandler); | ||
| return () => { | ||
| window.removeEventListener('resize', resizeHandler); | ||
| }; | ||
| }, [canvasRef, props, state, dep]); | ||
| }, [canvasRef, props, setState]); | ||
| }; | ||
|
|
||
| /** | ||
|
|
@@ -117,25 +125,25 @@ const useUpdateState = ( | |
| function drawImageSequence( | ||
| props: IImageSequenceAnimatorProps, | ||
| canvasRef: React.RefObject<HTMLCanvasElement>, | ||
| state: React.MutableRefObject<IAnimationState> | ||
| state: IAnimationState | ||
| ) { | ||
| if (!canvasRef.current) { | ||
| console.error('canvas not ready'); | ||
| return; | ||
| } | ||
| if (!state.current.isAllLoaded) { | ||
| console.warn('all are not loaded'); | ||
| return; | ||
| } | ||
| const containerHeight = state.current.containerHeight; | ||
| const containerWidth = state.current.containerWidth; | ||
| const animationDistance = state.current.animationDistance; | ||
| // if (!state.current.isAllLoaded) { | ||
| // console.warn('all are not loaded'); | ||
| // return; | ||
| // } | ||
| const containerHeight = state.containerHeight; | ||
| const containerWidth = state.containerWidth; | ||
| const animationDistance = state.animationDistance; | ||
|
|
||
| const { imgWidth, imgHeight } = props; | ||
|
|
||
| const sequences = state.current.imageSequence; | ||
| const sequences = state.imageSequence; | ||
|
|
||
| const ctx = state.current.ctx as CanvasRenderingContext2D; | ||
| const ctx = state.ctx as CanvasRenderingContext2D; | ||
|
|
||
| // offset top is exactly already scrolled vertical range, | ||
| // of canvas' container in sticky container | ||
|
|
@@ -171,7 +179,13 @@ function drawImageSequence( | |
| dy = (containerHeight - dHeight) / 2; | ||
| } | ||
|
|
||
| ctx.drawImage(sequences[currentIndex], dx, dy, dWidth, dHeight); | ||
| if (sequences[currentIndex]) { | ||
| ctx.drawImage(sequences[currentIndex], dx, dy, dWidth, dHeight); | ||
| } else { | ||
| ctx.clearRect(0, 0, dWidth, dHeight) | ||
| } | ||
|
|
||
| console.count('draw') | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -184,7 +198,7 @@ function drawImageSequence( | |
| const useUpdateCanvas = ( | ||
| props: IImageSequenceAnimatorProps, | ||
| canvasRef: React.RefObject<HTMLCanvasElement>, | ||
| state: React.MutableRefObject<IAnimationState> | ||
| state: IAnimationState, | ||
| ) => { | ||
| React.useEffect(() => { | ||
| const scrollHandler = () => { | ||
|
|
@@ -199,15 +213,7 @@ const useUpdateCanvas = ( | |
| }, [canvasRef, props, state]); | ||
| }; | ||
|
|
||
| const useUpdateOffscreenCanvases = ( | ||
| props: IImageSequenceAnimatorProps, | ||
| canvasRef: React.RefObject<HTMLCanvasElement>, | ||
| state: React.MutableRefObject<IAnimationState> | ||
| ) => { | ||
| // todo maybe do an offscreen canvas draw to enhance the process | ||
| }; | ||
|
|
||
| function loadImagePromise(url: string): Promise<HTMLImageElement> { | ||
| function loadImage(url: string): Promise<HTMLImageElement> { | ||
| return new Promise((resolve, reject) => { | ||
| const img = new Image(); | ||
| img.src = url; | ||
|
|
@@ -220,66 +226,68 @@ function loadImagePromise(url: string): Promise<HTMLImageElement> { | |
|
|
||
| const usePreload = ( | ||
| props: IImageSequenceAnimatorProps, | ||
| canvasRef: React.RefObject<HTMLCanvasElement>, | ||
| state: React.MutableRefObject<IAnimationState> | ||
| setState: React.Dispatch<React.SetStateAction<IAnimationState>> | ||
| ) => { | ||
| // preload images | ||
| // and then force the state change and draw | ||
| const { imgUrlList } = props; | ||
|
|
||
| React.useEffect(() => { | ||
| if (props.onProgress) { | ||
| } | ||
|
|
||
| const promises = imgUrlList.map(loadImagePromise); | ||
| Promise.all(promises) | ||
| .then(images => { | ||
| state.current.isAllLoaded = true; | ||
| state.current.imageSequence = images; | ||
| // if (props.onProgress) { | ||
| // } | ||
|
|
||
| let nextFrame = 0 | ||
|
|
||
| const imgUrlListLen = imgUrlList.length | ||
|
|
||
| imgUrlList.forEach((img, idx) => loadImage(img).then((imageEle) => { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 之前使用
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个我还真没注意到……那考虑直接跳过这次渲染?
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 所以还是推荐使用 isAllLoaded嘛。。作为动画而言,可以额外拿一张图片作为封面,是 fallback 用的图片(这个是设计,连 todo 都没加),如果 preload 挂了的话。
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. allLoad才显示这个事情我还是接受不了ww |
||
| setState(prevState => { | ||
| if (props.concatReverse) { | ||
| state.current.imageSequence.push(...images.slice(0).reverse()); | ||
| prevState.imageSequence[imgUrlListLen - 1 - idx] = imageEle | ||
| } else { | ||
| prevState.imageSequence[idx] = imageEle | ||
| } | ||
| state.current.forceRefresh(); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个 contact Reverse 的做法可以 merge,比我的好多了(
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 但是如果是一个
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 主要是我觉得用额外的变量来触发刷新有挺怪的
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 额外的变量 isAllLoaded 吗?这个主要是做 guard 用的,这样那几个 listener,resize 和 scroll 就知道自己是否可以 draw。 |
||
|
|
||
| prevState.imageSequence = [...prevState.imageSequence] | ||
|
|
||
| console.count('image load') | ||
|
|
||
| return prevState | ||
| }) | ||
| .catch(e => { | ||
| console.warn(e); | ||
| state.current.isAllLoaded = false; | ||
| }); | ||
| }, [canvasRef, imgUrlList, props, state]); | ||
| }; | ||
|
|
||
| const ImageSequenceAnimator: React.FC<IImageSequenceAnimatorProps> = props => { | ||
| const [loaded, toggle] = React.useState<boolean>(false); | ||
| const forceRefresh = React.useCallback(() => { | ||
| toggle(true); | ||
| }, [toggle]); | ||
| cancelAnimationFrame(nextFrame) | ||
| nextFrame = requestAnimationFrame(() => { | ||
| window.dispatchEvent(new Event('scroll')) | ||
| }) | ||
| })) | ||
|
|
||
| }, [imgUrlList, props.concatReverse, setState]); | ||
| }; | ||
|
|
||
| const ImageSequenceAnimator: React.FC<IImageSequenceAnimatorProps> = ( | ||
| props | ||
| ) => { | ||
| const canvasRef = React.useRef<HTMLCanvasElement>(null); | ||
| const stateRef = React.useRef<IAnimationState>({ | ||
| isAllLoaded: false, | ||
| const [state, setState] = React.useState<IAnimationState>({ | ||
| containerTop: 0, | ||
| containerWidth: 0, | ||
| containerHeight: 0, | ||
| imageSequence: [], | ||
| animationDistance: 0, | ||
| forceRefresh | ||
| }); | ||
|
|
||
| // hacky way to let useUpdateState know that | ||
| // loaded state has changed | ||
| const callback = React.useMemo(() => ({}), [loaded]); | ||
| }) | ||
| usePreload(props, setState); | ||
| useUpdateState(props, canvasRef, setState); | ||
| useUpdateCanvas(props, canvasRef, state); | ||
|
|
||
| usePreload(props, canvasRef, stateRef); | ||
| useUpdateState(props, canvasRef, stateRef, callback); | ||
| useUpdateCanvas(props, canvasRef, stateRef); | ||
| console.count('render') | ||
|
|
||
| return ( | ||
| <canvas | ||
| ref={canvasRef} | ||
| style={{ | ||
| width: '100%', | ||
| height: '100%', | ||
| background: `${loaded ? 'black' : 'gray'}` | ||
| background: 'gray', | ||
| }} | ||
| /> | ||
| ); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
是说,使用
这种形式?这样这个组件就和 React 的状态管理设计强绑定了,也会多不少的 diff 次数,我初衷是,这里的逻辑设计跟 React 是事实上无关的,同样的 draw 逻辑,搬到任何一个地方都适用~ 主要这也是从 @adobe/react-spectrum 学到的,他们也是通过这样的
props,ref,stateRef入参的设计,快速的适配了 vue 和 svetleUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
不是,是这种;
不过框架无关的这个设计有点意思,我阅读代码的时候完全没留意到这个点。我好好再看一下