作者:肖剑华


  • 可视化是前端可视化
  • 图形是计算机图形学
  • 向量就是那个向量,高中学过的,你懂的
  • 树是那棵贼丑的树

后果


首先先看看本文最终的后果。



是不是贼丑!是不是能在画展上卖个好价格!

过程


好了,话不多说, 看看这棵贼丑的树是怎么诞生的吧。

坐标系


坐标系,或者说立体直角坐标系,是几何图形学的根底,其次是点、线、面这些元素。


坐标系大家都很相熟, 最后接触坐标系应该是初中, 那时候的坐标系不知大家还有没有印象。


原点在两头, 程度轴是 x 轴, 竖轴是 y 轴, 分为四个象限。


然而呢, html canvas 这货, 默认原点在左上角, x 轴是跟立体直角坐标系是统一的, y 轴是向下的!!
置信这种坐标轴在日常工作中应用 canvas 绘图给前端人不晓得造成过多少麻烦, 计算起来麻烦费劲, 还容易出 bug。


那么如何把 canvas 的坐标系变成立体直角坐标系呢

Maaaaaaaaagic!
const canvas = document.querySelector('canvas')const ctx = canvas.getContext('2d')// 咱们这里把原点定位在canvas左下角ctx.translate(0, canvas.height)// 关键步骤: 将canvasY轴方向翻转ctx.scale(1, -1)


两行代码, 就实现了对坐标系的翻转。


咱们用一个 ???? 来验证一下


假如,咱们要在宽 512 * 高 256 的一个 Canvas 画布上实现如下的视觉效果。其中,山的高度是 100,底边 200,两座山的核心地位到中线的间隔都是 80,太阳的圆心高度是 150。


咱们这里应用 rough.js 减少一下趣味性

<canvas  width="512"  height="256"  style="display: block;margin: 0 auto;background-color: #ccc"></canvas>
const canvas = document.querySelector('canvas')const rc = rough.canvas(canvas)rc.ctx.translate(0, canvas.height)rc.ctx.scale(1, -1)const cSun = [canvas.width / 2, 106]const diameter = 100 // 直径const hill1Points = {  start: [76, 0], // 起始点  top: [176, 100], // 顶点  end: [276, 0] // 起点}const hill2Points = {  start: [236, 0], // 起始点  top: [336, 100], // 顶点  end: [436, 0] // 起点}const hill1Options = {  roughness: 0.8,  stokeWidth: 2,  fill: 'pink'}const hill2Options = {  roughness: 0.8,  stokeWidth: 2,  fill: 'chocolate'}function createHillPath(point) {  const { start, top, end } = point  return `M${start[0]} ${start[1]}L${top[0]} ${top[1]}L${end[0]} ${end[1]}`}function paint() {  rc.path(createHillPath(hill1Points), hill1Options)  rc.path(createHillPath(hill2Points), hill2Options)  rc.circle(cSun[0], cSun[1], diameter, {    stroke: 'red',    strokeWidth: 4,    fill: 'rgba(255, 255, 0, 0.4)',    fillStyle: 'solid'  })}paint()


这里咱们翻转了坐标系, 定义了 mountain1,mountain2,太阳 的各个点的坐标, 齐全是参照直角坐标系的坐标。


最终的实现成果如下





(是不是也能在画展上卖个不错的价格)

向量

定义


说完直角坐标系的转换, 咱们来探讨明天的正主, 向量(Vector)


向量的广泛定义是具备大小和方向的量, 咱们这里探讨的向量是 几何向量, 是用一组立体直角坐标系的坐标示意的
例如 (1, 1), 意思是, 顶点坐标为 x 为 1,y 为 0 的一条有向线段, 向量的方向是由 原点(0, 0) 指向顶点(1,1)的方向。


换言之, 晓得了向量的顶点, 就晓得了向量的大小和方向

向量的模


向量的大小也叫向量的模,是向量坐标的平方和的算术平方根, length = Math.pow((x2 + y2), 0.5)。

向量的方向


向量的方向一方面能够应用向量的顶点示意。


另外一方面应用向量和 x 轴的夹角,也可能示意一个向量。


应用 javascript Math 的内置办法能够失去,计算形式:

// 构造函数在本文稍后的中央介绍const v = new Vector2D(1, 10)const dir = Math.atan2(v.y, v.x)

四则运算

加减法


示意图:



如图所示: 向量 v1(x1, y1)和向量 v2(x2, y2)相加失去的新的向量就是两个向量对应坐标之和, 用公式表白就是
v1(x1, y1) + v2(x2, y2) = v3(x1 + x2, y1 + y2)


反之就是减法 v3(x1 + x2, y1 + y2) - v2 (x2, y2)= v1(x1, y1)

乘除


向量乘法有 叉乘和点乘

点乘示意图:





物理意义是, 方向为 va 方向,大小为 va.length 的力, 沿 vb 方向拉动 vb.length 间隔所做的功


va vb = va.length vb.length * cos(rad)

叉乘示意图:





va vb = va.length va.length * sin(rad)


也能够了解为长度为 va.length 的线段沿着 vb 方向挪动到 vb 顶点扫过的面积, 反之就是除法


乘除这里仅做概念上的介绍

单位向量


长度为 1 的向量叫做单位向量, 满足这个条件的向量有无数条, 一个非 0 的向量除以他的模,就是这个向量的单位向量, 咱们取与 x 轴夹角为 0 的向量:[1, 0]作为单位向量

向量的旋转


将一个向量转动肯定的角度 rad 之后的向量该如何计算呢。
这里有比较复杂的推导过程, 因而能够间接记住论断。


具体代码在上面构造函数外面展现

结构器

// 用一个长度为2的数组示意一个向量, 下标为0的地位示意x 下标为1的地位示意 yclass Vector2D extends Array {  constructor(x = 1, y = 0) {    super(x, y)  }  get x() {    return this[0]  }  get y() {    return this[1]  }  set x(v) {    this[0] = v  }  set y(v) {    this[1] = v  }  add(v) {    this.x = this.x + v.x    this.y = this.y + v.y    return this  }  length() {    return Math.hypot(this.x, this.y)  }  rotate(rad) {    const c = Math.cos(rad)    const s = Math.sin(rad)    const [x, y] = this    this.x = x * c + y * -s    this.y = x * s + y * c    return this  }}


至此,画出文章结尾的那个图形的基本要素都曾经筹备好了。
上面, 让咱们来见证一下世界名画的产生。

入手画图

  1. 筹备一个 512 * 512 的画布
<html>  ...  <canvas    width="512"    height="512"    style="display:block;margin:0 auto;background-color: #ccc"  ></canvas>  ...</html>
  1. 翻转 canvas 坐标系
const canvas = document.querySelector('canvas')const ctx = canvas.getContext('2d')ctx.translate(0, canvas.height)ctx.scale(1, -1)
  1. 定义绘制树枝的办法
/** * 1. ctx canvas ctx 上下文对象 * 2. 起始向量 * 3. length 向量长度(树枝长度) * 4. thickness 线段宽度 * 5. 单位向量 dir 旋转角度 * 6. bias 随机因子 */const canvas = document.querySelector('canvas')const ctx = canvas.getContext('2d')ctx.translate(0, canvas.height)ctx.scale(1, -1)ctx.lineCap = 'round'console.log(canvas.width)const v0 = new Vector2D(canvas.width / 2, 0)function drawBranch(ctx, v0, length, thickness, rad, bias) {  const v = new Vector2D().rotate(rad).scale(length)  console.log(v, rad, length)  const v1 = v0.copy().add(v)  ctx.beginPath()  ctx.lineWidth = thickness  ctx.moveTo(...v0)  ctx.lineTo(...v1)  ctx.stroke()  ctx.closePath()}// 定义好了之后咱们先画一个树枝试试看drawBranch(ctx, v0, 50, 10, Math.PI / 2, 1)
  1. 递归画图
// 先定义膨胀系数const LENGTH_SHRINK = 0.9const THICKNESS_SHRINK = 0.8const RAD_SHRINK = 0.5const BIAS_SHRINK = 1function drawBranch(ctx, v0, length, thickness, rad, bias) {  // ....  if (thickness > 2) {    // 画左树枝    const left =      Math.PI / 4 +      RAD_SHRINK * (rad + 0.2) +      drawBranch(        ctx,        v1,        length * LENGTH_SHRINK,        thickness * THICKNESS_SHRINK,        left,        bias      )    // 画右树枝    const right = Math.PI / 4 + RAD_SHRINK * (rad - 0.2)    drawBranch(      ctx,      v1,      length * LENGTH_SHRINK,      thickness * THICKNESS_SHRINK,      right,      bias    )  }}drawBranch(ctx, v0, 50, 10, Math.PI / 2, 1)


这一步画进去的是一个比拟规定的形态, 代码写到这一步,树的根本形态曾经进去了,然而 为了展现成果, 向量翻转上加一些随机性来画一颗更加靠近天然状态的树。代码如下:

function drawBranch(ctx, v0, length, thickness, rad, bias) {  // ....  if (thickness > 2) {    // 画左树枝    const left =      Math.PI / 4 + RAD_SHRINK * (rad + 0.2) + bias * (Math.random() - 0.5) // 加些随机数    drawBranch(      ctx,      v1,      length * LENGTH_SHRINK,      thickness * THICKNESS_SHRINK,      left,      bias    )    // 画右树枝    const right =      Math.PI / 4 + RAD_SHRINK * (rad - 0.2) + bias * (Math.random() - 0.5) // 加些随机数    drawBranch(      ctx,      v1,      length * LENGTH_SHRINK,      thickness * THICKNESS_SHRINK,      right,      bias    )  }}drawBranch(ctx, v0, 50, 10, Math.PI / 2, 1)




等等等等, 效果图:一棵赤裸裸的树





(是不是有点艺术内味儿了)


剩下的就是增加一些装点, 把果子挂上

function drawBranch(ctx, v0, length, thickness, rad, bias) {  // .....  if (thickness < 5 && Math.random() < 0.3) {    const th = 6 + Math.random()    ctx.save()    ctx.strokeStyle = '#e4393c'    ctx.lineWidth = th    ctx.beginPath()    ctx.moveTo(...v1)    ctx.lineTo(v1.x, v1.y + 2)    ctx.stroke()    ctx.closePath()    ctx.restore()  }}drawBranch(ctx, v0, 50, 10, Math.PI / 2, 3) // 这里增大了随机因子, 让树枝更加扩散


此时效果图就进去了:



(我再问一遍, 是不是很难看, 是不是很想花个几百万小钱买下它)


对于drawBranch第一调用, 能够尝试调一调参数,看看后果如何。


残缺代码地址:github

总结


本文首先展现了如何将 canvas 的坐标系转化为直角坐标系


其次用一个例子演示了,向量在图形学内的根本运算。


向量运算的意义并不仅仅只是用来算点的地位和结构线段,这只是最高级的用法。


可视化出现依赖于计算机图形学,而向量运算是整个计算机图形学的数学根底。而且,在向量运算中,除了加法示意挪动点和绘制线段外,向量的点乘、叉乘运算也有非凡的意义。

咱们是晓黑板前端,欢送关注咱们的知乎、Segmentfault、CSDN、简书、开源中国账号。