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++) {
    // ... 其他代码相同
  }

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

此时效果如下

相关推荐
大厂在职_fUk13 分钟前
Flutter完整开发实战详解(六、 深入Widget原理)
前端·javascript·flutter
liuhaikang22 分钟前
【鸿蒙HarmonyOS Next实战开发】实现组件动态创建和卸载-优化性能
java·前端·数据库
m0_748256141 小时前
Spring boot整合quartz方法
java·前端·spring boot
修己xj1 小时前
MediaGo:跨平台视频提取下载的开源神器
前端
m0_528723811 小时前
HTML5 新特性有哪些?
前端·html·html5
山野春茶1 小时前
响应式布局
前端
山野春茶1 小时前
Ajax原理和跨域问题解决方案
前端
林涧泣1 小时前
【Uniapp-Vue3】z-paging插件组件实现触底和下拉加载数据
前端·vue.js·uni-app
lbh2 小时前
调用 useState 之后发生了什么
前端·react.js
浪浪山小白兔2 小时前
CSS 伪类(Pseudo-classes)的详细介绍
前端·css