如何在HTML5Canvas中画出弯曲的spring?

How to draw a curved spring in a HTML5 Canvas?

我想在 HTML5 canvas 中绘制一个 spring,并显示 spring 是否处于静止长度。 我的 spring 附加到一些 X-Y 坐标的矩形上,定义如下:

function Spring(restLenght, width, numRounds){
  this.x1 = 0;
  this.y1 = 0;
  this.x2 = 0;
  this.y2 = 0;
  this.restLenght = restLenght;
  this.width = width;
  this.numRounds = numRounds;
  this.color = "green";
  this.lineWidth = 6;
}

参数说明如下图:

当spring处于静止长度时,线条应相互平行,否则表示spring被拉伸或压缩。那么马上就清楚spring是什么状态了。

我现在被 bezierCurveTo() 方法困住了:

这是我的Fiddle:https://jsfiddle.net/df3mm8kz/1/

var cv = document.getElementById('cv'),
  ctx = cv.getContext('2d'),
  mouse = capture(cv),
  box = new Box(120, 80, 0, 16),
  spring = new Spring(160, 20, 2, 0.03, 0.9),
  vx = 0,
  vy = 0;
function Spring(restLenght, width, numRounds, k, f){
 this.x1 = 0;
  this.y1 = 0;
  this.x2 = 0;
  this.y2 = 0;
  this.restLenght = restLenght;
  this.width = width;
  this.numRounds = numRounds;
  this.k = k;
  this.f = f;
  this.color = "green";
  this.lineWidth = 6;
}

Spring.prototype.draw = function(ctx) {
 var sPX, sPY, cP1X, cP1Y, cP2X, cP2Y, ePX, ePY;
  ctx.save();
  ctx.translate(this.x, this.y);
  ctx.rotate(this.rotation);
  ctx.lineWidth = this.lineWidth;
  ctx.strokeStyle = this.color;
  ctx.fillStyle = this.color;
  ctx.beginPath();
  ctx.moveTo(this.x1, this.y1);
  // length of one spring's round
  var l = this.restLenght/(this.numRounds + 2);
  // Initial segment, from spring anchor point to the first round
  sPX = this.x1+l; sPY = this.y2;
  ctx.lineTo(sPX, sPY);
  // half width of spring's rounds
  var hw = 0.5*this.width;
  // half length of one spring's round
  var hl = 0.5*l;
  for(var i=0, n=this.numRounds; i<n; i++) {
   cP1X = sPX + hl*i; cP1Y = sPY + hw;
    cP2X = sPX + l*i; cp2Y = sPY + hw;
    ePX = sPX + l*i; ePY = sPY;
   ctx.bezierCurveTo(cP1X,cP1Y,cP2X,cp2Y,ePX,ePY);
   cP1X = sPX + hl*i; cP1Y = sPY - hw;
    cP2X = sPX + l*i; cp2Y = sPY - hw;
    ePX = sPX + l*i; ePY = sPY;
   ctx.bezierCurveTo(cP1X,cP1Y,cP2X,cp2Y,ePX,ePY);
  }
  // Final segment, from last springs round to the center of mass
  ctx.lineTo(this.x2, this.y2);
  ctx.closePath();
  ctx.fill();
  ctx.stroke();
  ctx.restore();
};

function Box(w, h, mx, my) {
  this.x = 0;
  this.y = 0;
  this.w = w;
  this.h = h;
  this.mx = mx;
  this.my = my;
  this.vx = 0;
  this.vy = 0;
  this.rotation = 0;
  this.color = "red";
  this.lineWidth = 1;
}

Box.prototype.draw = function(ctx) {
  ctx.save();
  ctx.translate(this.x, this.y);
  ctx.rotate(this.rotation);
  ctx.lineWidth = this.lineWidth;
  ctx.strokeStyle = "black";
  ctx.fillStyle = this.color;
  ctx.beginPath();
  ctx.rect(-0.5*this.w, -0.5*this.h, this.w, this.h);
  ctx.closePath();
  ctx.fill();
  ctx.stroke();
  ctx.beginPath();
  ctx.strokeStyle = "yellow";
  ctx.fillStyle = "yellow";
  ctx.arc(this.mx, 0.5*this.h-this.my, 6, 0, 2 * Math.PI, false);
  ctx.stroke();
  ctx.closePath();
  ctx.fill();
  ctx.restore();
};

window.requestAnimFrame = (
  function(callback) {
    return window.setTimeout(callback, 1000/30);
  });

(function drawFrame() {
  window.requestAnimFrame(drawFrame, cv);
  ctx.clearRect(0, 0, cv.width, cv.height);

  var dx = box.x - mouse.x,
    dy = box.y - mouse.y,
    angle = Math.atan2(dy, dx),
    boxAngle = angle + 0.5*Math.PI,
    targetX = mouse.x + Math.cos(angle) * spring.restLenght,
    targetY = mouse.y + Math.sin(angle) * spring.restLenght;
   
  vx += (targetX - box.x) * spring.k;
  vy += (targetY - box.y) * spring.k;
  vx *= spring.f;
  vy *= spring.f;
  box.rotation = boxAngle;
  box.x += vx;
  box.y += vy;
  box.draw(ctx);
  spring.x1 = mouse.x;
  spring.y1 = mouse.y;
  spring.x2 = box.x;
  spring.y2 = box.y;
  spring.draw(ctx);
}());


function capture(element) {
  var mouse = {
      x: 0,
      y: 0,
      event: null
    },
    body_scrollLeft = document.body.scrollLeft,
    element_scrollLeft = document.documentElement.scrollLeft,
    body_scrollTop = document.body.scrollTop,
    element_scrollTop = document.documentElement.scrollTop,
    offsetLeft = element.offsetLeft,
    offsetTop = element.offsetTop;

  element.addEventListener('mousemove', function(event) {
    var x, y;
    if (event.pageX || event.pageY) {
      x = event.pageX;
      y = event.pageY;
    } else {
      x = event.clientX + body_scrollLeft + element_scrollLeft;
      y = event.clientY + body_scrollTop + element_scrollTop;
    }
    x -= offsetLeft;
    y -= offsetTop;
    mouse.x = x;
    mouse.y = y;
    mouse.event = event;
  }, false);

  return mouse;
}
<canvas id="cv" width="600" height="400"></canvas>

画一个spring

我没有使用实际上不符合 spring 曲线(但接近)的贝塞尔曲线,而是使用简单的路径并使用三角函数绘制每个绕组。该函数有一个开始 x1,y1 和结束 x2,y2,绕组(应该是一个整数),spring 的宽度,偏移量(末端的位),深色和浅色,以及笔画宽度(线的宽度)。

该演示画了一个额外的亮点,使 spring 更深入一点。它可以很容易地删除。

代码来自 ,它具有相同功能的更简单版本

    function drawSpring(x1, y1, x2, y2, windings, width, offset, col1, col2, lineWidth){
        var x = x2 - x1;
        var y = y2 - y1;
        var dist = Math.sqrt(x * x + y * y);
        
        var nx = x / dist;
        var ny = y / dist;
        ctx.strokeStyle = col1
        ctx.lineWidth = lineWidth;
        ctx.lineJoin = "round";
        ctx.lineCap = "round";
        ctx.beginPath();
        ctx.moveTo(x1,y1);
        x1 += nx * offset;
        y1 += ny * offset;
        x2 -= nx * offset;
        y2 -= ny * offset;
        var x = x2 - x1;
        var y = y2 - y1;
        var step = 1 / (windings);
        for(var i = 0; i <= 1-step; i += step){  // for each winding
            for(var j = 0; j < 1; j += 0.05){
                var xx = x1 + x * (i + j * step);
                var yy = y1 + y * (i + j * step);
                xx -= Math.sin(j * Math.PI * 2) * ny * width;
                yy += Math.sin(j * Math.PI * 2) * nx * width;
                ctx.lineTo(xx,yy);
            }
        }
        ctx.lineTo(x2, y2);
        ctx.lineTo(x2 + nx * offset, y2 + ny * offset)
        ctx.stroke();
        ctx.strokeStyle = col2
        ctx.lineWidth = lineWidth - 4;
        var step = 1 / (windings);
        ctx.beginPath();
        ctx.moveTo(x1 - nx * offset, y1 - ny * offset);
        ctx.lineTo(x1, y1);
        ctx.moveTo(x2, y2);
        ctx.lineTo(x2 + nx * offset, y2 + ny * offset)
        for(var i = 0; i <= 1-step; i += step){  // for each winding
            for(var j = 0.25; j <= 0.76; j += 0.05){
                var xx = x1 + x * (i + j * step);
                var yy = y1 + y * (i + j * step);
                xx -= Math.sin(j * Math.PI * 2) * ny * width;
                yy += Math.sin(j * Math.PI * 2) * nx * width;
                if(j === 0.25){
                    ctx.moveTo(xx,yy);
                
                }else{
                    ctx.lineTo(xx,yy);
                }
            }
        }
        ctx.stroke();
    }

    function display() { 
        ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
        ctx.globalAlpha = 1; // reset alpha
        ctx.clearRect(0, 0, w, h);
        ctx.lineWidth = 8;
        drawSpring(canvas.width / 2,10, mouse.x,mouse.y,8,100,40,"green","#0C0",15);
    }



    // Boiler plate code from here down and not part of the answer
    var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
    ;(function(){
        const RESIZE_DEBOUNCE_TIME = 100;
        var  createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
        createCanvas = function () {
            var c,
            cs;
            cs = (c = document.createElement("canvas")).style;
            cs.position = "absolute";
            cs.top = cs.left = "0px";
            cs.zIndex = 1000;
            document.body.appendChild(c);
            return c;
        }
        resizeCanvas = function () {
            if (canvas === undefined) {
                canvas = createCanvas();
            }
            canvas.width = innerWidth;
            canvas.height = innerHeight;
            ctx = canvas.getContext("2d");
            if (typeof setGlobals === "function") {
                setGlobals();
            }
            if (typeof onResize === "function") {
                if(firstRun){
                    onResize();
                    firstRun = false;
                }else{
                    resizeCount += 1;
                    setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
                }
            }
        }
        function debounceResize() {
            resizeCount -= 1;
            if (resizeCount <= 0) {
                onResize();
            }
        }
        setGlobals = function () {
            cw = (w = canvas.width) / 2;
            ch = (h = canvas.height) / 2;
        }
        mouse = (function () {
            function preventDefault(e) {
                e.preventDefault();
            }
            var mouse = {
                x : 0,
                y : 0,
                w : 0,
                alt : false,
                shift : false,
                ctrl : false,
                buttonRaw : 0,
                over : false,
                bm : [1, 2, 4, 6, 5, 3],
                active : false,
                bounds : null,
                crashRecover : null,
                mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
            };
            var m = mouse;
            function mouseMove(e) {
                var t = e.type;
                m.bounds = m.element.getBoundingClientRect();
                m.x = e.pageX - m.bounds.left;
                m.y = e.pageY - m.bounds.top;
                m.alt = e.altKey;
                m.shift = e.shiftKey;
                m.ctrl = e.ctrlKey;
                if (t === "mousedown") {
                    m.buttonRaw |= m.bm[e.which - 1];
                } else if (t === "mouseup") {
                    m.buttonRaw &= m.bm[e.which + 2];
                } else if (t === "mouseout") {
                    m.buttonRaw = 0;
                    m.over = false;
                } else if (t === "mouseover") {
                    m.over = true;
                } else if (t === "mousewheel") {
                    m.w = e.wheelDelta;
                } else if (t === "DOMMouseScroll") {
                    m.w = -e.detail;
                }
                if (m.callbacks) {
                    m.callbacks.forEach(c => c(e));
                }
                if ((m.buttonRaw & 2) && m.crashRecover !== null) {
                    if (typeof m.crashRecover === "function") {
                        setTimeout(m.crashRecover, 0);
                    }
                }
                e.preventDefault();
            }
            m.addCallback = function (callback) {
                if (typeof callback === "function") {
                    if (m.callbacks === undefined) {
                        m.callbacks = [callback];
                    } else {
                        m.callbacks.push(callback);
                    }
                }
            }
            m.start = function (element) {
                if (m.element !== undefined) {
                    m.removeMouse();
                }
                m.element = element === undefined ? document : element;
                m.mouseEvents.forEach(n => {
                    m.element.addEventListener(n, mouseMove);
                });
                m.element.addEventListener("contextmenu", preventDefault, false);
                m.active = true;
            }
            m.remove = function () {
                if (m.element !== undefined) {
                    m.mouseEvents.forEach(n => {
                        m.element.removeEventListener(n, mouseMove);
                    });
                    m.element.removeEventListener("contextmenu", preventDefault);
                    m.element = m.callbacks = undefined;
                    m.active = false;
                }
            }
            return mouse;
        })();

        // Clean up. Used where the IDE is on the same page.
        var done = function () {
            window.removeEventListener("resize", resizeCanvas)
            mouse.remove();
            document.body.removeChild(canvas);
            canvas = ctx = mouse = undefined;
        }


        function update(timer) { // Main update loop
            if(ctx === undefined){
                return;
            }
            globalTime = timer;
            display(); // call demo code
            if (!(mouse.buttonRaw & 2)) {
                requestAnimationFrame(update);
            } else {
                done();
            }
        }
        setTimeout(function(){
            resizeCanvas();
            mouse.start(canvas, true);
            mouse.crashRecover = done;
            window.addEventListener("resize", resizeCanvas);
            requestAnimationFrame(update);
        },0);
    })();
    /** SimpleFullCanvasMouse.js end **/

为了使绘图更容易,请使用 .translate().rotate() 移动到对齐的坐标系中。

ctx.translate(this.x1, this.y1);
ctx.rotate(Math.atan2(this.y2 - this.y1, this.x2 - this.x1));

然后你可以沿着局部x-axis绘制spring,它会出现在正确的位置和旋转。

您的段间距有误。 hl*i 是 spring 起点距离的一半,而不是线段起点的距离。

var cv = document.getElementById('cv'),
  ctx = cv.getContext('2d'),
  mouse = capture(cv),
  box = new Box(120, 80, 0, 16),
  spring = new Spring(160, 50, 2, 0.03, 0.9),
  vx = 0,
  vy = 0;

function Spring(restLenght, width, numRounds, k, f) {
  this.x1 = 0;
  this.y1 = 0;
  this.x2 = 0;
  this.y2 = 0;
  this.restLenght = restLenght;
  this.width = width;
  this.numRounds = numRounds;
  this.k = k;
  this.f = f;
  this.color = "green";
  this.lineWidth = 6;
}

Spring.prototype.draw = function(ctx) {
  var sPX, sPY, cP1X, cP1Y, cP2X, cP2Y, ePX, ePY;
  ctx.save();
  ctx.lineWidth = this.lineWidth;
  ctx.strokeStyle = this.color;
  ctx.fillStyle = this.color;

  var vx = this.x2 - this.x1;
  var vy = this.y2 - this.y1;
  var vm = Math.sqrt(vx * vx + vy * vy);
  ctx.translate(this.x1, this.y1);
  ctx.rotate(Math.atan2(vy, vx));

  ctx.beginPath();
  ctx.moveTo(0, 0);
  // length of one spring's round
  var l = vm / (this.numRounds + 2);
  // Initial segment, from spring anchor point to the first round
  sPX = l;
  sPY = 0;
  ctx.lineTo(sPX, sPY);
  // half width of spring's rounds
  var hw = 0.5 * this.width;
  for (var i = 0, n = this.numRounds; i < n; i++) {
    cP1X = sPX + l * (i + 0.0);
    cP1Y = sPY + hw;
    cP2X = sPX + l * (i + 0.5);
    cp2Y = sPY + hw;
    ePX = sPX + l * (i + 0.5);
    ePY = sPY;
    ctx.bezierCurveTo(cP1X, cP1Y, cP2X, cp2Y, ePX, ePY);
    cP1X = sPX + l * (i + 0.5);
    cP1Y = sPY - hw;
    cP2X = sPX + l * (i + 1.0);
    cp2Y = sPY - hw;
    ePX = sPX + l * (i + 1.0);
    ePY = sPY;
    ctx.bezierCurveTo(cP1X, cP1Y, cP2X, cp2Y, ePX, ePY);
  }
  // Final segment, from last springs round to the center of mass
  ctx.lineTo(vm, 0);
  //ctx.closePath();
  //ctx.fill();
  ctx.stroke();
  ctx.restore();
};

function Box(w, h, mx, my) {
  this.x = 0;
  this.y = 0;
  this.w = w;
  this.h = h;
  this.mx = mx;
  this.my = my;
  this.vx = 0;
  this.vy = 0;
  this.rotation = 0;
  this.color = "red";
  this.lineWidth = 1;
}

Box.prototype.draw = function(ctx) {
  ctx.save();
  ctx.translate(this.x, this.y);
  ctx.rotate(this.rotation);
  ctx.lineWidth = this.lineWidth;
  ctx.strokeStyle = "black";
  ctx.fillStyle = this.color;
  ctx.beginPath();
  ctx.rect(-0.5 * this.w, -0.5 * this.h, this.w, this.h);
  ctx.closePath();
  ctx.fill();
  ctx.stroke();
  ctx.beginPath();
  ctx.strokeStyle = "yellow";
  ctx.fillStyle = "yellow";
  ctx.arc(this.mx, 0.5 * this.h - this.my, 6, 0, 2 * Math.PI, false);
  ctx.stroke();
  ctx.closePath();
  ctx.fill();
  ctx.restore();
};

window.requestAnimFrame = (
  function(callback) {
    return window.setTimeout(callback, 1000 / 30);
  });

(function drawFrame() {
  window.requestAnimFrame(drawFrame, cv);
  ctx.clearRect(0, 0, cv.width, cv.height);

  var dx = box.x - mouse.x,
    dy = box.y - mouse.y,
    angle = Math.atan2(dy, dx),
    boxAngle = angle + 0.5 * Math.PI,
    targetX = mouse.x + Math.cos(angle) * spring.restLenght,
    targetY = mouse.y + Math.sin(angle) * spring.restLenght;

  vx += (targetX - box.x) * spring.k;
  vy += (targetY - box.y) * spring.k;
  vx *= spring.f;
  vy *= spring.f;
  box.rotation = boxAngle;
  box.x += vx;
  box.y += vy;
  box.draw(ctx);
  spring.x1 = mouse.x;
  spring.y1 = mouse.y;
  spring.x2 = box.x;
  spring.y2 = box.y;
  spring.draw(ctx);
}());


function capture(element) {
  var mouse = {
      x: 0,
      y: 0,
      event: null
    },
    body_scrollLeft = document.body.scrollLeft,
    element_scrollLeft = document.documentElement.scrollLeft,
    body_scrollTop = document.body.scrollTop,
    element_scrollTop = document.documentElement.scrollTop,
    offsetLeft = element.offsetLeft,
    offsetTop = element.offsetTop;

  element.addEventListener('mousemove', function(event) {
    var x, y;
    if (event.pageX || event.pageY) {
      x = event.pageX;
      y = event.pageY;
    } else {
      x = event.clientX + body_scrollLeft + element_scrollLeft;
      y = event.clientY + body_scrollTop + element_scrollTop;
    }
    x -= offsetLeft;
    y -= offsetTop;
    mouse.x = x;
    mouse.y = y;
    mouse.event = event;
  }, false);

  return mouse;
}
<canvas id="cv" width="600" height="400"></canvas>