uniapp、小程序中使用canvas绘制海报,长按可保存

717 阅读3分钟

uniapp、小程序中使用canvas绘制海报,长按可保存。 最近接受到一个海报分享的功能,因为后台还没有完成,所以二维码那块暂时用头像替代。

<template>
  <view class="vote-pull">
    <canvas
      class="vote-canvas"
      id="vote-canvas"
      canvas-id="vote-canvas"
      :style="{ display: canvasImg ? 'none' : 'block' }"
    ></canvas>

    <image :show-menu-by-longpress="true" :src="canvasImg" class="canvas-img" />
  </view>
</template>
<script>
var util = require("../../../utils/util.js");
export default {
  data() {
    return {
      idolInfo: {},
      votes: 0,
      screenWidth: 0,
      screenHeight: 0,
      commonWidth: 0,
      commonPadding: 0,
      benchWidth: 390,
      canvasImg: ''
    };
  },
  onLoad() {
    uni.setNavigationBarTitle({
      title: "应援拉票",
    });
    this.loadData();
    this.loadIdolInfo();
    this.createVoteCanvas();
  },
  methods: {
    loadData() {
      const systemInfo = uni.getSystemInfoSync();
      this.screenWidth = systemInfo.screenWidth;
      this.screenHeight = systemInfo.windowHeight;
      if (systemInfo.windowHeight <= 650) {
        this.screenHeight = this.screenHeight * 1.25;
      } else if (systemInfo.windowHeight <= 610) {
        this.screenHeight = this.screenHeight * 1.3;
      }
      this.commonWidth = parseInt(this.screenWidth * 0.84);
      this.commonPadding = parseInt(this.screenWidth * 0.08);
    },
    loadIdolInfo() {
      this.idolInfo = uni.getStorageSync("idolInfo");

      util
        .getScaleImageSize(this.idolInfo.img, 0.8)
        .then((res) => {
          this.idolInfo.height = res.height;
        })
        .catch((err) => {
          console.log(err);
        });
    },
    loadCanvasImg() {
      const query = uni.createSelectorQuery();
				query.select('#vote-canvas').node().exec((res) => {
					const canvasNode = res[0].node;

					// 将canvas内容转换为临时文件路径
					uni.canvasToTempFilePath({
					canvasId: 'vote-canvas',
					success: (res) => {
						this.canvasImg = res.tempFilePath;
            uni.hideLoading();
					},
					fail: (err) => {
						uni.hideLoading();
						console.log('转换失败', err);
					}
					}, canvasNode);
				});
    },
    async createVoteCanvas() {
      uni.showLoading();
      const w = this.getSw;
      const ctx = uni.createCanvasContext("vote-canvas", this);

      ctx.save();
      ctx.setFillStyle("#C5EBF1");
      ctx.fillRect(0, 0, this.screenWidth, this.screenHeight);
      ctx.restore();

      ctx.save();
      this.roundRect(
        ctx,
        this.commonPadding + w(30),
        this.commonPadding - w(15),
        this.commonWidth,
        this.commonWidth + w(105),
        32
      );
      ctx.rotate((6 * Math.PI) / 180);
      ctx.setFillStyle("#10BBCE");
      ctx.fill();
      ctx.restore();

      const main = {
        width: this.commonWidth,
        height: this.commonWidth + w(100),
        left: this.commonPadding,
        top: this.commonPadding + w(10),
        radius: 32,
      };
      this.roundRect(
        ctx,
        main.left,
        main.top,
        main.width,
        main.height,
        main.radius
      );
      ctx.setFillStyle("#FFFFFF");
      ctx.fill();

      ctx.font = "normal bold 16px Arial,sans-serif";
      ctx.fillStyle = "#3D3D3D";
      ctx.fillText(
        `编号: ${this.idolInfo.number}`,
        this.commonPadding + w(20),
        this.commonPadding + w(46)
      );

      const circle = {
        width: util.toDecimal(this.commonWidth * 0.68),
        height: util.toDecimal(this.commonWidth * 0.68),
        top: util.toDecimal(
          this.commonPadding + w(10) + this.commonWidth * 0.16
        ),
        left: util.toDecimal(this.commonPadding + this.commonWidth * 0.16),
      };
      ctx.save();
      this.roundRect(
        ctx,
        circle.left,
        circle.top,
        circle.width,
        circle.height,
        circle.width / 2
      );
      ctx.strokeStyle = "#0CBCCF";
      ctx.lineWidth = 2;
      ctx.stroke();
      ctx.restore();

      const img = await this.getImageInfo(this.idolInfo.img);
      const image = {
        src: img,
        width: util.toDecimal(this.commonWidth * 0.62),
        height: util.toDecimal(this.commonWidth * 0.62),
        top: util.toDecimal(
          this.commonPadding + w(10) + this.commonWidth * 0.19
        ),
        left: util.toDecimal(this.commonPadding + this.commonWidth * 0.19),
      };
      this.roundRectImg(
        ctx,
        image.left,
        image.top,
        image.width,
        image.height,
        image.width / 2,
        image.src
      );

      ctx.font = "normal bold 28px Arial,sans-serif ";
      ctx.fillStyle = "#333333";
      ctx.textAlign = "center";
      const name = {
        left: this.screenWidth / 2,
        top: circle.top + circle.height + w(52),
      };
      ctx.fillText(this.idolInfo.name, name.left, name.top);

      ctx.font = "16px Arial";
      ctx.fillStyle = "#4A4A4A";
      const declaration = {
        width: circle.width,
        left: name.left,
        top: name.top + w(42),
      };
      this.getWarpText(ctx, this.idolInfo.declaration, declaration.left, declaration.top, declaration.width, w(20), 3);

      const congratulate = {
        width: this.commonWidth,
        height: w(86),
        left: this.commonPadding,
        top: main.top + main.height + w(38),
        radius: w(18)
      }
      this.roundRect(ctx, congratulate.left, congratulate.top, congratulate.width, congratulate.height, congratulate.radius);
      ctx.setFillStyle("#10BBCE");
      ctx.fill()

      ctx.font = "normal bold 72px Arial,sans-serif";
      ctx.fillStyle = "#FFFFFF";
      const symbolTop = {
        left: congratulate.left + w(72),
        top: congratulate.top + w(62),
      };
      ctx.fillText('“', symbolTop.left, symbolTop.top);

      ctx.font = "18px Arial";
      ctx.fillStyle = "#FFFFFF";
      ctx.textAlign = "center";
      const textTop = {
        left: name.left,
        top: symbolTop.top - w(32),
      };
      ctx.fillText('你一票,我一票,', textTop.left, textTop.top);

      ctx.font = "18px Arial";
      ctx.fillStyle = "#FFFFFF";
      ctx.textAlign = "center";
      const textBottom = {
        left: name.left,
        top: textTop.top + w(32),
      };
      ctx.fillText(`${this.idolInfo.name.slice(-2)}明天就出道。`, textBottom.left, textBottom.top);

      ctx.font = "normal bold 72px Arial,sans-serif ";
      ctx.fillStyle = "#FFFFFF";
      const symbolBottom = {
        left: textBottom.left + w(84),
        top: textBottom.top + w(36),
      };
      ctx.fillText('”', symbolBottom.left, symbolBottom.top);

      const longpressTip = {
        left: this.commonPadding,
        top: congratulate.top + congratulate.height + w(52)
      }
      ctx.font = "16px Arial";
      ctx.fillStyle = "#687173";
      ctx.textAlign = "left";
      ctx.fillText('快来长按识别二维码', longpressTip.left, longpressTip.top);

      const vote = {
        width: util.toDecimal(this.commonWidth * .54),
        height: w(40),
        left: this.commonPadding,
        top: longpressTip.top + w(24)
      }

      ctx.save();
      ctx.setFillStyle("#10BBCE");
      ctx.fillRect(vote.left, vote.top, vote.width, vote.height);
      ctx.restore();

      const voteTxt = {
        left: vote.left + w(24),
        top: vote.top + w(26)
      }
      ctx.font = "normal bold 18px Arial,sans-serif ";
      ctx.fillStyle = "#FFFFFF";
      ctx.fillText('请为TA投票  ➤', voteTxt.left, voteTxt.top);

      const qrcodeImg = await this.getImageInfo(this.idolInfo.img);
      const qrwidth = util.toDecimal(this.commonWidth - vote.width - this.commonWidth * .12)
      const qrcode = {
        width: qrwidth,
        height: qrwidth,
        left: util.toDecimal(this.commonPadding + vote.width + this.commonWidth * .12),
        top: longpressTip.top - w(30)
      }
      this.roundRectImg(ctx, qrcode.left, qrcode.top, qrcode.width, qrcode.height, util.toDecimal(qrcode.width / 2), qrcodeImg)

      ctx.draw();

      this.loadCanvasImg();
    },
    getWarpText(canvas, text, x, y, maxWidth, lineHeight, maxLine) {
      // 对入参的类型进行检测
      if (
        typeof text != "string" ||
        typeof x != "number" ||
        typeof y != "number" ||
        typeof lineHeight != "number"
      ) {
        throw new Error("参数传入出错");
      }
      //如果最大宽度未定义 默认为canvas宽度
      if (typeof maxWidth == "undefined") {
        maxWidth = canvas && canvas.width;
      }
      if (typeof lineHeight == "undefined") {
        lineHeight =
          (canvas.canvas &&
            parseInt(window.getComputedStyle(canvas.canvas).lineHeight)) ||
          parseInt(window.getComputedStyle(document.body).lineHeight);
      }
      let arrText = text.split("");
      let line = "";
      let lines = [];
      let lastLine = "";
      let ellipsis = canvas.measureText("...");
      let ellipsisWidth = ellipsis.width;
      for (let n = 0; n < arrText.length; n++) {
        //每个循环累加字符
        let testLine = line + arrText[n];
        //检测累加字符 获取累加字符的高度和宽度
        let metrics = canvas.measureText(testLine);
        let testWidth = metrics.width;
        let lineWidth = canvas.measureText(line).width;

        // 如果当前添加行是最后一行 则替换最后一个字符为"..." 判断长度是否需要删去最后一个字符
        if (
          maxLine &&
          maxLine - 1 === lines.length &&
          lineWidth + ellipsisWidth > maxWidth
        ) {
          line = line.slice(0, line.length - 1) + "...";
          testWidth = lineWidth + ellipsisWidth;
        }
        //如果累加字符的宽度大于定义的绘制文本最大宽度 则绘制累加字符的文本 并且设置换行间距再次进行绘制
        if (testWidth > maxWidth && n > 0) {
          lastLine = line;
          lines.push({
            text: line,
            x: x,
            y: y,
          });
          if (maxLine && maxLine <= lines.length) {
            break;
          }
          line = arrText[n];
          y += lineHeight;
        } else {
          line = testLine;
        }
      }
      if (lastLine !== line) {
        lines.push({
          text: line,
          x: x,
          y: y,
        });
      }
      for (let i = 0; i < lines.length; i++) {
        const item = lines[i];
        canvas.fillText(item.text, item.x, item.y);
      }
      return lines;
    },
    getSw(num) {
      const rate = util.toDecimal(num / this.benchWidth);
      return util.toDecimal(rate * this.screenWidth);
    },
    roundRect(ctx, x, y, width, height, radius) {
      ctx.beginPath();
      ctx.moveTo(x + radius, y);
      ctx.arcTo(x + width, y, x + width, y + height, radius);
      ctx.arcTo(x + width, y + height, x, y + height, radius);
      ctx.arcTo(x, y + height, x, y, radius);
      ctx.arcTo(x, y, x + width, y, radius);
      ctx.closePath();
    },
    roundRectImg(ctx, x, y, width, height, radius, img) {
      ctx.save();
      this.roundRect(ctx, x, y, width, height, radius);
      ctx.clip();
      ctx.drawImage(img, x, y, width, height);
      ctx.restore();
    },
    getImageInfo(path) {
      return new Promise((resolve, reject) => {
        uni.getImageInfo({
          src: path,
          success: (res) => {
            resolve(res.path);
          },
          fail: (err) => {
            reject(err);
          },
        });
      });
    },
  },
};
</script>
<style scoped>
.vote-pull,
.vote-canvas,
.canvas-img {
  width: 100%;
  height: 100%;
}

@media screen and (max-height: 610px) {
  .vote-pull {
    height: 130vh;
  }
}

@media screen and (max-height: 650px) {
  .vote-pull {
    height: 125vh;
  }
}

.share-download {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding-bottom: 42rpx;
}

.share-download image {
  width: 92rpx;
  height: 92rpx;
  margin-bottom: 22rpx
}

.share-download span {
  font-size: 24rpx;
}
</style>