【结论】三点都补到点子上了,我再补几处「照抄会踩」的细节:DPR 那段 ctx.scale 不能无脑跑第二遍,rAF 首帧的 dt 必须特殊处理,跨域那张图要么在 src 之前设 crossOrigin、要么干脆别用远程图。
【展开】DPR 适配最大的坑是重复执行。你把 resize 和初始化复用了同一个 setup 函数,ctx.scale(dpr,dpr) 会在旧变换上再乘一次,第二次 resize 后坐标整体放大 dpr 倍。稳妥写法是 ctx.setTransform(dpr,0,0,dpr,0,0),或者每次 ctx.save() 前先 setTransform(1,0,0,1,0,0)。另外 dpr 不是常量——浏览器缩放、窗口拖到不同缩放的副屏都会变,监听 matchMedia(\(resolution: ${dpr}dppx)\) 的 change 事件重建一次即可。还有个小提醒:dpr=1 时 1px 描边仍会糊,那是坐标落在半像素的问题,translate(0.5,0.5) 或把奇数线宽对齐到 .5 坐标;dpr≥2 时基本不用管。
rAF 那边我补两点:一是首帧 t 不接近 0(是页面加载到现在的毫秒数),直接 t - last 会得到一个巨大的 dt 让物体瞬移,正确做法是首次调用只记录 last = t 然后直接 return;二是 dt clamp 到 50ms 只解决了「不瞬移」,物理类动画(碰撞、重力)建议再上一套固定步长累加器,否则不同刷新率的轨迹会漂。切后台还要顺手 cancelAnimationFrame,SPA 里忘了取消就是内存泄漏。
像素处理:img.crossOrigin='anonymous' 必须在赋值 src 之前,赋晚了浏览器已经发起无 CORS 请求,设置无效。用户本地选的文件(File → createObjectURL)属于同源 blob,不会被 taint,做上传前压缩走这条路最省事。getImageData 返回的是副本,别放进循环里反复调;只改局部时用 putImageData(data, dx, dy, sx, sy, sw, sh) 的脏矩形重载,性能差好几倍。补充一点你提到的 transform 无效——它同时也忽略 globalAlpha、globalCompositeOperation 和阴影,别指望用它做淡入。
【结尾】最后送一个冷门坑:iOS Safari 对单个 canvas 有约 1670 万像素的面积上限,超过就静默返回空白画布,做高清大图导出前先判断 w*h*dpr*dpr;保底方案是分块绘制再拼。