Canvas系列(20):画布中画满圆

今天的内容比较简单,我们学习如何在画布中画满圆。要求圆与圆之间不能相交,最终效果如下。


HTML结构

首先我们先展示我们基础的HTML结构。

html 复制代码
<style type="text/css">
  #canvas{
    background: #eeeeee;
    border: 1px solid #000000;
  }
</style>

<canvas id="canvas" width="600" height="400"></canvas>

这里绘制一个600*400的画布,并设置背景色为灰色,边框为1px的黑色。

绘制一个圆

我们从简单的开始,先绘制一个圆。由于我们这里不需要动画,所以不需要 update 方法,只需要 draw 方法即可。

JavaScript 复制代码
const canvas = document.querySelector('canvas')
const context = canvas.getContext('2d')

context.lineWidth = 2

class Circle {
  constructor(options = {}) {
    this.x = options.x || 0
    this.y = options.y || 0
    this.radius = options.radius || minRadius
  }

  draw() {
    context.beginPath()
    context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
    context.stroke()
  }
}

const circle = new Circle({
  x: canvas.width / 2,
  y: canvas.height / 2,
  radius: 100
});

circle.draw()

我们在画布的中心绘制了1个半径为100px的圆。效果如下

绘制500个圆

上面,我们绘制了1个圆,现在绘制500个圆。

JavaScript 复制代码
const minRadius = 2
const maxRadius = 120
const totalCircles = 500

function createAndDrawCircle() {
  const newCircle = new Circle({
    x: Math.floor(Math.random() * canvas.width),
    y: Math.floor(Math.random() * canvas.height),
    radius: 10
  })
  newCircle.draw()
}

for(let i = 0; i < totalCircles; i++) {
  createAndDrawCircle()
}

上面代码在随机位置生成了500个半径是10px的小圆,效果如下

绘制500个不相交的圆

上面绘制了500个圆,他们可能是相交的,现在我们绘制500个不相交的圆。 我们上面绘制的随机坐标很可能在某个圆内,为了避免这种情况,我们需要先判断圆心坐标是否在某个圆内。如果在某个圆内我们需要舍弃这个坐标,重新生成一个新坐标;如果不在某个圆内,那么我们可以在这里绘制圆。到底绘制多大圆呢?如果绘制的太大了则可能与旁边的圆相交或者超出画布边界,我们这里绘制一个尽可大的圆,尽可能大意味着刚好与其他圆或者边界相切。如果绘制一个这样的大圆呢?这里可以先使用很小的圆来计算,让其半径慢慢增大,当增大到刚好与某个圆或者边界相切时,则绘制它。如果一直不相切,我们最好给一个圆的最大半径,这样可以保证圆不会太大。

算法:

  1. 生成随机圆心坐标;
  2. 判断圆心坐标是否在某个圆内,如果在某个圆内,则舍弃这个坐标,重新生成一个新坐标(最坏情况下,可能一直没有符合要求的新坐标,则应该给限制,如最多尝试生成新坐标的500次);
  3. 如果圆心坐标不在某个圆内,则让圆的半径逐渐增大,从而找到最大刚好跟其它圆或边界相切的最大圆;
  4. 绘制圆;
  5. 重复1~4步,直到所有圆绘制完毕(我们这里限定最多绘制500个圆)。
JavaScript 复制代码
const circles = []
const createCircleAttempts = 500

function createAndDrawCircle() {
  let newCircle
  let circleSafeToDraw = false
  // 最多尝试500次寻找圆外坐标
  for(let tries = 0; tries < createCircleAttempts; tries++) {
    // 步骤1,生成随机圆心坐标
    newCircle = new Circle({
      x: Math.floor(Math.random() * canvas.width),
      y: Math.floor(Math.random() * canvas.height),
      radius: 10
    })

    // 步骤2,判断圆心坐标是否在某个圆内 如果与其他圆或者边界相交则舍弃当前坐标,重新生成一个新坐标,不相交则标记可以绘制
    if(doesCircleHaveACollision(newCircle)) {
      continue;
    } else {
      circleSafeToDraw = true
      break;
    }
  }

  // 如果循环500次都没有找到合适的新坐标则不绘制了
  if(!circleSafeToDraw) return

  // 保存需要绘制的圆
  circles.push(newCircle)
  // 步骤4,绘制圆
  newCircle.draw()
}

首先我们定义 circles 数组用来存放生成好的圆,createCircleAttempts 表示最多尝试 500 次寻找其他圆外新坐标。上面代码结合注释还是比较好理解的,我们还没有实现 doesCircleHaveACollision 方法,下面我们来实现它。

JavaScript 复制代码
function doesCircleHaveACollision(circle) {
  for(let i = 0; i < circles.length; i++) {
    const otherCircle = circles[i]
    const a = circle.radius + otherCircle.radius
    const x = circle.x - otherCircle.x
    const y = circle.y - otherCircle.y

    if (a >= Math.hypot(x, y)) {
      return true;
    }
  }

  if(circle.x + circle.radius >= canvas.width ||
    circle.x - circle.radius <= 0) {
    return true;
  }

  if(circle.y + circle.radius >= canvas.height ||
      circle.y - circle.radius <= 0) {
    return true;
  }

  return false
}

当调用 doesCircleHaveACollision(circle) 方法的时候,新圆并没有加入到 circles 数组中,所以我们只需要判断新圆是否与数组中的圆相交并判断是否与边界相交就可以了。圆与圆的相交可以通过圆心间的距离跟半径之和做比较来判断,圆与边界可以通过圆的坐标和半径跟上下左右边界的距离做比较来判断。对碰撞检测感兴趣的同学可以翻看之前的文章

此时效果如下

绘制500个不相交且大小不等的圆

上面我们少了第3步,只绘制了大小都是10px的圆,现在我们处理一下第3步,实际上也不复杂。

JavaScript 复制代码
// 最小半径
const minRadius = 2
// 最大半径
const maxRadius = 120

function createAndDrawCircle() {
  let newCircle
  let circleSafeToDraw = false
  // 最多尝试500次寻找圆外坐标
  for(let tries = 0; tries < createCircleAttempts; tries++) {
    // 步骤1,生成随机圆心坐标
    newCircle = new Circle({
      x: Math.floor(Math.random() * canvas.width),
      y: Math.floor(Math.random() * canvas.height),
      radius: minRadius, // 这里初始化使用最小半径
    })

    // 步骤2,判断圆心坐标是否在某个圆内 如果与其他圆或者边界相交则舍弃当前坐标,重新生成一个新坐标,不相交则标记可以绘制
    if(doesCircleHaveACollision(newCircle)) {
      continue;
    } else {
      circleSafeToDraw = true
      break;
    }
  }
  
  // 如果循环500次都没有找到合适的新坐标则不绘制了
  if(!circleSafeToDraw) return

  // 步骤3,让圆的半径逐渐增大,从而找到最大刚好跟其它圆或边界相切的圆
  for(let radiusSize = minRadius; radiusSize < maxRadius; radiusSize++) {
    newCircle.radius = radiusSize
    // 相切则跳出循环 由于描边宽度是2 所以需要减去1像素保证不相交
    if(doesCircleHaveACollision(newCircle)){
      newCircle.radius--
      break
    }
  }

  // 保存需要绘制的圆
  circles.push(newCircle)
  // 步骤4,绘制圆
  newCircle.draw()
}

此时的效果跟刚开始的效果一下了,点击这里查看

小优化

上面我们绘制圆的时候,由于第一个绘制的圆只受边界相交的限制,假设第一个圆的坐标在靠近中心的位置,就有很大概率绘制一个最大的圆,所以当你多次刷新网页的时候就会发现,通常有一个很大的圆,这样不是那么美观。我们现在让绘制的最大半径修改成 圆最大半径最小半径 + 1 之间的一个随机值,这样可以避免每次都有一个极大圆存在。

JavaScript 复制代码
function createAndDrawCircle() {
  // ... 其他代码相同

  // 新的最大值为圆最小值和最大值之间的随机值
  const max = Math.floor(Math.random() * (maxRadius - minRadius - 1) + minRadius + 1)
  for(let radiusSize = minRadius; radiusSize < max; radiusSize++) {
    // ... 其他代码相同
  }

  // ... 其他代码相同
}

此时效果如下

相关推荐
kyriewen2 分钟前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技3 分钟前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人14 分钟前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实14 分钟前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha25 分钟前
三目运算符
linux·服务器·前端
晓晨的博客32 分钟前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect40 分钟前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding1 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化
GISer_Jing1 小时前
AI全栈转型_TS后端学习路线
前端·人工智能·后端·学习
竹林8181 小时前
被The Graph的GraphQL查询坑了三天,我用一个真实DeFi项目把链上数据索引彻底搞懂了
前端·graphql