在canvas上繪制3d圖形

項(xiàng)目簡介

文章里有相當(dāng)多的用到中學(xué)數(shù)學(xué)中的知識,推導(dǎo)3d的幾何模型是如何繪制到2d平面中去的,最終利用推導(dǎo)出的結(jié)論編寫代碼,實(shí)現(xiàn)一個波紋的demo
項(xiàng)目地址:https://github.com/zz632893783/canvas-3d

效果13.gif

安裝項(xiàng)目依賴模塊

npm install

運(yùn)行項(xiàng)目

npm run dev

從z軸觀察yz平面上的點(diǎn)

01.png

想象一下有這么一個三維空間(如圖),有一個點(diǎn)B,我們從A點(diǎn)觀察B點(diǎn)。那么B點(diǎn)在xy平面上的投影即AB的延長線與平面xy的交點(diǎn)C。而xy平面不就是可以看一個二維的canvas畫布嗎。
我們暫且將A點(diǎn)放在z軸,B點(diǎn)放在yz平面,則A點(diǎn)的三維坐標(biāo)可以表示為
A(0,0,zA),B點(diǎn)的三維坐標(biāo)可以表示為B(0,yB,zB)。從B點(diǎn)做一條垂線垂z軸于D點(diǎn)。
ADB與AOC是相似三角形,所以有


圖片3.png

變換得


圖片6.png

其中DB即B點(diǎn)的y坐標(biāo),AO即A點(diǎn)的z坐標(biāo),DO即B點(diǎn)的z坐標(biāo),所以
圖片7.png

這里的OC也就是C點(diǎn)的y坐標(biāo)。

從z軸觀察xz平面上的點(diǎn)

02.png

同理我們從A點(diǎn)觀察平面xz上的某一點(diǎn)E(xE,0,zE),ADE與AOF是相似三角形


圖片8.png

變換得


圖片9.png

從z軸觀察空間內(nèi)任意坐標(biāo)

之前所觀測的B點(diǎn)是位于yz平面內(nèi),E點(diǎn)是位于xz平面內(nèi),但是如果是空間內(nèi)任意位置的點(diǎn)呢
其實(shí)道理都是一樣的,如下如


03.png

如果將直線BD平移到E點(diǎn),直線DE平移到B點(diǎn),那么將形成一個矩形DBGE,矩形DBGE在xy平面上的投影為矩形OCHF。
由于AGE與AHF相似,所以有


圖片10.png

并且由于ADE與AOF也是相似三角形,所以
圖片11.png

所以


圖片12.png

推導(dǎo)得


圖片13.png

其中GE也就是G點(diǎn)的y坐標(biāo),因?yàn)榫匦蜠BGE是平行于xy平面的,所以它們z坐標(biāo)相同,DO等價于G點(diǎn)的z坐標(biāo),所以對于空間內(nèi)任意位置G(xG,yG,zG)
圖片14.png

同樣的方法我們可以推導(dǎo)出
圖片15.png

變換得


圖片16.png

結(jié)合上兩步,CH是H點(diǎn)的x坐標(biāo),HF是H點(diǎn)的y坐標(biāo),所以從軸上的點(diǎn)A(0,0,zA)觀察空間內(nèi)任意位置G(xG,yG,zG)在平面xy上的投影可表示為


圖片17.png

從任意位置觀察空間內(nèi)任意坐標(biāo)

沿著x軸平移坐標(biāo)系

之前的推論到從z軸觀察空間內(nèi)任意位置的投影了,但是實(shí)際上A點(diǎn)是有特殊性的,因?yàn)樗俏挥趜軸上的某一個點(diǎn),其xy坐標(biāo)都為0,如果A是空間內(nèi)的任意一個點(diǎn),情況又如何,請看下圖

04.png

假設(shè)這個時候真正的坐標(biāo)系是xy'z',而坐標(biāo)系xyz是我們臨時所建立的一個虛擬的坐標(biāo)系,那么這個時候A點(diǎn)相對于坐標(biāo)系xy'z'來說,坐標(biāo)點(diǎn)可表示為A(xA,0,zA),G點(diǎn)依舊表示為(xG,yG,zG)
我們之前推導(dǎo)的相似三角形的關(guān)系,即使換了坐標(biāo)系,它們的關(guān)系依然成立,所以


圖片15.png

變換得


圖片18.png

只不過這個時候 BG=xG-xA,AO與DO與之前相同
求得
圖片19.png

之前推導(dǎo)出的相似三角形關(guān)系依舊成立,所以
圖片20.png

變換得


圖片13.png

由于GE,AO,DO與之前相比都沒有變化,
所以得
圖片14.png

與之前的推導(dǎo)一致,最后我們得出結(jié)論,我們沿著x軸方向移動坐標(biāo)系的時候(即圖中的坐標(biāo)系有xy'z'移動到了xyz位置),G點(diǎn)在平面xy的投影H點(diǎn)的y坐標(biāo)不會有變化,但是x坐標(biāo)為
圖片21.png
沿著y軸平移坐標(biāo)系

如下圖


05.png

假設(shè)x'yz'是真正的坐標(biāo)系,沿著y軸平移得到臨時坐標(biāo)系xyz,推導(dǎo)步驟和之前的相同,這里不再贅述,直接貼結(jié)果


圖片22.png

圖片23.png

也就是說當(dāng)沿著y軸方向移動坐標(biāo)系的時候,投影H的x坐標(biāo)不會有變化,y坐標(biāo)變?yōu)?br>
圖片24.png
對于空間內(nèi)任意位置

對于空間內(nèi)任意位置,我們都可以看成是在z軸上的某一點(diǎn)A(0,0,zA),先經(jīng)歷一次x軸方向的平移(此時投影H的y坐標(biāo)不變),再經(jīng)歷一次y軸方向的平移(此時投影H的x坐標(biāo)不變),平移之前點(diǎn)A觀察到點(diǎn)G的投影H坐標(biāo)可表示為


圖片25.png

對其進(jìn)行x軸方向的平移,(此時投影H的y坐標(biāo)不變),H的坐標(biāo)可表示為


圖片26.png

再對其進(jìn)行y軸方向的平移,(此時投影H的x坐標(biāo)不變),H的坐標(biāo)可表示為
圖片27.png

最終結(jié)論

從空間內(nèi)的任意點(diǎn)A(xA,yA,zA)觀察空間內(nèi)的任一點(diǎn)G(xG,yG,zG),它在xy平面內(nèi)的投影H的坐標(biāo)為


圖片27.png

首先我們嘗試寫一個簡單的幾何圖形


06.png

立方體邊長為100,則A(-50,50,50),B(-50,50,-50),C(50,50,-50),D(50,50,50),E(-50,-50,50),F(xiàn)(-50,-50,-50),G(50,-50,-50),H(50,-50,50),假定從z軸上某一點(diǎn)(0,0,300)觀察
<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>

繪制方法也很簡單,分別繪制矩形ABCD,矩形EFGH,然后再將AE,BF,CG,DH連線即可,只不過這里的ABCDEFGH點(diǎn)需要換算成投影在三維坐標(biāo)系xy平面上的點(diǎn),運(yùn)用我們之前得出的結(jié)論,我們定義一個轉(zhuǎn)換坐標(biāo)點(diǎn)的函數(shù)

        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函數(shù)

        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()
        }

查看代碼運(yùn)行結(jié)果

效果1.png

似乎是對的,但是有感覺怪怪的,我們嘗試將立方體繞著y軸旋轉(zhuǎn)
這里需要另一個數(shù)學(xué)關(guān)系的推導(dǎo)
06.png

想象一下從y軸俯視yz平面,這個時候點(diǎn)D的位置關(guān)系如下圖
07.png

這個時候假定D點(diǎn)與x軸的夾角是α,圓的半徑為R,將D點(diǎn)繞著y軸旋轉(zhuǎn)β旋轉(zhuǎn)至D'點(diǎn),這個時候D'與x軸夾角為α+β,此時D'的x坐標(biāo)為cos(α+β)R,D'的z坐標(biāo)為sin(α+β)R
回一下中學(xué)時候我們學(xué)過的三角形倍角公式

圖片29.png

圖片30.png

D'的x坐標(biāo)cos(α+β)R=Rcosαcosβ-Rsinαsinβ
D'的z坐標(biāo)sin(α+β)
R=Rsinαcosβ+Rcosαsinβ
而Rsinα就是旋轉(zhuǎn)之前D點(diǎn)的z坐標(biāo),Rcosα就是旋轉(zhuǎn)之前D點(diǎn)的x坐標(biāo),
D'的x坐標(biāo)為xcosβ-zsinβ
D'的z坐標(biāo)為zcosβ+xsinβ
將結(jié)論代入到我們的立方體的8個頂點(diǎn)ABCDEFGH中
對于任一點(diǎn)D(xD,yD,zD),其繞y軸旋轉(zhuǎn)β角的時候,它的三維坐標(biāo)變?yōu)?xDcosβ-zDsinβ,yD,zDcosβ+xDsinβ)
轉(zhuǎn)換為代碼

    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坐標(biāo)
                    let x = point[0]
                    let y = point[1]
                    let z = point[2]
                    // 變換后的x坐標(biāo)
                    point[0] = x * Math.cos(rotationAngle / 180 * Math.PI) - z * Math.sin(rotationAngle / 180 * Math.PI)
                    // 繞y軸旋轉(zhuǎn),y左邊不會發(fā)生變化
                    point[1] = y
                    // 變換后的z坐標(biāo)
                    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()
    }

代碼運(yùn)行效果


效果2.gif

繪制波浪

波浪是由若干條正弦函數(shù)組成的,我們先繪制一條正弦函數(shù)
中學(xué)數(shù)學(xué)中,描述一條正弦函數(shù)的方程式 y=a*sin(b * x + c) + d,所以我們構(gòu)造一個類,需要的參數(shù)也是a,b,c,d,為了確定函數(shù)的起始位置和結(jié)束位置,另外需要兩個參數(shù)start,end

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

實(shí)際上每條正弦函數(shù)曲線并不是真正的連線,而是由于一個個點(diǎn)組成,我們在增加一個參數(shù),確定每個點(diǎn)之間的間距,并在實(shí)例化的時候生成這些點(diǎn),我們這里保存在pointList中

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

在頁面中,Line實(shí)例保存在lineList中,遍歷lineList繪制點(diǎn)

<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>

看一下代碼效果


效果3.png

我們再試著讓它動起來,波浪的運(yùn)動改變的實(shí)際上是每個點(diǎn)的縱坐標(biāo),只要我們知道每個點(diǎn)距離原點(diǎn)的偏移量,我們就能計算出當(dāng)前的縱坐標(biāo),所以我們在生成點(diǎn)的時候,記錄偏移量,我們我們聲明一個updatePointList方法用以跟新點(diǎn)的位置

    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
        })
    }

在頁面中,我們定義一個變量lineOffset,通過調(diào)整它控制line實(shí)例的c值(也就是對直線進(jìn)行平移),并不斷地調(diào)用之前寫好的updatePointList方法,更新點(diǎn)的位置

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

代碼運(yùn)行效果


效果4.gif

但是這個只是二維平面的,想象一下空間中有很多條這樣的直線,然后有的直線離屏幕比較近,有的離屏幕比較遠(yuǎn),所以我們?nèi)绻谌S空間中描述直線的話,我們還需要知道三維坐標(biāo)系中的z坐標(biāo),除此之代直線的x,z與之前的相比并無變化

    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()
    }

我們之前已經(jīng)推導(dǎo)過,對于任一點(diǎn)D(xD,yD,zD),其繞y軸旋轉(zhuǎn)β角的時候,它的三維坐標(biāo)變?yōu)?xDcosβ-zDsinβ,yD,zDcosβ+xDsinβ),想象一下我們直線上的每一個點(diǎn),其實(shí)都是繞著y軸旋轉(zhuǎn)的,旋轉(zhuǎn)之后y軸的坐標(biāo)不會發(fā)生變化,然后看我們原型中聲明的updatePointList方法

    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軸的坐標(biāo)我們之前已經(jīng)寫好了,我們運(yùn)用(xDcosβ-zDsinβ,yD,zDcosβ+xDsinβ)推導(dǎo)每個點(diǎn)旋轉(zhuǎn)β角后的坐標(biāo)位置

    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)
        })
    }

代碼運(yùn)行效果


效果6.gif

但是此時的粒子并沒有沿著y軸方向移動,我們將兩步結(jié)合

    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
        })
    }

然后我們看一下運(yùn)行結(jié)果


效果7.gif

非常的怪異,我們似乎哪里寫錯了
回過頭來看我們的代碼,波紋的左右移動實(shí)際上是靠從新計算每個點(diǎn)的y坐標(biāo)實(shí)現(xiàn),而計算y坐標(biāo)我們用的函數(shù)是

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

但是我們實(shí)際上每計算一次item.y的值,我們通過控制this.c來實(shí)現(xiàn)平移,所以除了this.c之外,

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

中的 this.a,x(這里的x也就是item.x),this.b,item.offset,this.d都不應(yīng)該有變化,但是我們代碼中的

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

卻在不停地變化item.x的值,所以我們需要保存一份最開始時時候的x值

    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
        })
    }

繼續(xù)看運(yùn)行效果


效果8.gif

雖然代碼是對的,但是這個時候的這些點(diǎn)還只是平面上的點(diǎn),并沒有3d效果,我們回到最開始推導(dǎo)出的結(jié)論
從空間內(nèi)的任意點(diǎn)A(xA,yA,zA)觀察空間內(nèi)的任一點(diǎn)G(xG,yG,zG),它在xy平面內(nèi)的投影H的坐標(biāo)為


圖片27.png

我們頂一個個觀察點(diǎn)
    visual: {
        x: 0,
        y: -100,
        z: 1000
    }

并在每次updatePointList方法中調(diào)用它,計算這個點(diǎn)在平面xy上的投影位置

        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函數(shù)中,我們拿到傳入的視角點(diǎn)visual,并根據(jù)視角點(diǎn)計算空間內(nèi)的點(diǎn)在平面xy上的投影,我們記為(canvasX,canvasY)

    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)
        })
    }

由于我們現(xiàn)在是要繪制投影的坐標(biāo),所以我們的draw方法中的繪制圓點(diǎn)的方法需要換成(canvasX,canvasY)

        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()
                })
            })
        }

運(yùn)行結(jié)果


效果9.gif

然后我們試著加入更多的線條

            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)
            ]

運(yùn)行結(jié)果


效果10.gif

我們試著再對每條直線作不同的平移,我們平移直線是通過line構(gòu)造函數(shù)中的參數(shù)c控制的,在animationFrame方法中

        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()
            })
        }

代碼運(yùn)行結(jié)果


效果11.gif

實(shí)際上我們還忽略了一個點(diǎn),那就是點(diǎn)的遠(yuǎn)近大小關(guān)系,真實(shí)情況應(yīng)該是離我們屏幕較近的點(diǎn),看起來更大,離屏更遠(yuǎn)的點(diǎn),看起來更小,而離屏幕的距離不就是z的坐標(biāo)嗎
我們回到最開始推論的那副圖


01.png

在A點(diǎn)觀察直線DB在平面xy內(nèi)的投影OC,由相似三角形可知
圖片31.png

推導(dǎo)得


圖片32.png

所以假定小圓點(diǎn)的半斤是R,站在A(0,0,)點(diǎn)觀測小圓點(diǎn)位于平面xy上投影的半徑為
圖片33.png

我們將draw方法中的代碼做修改
        draw: function () {
            this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
            this.lineList.forEach(line => {
                line.pointList.forEach(item => {
                    this.ctx.beginPath()
                    // 暫且假定小圓點(diǎn)的原始半徑是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()
                })
            })
        }

運(yùn)行效果


效果12.gif

我們不斷調(diào)整實(shí)例化時候line的各個參數(shù),最終實(shí)現(xiàn)效果


效果13.gif

到此,請記住這篇文章最重要的一個結(jié)論
從空間內(nèi)的任意點(diǎn)A(xA,yA,zA)觀察空間內(nèi)的任一點(diǎn)G(xG,yG,zG),它在xy平面內(nèi)的投影H的坐標(biāo)為
圖片27.png

如果以后還有與canvas繪制3d圖形有關(guān)的文章,這個結(jié)論會一直用到

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)載自VR設(shè)計云課堂[http://www.itdecent.cn/u/c7ffdc4b379e]Unity S...
    水月凡閱讀 1,183評論 0 0
  • 前言 在前兩章,總結(jié)有頂點(diǎn)坐標(biāo),紋理坐標(biāo)。實(shí)際上在這之上還有更多的坐標(biāo)。作者經(jīng)過學(xué)習(xí)后,在本文總結(jié)一番。 上一篇:...
    yjy239閱讀 2,417評論 1 1
  • 對于大多數(shù)做動效的人來說,canvas實(shí)際應(yīng)用一般都是2D平面視覺動效,而3D,一般會出動webgl(或者thre...
    羽晞yose閱讀 6,919評論 3 11
  • 一、兩向量的數(shù)量積及其應(yīng)用 ****1****.?dāng)?shù)量積的定義**** 向量a=(a1,a2,a3),b=(b1,b...
    keeeeeenon閱讀 5,344評論 0 5
  • 等了很久的朋友終于來了,整整遲到了17天,也不知今天全然沒有效率和工作結(jié)果是不是和這個有關(guān)。 11點(diǎn)去天穆鎮(zhèn)社保,...
    張露deer閱讀 215評論 0 0

友情鏈接更多精彩內(nèi)容