Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 83 additions & 75 deletions src/image-sequence-animator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface IImageSequenceAnimatorProps {

interface IAnimationState {
/** all image loaded */
isAllLoaded: boolean;
// isAllLoaded: boolean;
/** container to viewport top */
containerTop: number;
/** animation container width */
Expand All @@ -35,7 +35,7 @@ interface IAnimationState {
/** canvas ctx ref */
ctx?: CanvasRenderingContext2D;
/** force react render */
forceRefresh: () => void;
// forceRefresh: () => void;

currentCanvas?: HTMLCanvasElement;
offscreenCanvases?: HTMLCanvasElement[];
Expand All @@ -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');
Expand All @@ -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);

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是说,使用

Suggested change
const drawImage = useCallback(() => {
oldDrawImage(props, canvasRef, state)
}, [props, canvasRef, state])

这种形式?这样这个组件就和 React 的状态管理设计强绑定了,也会多不少的 diff 次数,我初衷是,这里的逻辑设计跟 React 是事实上无关的,同样的 draw 逻辑,搬到任何一个地方都适用~ 主要这也是从 @adobe/react-spectrum 学到的,他们也是通过这样的 props, ref, stateRef 入参的设计,快速的适配了 vue 和 svetle

Copy link
Author

@Inori-Lover Inori-Lover Sep 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不是,是这种;

不过框架无关的这个设计有点意思,我阅读代码的时候完全没留意到这个点。我好好再看一下

return prevState
})

drawImageSequence(props, canvasRef, state);
};

resizeHandler();
window.addEventListener('resize', resizeHandler);
return () => {
window.removeEventListener('resize', resizeHandler);
};
}, [canvasRef, props, state, dep]);
}, [canvasRef, props, setState]);
};

/**
Expand All @@ -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
Expand Down Expand Up @@ -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')
}

/**
Expand All @@ -184,7 +198,7 @@ function drawImageSequence(
const useUpdateCanvas = (
props: IImageSequenceAnimatorProps,
canvasRef: React.RefObject<HTMLCanvasElement>,
state: React.MutableRefObject<IAnimationState>
state: IAnimationState,
) => {
React.useEffect(() => {
const scrollHandler = () => {
Expand All @@ -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;
Expand All @@ -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) => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

之前使用 Promise.all 和 promise 数组的设计是说,倘若有图片没有返回来,就不会开启动画。
倘若这里不使用 isAllLoaded,在 draw 的时候不设置 guard,而是 clearRect,譬如在 devTools 开启 slow Mode,就会看到加载过程中进行滚动的时候,出现一会儿有图片,一会儿是背景色的这个问题。

Copy link
Author

@Inori-Lover Inori-Lover Sep 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个我还真没注意到……那考虑直接跳过这次渲染?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

所以还是推荐使用 isAllLoaded嘛。。作为动画而言,可以额外拿一张图片作为封面,是 fallback 用的图片(这个是设计,连 todo 都没加),如果 preload 挂了的话。

Copy link
Author

Choose a reason for hiding this comment

The 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();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个 contact Reverse 的做法可以 merge,比我的好多了(

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

但是如果是一个 ref 而不是 state,就明显不用 [...prevState.imageSequence] 再来一遍啊~

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

主要是我觉得用额外的变量来触发刷新有挺怪的

Copy link
Owner

Choose a reason for hiding this comment

The 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',
}}
/>
);
Expand Down