You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Canvas绘图重放异常:如何实现带线条平滑的慢速重放?

解决Canvas绘图重放的平滑慢速问题

核心修改点

  • 拆分绘制逻辑:将录制和重放的绘图操作分离,避免重放时重复往coordinates数组添加数据
  • 控制重放速率:用setTimeout替代连续的requestAnimationFrame,通过调整时间间隔控制重放速度
  • 重置重放状态:每次重放前清空画布、重置绘图状态变量,保证重放从初始状态开始
  • 修复曲线绘制逻辑:重放时正确初始化贝塞尔曲线的控制点状态,确保线条平滑生成

修改后的完整代码

HTML代码

<!DOCTYPE HTML>
<html>
    <head>
        <title>Canvas Draw</title>
    </head>
    <body>
        <div id="coords">
            <div id="msg">Start drawing!</div>
            X: <span id="x"></span>
            Y: <span id="y"></span>
        </div>
        <button id="coordinatesBtn">Show coordinates of drawing</button>
         <button id="coordinatesDrawBtn">Use coordinates to draw</button>
        <div id="container">
            <canvas id="surface"></canvas>
        </div>
    </body>
</html>

CSS代码

* {
    -webkit-user-select:none;
}
body {
    margin:0;
    padding:0;
    width:100%;
    height:100%;
    overflow:hidden;
}
#msg {
    color:red;
    font-weight:bold;
}
#coords{ 
    background:#ffc; 
    border-bottom:1px solid #fc0;
}
#container {
    position:relative;
}
#surface {
    background:white;
}
.tp{
    visibility:hidden;
    position:absolute;
    width:30px;
    height:30px;
    background:#999;
    -webkit-border-radius:15px;
    -webkit-box-shadow:inset white 0 0 15px 5px, white 0 0 2px 2px;
}

JavaScript代码

const coordinates = [];
let replayInterval = 30; // 重放间隔(单位:ms),值越大速度越慢

document.getElementById('coordinatesBtn').addEventListener('click', () => {
  console.log(coordinates)
});

document.getElementById('coordinatesDrawBtn').addEventListener('click', () => {
  play()
});

let step = 0;
let replayTimer = null;

function play() {
  // 终止正在进行的重放
  if (replayTimer) {
    clearTimeout(replayTimer);
    replayTimer = null;
  }
  // 重置重放状态
  step = 0;
  inProgress = false;
  cp1x = cp1y = cp2x = cp2y = null;
  skip1 = true;
  skip2 = false;
  // 清空画布
  ctx.clearRect(0, 0, surface.width, surface.height);
  
  function steps() {
    if (step >= coordinates.length) {
      // 重放结束,重置状态
      inProgress = false;
      ctx.save();
      return;
    }
    // 调用重放专用绘制函数
    replayDraw(coordinates[step].x, coordinates[step].y);
    step++;
    replayTimer = setTimeout(steps, replayInterval);
  }
  
  steps();
}

var surface, ctx, target, inProgress, cp1x, cp1y, cp2x, cp2y, skip1, skip2;

function $(e) {
  return document.getElementById(e);
}

// 录制时的绘图函数
function draw(e) {
  var x = e.offsetX;
  var y = e.offsetY;

  target.x.innerHTML = x;
  target.y.innerHTML = y;

  coordinates.push({x, y});
  drawPath(x, y);
}

// 重放时的绘图函数
function replayDraw(x, y) {
  target.x.innerHTML = x;
  target.y.innerHTML = y;
  drawPath(x, y);
}

// 通用路径绘制逻辑
function drawPath(x, y) {
  ctx.shadowColor = "rgba(0,0,0,.5)";
  ctx.shadowBlur = 2;
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';
  ctx.lineWidth = 2;
  ctx.strokeStyle = 'red';
  
  if (!inProgress) {
    ctx.beginPath();
    ctx.moveTo(x, y);
    inProgress = true;
    skip1 = true;
    skip2 = false;
  } else {
    if (skip1) {
      cp1x = x;
      cp1y = y;
      skip1 = false;
      skip2 = true;
    } else if (skip2) {
      cp2x = x;
      cp2y = y;
      skip1 = false;
      skip2 = false;
    } else {
      ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
      skip1 = true;
      skip2 = false;
    }
  }
  ctx.stroke();
}

function captureTouch() {
  surface.addEventListener('selectstart', function(e) {
    e.preventDefault();
    return false;
  }, false);
  surface.addEventListener('dragstart', function(e) {
    e.preventDefault();
    return false;
  }, false);
  surface.addEventListener('mousedown', function() {
    $('msg').style.setProperty('display', 'none');
    surface.addEventListener('mousemove', draw, false);
  }, false);
  surface.addEventListener('mouseup', function() {
    surface.removeEventListener('mousemove', draw, false);
    inProgress = false;
    ctx.save();
  }, false);
}

function init() {
  surface = $('surface');
  surface.width = document.body.clientWidth;
  surface.height = window.innerHeight;
  ctx = surface.getContext('2d');
  target = {
    x: $('x'),
    y: $('y')
  };
  captureTouch();
  document.body.addEventListener('touchmove', function(e) {
    e.preventDefault();
  }, false);
}

document.addEventListener('DOMContentLoaded', init, false);

说明

  • 修改replayInterval的值可以调整重放速度,比如设为50ms会比30ms更慢
  • 拆分后的drawPath函数复用了绘制逻辑,减少代码冗余
  • 重放前的状态重置确保每次重放都是独立的全新过程

内容的提问来源于stack exchange,提问作者user21188932

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 07:57:33