编辑
2025-03-11
(灵机一线)前端小项目
00
请注意,本文编写于 35 天前,最后修改于 35 天前,其中某些信息可能已经过时。

目录

Taro小程序canvas绘制海报(并实现自定义分享5:4海报)
最终分享效果
开发思路
将数据显示到canvas上。封装配置化canvas绘制组件
使用组件绘制名片
canvas配置文件
canvas绘图组件传入格式说明
小程序分享以图片形式分享

Taro小程序canvas绘制海报(并实现自定义分享5:4海报)

接到需求开发一款电子名片小程序,前期将技术栈设为了Taro + React,在开发过程中,电子名片前期由原生标签制作,但显示效果差强人意,而且分享名片时,只能通过微信官方的分享将名片网页截取一部分,会将无关内容带到分享页中。因此,我将名片改为canvas实现,分享时将canvas转为临时图片,达到小程序的4:5比例。

最终分享效果

image.png

开发思路

  1. 接受后台返回数据,将数据显示到canvas指定位置。
  2. 分享时将canvas转为临时图片。

将数据显示到canvas上。封装配置化canvas绘制组件

lib文件夹文件可到以下链接获取。

https://github.com/Lsq-class/tuoersuo_game_cal/tree/main/lib

tsx
import Taro from "@tarojs/taro"; import { Canvas } from "@tarojs/components"; ​ import Pen from "./lib/pen"; import Downloader from "./lib/downloader"; import { getAuthSetting, saveImageToPhotosAlbum, equal } from "./lib/util"; import React from "react"; const downloader = new Downloader(); ​ // 最大尝试的绘制次数 const MAX_PAINT_COUNT = 5; ​ interface IProps {  customStyle: string; // canvas自定义样式  palette: object; // painter模板  widthPixels: number; // 像素宽度  dirty: boolean; // 启用脏检查,默认 false  onImgErr: Function; // 图片失败回调  onImgOK: Function; // 图片成功回调 } ​ interface IState {  painterStyle: string; // canvas 宽度+高度样式 } ​ export default class QyPoster extends React.Component<IProps, IState> {  static defaultProps = {    customStyle: "",    palette: {},    widthPixels: 0,    dirty: false,    onImgErr: () => null,    onImgOK: () => null }; ​  canvasId: string = "k-canvas"; // canvas-id ​  filePath: string = ''; // 生成的文件路径 ​  state: IState = {    painterStyle: "" }; ​  canvasWidthInPx: number = 0; // width to px  canvasHeightInPx: number = 0; // height to px  paintCount: number = 0; // 绘制次数  /**   * 判断一个 object 是否为空   * @param {object} object   */  isEmpty(object) {    for (const _i in object) {      return false;   }    return true; } ​  isNeedRefresh = (newVal, oldVal) => {    if (      !newVal ||      this.isEmpty(newVal) ||     (this.props.dirty && equal(newVal, oldVal))   ) {      return false;   }    return true; }; ​  setStringPrototype = (screenK, scale) => {    /**     * 是否支持负数     * @param {Boolean} minus 是否支持负数     */    //@ts-ignore    String.prototype.toPx = function toPx(minus) {      let reg;      if (minus) {        reg = /^-?[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g;     } else {        reg = /^[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g;     }      const results = reg.exec(this);      if (!this || !results) {        console.error(`The size: ${this} is illegal`);        return 0;     }      const unit = results[2];      const value = parseFloat(this); ​      let res = 0;      if (unit === "rpx") {        res = Math.round(value * screenK * (scale || 1));     } else if (unit === "px") {        res = Math.round(value * (scale || 1));     }      return res;   }; };  // 执行绘制  startPaint = (palette) => {    // 如果palette模板为空 则return    if (this.isEmpty(palette)) {      return;   } ​    if (!(Taro.getApp().systemInfo && Taro.getApp().systemInfo.screenWidth)) {      try {        Taro.getApp().systemInfo = Taro.getSystemInfoSync();     } catch (e) {        const error = `Painter get system info failed, ${JSON.stringify(e)}`;        console.error(error);        this.props.onImgErr && this.props.onImgErr(error);        return;     }   }    let screenK = Taro.getApp().systemInfo.screenWidth / 750;    this.setStringPrototype(screenK, 1); ​    this.downloadImages(palette).then((palette: any) => {      const { width, height } = palette; ​      if (!width || !height) {        console.error(          `You should set width and height correctly for painter, width: ${width}, height: ${height}`       );        return;     }      this.canvasWidthInPx = width.toPx();      if (this.props.widthPixels) {        // 如果重新设置过像素宽度,则重新设置比例        this.setStringPrototype(          screenK,          this.props.widthPixels / this.canvasWidthInPx       );        this.canvasWidthInPx = this.props.widthPixels;     } ​      this.canvasHeightInPx = height.toPx();      this.setState({        painterStyle: `width:${this.canvasWidthInPx}px;height:${this.canvasHeightInPx}px;`     });      const ctx = Taro.createCanvasContext(this.canvasId, this.$scope);      const pen = new Pen(ctx, palette);      pen.paint(() => {        this.saveImgToLocal();     });   }); }; ​  // 下载图片  downloadImages = (palette) => {    return new Promise(resolve => {      let preCount = 0;      let completeCount = 0;      const paletteCopy = JSON.parse(JSON.stringify(palette));      if (paletteCopy.background) {        preCount++;        downloader.download(paletteCopy.background).then(          path => {            paletteCopy.background = path;            completeCount++;            if (preCount === completeCount) {              resolve(paletteCopy);           }         },         () => {            completeCount++;            if (preCount === completeCount) {              resolve(paletteCopy);           }         }       );     }      if (paletteCopy.views) {        for (const view of paletteCopy.views) {          if (view && view.type === "image" && view.url) {            preCount++;            downloader.download(view.url).then(              path => {                view.url = path;                Taro.getImageInfo({                  src: view.url,                  //@ts-ignore                  success: res => {                    // 获得一下图片信息,供后续裁减使用                    view.sWidth = res.width;                    view.sHeight = res.height;                 },                  fail: error => {                    // 如果图片坏了,则直接置空,防止坑爹的 canvas 画崩溃了                    view.url = "";                    console.error(                      `getImageInfo ${view.url} failed, ${JSON.stringify(                        error                     )}`                   );                 },                  complete: () => {                    completeCount++;                    if (preCount === completeCount) {                      resolve(paletteCopy);                   }                 }               });             },             () => {                completeCount++;                if (preCount === completeCount) {                  resolve(paletteCopy);               }             }           );         }       }     }      if (preCount === 0) {        resolve(paletteCopy);     }   }); }; ​  // 保存图片到本地  saveImgToLocal = () => {    setTimeout(() => {      Taro.canvasToTempFilePath(       {          canvasId: this.canvasId,          success: res => {            this.getImageInfo(res.tempFilePath);         },          fail: error => {            console.error(              `canvasToTempFilePath failed, ${JSON.stringify(error)}`           );            this.props.onImgErr && this.props.onImgErr(error);         }       },        this.$scope     );   }, 300); }; ​  getImageInfo = filePath => {    Taro.getImageInfo({      src: filePath,      //@ts-ignore      success: infoRes => {        if (this.paintCount > MAX_PAINT_COUNT) {          const error = `The result is always fault, even we tried ${MAX_PAINT_COUNT} times`;          console.error(error);          this.props.onImgErr && this.props.onImgErr(error);          return;       }        // 比例相符时才证明绘制成功,否则进行强制重绘制        if (          Math.abs(           (infoRes.width * this.canvasHeightInPx -              this.canvasWidthInPx * infoRes.height) /           (infoRes.height * this.canvasHeightInPx)         ) < 0.01       ) {          this.filePath = filePath;          this.props.onImgOK && this.props.onImgOK({ path: filePath });       } else {          this.startPaint(this.props.palette);       }        this.paintCount++;     },      fail: error => {        console.error(`getImageInfo failed, ${JSON.stringify(error)}`);        this.props.onImgErr && this.props.onImgErr(error);     }   }); }; ​  // 保存海报到手机相册  saveImage() {    const scope = "scope.writePhotosAlbum";    getAuthSetting(scope).then((res: boolean) => {      if (res) {        // 授权过 直接保存        this.saveImageToPhotos();        return false;     }      // 未授权过 先获取权限      getAuthSetting(scope).then((status: boolean) => {        if (status) {          // 获取保存图片到相册权限成功          this.saveImageToPhotos();          return false;       }        // 用户拒绝授权后的回调 获取权限失败        Taro.showModal({          title: "提示",          content: "若不打开授权,则无法将图片保存在相册中!",          showCancel: true,          cancelText: "暂不授权",          cancelColor: "#000000",          confirmText: "去授权",          confirmColor: "#3CC51F",          success: function (res) {            if (res.confirm) {              // 用户点击去授权              Taro.openSetting({                //调起客户端小程序设置界面,返回用户设置的操作结果。             });           } else {              //           }         }       });     });   }); }  getImportUrl() {    return this.filePath }  saveImageToPhotos = () => {    saveImageToPhotosAlbum(this.filePath)     .then(() => {        // 成功保存图片到本地相册        // 保存失败        Taro.showToast({          title: "保存成功",          icon: "none"       });     })     .catch(() => {        // 保存失败        Taro.showToast({          title: "保存失败",          icon: "none"       });     }); }; ​  componentWillMount() {    this.startPaint(this.props.palette); }  componentWillReceiveProps(nextProps) {    if (nextProps.palette !== this.props.palette) {      this.paintCount = 0;      this.startPaint(nextProps.palette);   } } ​  render() {    return (        <Canvas          canvasId={this.canvasId}          style={`${this.state.painterStyle}${this.props.customStyle}`}        />   ); } } ​
使用组件绘制名片
tsx
import Taro, { Config } from "@tarojs/taro"; import { View, Button } from "@tarojs/components"; ​ import Card from "./card"; import Poster from "../poster"; import "./index.scss"; // eslint-disable-next-line import/first import { Component } from "react"; // eslint-disable-next-line import/first import { deepEqual } from "~/servers/methods/utils"; // 名片内字段定义 export interface CardIProps {    avatarUrl,    name,    jobTitle,    company,    location,    email,    phone,    BackgroundUrl,    background,    isVisibel?: boolean, } interface IState {    imagePath: string;    template: object; } ​ export default class Index extends Component<CardIProps, IState> {    /**     * 指定config的类型声明为: Taro.Config     *     * 由于 typescript 对于 object 类型推导只能推出 Key 的基本类型     * 对于像 navigationBarTextStyle: 'black' 这样的推导出的类型是 string     * 提示和声明 navigationBarTextStyle: 'black' | 'white' 类型冲突, 需要显示声明类型     */    config: Config = {        navigationBarTitleText: "painter"   };    state: IState = {        template: new Card().palette(),        // eslint-disable-next-line react/no-unused-state        imagePath: "",   };    // 图片生成成功回调    onImgOK = e => {        this.setState({            // eslint-disable-next-line react/no-unused-state            imagePath: e.path       });   };    // 图片生成失败回调    onImgErr = error => {        console.log(            "%cerror: ",            "color: MidnightBlue; background: Aquamarine; font-size: 20px;",            error       );   };    public painterRef: Poster | null; ​    // 保存图片到本地相册    getImageUrl() {        if (this.painterRef) {            return this.painterRef.getImportUrl()       }   } ​    componentWillReceiveProps(nextProps: Readonly<CardIProps>, nextContext: any): void {        if (!deepEqual(nextProps, this.props)) {            this.setState({ template: new Card().palette({ ...nextProps }) })       }   } ​ ​    render() {        const { isVisibel } = this.props        return (            <View className="index">                <Poster                  customStyle={isVisibel ? "display: none;" : ""}                  palette={this.state.template}                  onImgOK={this.onImgOK}                  onImgErr={this.onImgErr}                  ref={node => {                        this.painterRef = node                   }}                  dirty={false}                />            </View>       );   } } ​

canvas配置文件

由于需要不同背景,canvas展示位置不同,因此这里对样式做了特殊判断

tsx
import { CardIProps } from "./ExportImg"export default class LastMayday {  palette(cardProps?: CardIProps) {    const isBlueBack = cardProps?.background === 1 || cardProps?.background === 4    const isOneStyle = cardProps?.background === 3 || cardProps?.background === 5 || cardProps?.background === 1 || cardProps?.background === 4    const fontColor = {      color: isBlueBack ? '#FFFFFF' : undefined   };      const infoFontColor = {      color: isBlueBack ? '#d4eafe' : undefined   };    const infoPosition = {      left: isOneStyle ? "40rpx" : '220rpx'   }    const avatarPosition = {      left: isOneStyle ? "" : '40rpx',      right: isOneStyle && '40rpx'   }    const textPositon = {      left: isOneStyle ? "82rpx" : '262rpx' }    return (     {        "width": "702rpx",        "height": "484rpx",        "background": "链接地址",        borderRadius: '24rpx',        border: "4rpx solid #F4F4F4",        backgroundSize: 'cover',        "views": [         {            type: 'text',            text: cardProps?.company ? cardProps?.company : "",            css: [{              top: `44rpx`,              fontWeight: 'bold',              color: '#262832',              fontSize: '32rpx',              left: '40rpx'           }, fontColor],         },         {            "type": "image",            "url": cardProps?.avatarUrl ? cardProps?.avatarUrl : "",            "css": [{              "width": "140rpx",              "height": "140rpx",              "top": "110rpx",              "rotate": "0",              "borderRadius": "70rpx",              "borderWidth": "",              "borderColor": "#000000",              "shadow": "",              "mode": "aspectFill"           }, avatarPosition]         },         {            type: 'text',            text: cardProps?.name ? cardProps?.name : "",            css: [{              top: `121rpx`,              fontWeight: 'bold',              fontSize: '44rpx',              color: '#262832'           }, fontColor, infoPosition],         }, {            type: 'text',            text: cardProps?.jobTitle ? cardProps?.jobTitle : "",            css: [{              top: `195rpx`,              // fontWeight: 'bold',              fontSize: '30rpx',              color: '#262832'           }, fontColor, infoPosition],         },         {            "type": "image",            "url": "链接地址",            "css": [{              "width": "24rpx",              "height": "24rpx",              "top": "291rpx",              "rotate": "0",              "borderWidth": "",              "borderColor": "#000000",              "shadow": "",              "mode": "aspectFill"           }, infoPosition]         },         {            type: 'text',            text: cardProps?.phone ? cardProps?.phone : "",            css: [{              top: `282rpx`,              // left: '242rpx',              fontSize: '28rpx',              color: '#17171A'           }, infoFontColor, textPositon],         }, {            "type": "image",            "url": "链接地址",            "css": [{              "width": "24rpx",              "height": "24rpx",              "top": "339rpx",              "rotate": "0",              "borderWidth": "",              "borderColor": "#000000",              "shadow": "",              "mode": "aspectFill"           }, infoPosition]         },         {            type: 'text',            text: cardProps?.email ? cardProps?.email : "",            css: [{              top: `330rpx`,              // left: '242rpx',              fontSize: '28rpx',              color: '#17171A'           }, infoFontColor, textPositon],         }, {            "type": "image",            "url": "链接地址",            "css": [{              "width": "24rpx",              "height": "24rpx",              "top": "387rpx",              "rotate": "0",              "borderWidth": "",              "borderColor": "#000000",              "shadow": "",              "mode": "aspectFill"           }, infoPosition]         },         {            type: 'text',            text: cardProps?.location ? cardProps?.location : "",            css: [{              top: `378rpx`,              // left: '242rpx',              fontSize: '28rpx',              lineHeight: '38rpx',              color: '#17171A',              width: isOneStyle ? "580rpx" :'420rpx'           }, infoFontColor, textPositon],         },       ]     }   ); } }
canvas绘图组件传入格式说明
tsx
{        "width": "702rpx",  //canvas宽度        "height": "484rpx", // canvas高度        "background": "链接地址", // 背景图        borderRadius: '24rpx', // 边框圆角        border: "4rpx solid #F4F4F4", // 边框        backgroundSize: 'cover', // 背景设置覆盖方式        "views": [  // 配置canvas内子元素         {            type: 'text', // 文字类型            text: "lsq_137", // 文字内容            css: [{    // 给文字设置样式(支持多对象插入)              top: `44rpx`,              fontWeight: 'bold',              color: '#262832',              fontSize: '32rpx',              left: '40rpx'           }],         },         {            "type": "image", // 图片类型            "url": "图片链接地址", // 链接地址            "css": [{ // 样式(支持多对象插入)              "width": "140rpx",              "height": "140rpx",              "top": "110rpx",              "rotate": "0",              "borderRadius": "70rpx",              "borderWidth": "",              "borderColor": "#000000",              "shadow": "",              "mode": "aspectFill"           }, {color: "#fff"}           ]         }       ]

小程序分享以图片形式分享

tsx
import { useEffect, useRef, useState } from "react"; import { View, Image, Canvas } from "@tarojs/components"; import { Icon } from "@antmjs/vantui"; import Taro, {  useShareAppMessage, } from "@tarojs/taro"; ​ ​ function Index() {  const [cardInfo, cardInfoSet] = useState<any>();  let painterRef: any = useRef(null) ​  const onInit = async () => { };  /** * 生成分享5:4的图片 */  const makeCanvas = (imgUrl) => {    return new Promise((resolve, reject) => {      // 获取图片信息,小程序下获取网络图片信息需先配置download域名白名单才能生效      const sysInfo = Taro.getSystemInfoSync()      Taro.getImageInfo({        src: imgUrl,        success: (imgInfo) => {          // 获取设备像素比 适配图片          const pixelRatio = sysInfo.pixelRatio          let ctx = Taro.createCanvasContext('canvas')          let canvasW = imgInfo.width / pixelRatio          let canvasH = ((imgInfo.width * 4) / 5) / pixelRatio          // 把比例设置为 宽比高 5:4          // canvasW = (imgInfo.height * 5) / 4          // 为画框设置背景色,注意要放在画图前,图会覆盖在背景色上          ctx.fillStyle = "#fff";          ctx.fillRect(0, 0, canvasW, canvasH)          ctx.drawImage(            imgInfo.path,            0,           (canvasH - imgInfo.height / pixelRatio) / 2,            canvasW,            imgInfo.height / pixelRatio         )          // } ​          ctx.draw(false, () => {            setTimeout(() => {              Taro.canvasToTempFilePath({                width: canvasW,                height: canvasH,                // destWidth: 750, // 标准的iphone6尺寸的两倍,生成高清图                // destHeight: 600,                canvasId: "canvas",                fileType: "png", // 注意jpg默认背景为透明                success: (res) => {                  resolve(res.tempFilePath)               },                fail: (err) => {                  reject(err)               }             })           }, 0)         })       },        fail: (err) => {          reject(err)       }     })   }) }  useShareAppMessage((res) => {    if (res.from === "button") {      // 来自页面内转发按钮      console.log(res.target);   }    let imgUrl = ""    if (painterRef) {      imgUrl = painterRef?.getImageUrl()   }    return new Promise((resolve, reject) => {      return makeCanvas(imgUrl).then(imgPath => {        resolve({          title: "请保存!",          path: `/pages/mineHome/index?open_code=${cardInfo?.open_code}&visit_from=1`,          imageUrl: imgPath       })     }).catch(err => {        resolve({          title: "请保存!",          path: `/pages/mineHome/index?open_code=${cardInfo?.open_code}&visit_from=1`,          // imageUrl: '处理失败后展示的图片,可以用原图shareMessage.imageUrl'       })     })   }) });  const cardInfoProps = {    name: cardInfo?.name,    company: cardInfo?.company,    jobTitle: cardInfo?.job?.[0],    phone: cardInfo?.cellphone,    email: cardInfo?.email?.[0],    location: cardInfo?.address,    avatarUrl:      cardInfo?.avatar,    BackgroundUrl:      cardInfo?.background_picture,    background: cardInfo?.background }  // 获取当前设备屏幕宽度  const sysInfo = Taro.getSystemInfoSync()  const canvasHeight = (sysInfo?.screenWidth * 4) / 5 ​  return (    <View className="page">      <View id="export-element">        <ExportImg ref={node => {          painterRef = node       }}          isVisibel={isVisit}         {...cardInfoProps}        />      </View>     {* 导出图片使用到的canvas *}       <Canvas        canvasId={"canvas"}        style={`position: absolute; top: -1000px; left: -1000px; width: ${sysInfo?.screenWidth}px; height: ${canvasHeight}px;`}      />    </View> ); } ​ export default Index;

纯js开发微信小游戏外挂(开局托儿所)

本文作者:lsq_137

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!