Skip to content

Instantly share code, notes, and snippets.

@lumixraku
Created February 12, 2024 07:14
Show Gist options
  • Save lumixraku/bdcf9aafea54dc98596fd24797ab68f8 to your computer and use it in GitHub Desktop.
Save lumixraku/bdcf9aafea54dc98596fd24797ab68f8 to your computer and use it in GitHub Desktop.

Revisions

  1. lumixraku created this gist Feb 12, 2024.
    172 changes: 172 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,172 @@
    <!DOCTYPE html>
    <html lang="en">

    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>上下分层提高 Canvas 书写性能</title>
    <style>
    body,
    html {
    margin: 0;
    padding: 0;
    overflow: hidden;
    }

    #app-container {
    position: absolute;
    width: 100%;
    height: 100%;
    }

    #draw {
    border: 1px solid black;
    position: absolute;
    z-index: 9999;
    }

    #draw-content {
    border: 1px solid black;
    position: absolute;
    z-index: 9998;
    pointer-events: none;
    }
    </style>
    <!-- <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> -->
    </head>

    <body>
    <!-- 动态层 Canvas -->
    <canvas id="draw"></canvas>
    <!-- 静态层 Canvas -->
    <canvas id="draw-content"></canvas>
    <script>
    let start = false; // 是否开始绘制
    let points = []; // 记录鼠标移动的点
    let history = []; // 记录之前绘制的内容
    const dpr = window.devicePixelRatio || 1;
    const canvas = document.getElementById('draw');
    const canvasContent = document.getElementById('draw-content');
    const width = window.innerWidth;
    const height = window.innerHeight;
    // 上层 canvas 用来动态绘制绘制鼠标移动的轨迹
    canvas.width = Math.round(width * dpr);
    canvas.height = Math.round(height * dpr);
    canvas.style.width = width + "px";
    canvas.style.height = height + "px";
    const ctx = canvas.getContext('2d');
    ctx.scale(dpr, dpr);
    // 上层 canvas end

    // 下层 canvas 用来保存绘制的内容
    canvasContent.width = Math.round(width * dpr);
    canvasContent.height = Math.round(height * dpr);
    canvasContent.style.width = width + "px";
    canvasContent.style.height = height + "px";
    const ctxContent = canvasContent.getContext('2d');
    ctxContent.scale(dpr, dpr);
    // 下层 canvas end

    /**
    * 自由画笔的实现思路
    * 1 监听鼠标事件
    * 2 将鼠标移动的轨迹记录下来
    * 3 然后将这些点连接成线
    */
    canvas.addEventListener('pointerdown', (e) => {
    start = true; // 通过监听鼠标按下事件,来判断是否开始绘制
    addPoint((e)); // 将鼠标按下的点添加到points数组中
    });

    canvas.addEventListener('pointermove', (e) => {
    if (!start) return; // 如果没有按下,则不绘制
    addPoint((e)); // 将鼠标移动的点添加到points数组中
    renderUpperCanvas(ctx); // 绘制上层
    });

    canvas.addEventListener('pointerup', (e) => {
    start = false;
    // 将上层 canvas 绘制的内容保存到下层 canvas 中
    history.push(points);
    points = []; // 绘制完毕后,清空points数组
    renderLowerCanvas(ctx, ctxContent);
    });

    /*
    * 将鼠标事件的点转化为相对于canvas的坐标上的点
    */
    function addPoint(e) {
    points.push({
    x: e.clientX,
    y: e.clientY
    });
    }


    /**
    * 绘制函数
    * @param {*} ctx - canvas 尺寸
    * @param {*} points - 鼠标移动的点集
    */
    function render(ctx, points) {
    ctx.strokeStyle = 'red'; // 设置线条颜色
    ctx.lineWidth = 6; // 设置线条宽度
    ctx.lineJoin = 'round'; // 设置线条连接处的样式
    ctx.lineCap = 'round'; // 设置线条末端的样式

    /*
    beginPath() 是 Canvas 2D API 中的一个方法,用于开始一个新的路径。当你想创建一个新的路径时,你需要调用这个方法。
    例如,你可能会这样使用它:
    context.beginPath();
    context.moveTo(50, 50);
    context.lineTo(200, 50);
    context.stroke();
    在这个例子中,beginPath() 开始一个新的路径,moveTo(50, 50) 将路径的起点移动到 (50, 50),lineTo(200, 50) 添加一条从当前位置到 (200, 50) 的线,
    最后 stroke() 方法绘制出路径。
    其中 context 是你的 canvas 上下文。
    */
    ctx.beginPath(); // 开始绘制

    ctx.moveTo(points[0].x, points[0].y); // 将画笔移动到起始点

    for (let i = 1; i < points.length; i++) {
    // 取终点,将上一个点作为控制点,平滑过渡
    const cx = (points[i].x + points[i - 1].x) / 2;
    const cy = (points[i].y + points[i - 1].y) / 2;
    ctx.quadraticCurveTo(points[i - 1].x, points[i - 1].y, cx, cy);
    }

    ctx.stroke(); // 绘制路径
    }

    function renderUpperCanvas(ctx) {
    if (!points.length) return;
    ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); // 清空画布
    render(ctx, points);
    }

    function renderLowerCanvas(ctx, ctxContent) {
    if (!history.length) return;
    ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); // 清空画布
    ctxContent.clearRect(0, 0, window.innerWidth, window.innerHeight); // 清空画布
    history.forEach(points => {
    console.log(`points--->`, points);
    render(ctxContent, points);
    });
    }

    function throttle(fn, delay = 16 / 1000) {
    let timer = null;
    return function () {
    if (timer) return;
    timer = setTimeout(() => {
    fn.apply(this, arguments);
    timer = null;
    }, delay);
    }
    }
    </script>
    </body>

    </html>