微信小程序分享海报开发总结

1,827

最近开发一个项目需要在微信小程序里合成海报图片,中间遇到过不少问题,记录一下以便日后查阅

效果展示

!

绘制流程简述

  • 请求海报数据
  • 加载图片
  • 初始化canvas
  • 绘制文字
  • 绘制区块
  • 微信小程序码获取
  • 绘制图片
  • 导出图片内容
  • 小程序码解析

绘制关键点

Canvas 初始化

正常情况下,1px在屏幕上就显示1px,但是在高清屏幕上,这个情况就不一样了,以iPhone4S为例,它的devicePixelRatio为2,将看到100px的逻辑值等于200px的设备值。在元素的逻辑像素宽度上创建图像,当它们被绘制出来时,它们会被设备像素按比例放大,并且会变得模糊。解决这个问题的方法是创建按devicePixelRatio缩放的图像,然后使用CSS按相同的比例缩小.

image.png

canvas 的width 和 height控制元素位图的属性,如不设置默认为width=300,height=150

// 获取canvas
function getCanvas() {
    return new Promise((resolve, reject) => {
        Taro.createSelectorQuery()
            .select('#myCanvas')
            .fields({
                node: true,
                size: true
            })
            .exec(res => {
                res ? resolve(res[0]) : reject(null);
            });
    });
}
/**
 * 初始化canvas并返回 ctx
 *
 * @param {Object} res
 * @returns {*}
 */
function setContext(res: Object): any {
    let {canvas, width, height} = res;
    const ctx = canvas.getContext('2d');
   
    const dpr = Taro.getSystemInfoSync().pixelRatio;
    // 防止重复放大处理
    if (canvas.width !== width * dpr) {
       // 设置画布绘画尺寸 = CSS尺寸 * 设备像素比率。
        canvas.width = width * dpr;
        canvas.height = height * dpr;
       // 通过dpr缩放所有绘图操作
        ctx.scale(dpr, dpr);
    }

    return ctx;
}

let {node: canvas, width, height} = await getCanvas();
let ctx = setContext({canvas, width, height});
// 绘制前先清除,以防被原有图案影响
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#ffffff';

canvas标签,微信对Canvas做了同层渲染的优化,特别注意不要将canvas标签放到子组件,必须放到page页,否则无法获取的canvas对象

<Canvas type='2d' id='myCanvas' style='width: 414px; height: 736px;' />

微信小程序码获取

我们使用的是接口B,需要注意的点

  • scene参数只接收32的长度
  • 小程序未发布时,page参数必须为空,scene=xxxx,才可以获取到二维码
  • 接口返回的数据需要为base64格式方便canvas绘制时使用

图片绘制前获取

Taro.getImageInfo({
    src: imgUrl
}).than(res => {
    let { path } = res;
    let imgtag = canvas.createImage();
    imgtag.src = path;
    imgtag.onload = res => {
        ctx.save();
        ctx.drawImage(imgtag, 26, 615, 2 * 27, 2 * 27);
        ctx.restore();
    };
});

图片裁剪

/**
 * 画圆
 *
 * @export
 * @param {IDrawRound} options
 * @example
 * drawRound({
 *     ctx,
 *     r: 19,
 *     x: 22,
 *     y: 15,
 *     next() {
 *         ctx.fillStyle = '#F5F5F5';
 *         ctx.fill();
 *     }
 * });
 */
export function drawRound(options: IDrawRound) {
    let { ctx, r, x, y, next } = options;
    ctx.save(); // 保存之前的
    let cx = x + r; // 圆弧坐标x
    let cy = y + r; // 圆弧坐标 y
    ctx.arc(cx, cy, r, 0, 2 * Math.PI);
    if (typeof next === 'function') {
        next();
    }
    ctx.restore(); // 返回上一状态
}
// 绘制圆形头像
drawRound({
    ctx,
    r: 27,
    x: 26,
    y: 615,
    next() {
        ctx.clip();
        ctx.drawImage(imgtag, 26, 615, 2 * 27, 2 * 27); // 画头像
    }
});

绘制圆角矩形

/**
 * 绘制圆角矩形,需自行填充颜色
 *
 * @export
 * @param {IDrawRound} options
 * 
 * @example
 * drawRoundRect({
 *     ctx,
 *     r: 13,
 *     x: 26,
 *     y: 80,
 *     w: 361,
 *     h: 361,
 *     next() {
 *         ctx.clip();
 *         ctx.drawImage(imgtag, 26, 80, 361, 361);
 *     }
 * });
 */
export function drawRoundRect(options: IDrawRoundRect) {
    let {ctx, x, y, w, h, r, next} = options;
    ctx.save();
    if (w < 2 * r) {
        r = w / 2;
    }
    if (h < 2 * r) {
        r = h / 2;
    }
    ctx.beginPath();
    ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 3 / 2);
    ctx.lineTo(w - r + x, y);
    ctx.arc(w - r + x, r + y, r, Math.PI * 3 / 2, Math.PI * 2);
    ctx.lineTo(w + x, h + y - r);
    ctx.arc(w - r + x, h - r + y, r, 0, Math.PI * 1 / 2);
    ctx.lineTo(r + x, h + y);
    ctx.arc(r + x, h - r + y, r, Math.PI * 1 / 2, Math.PI);
    ctx.closePath();
    if (typeof next === 'function') {
        next();
    }
    ctx.restore(); // 返回上一状态
}
// 底部粉色圆角矩形
drawRoundRect({
    ctx,
    r: 15,
    x: 26,
    y: 687,
    w: 147,
    h: 23,
    next() {
        // 渐变填充
        var grd = ctx.createLinearGradient(0, 0, 200, 0);
        grd.addColorStop(0, '#E828FF');
        grd.addColorStop(0.7, '#FF1D74');
        grd.addColorStop(1, '#FFB050');
        ctx.fillStyle = grd;
        ctx.fill();
        ctx.strokeStyle = grd;
        ctx.stroke();
    }
});

绘制文字

/**
 * 绘制文字
 *
 * @export
 * @param {*} ctx canvas的 2d 对象
 * @param {string} t 绘制的文字
 * @param {number} x
 * @param {number} y
 * @param {number} w 文字宽度
 * @param {number} [l=30] 行高
 * @param {number} [limit=2] 行数
 */
export function drawText(ctx: any, t: string, x: number, y: number, w: number, l:number = 30, limit:number = 2) {
    // 参数说明
    // ctx:canvas的 2d 对象,t:绘制的文字,x,y:文字坐标,w:文字最大宽度
    let chr = t.split('');
    let temp = '';
    let row = [];
    let limitIndex = 0;
    let wordsNum = 0;

    for (let a = 0; a < chr.length; a++) {
        wordsNum++;
        if (ctx.measureText(temp).width < w && ctx.measureText(temp + (chr[a])).width <= w) {
            temp += chr[a];
        }
        else {
            // 行数+1
            limitIndex++;
            if (limitIndex < limit) {
                row.push(temp);
                temp = chr[a];
            }
            else {
                break;
            }
        }
    }
    // 最后一行超出最大字数
    if (limitIndex === limit && chr.length > wordsNum) {
        temp = temp.substring(0, temp.length - 1) + '...';
    }

    row.push(temp);
    for (let b = 0; b < row.length; b++) {
        ctx.fillText(row[b], x, y + (b + 1) * l);// 每行字体y坐标间隔30
    }
};

绘制完成导出图片

export async function draw() {
    let { node: canvas, width, height } = await getCanvas();
    let ctx = setContext({ canvas, width, height });
    // 绘制前先清除,以防原有图案被影响
    ctx.clearRect(0, 0, width, height);
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, 414, 736);
    // 底部灰色块
    ctx.fillStyle = '#f5f5f5';
    ctx.fillRect(0, 596, 414, 140);
    // 底部粉色圆角矩形
    drawRoundRect({
        ctx,
        r: 15,
        x: 26,
        y: 687,
        w: 147,
        h: 23,
        next() {
            // 渐变填充
            var grd = ctx.createLinearGradient(0, 0, 200, 0);
            grd.addColorStop(0, '#E828FF');
            grd.addColorStop(0.7, '#FF1D74');
            grd.addColorStop(1, '#FFB050');
            ctx.fillStyle = grd;
            ctx.fill();
            ctx.strokeStyle = grd;
            ctx.stroke();
        }
    });
    // 推荐人
    ctx.fillStyle = '#666666';
    ctx.font = '13px/1.2 PingFangSC-Regular';
    ctx.fillText(`${nickName}`, 93, 635);

    // 推荐语
    ctx.fillStyle = '#222222';
    ctx.font = '15px/1.2 PingFangSC-Regular';
    ctx.fillText(`${recommendation || '推荐你一个超值的变美项目'}`, 93, 660);

    // 底部文字
    ctx.fillStyle = '#ffffff';
    ctx.font = '13px/1.2 PingFangSC-Regular';
    ctx.fillText('长按识别小程序购物', 43, 703);
  	// 省略部分图片绘制代码
    // 获取图片临时路径
    let { tempFilePath } = await Taro.canvasToTempFilePath({ canvas } as any);
    return Promise.resolve(tempFilePath);
};

以上导出的图片地址可以直接给img标签赋值

图片保存到相册

const res = await Taro.saveImageToPhotosAlbum({
    filePath: tempFilePath
});
if (res.errMsg === 'saveImageToPhotosAlbum:ok') {
    Taro.showToast({
        title: '保存图片成功',
        icon: 'success',
        duration: 2000
    });
}

小程序码解析

小程序码的解析后,在page里收到的参数在scene里

componentWillMount() {
  let params = this.$router.params;
  // 通过小程序码进入,参数在scene里
  if (params.scene) {
    let scene = decodeURIComponent(params.scene);
    let {id} = query2parmas(scene);
    this.setState({
      goodsId: id
    });
  }
  // 通过分享卡片进入参数直接在params里 
  if (params.id) {
    this.setState({
      goodsId: params.id
    });
  }
}