SOURCE

console 命令行工具 X clear

                    
>
console
class Point {
    constructor(x, y) {
        this.x = x
        this.y = y
    }
    X = () => { return this.x }
    Y = () => { return this.y }
}

let calcMoveLine = (line, offset) => {
    let [p1, p2] = line
    // let toggle = false
    // if (p1.Y() < p2.Y()) {
    //     let p = p1
    //     p1 = p2
    //     p2 = p
    //     toggle = true
    // }
    
    // let lVer = p2.Y() - p1.Y() // vertical
    // let lHor = p2.X() - p1.X() // horizontal
    let p1p2 = [p1.X()-p2.X(), p1.Y()-p2.Y()]
    let len = Math.hypot(p1p2[0], p1p2[1])
     let n = null
    // if (p1.Y() > p2.Y()){}
    if (p1.Y() < p2.Y()) {
        n = new Point((p2.Y() - p1.Y())/len, (p1.X() - p2.X())/len)
    } else {
        n = new Point((p1.Y() - p2.Y())/len, (p2.X() - p1.X())/len)
    }

    let newline = [
        new Point(p1.X() + n.X() * offset, p1.Y() + n.Y() * offset),
        new Point(p2.X() + n.X() * offset, p2.Y() + n.Y() * offset)
    ]

    return [
        new Point(p1.X() + n.X() * offset, p1.Y() + n.Y() * offset),
        new Point(p2.X() + n.X() * offset, p2.Y() + n.Y() * offset)
    ]
}

let calcMoveLine2 = (line, offset) => {
    let [p1, p2] = line
    let lVer = p2.Y() - p1.Y() // vertical
    let lHor = p2.X() - p1.X() // horizontal
    let len = Math.hypot(lVer, lHor)
    
    let rad = offset*1.0/len
    let ver = lVer*rad*-1
    let hor = lHor*rad*-1
    return [
        new Point(p1.X()+ver, p1.Y()+hor),
        new Point(p2.X()+ver, p2.Y()+hor),
    ]
}

let _index = 1
    let img = new Image()
    img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIBJREFUeNpiYBj04P////xAnADEzDAxJiI1egOpC0DMzsjI+JdY27SBeP9/CHgFxJzEaBIA4qb/qKCEGI0pQPwcTeN7grYCFcz4jx1MJ9afaUD8Ek3zc1KiRQiIJ6MZkEZq3BoA8Qmo5qfkJpAYkGZQYJJrACj6EpFT2MABgAADADTCu3dUzSmkAAAAAElFTkSuQmCC'

let drawLine = (ctx, lines) => {
    ctx.beginPath()
    ctx.lineCap = 'round'
    lines.forEach((line) => {
// ctx.beginPath()
        // layer 1
        ctx.moveTo(line[0].X(), line[0].Y())
        ctx.lineTo(line[1].X(), line[1].Y())
        ctx.lineWidth = 10
        ctx.strokeStyle = '#0A404A'
    })
    ctx.stroke()
    ctx.closePath()

    ctx.beginPath()
    ctx.lineCap = 'round'
    lines.forEach((line) => {
        // layer 2
        let [d1, d2] =  calcMoveLine(line, 2)
        ctx.moveTo(line[0].X(), line[0].Y())
        ctx.lineTo(line[1].X(), line[1].Y())
        ctx.lineWidth = 6
        ctx.strokeStyle = '#146573'
    })
    ctx.stroke()
    ctx.closePath()

    ctx.beginPath()
    ctx.lineCap = 'round'
    lines.forEach((line) => {

        // layer 3
        let [d3, d4] = calcMoveLine(line, 0)
        ctx.lineWidth = 1
        ctx.strokeStyle = '#1F9EBD'
        ctx.moveTo(line[0].X(), line[0].Y())
        ctx.lineTo(line[1].X(), line[1].Y())
        ctx.stroke()
    })
    lines.forEach(([p1, p2]) => {

      var offset = (_index%11) * 0.1;
        // arrowTo(ctx, 
        //     { x: p1.X(), y: p1.Y() }, 
        //     { x: p2.X(), y: p2.Y() }, 
        //     { offset: offset, color: "white", justifyAlign: false, lineWidth:0, arrowLineWidth: 0 }
        // );
        console.log(offset)
        generatePoints(p1, p2, 20, ctx , offset, img)
    
        _index = ++_index>=10 ? 0 : _index
    })
ctx.closePath()
}


let canvas = document.getElementById('c1')
let c2 = document.getElementById('c2')
canvas.width = 450
canvas.height = 700
c2.width= 450
c2.height = 700
let ctx = canvas.getContext('2d')
let ctx2 = c2.getContext('2d')
ctx2.fillStyle = '#ccc'
ctx2.fillRect(0, 0, 450, 700)
// ctx.scale(2, 2)



draw = () => {
    ctx.clearRect(0, 0, 700, 700)
    ctx.moveTo(-10, 0)
    ctx.lineTo(10, 0)
    ctx.moveTo(0, -10)
    ctx.lineTo(0, 10)
    ctx.stroke()
    ctx.translate(200,200)
    drawLine(ctx, [
        [new Point(250, 200.0), new Point(200.0, 50.0)],
        [new Point(100, 50.0), new Point(200.0, 50.0)],
        [new Point(150, 50.0), new Point(100.0, 150.0)],
        [new Point(250, 150.0), new Point(100.0, 50.0)],
        // [new Point(100.0, 210.0), new Point(10.0, 10.0)],
        // [new Point(100, 210), new Point(300, 200)]
    ])


    ctx2.clearRect(0, 0, 350, 700)
    ctx2.drawImage(canvas, 0, 0)
    // requestAnimationFrame(draw)
}

// setInterval(draw, 5000)
draw()




    function generatePoints (startP, endP, stepSize = 30, ctx, aniOffset = 0.5, img) {
      let radA = Math.atan((endP.Y() - startP.Y()) / (endP.X() - startP.X()))

    //   radA = Math.atan2(startP, endP)
      if ((endP.X() - startP.X()) < 0) {
        radA += Math.PI
      }
      let p1p2 = [startP.X() - endP.X(), startP.Y() - endP.Y()]
      let dist = Math.hypot(p1p2[0], p1p2[1])

    // console.log('draw arrow: (' + startP.X() + ',' + startP.Y() + ') - (' + endP.X() + ',' + endP.Y() + ') = ' + dist, radA)
      // const dist = calcDist(startP, endP)
      let points = []
      const steps = dist / stepSize

      const drawImg = (pX, pY) => {
        if (img && ctx) {
          ctx.save()
          ctx.translate(pX, pY) // consider img position and imgWidth/Height.
          ctx.rotate(radA)
            // ctx.drawImage(img, pX, pY)
          ctx.scale(0.5, 0.5)
          ctx.drawImage(img, -img.width / 2, -img.height / 2)
          ctx.restore()
        }
      }

      // gen points by stepSize.. if enable corner arrow, start s with (0~1) float number.
      for (let s = aniOffset; s <= steps; s += 1) {
        const pX = Math.round(startP.X() + s * stepSize * Math.cos(radA))
        const pY = Math.round(startP.Y() + s * stepSize * Math.sin(radA))
        // console.log('arrow coorinate ', pX, pY)

        points.push([pX, pY])
        drawImg(pX, pY)
    //     arrowTo(ctx, { x: 200, y: 400 }, { x: 500, y: 400 }, { offset: offset, color: "white", justifyAlign: false });
      }
      // console.warn(`icon Number: ${points.length}`);
      return points
    }

    //   var offset = (_index % 3) * 5;
    //     arrowTo(ctx, { x: 200, y: 400 }, { x: 500, y: 400 }, { offset: offset, color: "white", justifyAlign: false });
    //     arrowTo(ctx, { x: 500, y: 400 }, { x: 450, y: 580 }, { offset: offset, color: "red", justifyAlign: false });
    //     arrowTo(ctx, { x: 450, y: 580 }, { x: 160, y: 580 }, { offset: offset, color: "red", justifyAlign: false });
    //     arrowTo(ctx, { x: 160, y: 580 }, { x: 200, y: 400 }, { offset: offset, color: "red", justifyAlign: false });


    // 画两点间箭头
    // ctx - 画布上下文(我理解成画笔)
    // p1 - 起点
    // p2 - 终点
    // arrowOptions -- 画箭头的选项
    function arrowTo(ctx, p1, p2, arrowOptions) {
        // 初始化参数
        var opts = {
            startOffset: 5,             // 起点的留空长度
            endOffset: 5,               // 终点的留空长度
            offset: 0,                  // 偏移位(模拟动画效果用, 使用时建议将justifyAlign设为false) 
            color: '#E6E6FA',           // 默认颜色 
            activeIndex: -1,            // 高亮箭头的索引, 超出回到一圈起始位置。(默认-1,不做高亮处理) 
            activeColor: "#00FF00",     // 高亮颜色(Highligh Color)
            stepLength: 10,             // 间隔(步长)
            justifyAlign: true,         // 两端对齐(两边撑满, 配合activeIndex > 0时使用) 
            arrowLength: 0,            // 箭头长度(柄到顶点)            
            arrowTheta: 25,             // 箭头两边的夹角(度数) 
            arrowHeadlen: 6,            // 箭头两边斜边长度 
            arrowLineWidth: 1,          // 画箭头的线宽度
            lineWidth: 1,               // 两点间的连丝宽度(>0时,有效)
        };
 
 
        if (arrowOptions !== undefined && arrowOptions !== null) {
            opts = Object.assign(opts, arrowOptions);
        }
 
 
        // 画连结两点的线
        if (opts.lineWidth > 0) {
            ctx.beginPath();
            ctx.moveTo(p1.x, p1.y);
            ctx.lineTo(p2.x, p2.y);
            //颜色,线宽
            ctx.strokeStyle = opts.color;
            ctx.lineWidth = opts.lineWidth;
            ctx.stroke();
            ctx.closePath();
        }
 
 
        // 计两点距离
        var len = Math.floor(Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)));
 
 
        // 计算画多少个箭头(注意:最后一个箭头是不需要间隔(步长),所以可用长度要加一个opts.stepLength)
        var loops = Math.floor((len - (opts.startOffset + opts.offset + opts.endOffset) + opts.stepLength) / (opts.arrowLength + opts.stepLength));
 
 
        // 两端对齐(两边撑满),重算步长 
        if (opts.justifyAlign === true) {
            opts.stepLength = (len - (opts.startOffset + opts.offset + opts.endOffset) - (opts.arrowLength * loops)) / (loops - 1);
        }
 
 
        // 高亮箭头的索引, 超出回到一圈起始位置。(用于动画效果) 
        var highlightIndex = 0;               // 0 - 无动画效果                
        if (opts.activeIndex > 0) {
            if ((opts.activeIndex % loops) === 0) {
                highlightIndex = loops;
            }
            else {
                highlightIndex = opts.activeIndex % loops;
            }
        }
 
 
        var hudu = Math.atan2(p1.y - p2.y, p2.x - p1.x);    // 计算p1, p2两点的倾斜度(弧度)。(注意参数:p1.y - p2.y, p2.x - p1.x, 请勿搞错)
        var p0 = { x: p1.x, y: p1.y };                      // 原点坐标, 作为圆心。 (辅助计算箭头起点(柄)与顶点的坐标)
        var r;                                              // 半径。 (辅助计算箭头起点(柄)与顶点的坐标)
        var color;
        for (var i = 0; i < loops; i++) {
            // 箭头起点(柄)
            r = (opts.startOffset + opts.offset) + (opts.arrowLength + opts.stepLength) * i;     // 原点到箭头起点(柄)的半径
            p1 = {
                x: p0.x + Math.cos(hudu) * r,
                y: p0.y - Math.sin(hudu) * r
            };
 
 
            // 箭头终点(顶点)
            r = r + opts.arrowLength;                       // 原点到箭头顶点(柄)的半径
            p2 = {
                x: p0.x + Math.cos(hudu) * r,
                y: p0.y - Math.sin(hudu) * r
            };
 
 
            // 画一个箭头
            if (highlightIndex > 0 && i === (highlightIndex - 1)) {
                color = opts.activeColor;       //高亮箭头(动画效果)
            }
            else {
                color = opts.color;
            }
            drawArrow(ctx, p1, p2, opts.arrowTheta, opts.arrowHeadlen, opts.arrowLineWidth, color);
        }
    }
 
 
    // 画箭头
    // ctx - 画布上下文(我理解成画笔)
    // p1 - 起点
    // p2 - 终点
    // theta -- 夹角theta (是度数,不是弧度)
    // headlen -- 斜边长度
    // width -- 线宽
    // color -- 颜色
    function drawArrow(ctx, p1, p2, theta, headlen, width, color) {
        theta = (theta !== undefined && theta !== null) ? theta : 25;       //夹角(度数)
        headlen = (headlen !== undefined && headlen !== null) ? headlen : 6;   //斜边长度
        width = (width !== undefined && width !== null) ? width : 1;        //线宽
        color = (color !== undefined && color !== null) ? color : '#000';       //颜色
 
 
        var angle = Math.atan2(p1.y - p2.y, p1.x - p2.x) * 180 / Math.PI,   //倾斜度(度数)
            angle1 = (angle + theta) * Math.PI / 180,            //夹角1
            angle2 = (angle - theta) * Math.PI / 180,            //夹角2   
            topX = headlen * Math.cos(angle1),                   //箭头上面点, X偏移位
            topY = headlen * Math.sin(angle1),                   //箭头上面点, Y偏移位
            botX = headlen * Math.cos(angle2),                   //箭头下面点, X偏移位   
            botY = headlen * Math.sin(angle2);                   //箭头下面点, Y偏移位      
 
 
        ctx.save();
 
 
        ctx.beginPath();
 
 
        //连结两点的线
        // ctx.moveTo(p1.x, p1.y);
        // ctx.lineTo(p2.x, p2.y);
 
 
        //终点箭头的两侧
        var arrowX = p2.x + topX;
        var arrowY = p2.y + topY;
        ctx.moveTo(arrowX, arrowY);
        ctx.lineTo(p2.x, p2.y);           //终点
        let arrowX2 = p2.x + botX;
        let arrowY2 = p2.y + botY;
        ctx.lineTo(arrowX2, arrowY2);
        ctx.lineTo(arrowX, arrowY);
 
 
        //颜色,线宽
        ctx.strokeStyle = color;
        ctx.lineWidth = width;
        ctx.stroke();
 
 
        ctx.restore();
    }
 
 
    // 为Object扩展assign方法(合拼多个对象的属性,返回一个新对象)
    if (!Object.assign) {
        Object.defineProperty(Object, "assign", {
            enumerable: false,
            configurable: true,
            writable: true,
            value: function (target, firstSource) {
                "use strict";
                if (target === undefined || target === null)
                    throw new TypeError("Cannot convert first argument to object");
                var to = Object(target);
                for (var i = 1; i < arguments.length; i++) {
                    var nextSource = arguments[i];
                    if (nextSource === undefined || nextSource === null) continue;
                    var keysArray = Object.keys(Object(nextSource));
                    for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
                        var nextKey = keysArray[nextIndex];
                        var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
                        if (desc !== undefined && desc.enumerable) to[nextKey] = nextSource[nextKey];
                    }
                }
                return to;
            }
        })
    }
<canvas id='c1'></canvas>
<canvas id='c2'></canvas>