分类 前端 下的文章

  之前总结了一些前端截图方案,其中有一个是手动调用canvas的api进行绘制,多次使用之后发现每次都要写很复杂的代码,所以就把主要逻辑封装了一下,改成了通过json配置文字和图片内容,虽然网上已经有一些了,但是都比较臃肿,有很多功能用不到,这里实现的是最轻量的。

// 兼容低版本浏览器
if (typeof Object.assign != 'function') {
  Object.assign = function (target) {
    'use strict';
    if (target == null) {
      throw new TypeError('Cannot convert undefined or null to object');
    }

    target = Object(target);
    for (var index = 1; index < arguments.length; index++) {
      var source = arguments[index];
      if (source != null) {
        for (var key in source) {
          if (Object.prototype.hasOwnProperty.call(source, key)) {
            target[key] = source[key];
          }
        }
      }
    }
    return target;
  };
}

// 通过配置实现canvas绘制图片,callback可以拿到base64的图片
function drawCanvas(canvas, data, callback, callbackError) {
  var defaultData = {
    width: 750,
    height: 10,
    lineHeight: 1.5,
    color: '#263238',
    textAlign: 'left',
    fontSize: 14,
    fontFamily: '"PingFang SC",tahoma,arial,"helvetica neue","hiragino sans gb","microsoft yahei",sans-serif',
    autoHeight: false,
  };

  var ctx = canvas.getContext('2d');

  var newData = Object.assign({}, defaultData, data);

  var currentY = 0,
    loadElementLength = 0,
    loadImgLength = 0,
    elementLength = 0,
    imgElementLength = 0;

  // 设置canvas宽高
  canvas.width = newData.width;
  canvas.height = 2000;

  ctx.fillStyle = newData.bgColor || '#fff';
  ctx.fillRect(0, 0, newData.width, 2000);

  // 指定宽高内绘制图片(background-size:cover方式)
  var drawImgCover = function (img, contW, contH, startX, startY) {
    if (img.width / img.height >= contW / contH) {
      var dH = img.height;
      var dW = Math.ceil(contW / contH * dH);
      ctx.drawImage(img, (img.width - dW) / 2, 0, dW, img.height, startX, startY, contW, contH);
    } else {
      var dW = img.width;
      var dH = Math.ceil(contH / contW * dW);
      ctx.drawImage(img, 0, (img.height - dH) / 2, img.width, dH, startX, startY, contW, contH);
    }
  };

  // 自动换行
  var canvasTextAutoLine = function (str, initX, initY, width, height, lineHeight) {
    var lineWidth = 0;
    var lastSubStrIndex = 0;
    var strLen = str.length;
    for (var i = 0; i < strLen; i++) {
      lineWidth += ctx.measureText(str[i]).width;
      if (lineWidth > width) {
        ctx.fillText(str.substring(lastSubStrIndex, i), initX, initY);
        initY += lineHeight;
        console.log(initY, height);
        if (newData.autoHeight && initY >= height) {
          currentY += lineHeight;
        }
        lineWidth = ctx.measureText(str[i]).width;
        lastSubStrIndex = i;
      }
      if (i == str.length - 1) {
        ctx.fillText(str.substring(lastSubStrIndex, i + 1), initX, initY);
      }
    }
  };

  // 绘制圆角并裁剪
  var drawRoundedRect = function (x, y, width, height, r) {
    ctx.save();
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.arcTo(x + width, y, x + width, y + r, r);
    ctx.arcTo(x + width, y + height, x + width - r, y + height, r);
    ctx.arcTo(x, y + height, x, y + height - r, r);
    ctx.arcTo(x, y, x + r, y, r);
    ctx.closePath();
    ctx.clip();
  };

  // 居中写字
  var centerText = function (str, startY) {
    var textW = ctx.measureText(str).width;
    var x = (newData.width - textW) / 2;
    ctx.fillText(str, x, startY);
  };

  // 居中图片
  var centerImg = function (img, startY) {
    var w = img.width;
    var x = (newData.width - w) / 2;
    ctx.drawImage(img, x, startY);
  };

  // 最大长度
  var ellipsis = function (str, maxWidth) {
    var strLen = str.length;
    for (var i = 0; i < strLen; i++) {
      if (ctx.measureText(str.substr(0, i + 1)).width > maxWidth) {
        return `${str.substr(0, i)}...`;
      }
    }
    return str;
  };

  // 导出
  var output = function () {
    // 获取可变部分图像
    var contW = newData.width,
      contH = newData.height;

    if (newData.autoHeight) {
      contH += currentY;
    }
    console.log(contH, currentY);
    var main = ctx.getImageData(0, 0, contW, contH);

    // 重新建空画布
    canvas.width = contW;
    canvas.height = contH;
    ctx.clearRect(0, 0, contW, 2000); //清空

    // 绘制可变部分图像
    ctx.putImageData(main, 0, 0);

    // 导出图片url
    var dataUrl = canvas.toDataURL('image/png', 1);
    setTimeout(function () {
      callback && callback(dataUrl);
    }, 100);
  };

  var checkOutput = function () {
    loadElementLength++;
    if (loadElementLength === elementLength) {
      output();
    }
  };

  // 加载图片
  loadImage = function (imgArr, callback) {
    imgArr.forEach(function (item) {
      if (!item) return;
      var Img = new Image();
      Img.crossOrigin = "anonymous";
      Img.src = item.content;

      Img.onload = function () {
        item.imgObj = this;
        loadImgLength++;
        if (loadImgLength === imgElementLength) {
          callback && callback();
        };
      }

      Img.onerror = function (err) {
        callbackError && callbackError(err);
        return false;
      };
    });
  };

  // 一个字一个字绘制
  drawTextArr = function (contentArr, x, y, w, lineHeight, color, fontSize, fontfamily) {
    if (!contentArr || !contentArr.length) return;
    var lineWidth = 0;
    var initX = x;
    var initY = y;

    contentArr.forEach(function (item) {
      ctx.fillStyle = item.color || color;
      var font = (item.fontSize || fontSize) + 'px ' + (item.fontfamily || fontfamily);
      if (item.bold) {
        font = item.bold + ' ' + ctx.font;
      }
      ctx.font = font;
      var strtxt = item.content;

      for (let i = 0; i < strtxt.length; i++) {
        lineWidth += ctx.measureText(strtxt[i]).width;
        if (lineWidth > w) {
          initY += lineHeight;
          if (newData.autoHeight) {
            currentY += lineHeight;
          }
          lineWidth = ctx.measureText(strtxt[i]).width;
        }
        ctx.fillText(strtxt[i], initX + lineWidth - ctx.measureText(strtxt[i]).width, initY);
      }
    });
  };

  // 绘制图片
  var drawImage = function (element) {
    var x = element.x || 0,
      y = element.y || 0,
      w = element.width || 0,
      h = element.height || 0;

    if (currentY !== 0) {
      y += currentY;
    }

    var Img = element.imgObj;

    ctx.save();
    if (element.autoHeight) {
      var newH = Img.height * (w / Img.width);
      if (newData.autoHeight) {
        currentY += newH - h;
      }
      h = newH;
    }
    ctx.drawImage(Img, x, y, w, h);
    ctx.restore();

    if (element.r > 0) {
      drawRoundedRect(x, y, w, h, element.r);
    }
    checkOutput();
  };

  // 绘制文本
  var drawText = function (element) {
    var fontfamily = element.fontfamily || newData.fontFamily,
      fontSize = element.fontSize || newData.fontSize,
      cont = element.content,
      w = element.width || 0,
      h = element.height || 0,
      x = element.x || 0,
      y = element.y || 0,
      color = element.color || newData.color,
      font = fontSize + 'px ' + fontfamily,
      lh = element.lineHeight || newData.lineHeight;
    var lineHeight = lh * fontSize;

    if (element.bold) {
      font = element.bold + ' ' + font;
    }

    if (currentY !== 0) {
      y += currentY;
    }

    if (element.maxWidth) {
      cont = ellipsis(cont, element.maxWidth);
    }

    y += fontSize + (lineHeight - fontSize) / 4;

    ctx.beginPath();
    ctx.fillStyle = color;
    ctx.font = font;
    if (typeof cont !== 'string') {
      drawTextArr(cont, x, y, w, lineHeight, color, fontSize, fontfamily);
    } else if (element.textAlign === 'center') {
      centerText(cont, y);
    } else if (element.autoHeight) {
      canvasTextAutoLine(cont, x, y, w, y + h, lineHeight);
    } else {
      ctx.fillText(cont, x, y);
      ctx.fill();
    }
    ctx.closePath();
    checkOutput();
  };

  if (newData.elements && newData.elements.length) {
    elementLength = newData.elements.length;
    var imgArr = newData.elements.map(function (item) {
      if (item.type === 'img') {
        imgElementLength++;
        return item;
      }
    });
    loadImage(imgArr, function () {
      newData.elements.forEach(function (item, index) {
        if (item.type === 'img') {
          item.imgObj = imgArr[index].imgObj;
          drawImage(item);
        } else {
          drawText(item);
        }
      });
    });
  }
}

使用

function drawShare(canvas, data, callback, callbackError) {
  var newData = {
    width: 686,
    height: 954,
    color: '#263238',
    bgColor: '#fff',
    autoHeight: true,
    elements: [
      {
        type: 'img',
        x: 0,
        y: 0,
        width: 686,
        height: 514,
        autoHeight: true,
        content: data.pic,
      },
      {
        type: 'text',
        width: 622,
        height: 72,
        x: 32,
        y: 546,
        fontSize: 48,
        bold: 500,
        autoHeight: true,
        content: data.title,
      },
      {
        type: 'text',
        width: 622,
        height: 112,
        x: 32,
        y: 634,
        color: '#3C5A6E',
        fontSize: 32,
        autoHeight: true,
        lineHeight: 1.75,
        content: data.info,
      },
      {
        type: 'img',
        x: 32,
        y: 776,
        width: 622,
        height: 2,
        content: 'images/achievement/draw-share-line.png',
      },
      {
        type: 'img',
        x: 24,
        y: 802,
        width: 128,
        height: 128,
        content: data.qrcode,
      },
      {
        type: 'text',
        x: 176,
        y: 814,
        color: '#3C5A6E',
        fontSize: 24,
        content: '长按或扫描二维码',
      },
      {
        type: 'text',
        x: 176,
        y: 850,
        color: '#3C5A6E',
        fontSize: 24,
        content: '查看公益项目',
      },
      {
        type: 'img',
        x: 176,
        y: 894,
        width: 269,
        height: 36,
        content: 'images/achievement/draw-share-logo.png',
      },
      {
        type: 'img',
        x: 476,
        y: 780,
        width: 208,
        height: 176,
        content: 'images/achievement/draw-share-flower.png',
      },
    ],
  };
  drawCanvas(canvas, newData, callback, callbackError);
}

  最近有一个需求,需要把珊瑚图表的气泡词云图空间利用率最大化,一开始没有思路,研究的比较久,最后勉强实现了。涉及的知识点也比较多,这个算是我写代码以来,用到的数学、物理知识最深的一次了,所以在这里记录一下。

1 需求描述

  气泡词云图之前使用d3-hierarchy实现的,该实现是一个圆形,如果在一个长宽不等的矩形容器中,空间利用率较低,会出现大片留白,希望实现为尽可能填满容器,将空间利用率最大化。

2 问题本质

  之前没有遇到过类似的问题,没什么思路,经过不懈的努力,终于发现,这个问题的本质其实是--不等圆packing,不等圆packing问题是如何将N个任意半径的圆形物体互不相嵌的放入一个圆形容器中,找出一种布局使得这个圆形容器的半径最小。

  不等圆packing问题具有非常广泛的应用场景,如裁缝如何最大效率的利用布料、设计航天器时需要把若干个半径给定的圆柱形部件放入一个尽可能小的圆柱形腔体内,此外,在无线通信、汽车工业材料切割等工业领域也有着广泛的应用。

  该问题是一个典型的NP难度问题,使用传统的数学方法很难求解,迄今为止尚未设计出既严格(确保能找到全局最优解)又迅速(多项式计算复杂度 )的求解算法。现有的求解算法几乎均为启发式算法,主要包括两大类,一是基于构造规则的构造算法,如占角算法(最大穴度算法)、PERM、Beam Search、Algorithm2等,二是基于演化规则的演化算法,如拟人拟物算法、禁忌搜索算法、粒子群算法、遗传算法、模拟退火算法、以及混合算法SATS等。

3 算法分析

  算法主要分两种,构造型和演化型,本文选取其中的占角算法、拟人拟物算法、分子动力学模拟方法进行分析。

3.1 占角算法(最大穴度算法)

  在不考虑n个圆的各种排列情况下首先将第一个圆放进与大圆R相切的一个位置,接着把第二个圆放到与前两个圆相切的位置之一,然后把第三个圆也放置到与前三个圆中的两个圆相切的位置之一,就这样将下一个圆放到前i个圆中任意两个圆相切的位置,如果这个位置导致了新放入的圆与其他圆有重叠则将新圆放置在满足与其他两个圆相切的另一位置,如果发现所有位置都不满足不重叠的条件,则回溯到之前的一个圆,并重新选取之前圆的位置。按照这样的规则满足条件则继续添加新圆,如果不满足则回溯到上一个圆放置的位置,直到所有的圆都放入到了大圆R中,则找到问题的解,或者遍历所有情况仍旧找不到解,则认为该问题没有解。

  d3的打包图就是使用占角算法,首先将第一个圆放到原点,然后将第二个圆与第一个圆相切,将其居中:

  // Place the first circle.
  a = circles[0], a.x = 0, a.y = 0;
  if (!(n > 1)) return a.r;

  // Place the second circle.
  b = circles[1], a.x = -b.r, b.x = a.r, b.y = 0;
  if (!(n > 2)) return a.r + b.r;

 

  然后将第三个圆放到与两个圆同时相切的位置(占角),可用余弦定理或者两点间的距离公式求得第三个圆的坐标:

function place(b, a, c) {
  var dx = b.x - a.x, x, a2,
      dy = b.y - a.y, y, b2,
      d2 = dx * dx + dy * dy;
  if (d2) {
    a2 = a.r + c.r, a2 *= a2;
    b2 = b.r + c.r, b2 *= b2;
    if (a2 > b2) {
      x = (d2 + b2 - a2) / (2 * d2);
      y = Math.sqrt(Math.max(0, b2 / d2 - x * x));
      c.x = b.x - x * dx - y * dy;
      c.y = b.y - x * dy + y * dx;
    } else {
      x = (d2 + a2 - b2) / (2 * d2);
      y = Math.sqrt(Math.max(0, a2 / d2 - x * x));
      c.x = a.x + x * dx - y * dy;
      c.y = a.y + x * dy + y * dx;
    }
  } else {
    c.x = a.x + c.r;
    c.y = a.y;
  }
}

 

  计算过程中防止重叠,还需要判断两圆之间的碰撞,碰撞检测使用两圆圆心的距离小于等于两圆的半径之和:

function intersects(a, b) {
  var dr = a.r + b.r - 1e-6, dx = b.x - a.x, dy = b.y - a.y;
  return dr > 0 && dr * dr > dx * dx + dy * dy;
}

  全部小圆放入之后再根据容器较短边等比例缩放,然后同时移动至容器中心。

  效果如下:
请输入图片描述

  这个过程一直是寻找最大凹点,也就是最大穴度,所以最终会是一个像圆的形状,要控制矩形边界的话不太容易,经过多次尝试,还是不能控制矩形边界。

3.2 拟人拟物算法

  这是一个求解不等圆packing 问题效率较高的方法。拟物算法将大圆R看做封闭刚性不可改变空的圆形容器,将每个小圆看做光滑弹性的小球,开始将这n个小球强行放到容器中,我们定义小球之间因为弹性挤压产生的能量为弹性势能(U=U1+U2+···+Un),接着小球由于相互间以及和容器间的作用力不断运动,每个小球都向受挤压的方向运动,直至所有小球受力到达平衡或者不再受力。拟物方法就是模拟这样一个过程。

  拟人方法是为了解决拟物方法中可能出现的一种“死锁”的情况,即n个小球在局部最小解的情况中反复移动无法跳出的情况(这时的弹性势能U不再变化且不为0),“人为地”从容器中取出最拥挤的小球重新放入容器内进行拟物计算,这样就解决了拟物算法可能遇到的僵持平衡的状态。整个过程直到所有的小球都不受到弹性力,弹性势能为零(U≈0)则认为找到了解。

  代码实现:

// 开始计算数据
  startSetData(circles, width, height, padding0, padding1) {
    let step = 0.1;
    let oldmaxPE = circles[0];
    let maxPE = circles[1];
    let minPE = circles[1];
    let newPE;
    let oldPE = 0;
    let t = 0;
    const num = circles.length;

    while (true) {
      if (step >= 0.001) {
        // 计算所有圆的总势能,找出最大势能圆和最小势能圆
        const peObj = this.calcPE(circles, maxPE, minPE);
        newPE = peObj.PE;
        maxPE = peObj.maxPE;
        minPE = peObj.minPE;
        // 总势能约等于0,结束循环
        if (newPE <= 0.0001) {
          break;
        } else {
          if (newPE >= oldPE) {
            step = 0.8 * step;
          }
          oldPE = newPE;
          for (let i = 0; i < num; i++) {
            // 计算每个圆新的位置
            circles[i].x -= step * circles[i].dx;
            circles[i].y -= step * circles[i].dy;

            // 边界检测
            if (circles[i].x + circles[i].r >= width - padding0) {
              circles[i].x = width - circles[i].r - padding0;
            }
            if (circles[i].x - circles[i].r <= padding0) {
              circles[i].x = circles[i].r + padding0;
            }
            if (circles[i].y + circles[i].r >= height - padding1) {
              circles[i].y = height - circles[i].r - padding1;
            }
            if (circles[i].y - circles[i].r <= padding1) {
              circles[i].y = circles[i].r + padding1;
            }
          }
        }
      }
      else {
        if (maxPE == oldmaxPE) {
          t = t + 1;
        }
        const NextDouble = Math.random();
        if (t < 1) {
          // 在圆盘上随机选点,重新摆放势能最大的圆
          // 注意此时的maxPE 和 oldmaxPE均指向old_Circles中的对象。
          maxPE.x = width * NextDouble;
          maxPE.y = height * NextDouble;
          oldmaxPE = maxPE;
          step = 0.1;
        }
        else if (t == 1) {
          minPE.x = width * NextDouble;
          minPE.y = height * NextDouble;
          t = 0;
          oldmaxPE = circles[0];
          step = 0.1;
        }
      }
    }
  }

  // 计算势能
  calcPE(circles, maxPE, minPE) {
    let PE = 0;
    const num = circles.length;
    // 计算前,将以前的计算结果清零!!!
    for (let i = 0; i < num; i++) {
      circles[i].dx = 0;
      circles[i].dy = 0;
      circles[i].PE = 0;
    }
    // 逐个计算圆的势能以及移动量
    for (let i = 0; i < num; i++) {
      this.moveDirection(circles[i], num, circles, i);
    }
    // 累加势能
    for (let i = 0; i < num; i++) {
      PE += circles[i].PE;
    }
    // 找出最大势圆和最小势能圆
    maxPE = minPE = circles[0];
    for (let i = 1; i < num; i++) {
      maxPE = maxPE.PE < circles[i].PE ? circles[i] : maxPE;
      minPE = minPE.PE > circles[i].PE ? circles[i] : minPE;
    }
    return { PE, maxPE, minPE };
  }

  // 计算单个圆势能、移动量
  moveDirection(circle, num, circles, numth) {
    for (let i = 0; i < num; i++)
    {
      // 计算当前圆和其他圆之间的势能和移动量
      if (numth != i) {
        const dij = Math.sqrt(Math.pow(circle.x - circles[i].x, 2.0) + Math.pow(circle.y - circles[i].y, 2.0));
        if (dij < circle.r + circles[i].r) {
          if (circles[i].x === circle.x) {
            circle.dx += circle.r + circles[i].r - dij;
          } else {
            circle.dx += (circles[i].x - circle.x) / dij * (circle.r + circles[i].r - dij);
          }
          if (circles[i].y === circle.y) {
            circle.dy += circle.r + circles[i].r - dij
          } else {
            circle.dy += (circles[i].y - circle.y) / dij * (circle.r + circles[i].r - dij);
          }
          circle.PE += Math.pow(circle.r + circles[i].r - dij, 2.0);
        }
      }
    }
  }

 

  效果如下:
请输入图片描述
请输入图片描述

  这种方法也有一些缺点,经过多次测试,发现有些数据可能出现计算很久的情况,还有一些数据无解,计算太久会卡住。

3.3 分子动力学模拟方法

  以上两种方法均有不足之处,为了更加完美的解决问题,还可以使用分子动力学模拟的方法进行仿真模拟,它假设任意单位时间步长 Δt = 1,所有的粒子的单位质量常量 m = 1。作用在每个粒子上的合力 F 相当于在单位时间 Δt 内的恒定加速度 a。并且可以简单的通过为每个粒子添加速度并计算粒子的位置来模拟仿真。

  d3-force提供了一个力模型,可以施加一些力,使圆之间不重叠,但是这个模型没有边界检测,需要新创建一种力,用于边界检测。

  实现方式如下:

const x0 = boxPadding[0],
  y0 = boxPadding[1],
  x1 = svgWidth - boxPadding[0],
  y1 = svgHieght - boxPadding[1] * 2;
const simulation = d3
  .forceSimulation(rootData)
  .force(
    "collide",
    d3
      .forceCollide()
      .radius(function (d) {
        return d.r + 5;
      })
      .strength(2)
      .iterations(2)
  )
  .force('charge', d3.forceManyBody().strength(20))
  .force('boundary', forceBoundary(x0, y0, x1, y1));
simulation.tick(300);

  其中,用于边界检测的力模型如下:

function constant(x) {
  return function() {
    return x;
  };
}

function forceBoundary(x0, y0, x1, y1) {
  var strength = constant(0.1),
      hardBoundary = true,
      border = constant( Math.min((x1 - x0)/2, (y1 - y0)/2) ),
      nodes,
      strengthsX,
      strengthsY,
      x0z, x1z,
      y0z, y1z,
      borderz,
      halfX, halfY;


  if (typeof x0 !== "function") x0 = constant(x0 == null ? -100 : +x0);
  if (typeof x1 !== "function") x1 = constant(x1 == null ? 100 : +x1);
  if (typeof y0 !== "function") y0 = constant(y0 == null ? -100 : +y0);
  if (typeof y1 !== "function") y1 = constant(y1 == null ? 100 : +y1);

  function getVx(halfX, x, strengthX, border, alpha) {
    return (halfX - x) *  Math.min(2, Math.abs( halfX - x) / halfX) * strengthX * alpha;
  }

  function force(alpha) {
    for (var i = 0, n = nodes.length, node; i < n; ++i) {
      node = nodes[i];

      if ((node.x - node.r <= (x0z[i] + borderz[i]) || node.x + node.r >= (x1z[i] - borderz[i])) ||
          (node.y - node.r <= (y0z[i] + borderz[i]) || node.y + node.r >= (y1z[i] - borderz[i])) ) {
        node.vx += getVx(halfX[i], node.x, strengthsX[i], borderz[i], alpha);
        node.vy += getVx(halfY[i], node.y, strengthsY[i], borderz[i], alpha);
      } else {
        node.vx = 0;
        node.vy = 0;
      }

      if (hardBoundary) {
        if (node.x + node.r >= x1z[i]) node.vx += x1z[i] - node.x - node.r;
        if (node.x - node.r <= x0z[i]) node.vx += x0z[i] - node.x + node.r;
        if (node.y + node.r >= y1z[i]) node.vy += y1z[i] - node.y - node.r;
        if (node.y - node.r <= y0z[i]) node.vy += y0z[i] - node.y + node.r;
      }
    }
  }

  function initialize() {
    if (!nodes) return;
    var i, n = nodes.length;
    strengthsX = new Array(n);
    strengthsY = new Array(n);
    x0z = new Array(n);
    y0z = new Array(n);
    x1z = new Array(n);
    y1z = new Array(n);
    halfY = new Array(n);
    halfX = new Array(n);
    borderz = new Array(n);

    for (i = 0; i < n; ++i) {
      strengthsX[i] = (isNaN(x0z[i] = +x0(nodes[i], i, nodes)) ||
        isNaN(x1z[i] = +x1(nodes[i], i, nodes))) ? 0 : +strength(nodes[i], i, nodes);
      strengthsY[i] = (isNaN(y0z[i] = +y0(nodes[i], i, nodes)) ||
        isNaN(y1z[i] = +y1(nodes[i], i, nodes))) ? 0 : +strength(nodes[i], i, nodes);
      halfX[i] = x0z[i] + (x1z[i] - x0z[i])/2,
      halfY[i] = y0z[i] + (y1z[i] - y0z[i])/2;
      borderz[i] = +border(nodes[i], i, nodes)
    }
  }

  force.initialize = function(_) {
    nodes = _;
    initialize();
  };

  force.x0 = function(_) {
    return arguments.length ? (x0 = typeof _ === "function" ? _ : constant(+_), initialize(), force) : x0;
  };

  force.x1 = function(_) {
    return arguments.length ? (x1 = typeof _ === "function" ? _ : constant(+_), initialize(), force) : x1;
  };

  force.y0 = function(_) {
    return arguments.length ? (y0 = typeof _ === "function" ? _ : constant(+_), initialize(), force) : y0;
  };

  force.y1 = function(_) {
    return arguments.length ? (y1 = typeof _ === "function" ? _ : constant(+_), initialize(), force) : y1;
  };

  force.strength = function(_) {
    return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
  };

  force.border = function(_) {
    return arguments.length ? (border = typeof _ === "function" ? _ : constant(+_), initialize(), force) : border;
  };

  force.hardBoundary = function(_) {
    return arguments.length ? (hardBoundary = _, force) : hardBoundary;
  };

  return force;
}
 

  这种方法实现效果和拟人拟物效果相似,计算速度更快,也不会出现无解的情况,算是比较完美的一种,所以最终选用了这种方式勉强完成了本次需求。

4 总结

  以前在学校学习数学、物理知识的时候总感觉好像用不到,但是从这次需求中发现,没有这些基本的知识,要解决问题是比较困难的,特别是在数据可视化中,之前一直做一些比较基础的前端工作,会产生一种错觉,前端就是新时代的搬砖工,只能做基础的搬砖工作,接触到图表之后才发现,这里面水很深,还是有很多算法没有js版本,前端在算法领域成熟度还是有待提高。虽然在平时使用不到,但是真正用的时,如果不能理解算法底层原理,要解决这种类似的问题是比较困难的,甚至无从下手。

  转眼间,来到公司已经两个月了,做的第一个项目刚刚上线,因为项目用到的react、taro、ts都是第一次使用,在这个过程中,遇到了许许多多的问题,所以在这里记录一下。

1 项目概述

  日行一善是腾讯公益下的一个子项目,一期需求主要有三个页面,列表页、个人列表页、详情页。

  列表页展示很多从公益导入的活动,点击列表进入详情页。

  进入详情页时如果没有登录,则跳转登录页面登录,详情页有项目信息,邀请人信息、打卡信息、打卡日历、邀请弹窗、荣誉弹窗、打卡按钮等。点击打卡后进入支付界面,支付成功后打卡信息和打卡日历做相应的改变。

  已经开始打卡的活动会出现在个人列表页中,项目下方会显示对应的打卡信息。

  咋一看,好像没有几个页面,应该会很简单,但是真正开发的时候还是有比较多的问题的。

2 框架选型

  最开始使用的是cdc比较成熟前端开发流水线webshop,但是因为后面要出小程序版本,所以转成了taro框架,第一次使用taro框架,初始化时会选择jsx还是tsx,我之前读到过一篇文章,说能使用ts就尽量使用ts,虽然在开始的时候可能会多花一些时间,但是越到后面他是重要性越来越大。所以我果断选择了tsx。

  虽然是第一次使用taro,但是我看过文档之后发现,taro框架还是比较人性化的,基本上就是换了标签的react,生命周期什么的基本相同,只是稍微多了一些生命周期。

3 组件拆分

  列表页和个人列表页都是常规列表,比较简单,个人列表页和详情页都有用到打卡信息,打卡信息可以抽出来做成一个组件。项目的主要难点在详情页,逻辑也比较复杂,一个是因为项目存在多种状态,还未开始、已经关联、已经打卡、已经结束等,不同的状态打卡信息和打卡日历的显示会有所不同,荣誉弹窗的状态也有所不同,还有一个涉及到是否登录,支付是否成功等。

  根据视觉可见,多个地方存在按钮,有多个弹窗,打卡日历和打卡信息内部比较复杂等,所以将按钮、弹窗、日历、打卡信息分别抽出作为组件。登录和支付也需要作为单独的组件,一个是显得简洁,还有一个方便后面转小程序时写适配代码。

4 项目难点和问题

  在做项目时遇到的问题还是比较多的,我曾几度怀疑我不适合程序员,准备提桶回家种地了,但是脸皮太厚,舍不得我这盛世美颜,现在回头一看,好像又没那么多问题了,我想,可能是年纪大了,记忆力下降了吧。

4.1 难点

  ①  打卡日历,之前没有做过日历相关的项目,感觉无从下手,还是要感谢神奇的网络呀,只有你想不到的,没有网上找不到的,主要在不知道如何判断一个月有多少天,额,好像这是小学的知识没有学好,网上找了之后才知道,4,6,9,11有30天,2月一般是28天,如果闰年是29天,其他月份都是31天。闰年分为普通闰年(year % 4 === 0 && year % 100 !== 0)和世纪闰年(year % 400 === 0)。因为日历需要横向滚动,但是taro标签在h5设置scrollLeft不生效,最终是通过判断,非小程序的话使用div代替。

  ② 邀请卡和荣誉卡,这个需要用户可以保存为图片,这是在我写好样式后才知道的,为了方便,我想直接把dom节点变成截图,找到了一个库dom-to-image,因为这个库很久没有维护了,而且导出图片有点模糊,想换成html2canvas,但是发现其官方文档写着还处于实验期,不推荐生产环境使用,所以把dom-to-image改成了es6,解决一下导出模糊的问题。

  另外一个问题是,在微信浏览器中不能通过js保存图片,只能先把图片变成img标签,让用户主动长按保存,最终,经过多方权衡,我使用了非常繁琐的canvas原生api。

  不过,在这个过程中也了解到了很多东西,详见网页截图方案总结,本来想多写一些的,但是写着写着发现网上有一模一样的,而且口才比我好,讲的比我详细,就没有动力再写下去了。

4.2 问题

  ① 做上拉加载更多时的兼容问题,写好之后在谷歌可以,但是微信浏览器中总是不行,需要用到兼容写法:const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;

  ② 在imac中使用taro命令时需要添加sudo,不然会报权限不够。

  ③ 使用Taro.navigateTo时不会触发componentWillUnmount和componentDidMount,但是会触发componentDidShow和componentDidHide。

5 总结

  好像在做的过程中遇到的问题挺多的,但是现在就是想不起来,感觉大脑一片空白,看来下次还是遇到问题之后要马上记下来才行。

  最近在做一个项目,写好样式后,发现在微信端需要保存图片,为了方便(偷懒),第一个想到用什么办法把dom节点直接变成图片。在知识的海洋一番畅游,发现了几种方案,网上现有的文章都不是很齐全,所以在这里汇总一下。

概述

  网页截图可以在后端进行,也可以在前端进行,后端比较常用的是用无头浏览器phantomjs配合实现截图。后端截图适用于一次截图保存,多次使用的情况。如果截图次数较多,请求比较频繁的话,对服务器的压力还是比较大的,这种情况,最好的方式就是在前端实现截图,本文主要介绍前端截图方案。

1  原理

  原理1:把内容绘制到canvas上,然后通过toDataURL、toBlob等得到图像资源,再根据情况转换为不同的图片格式。

  原理2:使用svg,通过createObjectURL或encodeURIComponent处理svg得到图像资源,可以把svg绘制到canvas。

  原理3:使用免费或付费现成API,pdfcrowd、web2pdfconvert、url2png等。

2  方案

  方案1:将内容直接绘制到canvas。

  方案2:遍历dom节点绘制到canvas。

  方案3:将dom节点处理后嵌入到svg的foreignObject中。

2.1  各种方案优缺点对比

方案1(直接绘制)方案2(遍历dom)方案3(嵌入svg)
优点可完全还原设计稿简单快捷,方便复用更简单快捷
缺点不能复用,代码繁琐需要考虑各种复杂的css样式搭配,各种特殊标签,精确度问题等,可能出现部分内容丢失等情况svg中不允许外部资源(js,css,img的url等),svg中不支持执行js,需要经过处理,也不能完全还原

2.2  根据以上方案,比较成熟的解决方式有

  方案1(直接绘制):原生canvas的api(代码复杂,繁琐)、konva.js(面向对象方式,比原生好用一点)

  方案2(遍历dom):html2canvas.js(导出图片格式需要搭配canvas2image.js,虽然官方文档说处于试验环境,还有重大改版,不推荐正式使用,但是14年就被Twitter等用于生产环境)、dom-to-image.js(自带导出图片格式,但是很久没有维护了)

  方案3(嵌入svg):rasterizeHTML.js(可兼容方案2,也有很多限制详见Limitations)

3  常见问题

3.1  图片跨域问题

       js异步获取图片在不同源的情况下会存在跨域问题。

       ① 服务器设置cors,前端设置img的属性crossOrigin='anonymous'。

       ②  如果图片位于第三方,不能设置cors的话,可以通过服务器代理把图片代理到当前域下。

       注意:因为有些图片服务器设置不同,如果网页其他地方有img标签请求相同的图片,可能会缓存不允许跨域的响应头,在服务器设置是需要注意。如果图片不多的情况可以在js请求的图片url中添加参数以区别img标签,防止缓存影响跨域设置。

3.2  导出图片模糊问题

     ①  如果按照一比一绘制导出图片,一般都会有点模糊,可以在绘制时将canvas和内容等比例放大绘制,导出时再缩放为原尺寸。

     ② 建议px为单位,单位如果使用不是px的话,在计算时会出现很多小数,导致模糊问题。

     ③ 使用img标签代替背景图。

    ④ canvas的抗锯齿是默认开启的,需要关闭抗锯齿来实现图像的锐化MDN: imageSmoothingEnabled

3.3  由于资源加载不全导致的内容不完整问题

     在绘制canvas时,如果部分资源没有加载完成,绘制内容会丢失,除了设置一定的延迟外,还可以通过Promise.all配合onload事件确保资源已经加载完成。

3.4  canvas如何居中绘制

     很多时候有居中绘制的需求,可以通过ctx.measureText或者img.width获取宽度,然后x = (总宽度 - 绘制宽度) / 2。

ctx.measureText还可以用来绘制自动换行,超出省略等。

3.5  canvas绘制非正圆圆角

 // 绘制圆角并裁剪
 const drawRoundedRect = function (ctx, x, y, width, height, r) {
   ctx.save();
   ctx.beginPath();
   ctx.moveTo(x + r, y);
   ctx.arcTo(x + width, y, x + width, y + r, r);
   ctx.arcTo(x + width, y + height, x + width - r, y + height, r);
   ctx.arcTo(x, y + height, x, y + height - r, r);
   ctx.arcTo(x, y, x + r, y, r);
   ctx.closePath();
   ctx.clip();
 }