原生js+canvas實現骰子作圖

最終效果大家可以試一下:https://areocrystal.github.io/drawbydice/點擊打開鏈接,上傳圖片後記得要點一下那個start(那個滑動條最好不要拉到最左邊去了,雖然渲染出的最接近原圖,但cpu不好的同學瀏覽器會卡死)。

其實這個個人小玩意早在幾個月前就做好了,主要就是用大小不一的點(或其他)來重新繪製圖片。

首先在一個閉包中進行初始化,並進行原生代碼的封裝

(function(doc){

    var concatImg = {

        init(){
            var getId = doc.getElementById.bind(doc)
            this.file = getId('uoploadImg')
            this.cas = getId('canvas')
            this.canvas = doc.createElement('canvas')
            this.ctx = this.cas.getContext('2d')
            this.ctx2 = this.canvas.getContext('2d')
            this.img = new Image()
            this.fr = new FileReader()
            this.range = getId('range')
            this.autoCalc = getId('autocalc')
            this.trigger = getId('start')
            this.colour = getId('colour')
            this.path = getId('pathinfo')
            this.selection = getId('selection')
            this.maxUnitPixel = +this.range.value
            this.unitPixelColor = this.colour.value
            this.isAutoCalc = this.autoCalc.checked
            this.selectionValue = this.selection.value
            this.pixelCollection =
                [...'1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+{}|:"<>?-=[]\\;\',./`~']
            this.pixelCollectionLen = this.pixelCollection.length
            this.drawDetail = this.primitiveDrawDetail
            this.bindEvent.call(this)
        },
}
}(document)

這裏是表單控制

<div class="row">
    <div class="col-lg-4">
        <div class="input-group">
            <section class="input-group-btn">
                <button class="btn btn-success" type="button" id="start">Start</button>
            </section>
            <input type="text" class="form-control" placeholder="name of the file:" id="pathinfo">
            <input type="file" id="uoploadImg" class="form-control" accept="image/*">
            <span class="input-group-addon">auto calculate                <input type="checkbox" aria-label="..." id="autocalc">
            </span>
        </div>
        <select id="selection" class="form-control">
            <option value="1">dot</option>
            <option value="2">character</option>
            <option value="3">brick</option>
        </select>
    </div>
    <div class="col-lg-2">
        long of side setting        <input type="range" max=100 min=1 class="form-control" id="range">
    </div>
    <div class="col-lg-2">
        color setting        <input type="color" class="form-control" id="colour" value="#ffffff">
    </div>
</div>
<canvas id="canvas"></canvas>

由於此處有多個input輸入框來控制,一個個綁定“change”事件實在過於繁瑣。這裏可以利用事件委託給document綁定“onchange”事件,這樣事件就能依次傳遞到以下各個input之中,並且涉及文件上傳就要使用fileReader對象,當中有文件讀取與圖片上傳的兩個異步操作,可以使用Promise來進行初步的封裝,整體代碼如下:

bindEvent(){
    doc.addEventListener('change', function (e) {
        switch (e.target) {
            case this.range:
                this.maxUnitPixel = +e.target.value
                break
            case this.colour:
                this.unitPixelColor = e.target.value
                break
            case this.selection:
                this.selectionValue = e.target.value
                this.drawDetail = this.primitiveDrawDetail
                break
            case this.autoCalc:
                this.isAutoCalc = e.target.checked
                break
            case this.file:
                let fe = e.target.files,
                    primitivePath = e.target.value
                this.path.value = primitivePath.substr(primitivePath.search(/[\\\/][^\\\/]+$/) + 1)
                if (fe.length > 0 && /image\/\w{3,4}/.test(fe[0].type)) {
                    this.loadFileReader(fe[0]).then(progressEvent => {
                        return this.loadImage(progressEvent)
                    }).then(() => {
                        this.cw = this.cas.width = this.canvas.width = this.img.width
                        this.ch = this.cas.height = this.canvas.height = this.img.height
                        this.trigger.onclick = () => this.getImgSegments()
                    })
                }
                break
        }
    }.bind(this), false)
}
loadFileReader(f){
    return new Promise(resolve => {
        this.fr.readAsDataURL(f)
        this.fr.onload = (progressEvent) => resolve(progressEvent)
    })
},
loadImage(progressEvent){
    return new Promise(resolve => {
        this.img.src = progressEvent.target.result
        this.img.onload = () => resolve()
    })
},

接下來可以把圖片分割而成的每個最小單元都當作一個對象:

class imgElements {
    constructor(x, y) {
        this._x = x
        this._y = y
        this.edge = concatImg.unitImgEdge
        this.r = this.edge / 6
        this.drawElement()
    }

    drawElement() {
        concatImg.ctx2.drawImage(
            concatImg.img,
            this._x, this._y,
            this.edge, this.edge,
            this._x, this._y,
            this.edge, this.edge)
        this.acquireGrayscale()
    }

    acquireGrayscale() {
        var imgData = concatImg.ctx2.getImageData(
            this._x, this._y,
            this.edge, this.edge).data,
            average = 0,
            len = imgData.length,
            count = 0
        for (let i = 0; i < len; i += 4) {
            average +=
            (imgData[i] + imgData[i + 1] + imgData[i + 2]) / 3
            count++
        }
        this.grayscale = average / count
        concatImg.drawUnit(this)
    }
}

值得一提的是上面的acquireGrayscale方法來獲取圖片的灰度值,當中使用了canvas的getImageData的方法,所得到的imgData是一個二進制數組(ArrayBuffer),這個數組就是所得圖像數據非常龐大,當中成員每四個爲一組,分別表示圖像的rgba值,由於a(alpha)是沒有用的,所以再接下來的for循壞中要+4,來把rgb三個值進行混合來取平均數。接着進入封裝在concatImg.drawUnit來畫出每一個單元:

drawUnit(_self){
    var step = _self.grayscale / 25.6 | 0
    this.ctx2.fillStyle = '#000'
    this.ctx2.fillRect(_self._x, _self._y, _self.edge, _self.edge)
    for (let i = dice[step].length - 1; i > -1 ; i--) {
        for (let j = dice[step][i].length - 1; j > -1 ; j--) {
            dice[step][i][j] === 1 &&
            this.drawDetail(_self._x, _self._y, _self.r, i, j)
        }
    }
},

很容易知道所得的灰度平均值肯定不會超過0xff,也就是256,所以以25.6來劃分成十份來取整,來根據這個dice這個三維數組來進行繪製。

最後的效果圖就是用canvas來繪製canvas,寫的比較倉促,可能一些小的細節沒有到位。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章