原生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,写的比较仓促,可能一些小的细节没有到位。

                                    
展开阅读全文

没有更多推荐了,返回首页