用于 Canvas 实时绘画的钩子,大概为每秒 60
次的刷新。
主要采用 requestAnimationFrame
来进行实时绘画,并对高刷屏和高分辨率屏做了一定兼容。对于不支持 requestAnimationFrame
的采用 setInterval
替代。
使用方式
npm i lhh-ui
只需导入 useRTDraw
钩子,通过参数中的 onDraw
回调即可对 Canvas
进行实时绘画。其中的 onDraw
大概为每秒触发 60
次。
import React, { useEffect, useState } from "react"
import { useRTDraw } from "lhh-ui";
export default () => {
const {drawState, canvasRef, canvasInfo, setCanvasInfo, startAnimation} = useRTDraw({
onDraw() {
// 注意此时 canvas 内部的尺寸,已经根据分辨率等比例缩放。
drawState.ctx.clearRect(0, 0, canvasInfo.w * drawState.ratio, canvasInfo.h * drawState.ratio);
// ... 对 canvas 进行绘画
},
})
useEffect(() => {
startAnimation()
}, [])
return (
<canvas
ref={canvasRef}
// 使 canvas 绘画区域增大
width={canvasInfo.w * drawState.ratio}
height={canvasInfo.h * drawState.ratio}
style={{
width: canvasInfo.w + 'px',
height: canvasInfo.h + 'px',
}}
/>
)
}
使用代码可以参考码上掘金
注意事项
- 需要和
canvas
的ref
进行绑定 - 需要根据屏幕实际分辨率,对
canvas
的宽高进行对应比例增加(主要是为了增加每1px
中的像素点,使绘画更清晰)。 onDraw
绘画时,记得对尺寸与ratio
进行乘积。
例: 绘画一个宽高为 100px,在 canvas 中为横向 20px 纵向 50px 位置的矩形。
onDraw({ctx, ratio}) {
// ...
const rect = {
w: 100 * ratio,
x: 20 * ratio,
y: 50 * ratio,
}
ctx.fillStyle = 'green';
ctx.fillRect(rect.x, rect.y, rect.w, rect.w);
}
源码解析
钩子挂载获取数据
const [ratio, setRatio] = useState(1);
const drawState = useRef({
ctx: void 0 as CanvasRenderingContext2D | undefined,
isHighRefreshScreen: void 0 as boolean | undefined,
})
useEffect(() => {
getCtx()
// 获取分辨率
setRatio(window.devicePixelRatio)
getScreenFps().then(fps => {
drawState.current.isHighRefreshScreen = fps > 65
})
return () => {
cancelAnimation()
}
}, [])
获取 canvas 上下文
注意,如果你的 canvas
为异步创建的,在挂载阶段没获取到也没关系,当调用 startAnimation
时也会获取一遍,只需确保调用 startAnimation
前 canvas
已经挂载好即可。
function getCtx() {
if(drawState.current.ctx) return
const ctx = canvasRef.current?.getContext('2d')
if(ctx) {
drawState.current.ctx = ctx
}
}
判断是否为高刷屏
根据传参,计算出 60
次的累加,requestAnimationFrame
需要的用时是多少,非高刷屏该值一般在 60 左右。上方用 65 来比较是否为高刷屏即可。
/** 经过多少次计算后,获取fps */
export const getScreenFps = (total: number = 60): Promise<number> => {
return new Promise(resolve => {
if(typeof requestAnimationFrame === 'undefined') {
return resolve(60)
}
const begin = Date.now();
let count = 0;
(function run() {
requestAnimationFrame(() => {
if (++count >= total) {
const fps = Math.ceil((count / (Date.now() - begin)) * 1000)
return resolve(fps)
}
run()
})
})()
})
}
startAnimation 开始绘画
前面几行只是一些判断,没什么好说的;从 runDraw
开始触发判断,如果是高刷屏调用 startAnimationLockFrame
锁定 60 帧触发回调,否则就是根据 requestAnimation
正常触发函数回调来进行绘画。
function onDrawFn() {
latest.current.onDraw({ctx: drawState.current.ctx!, ratio});
}
/** 开启动画绘画 */
function startAnimation() {
getCtx()
if(!canvasRef.current) {
console.warn('useRTDraw: Please bind the ref of canvas')
return
}
if(!drawState.current.ctx) {
console.warn('useRTDraw: Canvas context retrieval failed, can call getCtx to retrieve again')
return
}
// 兼容性处理
if(typeof requestAnimationFrame === 'undefined') {
clearInterval(timer.current)
timer.current = setInterval(() => {
onDrawFn()
}, 16.6)
return
}
function runDraw() {
if(timer.current) {
cancelAnimationFrame(timer.current as number);
}
if(drawState.current.isHighRefreshScreen) {
startAnimationLockFrame()
} else {
(function go() {
timer.current = requestAnimationFrame(go);
onDrawFn()
})();
}
}
// 等待是否为高刷屏的判断
if(drawState.current.isHighRefreshScreen === void 0) {
sleep(1200).then(() => {
runDraw()
})
return
}
runDraw()
}
startAnimationLockFrame 锁帧绘画
用于高刷屏的锁帧操作,但是锁帧的同时会使绘画出现掉帧;简单来说就是某一时段的绘画函数不执行了,到下一时段再执行,所以感观上就是掉帧。
function startAnimationLockFrame() {
const fps = 60;
let fpsInterval = 1000 / fps;
let then = Date.now();
(function go() {
timer.current = requestAnimationFrame(go);
const now = Date.now();
const elapsed = now - then;
if (elapsed > fpsInterval) {
onDrawFn()
then = now - (elapsed % fpsInterval);
}
})();
}
返回参数
属性名 | 描述 | 类型 | |
---|---|---|---|
canvasRef | 用于和 canvas 的 ref 相绑定 | HTMLCanvasElement | |
drawState | 用于绘画的 state 包含 上下文,设备像素比和是否为高刷屏的判断 | {ctx: CanvasRenderingContext2D | null; ratio: number; isHighRefreshScreen: boolean | undefined;} |
canvasInfo | 用于设置 canvas 的宽高 | {w: number, h: number} | |
setCanvasInfo | 设置 canvas 的宽高信息 | (p: {w: number, h: number}) => void | |
getCtx | 用于获取 canvas 的上下文 | () => void | |
startAnimation | 开启动画的回调 | () => void | |
cancelAnimation | 结束动画的回调 | () => void |
后言
该实现主要是根据我的 塔防小游戏 ,以及平时使用 Canvas 做绘画时总结封装而来。虽然很一般,但要是感兴趣可以前往玩玩。