又又又摆弄了一下表情包制作工具

276 阅读2分钟

先上个图

微信图片_20230216163606.jpg

我的小程序 完美工具 前前后后搞了2年多了,期间搞了很多乱七八糟的功能,各种拼凑。什么时钟啦,头像制作啦、图片水印啦、昵称装扮啦、红眼等等一大堆。

中间也不知道啥原因,有几天访问量暴增,导致调用的腾讯AI人脸五官识别用量暴增,支出费用蹭蹭上涨,后来云开发调整收费策略,就把所有与腾讯云相关的应用全部下了。

前几天重新捣鼓了一下表情包制作工具--斗图高手,优化了界面,优化了一下代码,现在终于重新发布了。

主要用了小程序的canvas接口,canvas本来有很多不错的封装框架,比如fabricjs、konvasjs,可惜因为小程序无DOM操作,不能直接使用,只好自己参考别人的自己简单写了一下

核心类

const dragLayer = function ({
  centered = true,
  w,
  h,
  scale = 1,
  x = 30,
  y = 30,
  type,
  rotate = 0,
  locked = false,
  selected = false,

  text,
  fontSize = 30,
  fontWeight = "bold",
  fontStyle = 'normal',
  color = 'rgba(255,255,255,1)',
  bgcolor = '',
  strokeTextColor = "",
  shadowColor = "",

  url = null,
  img,
  loaded = false,
}) {


  // w = w * factor
  // h = h * factor
  if (type === 'text') {
    ctx.font = `${fontSize}px  ${this.fontWeight} Arial`;
    w = ctx.measureText(text).width + 30;
    h = fontSize + 30;
  }

  if (centered) {
    x = (canvasWidth - w) / 2
    y = (canvasHeight - h) / 2
  }
  this.centerX = x + w / 2;
  this.centerY = y + h / 2;

  this.w = w;
  this.h = h;

  this.scale = scale;

  this.x = x;
  this.y = y;

  // 4个顶点坐标
  this.square = [
    [this.x, this.y],
    [this.x + this.w, this.y],
    [this.x + this.w, this.y + this.h],
    [this.x, this.y + this.h]
  ];
  this.type = type;
  this.centered = centered
  this.rotate = rotate;
  this.locked = locked;
  this.selected = selected;

  this.url = url;
  this.img = img
  this.loaded = loaded;

  this.text = text;
  this.fontSize = fontSize;
  this.fontWeight = fontWeight;
  this.fontStyle = fontStyle;
  this.color = color;
  this.bgcolor = bgcolor;
  this.shadowColor = shadowColor;
  this.strokeTextColor = strokeTextColor;
};

dragLayer.prototype = {
  /**
   * 绘制元素
   */
  paint() {


    // 由于measureText获取文字宽度依赖于样式,所以如果是文字元素需要先设置样式
    if (this.type === 'text') {
      ctx.font = `${this.fontStyle} ${this.fontWeight}  ${this.fontSize}px Arial, sans-serif`;
      ctx.fillStyle = this.color;
      ctx.textAlign = 'center'
      ctx.textBaseline = 'middle'
      // ctx.shadowColor = this.shadowColor; //设定阴影颜色效果


      let lines = this.text.split('\n')
      let temp = lines.sort(function (a, b) {
        return a.length - b.length;
      });
      this.w = ctx.measureText(temp[temp.length - 1]).width + 30;
      this.h = this.fontSize * temp.length + 30;


      // 字体区域中心点不变,左上角位移
      this.x = this.centerX - this.w / 2;
      this.y = this.centerY - this.h / 2;
    }
    // 旋转元素
    ctx.translate(this.centerX, this.centerY);
    ctx.rotate(this.rotate * Math.PI / 180);
    ctx.translate(-this.centerX, -this.centerY);

    // 渲染元素
    switch (this.type) {
      case 'text':
        ctx.save()
        ctx.shadowBlur = this.fontSize * 0.2; //设定阴影的模糊程度 默认0
        let lines = this.text.split('\n')
        // console.log(lines)
        ctx.translate(0, -(this.h - 30 - this.fontSize) / 2);
        lines.forEach((item, index) => {
          ctx.fillText(item, this.centerX, this.centerY + this.fontSize * (index));
        })
        ctx.restore()
        this._drawSelect()
        break;
      case 'bg':
        ctx.fillStyle = this.color;
        ctx.fillRect(0, 0, canvasWidth, canvasHeight);
        break
      default:
        if (this.loaded) {
          ctx.drawImage(this.img, this.x, this.y, this.w, this.h);
          this._drawSelect()
        } else {
          loadImage(this.url).then(res => {
            this.img = res
            this.loaded = true
            ctx.drawImage(this.img, this.x, this.y, this.w, this.h);
            this._drawSelect()
          })
        }
        break
    }




  },
  _drawSelect() {

    // 如果是选中状态,绘制选择虚线框,和缩放图标、删除图标
    const handleRadius = HANDLE_RADIUS
    if (this.selected) {

      ctx.lineWidth = STROKE_WIDTH;
      if (this.locked) {
        ctx.strokeStyle = '#ccc';
        ctx.setLineDash([3, 3]);
        //

        ctx.beginPath();
        ctx.arc(this.x + this.w, this.y, handleRadius, 0, Math.PI * 2);
        ctx.stroke();
        ctx.closePath();
        ctx.drawImage(imgObj.LOCK_ICON, this.x + this.w - handleRadius, this.y - handleRadius, handleRadius * 2, handleRadius * 2);
      } else {
        ctx.strokeStyle = STROKE_COLOR;
      }
      ctx.strokeRect(this.x, this.y, this.w, this.h);
      ctx.fillStyle = '#fff';


      if (!this.locked) {
        //画del
        ctx.drawImage(imgObj.DELETE_ICON, this.x + this.w - handleRadius, this.y - handleRadius, handleRadius * 2, handleRadius * 2);
        ctx.beginPath();
        ctx.arc(this.x + this.w, this.y, handleRadius, 0, Math.PI * 2);
        ctx.stroke();
        ctx.closePath();

        //缩放
        ctx.drawImage(imgObj.SCALE_ICON, this.x - handleRadius, this.y - handleRadius, handleRadius * 2, handleRadius * 2);
        ctx.beginPath();
        ctx.arc(this.x, this.y, handleRadius, 0, Math.PI * 2);
        // ctx.fill()
        ctx.stroke();
        ctx.closePath();
        //缩放
        ctx.drawImage(imgObj.SCALE_ICON, this.x + this.w - handleRadius, this.y + this.h - handleRadius, handleRadius * 2, handleRadius * 2);
        ctx.beginPath();
        ctx.arc(this.x + this.w, this.y + this.h, handleRadius, 0, Math.PI * 2);
        ctx.stroke();
        ctx.closePath();
        //旋转
        ctx.drawImage(imgObj.ROTATE_ICON, this.x - handleRadius, this.y + this.h - handleRadius, handleRadius * 2, handleRadius * 2);
        ctx.beginPath();
        ctx.arc(this.x, this.y + this.h, handleRadius, 0, Math.PI * 2);
        ctx.stroke();
        ctx.closePath();
      }
    }
  },
  /**
   * 判断点击的坐标落在哪个区域
   * @param {*} x 点击的坐标
   * @param {*} y 点击的坐标
   */
  isInLayer(x, y) {

    //leftTop
    const leftTopCenter = this._rotatePoint(this.x, this.y, this.centerX, this.centerY, this.rotate);
    const leftTopX = leftTopCenter[0] - BUTTON_WIDTH / 2;
    const leftTopY = leftTopCenter[1] - BUTTON_HEIGHT / 2;
    const leftTopArea = [
      [leftTopX, leftTopY],
      [leftTopX + BUTTON_WIDTH, leftTopY],
      [leftTopX + BUTTON_WIDTH, leftTopY + BUTTON_HEIGHT],
      [leftTopX, leftTopY + BUTTON_HEIGHT]
    ]
    //rightTop
    const rightTopCenter = this._rotatePoint(this.x + this.w, this.y, this.centerX, this.centerY, this.rotate);
    const rightTopX = rightTopCenter[0] - BUTTON_WIDTH / 2;
    const rightTopY = rightTopCenter[1] - BUTTON_HEIGHT / 2;
    const rightTopArea = [
      [rightTopX, rightTopY],
      [rightTopX + BUTTON_WIDTH, rightTopY],
      [rightTopX + BUTTON_WIDTH, rightTopY + BUTTON_HEIGHT],
      [rightTopX, rightTopY + BUTTON_HEIGHT]
    ]
    //rightBottom
    const rightBottomCenter = this._rotatePoint(this.x + this.w, this.y + this.h, this.centerX, this.centerY, this.rotate);
    const rightBottomX = rightBottomCenter[0] - BUTTON_WIDTH / 2;
    const rightBottomY = rightBottomCenter[1] - BUTTON_HEIGHT / 2;
    const rightBottomArea = [
      [rightBottomX, rightBottomY],
      [rightBottomX + BUTTON_WIDTH, rightBottomY],
      [rightBottomX + BUTTON_WIDTH, rightBottomY + BUTTON_HEIGHT],
      [rightBottomX, rightBottomY + BUTTON_HEIGHT]
    ]
    //leftBottom
    const leftBottomCenter = this._rotatePoint(this.x, this.y + this.h, this.centerX, this.centerY, this.rotate);
    const leftBottomX = leftBottomCenter[0] - BUTTON_WIDTH / 2;
    const leftBottomY = leftBottomCenter[1] - BUTTON_HEIGHT / 2;
    const leftBottomArea = [
      [leftBottomX, leftBottomY],
      [leftBottomX + BUTTON_WIDTH, leftBottomY],
      [leftBottomX + BUTTON_WIDTH, leftBottomY + BUTTON_HEIGHT],
      [leftBottomX, leftBottomY + BUTTON_HEIGHT]
    ]

    //判断位置区域
    if (this.insidePolygon(leftBottomArea, [x, y]))
      return 'rotate'
    else if (this.insidePolygon(rightTopArea, [x, y]))
      return 'del'
    else if (this.insidePolygon(leftTopArea, [x, y]) || this.insidePolygon(rightBottomArea, [x, y]))
      return 'scale';
    else if (this.insidePolygon(this.square, [x, y]))
      return 'move';
    // 不在选择区域里面
    return false;
  },
  /**
   *  判断一个点是否在多边形内部
   *  @param points 多边形坐标集合
   *  @param testPoint 测试点坐标
   *  返回true为真,false为假
   *  */
  insidePolygon(points, testPoint) {
    let x = testPoint[0] * factor,
      y = testPoint[1] * factor;
    let inside = false;
    for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
      let xi = points[i][0],
        yi = points[i][1];
      let xj = points[j][0],
        yj = points[j][1];

      let intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
      if (intersect) inside = !inside;
    }
    return inside;
  },
  /**
   * 计算旋转后矩形四个顶点的坐标(相对于画布)
   * @private
   */
  _rotateSquare() {
    this.square = [
      this._rotatePoint(this.x, this.y, this.centerX, this.centerY, this.rotate),
      this._rotatePoint(this.x + this.w, this.y, this.centerX, this.centerY, this.rotate),
      this._rotatePoint(this.x + this.w, this.y + this.h, this.centerX, this.centerY, this.rotate),
      this._rotatePoint(this.x, this.y + this.h, this.centerX, this.centerY, this.rotate),
    ];
  },
  /**
   * 计算旋转后的新坐标(相对于画布)
   * @param x
   * @param y
   * @param centerX
   * @param centerY
   * @param degrees
   * @returns {*[]}
   * @private
   */
  _rotatePoint(x, y, centerX, centerY, degrees) {

    let newX = (x - centerX) * Math.cos(degrees * Math.PI / 180) - (y - centerY) * Math.sin(degrees * Math.PI / 180) + centerX;
    let newY = (x - centerX) * Math.sin(degrees * Math.PI / 180) + (y - centerY) * Math.cos(degrees * Math.PI / 180) + centerY;
    return [newX, newY];
  },
  /**
   *
   * @param {*} px 手指按下去的坐标
   * @param {*} py 手指按下去的坐标
   * @param {*} x 手指移动到的坐标
   * @param {*} y 手指移动到的坐标
   * @param {*} currentLayer 当前图层的信息
   */
  transform(px, py, x, y, currentLayer, action) {
    // 获取选择区域的宽度高度


    const diffXBefore = px - this.centerX;
    const diffYBefore = py - this.centerY;
    const diffXAfter = x - this.centerX;
    const diffYAfter = y - this.centerY;

    const angleBefore = Math.atan2(diffYBefore, diffXBefore) / Math.PI * 180;
    const angleAfter = Math.atan2(diffYAfter, diffXAfter) / Math.PI * 180;

    // 旋转的角度
    if (action == 'rotate' && ROTATE_ENABLED) {
      this.rotate = currentLayer.rotate + angleAfter - angleBefore;
    }

    const lineA = Math.sqrt(Math.pow((this.centerX - px), 2) + Math.pow((this.centerY - py), 2));
    const lineB = Math.sqrt(Math.pow((this.centerX - x), 2) + Math.pow((this.centerY - y), 2));
    if (this.type === 'text') {
      const fontSize = currentLayer.fontSize * ((lineB - lineA) / lineA + 1);
      this.fontSize = fontSize <= MIN_FONTSIZE ? MIN_FONTSIZE : fontSize;
      // 旋转位移后重新计算坐标          
      ctx.font = `${this.fontSize}px Arial`;
      // this.w = ctx.measureText(this.text).width + 30;
      // this.h = this.fontSize + 30;
      let lines = this.text.split('\n')
      let temp = lines.sort(function (a, b) {
        return a.length - b.length;
      });
      this.w = ctx.measureText(temp[temp.length - 1]).width + 30;
      this.h = this.fontSize * temp.length + 30;
      // 字体区域中心点不变,左上角位移
      this.x = this.centerX - this.w / 2;
      this.y = this.centerY - this.h / 2;

    } else //if (this.type === 'image') 
    {
      let resize_rito = lineB / lineA;
      let new_w = currentLayer.w * resize_rito;
      let new_h = currentLayer.h * resize_rito;
      this.scale = resize_rito

      if (currentLayer.w < currentLayer.h && new_w < MIN_WIDTH) {
        new_w = MIN_WIDTH;
        new_h = MIN_WIDTH * currentLayer.h / currentLayer.w;
      } else if (currentLayer.h <= currentLayer.w && new_h <= MIN_WIDTH) {
        new_h = MIN_WIDTH;
        new_w = MIN_WIDTH * currentLayer.w / currentLayer.h;
      }

      this.w = new_w;
      this.h = new_h;
      this.x = currentLayer.x - (new_w - currentLayer.w) / 2;
      this.y = currentLayer.y - (new_h - currentLayer.h) / 2;

    }
  },

};

欢迎体验

微信图片_20210517111505.jpg