招聘岗位要生成分享海报,设计师给了 750×1334 的设计稿,要求小程序端 Canvas 渲染。真上手才发现,Canvas 画图这活儿水太深——设计稿 1:1 照搬不行,网络图直接 draw 不行,多渲染几次画面还会越变越小。这套逻辑我写进了 job-uniapp 的 posterRenderer.js,把坑一个个填平了,分享下过程。
设计稿 750×1334,代码里就认这个数
海报所有坐标都按设计稿 750 宽来写,绘制时再整体缩放。这样设计师给的标注能直接抄,不用每个元素单独换算:
constDESIGN_WIDTH=750;constDESIGN_HEIGHT=1334;// 绘制入口:把 750×1334 的坐标系缩放到实际画布exportasyncfunctiondrawPoster(ctx,options){const{canvasW,canvasH}=options;constscale=Math.min(canvasW/DESIGN_WIDTH,canvasH/DESIGN_HEIGHT);ctx.scale(scale,scale);// ... 后续所有 draw 都用设计稿坐标ctx.fillText(hireTitle,padL,curY+34);ctx.fillText(metaLine,padL,curY+22);}重点来了:scale 只做一次。如果每次渲染都无条件ctx.scale(scale, scale),transform 矩阵会叠加,画着画着整个画面就缩小甚至看不见了。所以绘制前必须先重置:
/** 绘制前重置变换矩阵,避免多次 render 后 scale 累积导致画面缩小 */exportfunctionresetPosterTransform(ctx,designScale=1){if(!ctx||!ctx.__isModern||!ctx.__native)return;ctx.setTransform(1,0,0,1,0,0);if(designScale&&designScale!==1){ctx.scale(designScale,designScale);}}网络图不能直接画,先下载成本地路径
岗位主视觉是后台配置的 CDN 图片,小程序 Canvas 对网络图支持不稳定(安卓尤其明显),直接 draw 可能白屏。正确姿势是先用uni.downloadFile拉成本地临时路径再画,下载失败就退回uni.getImageInfo:
/** 网络图预下载为本地临时路径,供小程序 Canvas 稳定绘制 */exportfunctionprefetchImageForCanvas(src){returnnewPromise((resolve,reject)=>{consturl=resolveImageSrc(src);if(!url){resolve('');return;}if(!isNetworkImageSrc(url)){resolve(url);return;}constfallbackGetInfo=()=>{uni.getImageInfo({src:url,success:(info)=>resolve(info.path||url),fail:()=>reject(newError('prefetch fail'))});};uni.downloadFile({url,success:(res)=>{if(res.statusCode===200&&res.tempFilePath){resolve(res.tempFilePath);}else{fallbackGetInfo();}},fail:fallbackGetInfo});});}多张图要串行下载再画,不能一张张异步撒出去——不然第二张还没下完,Canvas 已经开始画了,图的位置全是空的。
高 DPR 屏不糊:逻辑坐标和像素坐标分开
iPhone 的 DPR 是 3,安卓一堆 2.x。如果画布逻辑尺寸直接设成设计稿尺寸,高分屏上必然糊。处理方式是:画布的实际像素尺寸 = 逻辑尺寸 × DPR,然后ctx.scale(dpr, dpr)把坐标系拉回逻辑坐标:
exportfunctioninitPosterCanvas2d(componentInstance,selector,logicalWidth,logicalHeight){constquery=uni.createSelectorQuery().in(componentInstance);// ...constdpr=uni.getSystemInfoSync().pixelRatio||2;constcanvas=res[0].node;canvas.width=logicalWidth*dpr;canvas.height=logicalHeight*dpr;constctx=canvas.getContext('2d');ctx.scale(dpr,dpr);// 之后画布代码全部用逻辑坐标returnctx;}这块是老生常谈但特别容易漏——漏了canvas.width = w * dpr这行,真机预览直接糊成一团,模拟器上还看不出来,非得拿手机测。
踩坑记录:分享到微信后,海报上的字消失了
现象:海报在页面里预览正常,用户点"保存到相册"也正常,唯独"分享给微信好友"时,分享出去的图片上文字全部消失,只剩背景和头像。
排查过程:先怀疑是分享时机问题——分享发生在 canvas 还没画完时,于是给分享加了个 await 等待drawPoster完成,没用。又怀疑是canvasToTempFilePath的时机,加了setTimeout延迟 500ms,还是没用。最后把生成好的图片路径在本地预览打开,发现本地路径的图是完整的,只有通过分享通道出去的图缺字。
定位思路:本地正常、分享异常,问题不在绘制而在分享时的图片来源。看代码发现分享用的是"实时画布再截一次图"的逻辑:分享前临时调用canvasToTempFilePath重新生成,而这次生成时背景图还没绘制完(异步加载没等到)——文字画在还没出现的背景上,或者画布在截图瞬间被清了。
最终解决:分享和保存统一走同一张已生成好的临时文件,先生成一次存到tempFilePath,分享和保存都引用它,绝不在分享瞬间重新截画布:
exportasyncfunctionbuildPosterFile(canvas,fileType='jpg'){consttemp=awaitnewPromise((resolve,reject)=>{uni.canvasToTempFilePath({canvasId:'posterCanvas',success:(res)=>resolve(res.tempFilePath),fail:reject,});});returntemp;}// 分享 / 保存统一使用 buildPosterFile 产出的同一张图顺便说一句,保存相册还要先弹授权,用户拒绝过一次再调用uni.saveImageToPhotosAlbum会直接失败——先uni.getSetting查授权状态,没授权就先uni.openSetting引导用户开,这坑我一开始也没接住。
可直接复用的清单
- 设计稿坐标 + 一次性 scale:多渲染必须 resetPosterTransform,防 transform 累积画面缩小。
- 网络图先
downloadFile再画,多图串行预下载,画布上永远画本地路径。 - 高 DPR 屏:
canvas.width = logicalW * dpr+ctx.scale(dpr, dpr),真机不糊。 - 分享与保存复用同一张 tempFilePath,别在分享瞬间重截画布。
- 保存相册前先查授权,拒绝过就
openSetting引导,别硬调saveImageToPhotosAlbum。
项目源码:https://gitee.com/gzqkl/job-uniapp