# 在canvas上绘制3d图形

## 安装项目依赖模块

npm install

npm run dev

### 从z轴观察yz平面上的点

01.png

A(0，0，zA)，B点的三维坐标可以表示为B(0，yB，zB)。从B点做一条垂线垂z轴于D点。

02.png

03.png

04.png

05.png

### 最终结论

06.png

<template>
<div class="cube">
<canvas ref="cube" v-bind:width="canvasWidth" v-bind:height="canvasHeight"></canvas>
</div>
</template>
<script>
export default {
data: function () {
return {
canvasWidth: 600,
canvasHeight: 400,
ctx: null,
visual: {
x: 0,
y: 0,
z: 300
},
pointMap: {
A: (-50, 50, 50),
B: (-50, 50, -50),
C: (50, 50, -50),
D: (50, 50, 50),
E: (-50, -50, 50),
F: (-50, -50, -50),
G: (50, -50, -50),
H: (50, -50, 50)
}
}
},
methods: {
init: function () {
this.ctx = this.\$refs.cube.getContext('2d')
},
draw: function () {}
},
mounted: function () {
this.init()
this.draw()
}
}
</script>

transformCoordinatePoint: function (x, y, z, offsetX = this.canvasWidth / 2, offsetY = this.canvasHeight / 2) {
return {
x: (x - this.visual.x) * this.visual.z / (this.visual.z - z) + offsetX,
y: (y - this.visual.y) * this.visual.z / (this.visual.z - z) + offsetY
}
}

draw: function () {
let point
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
// 绘制矩形ABCD
this.ctx.beginPath()
point = this.transformCoordinatePoint(...this.pointMap.A)
this.ctx.moveTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.B)
this.ctx.lineTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.C)
this.ctx.lineTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.D)
this.ctx.lineTo(point.x, point.y)
this.ctx.closePath()
this.ctx.stroke()
// 绘制矩形EFGH
this.ctx.beginPath()
point = this.transformCoordinatePoint(...this.pointMap.E)
this.ctx.moveTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.F)
this.ctx.lineTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.G)
this.ctx.lineTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.H)
this.ctx.lineTo(point.x, point.y)
this.ctx.closePath()
this.ctx.stroke()
// 绘制直线AE
this.ctx.beginPath()
point = this.transformCoordinatePoint(...this.pointMap.A)
this.ctx.moveTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.E)
this.ctx.lineTo(point.x, point.y)
this.ctx.stroke()
this.ctx.closePath()
// 绘制直线BF
this.ctx.beginPath()
point = this.transformCoordinatePoint(...this.pointMap.B)
this.ctx.moveTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.F)
this.ctx.lineTo(point.x, point.y)
this.ctx.stroke()
this.ctx.closePath()
// 绘制直线CD
this.ctx.beginPath()
point = this.transformCoordinatePoint(...this.pointMap.C)
this.ctx.moveTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.G)
this.ctx.lineTo(point.x, point.y)
this.ctx.stroke()
this.ctx.closePath()
// 绘制直线DH
this.ctx.beginPath()
point = this.transformCoordinatePoint(...this.pointMap.D)
this.ctx.moveTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.H)
this.ctx.lineTo(point.x, point.y)
this.ctx.stroke()
this.ctx.closePath()
}

06.png

07.png

D'的x坐标cos(α+β)R=Rcosαcosβ-Rsinαsinβ
D'的z坐标sin(α+β)
R=Rsinαcosβ+Rcosαsinβ

D'的x坐标为xcosβ-zsinβ
D'的z坐标为zcosβ+xsinβ

methods: {
init: function () {
this.ctx = this.\$refs.cube.getContext('2d')
},
transformCoordinatePoint: function (x, y, z, offsetX = this.canvasWidth / 2, offsetY = this.canvasHeight / 2) {
return {
x: (x - this.visual.x) * this.visual.z / (this.visual.z - z) + offsetX,
y: (y - this.visual.y) * this.visual.z / (this.visual.z - z) + offsetY
}
},
draw: function () {
let point
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
// 绘制矩形ABCD
this.ctx.beginPath()
point = this.transformCoordinatePoint(...this.pointMap.A)
this.ctx.moveTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.B)
this.ctx.lineTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.C)
this.ctx.lineTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.D)
this.ctx.lineTo(point.x, point.y)
this.ctx.closePath()
this.ctx.stroke()
// 绘制矩形EFGH
this.ctx.beginPath()
point = this.transformCoordinatePoint(...this.pointMap.E)
this.ctx.moveTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.F)
this.ctx.lineTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.G)
this.ctx.lineTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.H)
this.ctx.lineTo(point.x, point.y)
this.ctx.closePath()
this.ctx.stroke()
// 绘制直线AE
this.ctx.beginPath()
point = this.transformCoordinatePoint(...this.pointMap.A)
this.ctx.moveTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.E)
this.ctx.lineTo(point.x, point.y)
this.ctx.stroke()
this.ctx.closePath()
// 绘制直线BF
this.ctx.beginPath()
point = this.transformCoordinatePoint(...this.pointMap.B)
this.ctx.moveTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.F)
this.ctx.lineTo(point.x, point.y)
this.ctx.stroke()
this.ctx.closePath()
// 绘制直线CD
this.ctx.beginPath()
point = this.transformCoordinatePoint(...this.pointMap.C)
this.ctx.moveTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.G)
this.ctx.lineTo(point.x, point.y)
this.ctx.stroke()
this.ctx.closePath()
// 绘制直线DH
this.ctx.beginPath()
point = this.transformCoordinatePoint(...this.pointMap.D)
this.ctx.moveTo(point.x, point.y)
point = this.transformCoordinatePoint(...this.pointMap.H)
this.ctx.lineTo(point.x, point.y)
this.ctx.stroke()
this.ctx.closePath()
},
animationFrame: function () {
let rotationAngle = 1
window.requestAnimationFrame(() => {
for (let key in this.pointMap) {
let point = this.pointMap[key]
// 保存x,y,z坐标
let x = point[0]
let y = point[1]
let z = point[2]
// 变换后的x坐标
point[0] = x * Math.cos(rotationAngle / 180 * Math.PI) - z * Math.sin(rotationAngle / 180 * Math.PI)
// 绕y轴旋转，y左边不会发生变化
point[1] = y
// 变换后的z坐标
point[2] = z * Math.cos(rotationAngle / 180 * Math.PI) + x * Math.sin(rotationAngle / 180 * Math.PI)
}
this.draw()
this.animationFrame()
})
}
},
mounted: function () {
this.init()
this.animationFrame()
}

### 绘制波浪

class Line {
constructor (a, b, c, d, start, end) {
this.a = a
this.b = b
this.c = c
this.d = d
this.start = start
this.end = end
}
}
export default Line

class Line {
constructor (a, b, c, d, start, end, gap) {
this.a = a
this.b = b
this.c = c
this.d = d
this.start = start
this.end = end
this.gap = gap
this.pointList = []
this.computePointList()
}
computePointList () {
this.pointList = []
for (let i = this.start; i <= this.end; i = i + this.gap) {
let x = i
let y = this.a * Math.sin((this.b * x + this.c) / 180 * Math.PI) + this.d
this.pointList.push({
x,
y
})
}
}
}
export default Line

<template>
<canvas class="wave" ref="wave" v-bind:width="canvasWidth" v-bind:height="canvasHeight"></canvas>
</template>
<script>
import Line from './line'
export default {
props: {},
data: function () {
return {
canvasWidth: 600,
canvasHeight: 400,
ctx: null,
visual: {
x: 0,
y: -100,
z: 1000
},
lineList: [
new Line(20, 2, 0, 0, -200, 200, 10)
]
}
},
methods: {
init: function () {
this.ctx = this.\$refs.wave.getContext('2d')
},
draw: function () {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
this.lineList.forEach(line => {
line.pointList.forEach(item => {
this.ctx.beginPath()
this.ctx.arc(item.x + this.canvasWidth / 2, item.y + this.canvasHeight / 2, 2, 0, 2 * Math.PI)
this.ctx.closePath()
this.ctx.fill()
})
})
}
},
mounted: function () {
this.init()
this.draw()
}
}
</script>
<style lang="stylus" scoped>
.wave {
border: 1px solid;
}
</style>

computePointList () {
this.pointList = []
for (let i = this.start; i <= this.end; i = i + this.gap) {
let x = i
let y = this.a * Math.sin((this.b * x + this.c) / 180 * Math.PI) + this.d
let offset = i
this.pointList.push({
x,
y,
offset
})
}
}
updatePointList () {
this.pointList.forEach(item => {
item.y = this.a * Math.sin((this.b * item.x + this.c + item.offset) / 180 * Math.PI) + this.d
})
}

animationFrame: function () {
window.requestAnimationFrame(() => {
this.lineList.forEach(line => {
line.c = this.lineOffset
line.updatePointList()
})
this.lineOffset = this.lineOffset + 1
this.draw()
this.animationFrame()
})
}

constructor (a, b, c, d, z, start, end, gap) {
this.a = a
this.b = b
this.c = c
this.d = d
this.z = z
this.start = start
this.end = end
this.gap = gap
this.pointList = []
this.computePointList()
}

updatePointList () {
this.pointList.forEach(item => {
item.y = this.a * Math.sin((this.b * item.x + this.c + item.offset) / 180 * Math.PI) + this.d
})
}

y轴的坐标我们之前已经写好了，我们运用(xDcosβ-zDsinβ，yD，zDcosβ+xDsinβ)推导每个点旋转β角后的坐标位置

updatePointList (rotationAngleSpeed) {
this.pointList.forEach(item => {
let x = item.x
let z = item.z
item.x = x * Math.cos(rotationAngleSpeed / 180 * Math.PI) - z * Math.sin(rotationAngleSpeed / 180 * Math.PI)
item.z = z * Math.cos(rotationAngleSpeed / 180 * Math.PI) + x * Math.sin(rotationAngleSpeed / 180 * Math.PI)
})
}

updatePointList (rotationAngleSpeed, visual) {
this.pointList.forEach(item => {
let x = item.x
let y = item.y
let z = item.z
item.x = x * Math.cos(rotationAngleSpeed / 180 * Math.PI) - z * Math.sin(rotationAngleSpeed / 180 * Math.PI)
item.z = z * Math.cos(rotationAngleSpeed / 180 * Math.PI) + x * Math.sin(rotationAngleSpeed / 180 * Math.PI)
item.y = this.a * Math.sin((this.b * x + this.c + item.offset) / 180 * Math.PI) + this.d
})
}

item.y = this.a * Math.sin((this.b * x + this.c + item.offset) / 180 * Math.PI) + this.d

this.a * Math.sin((this.b * x + this.c + item.offset) / 180 * Math.PI) + this.d

item.x = x * Math.cos(rotationAngleSpeed / 180 * Math.PI) - z * Math.sin(rotationAngleSpeed / 180 * Math.PI)

computePointList () {
this.pointList = []
for (let i = this.start; i <= this.end; i = i + this.gap) {
let x = i
let y = this.a * Math.sin((this.b * x + this.c) / 180 * Math.PI) + this.d
let offset = i
this.pointList.push({
x,
y,
z: this.z,
originX: x,
offset
})
}
}
updatePointList (rotationAngleSpeed, visual) {
this.pointList.forEach(item => {
let x = item.x
let y = item.y
let z = item.z
item.x = x * Math.cos(rotationAngleSpeed / 180 * Math.PI) - z * Math.sin(rotationAngleSpeed / 180 * Math.PI)
item.z = z * Math.cos(rotationAngleSpeed / 180 * Math.PI) + x * Math.sin(rotationAngleSpeed / 180 * Math.PI)
item.y = this.a * Math.sin((this.b * item.originX + this.c + item.offset) / 180 * Math.PI) + this.d
})
}

visual: {
x: 0,
y: -100,
z: 1000
}

animationFrame: function () {
window.requestAnimationFrame(() => {
this.lineList.forEach(line => {
line.c = this.lineOffset
line.updatePointList(this.rotationAngleSpeed, this.visual)
})
this.lineOffset = this.lineOffset + 1
this.draw()
this.animationFrame()
})
}

updatePointList (rotationAngleSpeed, visual) {
this.pointList.forEach(item => {
let x = item.x
let y = item.y
let z = item.z
item.x = x * Math.cos(rotationAngleSpeed / 180 * Math.PI) - z * Math.sin(rotationAngleSpeed / 180 * Math.PI)
item.z = z * Math.cos(rotationAngleSpeed / 180 * Math.PI) + x * Math.sin(rotationAngleSpeed / 180 * Math.PI)
item.y = this.a * Math.sin((this.b * item.originX + this.c + item.offset) / 180 * Math.PI) + this.d
item.canvasX = (item.x - visual.x) * visual.z / (visual.z - z)
item.canvasY = (item.y - visual.y) * visual.z / (visual.z - z)
})
}

draw: function () {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
this.lineList.forEach(line => {
line.pointList.forEach(item => {
this.ctx.beginPath()
this.ctx.arc(item.canvasX + this.canvasWidth / 2, item.canvasY + this.canvasHeight / 2, 2, 0, 2 * Math.PI)
this.ctx.closePath()
this.ctx.fill()
})
})
}

lineList: [
new Line(20, 2, 0, 0, -150, -200, 200, 10),
new Line(20, 2, 0, 0, -120, -200, 200, 10),
new Line(20, 2, 0, 0, -90, -200, 200, 10),
new Line(20, 2, 0, 0, -60, -200, 200, 10),
new Line(20, 2, 0, 0, -30, -200, 200, 10),
new Line(20, 2, 0, 0, 0, -200, 200, 10),
new Line(20, 2, 0, 0, 30, -200, 200, 10),
new Line(20, 2, 0, 0, 60, -200, 200, 10),
new Line(20, 2, 0, 0, 90, -200, 200, 10),
new Line(20, 2, 0, 0, 120, -200, 200, 10),
new Line(20, 2, 0, 0, 150, -200, 200, 10)
]

animationFrame: function () {
window.requestAnimationFrame(() => {
this.lineList.forEach((line, index) => {
line.c = this.lineOffset
line.updatePointList(this.rotationAngleSpeed, this.visual)
})
this.lineOffset = this.lineOffset + 1
this.draw()
this.animationFrame()
})
}

line.c是被赋值为this.lineOffset，所以我们看到每条直线的偏移量都是一致的，我们试着修改代码，使每条直线的偏移量不一致

animationFrame: function () {
window.requestAnimationFrame(() => {
this.lineList.forEach((line, index) => {
line.c = this.lineOffset + index * 30
line.updatePointList(this.rotationAngleSpeed, this.visual)
})
this.lineOffset = this.lineOffset + 1
this.draw()
this.animationFrame()
})
}

01.png

draw: function () {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
this.lineList.forEach(line => {
line.pointList.forEach(item => {
this.ctx.beginPath()
// 暂且假定小圆点的原始半径是2,则投影半径可表示为
let pointSize = 2 * this.visual.z / (this.visual.z - item.z)
this.ctx.arc(item.canvasX + this.canvasWidth / 2, item.canvasY + this.canvasHeight / 2, pointSize, 0, 2 * Math.PI)
this.ctx.closePath()
this.ctx.fill()
})
})
}

### 推荐阅读更多精彩内容

• 转载自VR设计云课堂[https://www.jianshu.com/u/c7ffdc4b379e]Unity S...
水月凡阅读 485评论 0 0
• 对于大多数做动效的人来说，canvas实际应用一般都是2D平面视觉动效，而3D，一般会出动webgl（或者thre...
羽晞yose阅读 4,256评论 3 11
• 一、两向量的数量积及其应用 ****1****．数量积的定义**** 向量a=(a1,a2,a3),b=(b1,b...
keeeeeenon阅读 2,397评论 0 4
• 等了很久的朋友终于来了，整整迟到了17天，也不知今天全然没有效率和工作结果是不是和这个有关。 11点去天穆镇社保，...
张露deer阅读 64评论 0 0
• 1 响应数据和结果视图 响应数据和结果视图 字符串controller方法返回字符串可以指定逻辑视图名字，通过视图...
LiMingRan阅读 77评论 0 0