JavaScript 更平滑的乘法循环 可能的解决方案

问题描述

理念

我从 YouTube 视频中偶然发现了乘法圆的想法,我认为尝试使用 JavasSript 和 canvas 元素重新创建它会很有趣。 The Original Video

问题

我尽我所能地平滑了动画,但它看起来仍然没有我想要的那么合适。我怀疑想出一个解决方案需要大量的数学知识。详细掌握问题我觉得看代码比较容易

window.onload = () => {
  const app = document.querySelector('#app')
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')
  const { cos,sin,PI } = Math
  const Tao = PI * 2
  const width = window.innerWidth
  const height = window.innerHeight
  const cx = width / 2
  const cy = height / 2
  const baseNumberingSystem = 200
  const stop = 34
  let color = 'teal'
  let multiplier = 0
  let drawQue = []

  // setup canvas
  canvas.width = width
  canvas.height = height

  class Circle {
    constructor(x,y,r,strokeColor,fillColor) {
      this.x = x
      this.y = y
      this.r = r
      this.strokeColor = strokeColor || '#fff'
      this.fillColor = fillColor || '#fff'
    }

    draw(stroke,fill) {
      ctx.moveto(this.x,this.y)
      ctx.beginPath()
      ctx.arc(this.x,this.y,this.r,Tao)
      ctx.closePath()

      if (fill) {
        ctx.fillStyle = this.fillColor
        ctx.fill()
      }

      if (stroke) {
        ctx.strokeStyle = this.strokeColor
        ctx.stroke()
      }
    }

    createChildCircleAt(degree,radius,fillColor) {
      const radian = degreetoRadian(degree)
      const x = this.x + (this.r * cos(radian))
      const y = this.y + (this.r * sin(radian))
      return new Circle(x,fillColor)
    }

    divideCircle(nTimes,radius) {
      const degree = 360 / nTimes
      let division = 1;
      while (division <= nTimes) {
        drawQue.push(this.createChildCircleAt(division * degree,radius))
        division++
      }
    }
  }

  function degreetoRadian(degree) {
    return degree * (PI / 180)
  }

  function draw() {
    const mainCircle = new Circle(cx,cy,cy * 0.9)
    
    // empty the que
    drawQue = []
    // clear canvas
    ctx.clearRect(0,width,height)
    ctx.fillStyle = "black"
    ctx.fillRect(0,height)

    // redraw everything
    mainCircle.draw()
    mainCircle.divideCircle(baseNumberingSystem,4)
    drawQue.forEach(item => item.draw())

    // draw modular times table
    for (let i = 1; i <= drawQue.length; i++) {
      const product = i * multiplier;
      const firstPoint = drawQue[i]
      const secondPoint = drawQue[product % drawQue.length]

      if (firstPoint && secondPoint) {
        ctx.beginPath()
        ctx.moveto(firstPoint.x,firstPoint.y)
        ctx.strokeStyle = color
        ctx.lineto(secondPoint.x,secondPoint.y)
        ctx.closePath()
        ctx.stroke()
      }
    }
  }

  function animate() {
    multiplier+= 0.1
    multiplier = parseFloat(multiplier.toFixed(2))
    draw()
  
    console.log(multiplier,stop)

    if (multiplier === stop) {
      clearInterval(animation)
    }
  }

  app.appendChild(canvas)
  let animation = setInterval(animate,120)
}

所以主要问题来自于我们将乘数增加小于 1 的值以试图使动画感觉更流畅。示例:multiplier+= 0.1。当我们这样做时,它会增加我们绘制函数中的 if 块失败的次数,因为 secondPoint 将返回 null。

      const product = i * multiplier; // this is sometimes a decimal
      const firstPoint = drawQue[i]
      const secondPoint = drawQue[product % drawQue.length] // therefore this will often not be found

      // Then this if block wont execute. Which is good because if it did we the code would crash
      // But I think what we need is an if clause to still draw a line to a value in between the two
      // closest indices of our drawQue 
      if (firstPoint && secondPoint) {
        //...
      }

可能的解决方

我认为我需要做的是当我们找不到 secondPoint 时获取 product % drawQue.length 的剩余部分并使用它在 drawQue 数组中两个最近的圆之间创建一个新圆并使用它新圆作为我们线的第二个点。

解决方法

如果你使用 requestAnimationFrame 它看起来很流畅

function animate() {
    if (multiplier != stop) {
    multiplier+= 0.1
    multiplier = parseFloat(multiplier.toFixed(2))
    draw()
    requestAnimationFrame(animate);
    }
  }

  app.appendChild(canvas)
  animate()
,

我可能的解决方案最终奏效了。对于任何感兴趣的人,我会在此处留下添加的 else if 块。我还必须在制作圆对象时将度数值存储在它们中,并计算圆的每个细分之间的距离。

添加 If Else 语句

for (let i = 1; i <= drawQue.length; i++) {
  const product = i * multiplier;
  const newIndex = product % drawQue.length
  const firstPoint = drawQue[i]
  const secondPoint = drawQue[newIndex]

  if (firstPoint && secondPoint) {
    ctx.beginPath()
    ctx.moveTo(firstPoint.x,firstPoint.y)
    ctx.strokeStyle = color
    ctx.lineTo(secondPoint.x,secondPoint.y)
    ctx.closePath()
    ctx.stroke()
  } else if (!secondPoint) {
    const percent = newIndex % 1
    const closest = drawQue[Math.floor(newIndex)]
    const newDegree = closest.degree + (degreeIncriments * percent)
    const target = mainCircle.createChildCircleAt(newDegree,4)

    if (firstPoint && target) {
      ctx.beginPath()
      ctx.moveTo(firstPoint.x,firstPoint.y)
      ctx.strokeStyle = color
      ctx.lineTo(target.x,target.y)
      ctx.closePath()
      ctx.stroke()
    }
  }

其他变化

  // ...
  const degreeIncriments = 360 / baseNumberingSystem
  // ... 
  class Circle {
    constructor(/* ... */,degree )
      // ... 
      this.degree = degree || 0
  }

希望有人觉得这很有用...