绘制图形的几种方式,性能优化测试

canvas的绘制优化一般都是从算法和数据结构上进行优化。我其实也不是很懂,讲讲我的知道的一些。讲的不对,请指正

比如:

更新:尽量更新的区域小,而且不要clearRect清除全部,那样会导致又要重新绘制所有的图形。

更新策略:通过收集所有需要更新的包围合,再进行包围合是否相交,来合并包围盒,再进行剪裁。包围盒同样也可能是动态的,这块最好做成懒获取,懒更新。也可以通过四叉树这种结构存储元素。四叉树本身就为元素分配好了空间,所以得到元素就能得到所在空间。还有一种就是cavans分层.把更新频率的优先级排一下,用不同canvs来处理.举个例子:假始我们有一万个元素。那我用一个万个canvas,每个canvas来渲一个元素,那我修改一个元素时,我只需要更新一个canvas就行了。大概就这个意思

捕获策略:除了更新绘制耗时,还有就是交互上面,无非就是用什么方法来判断点是否在图形的包盒围内,canvas.isPointInPath或几何方法计算或像素点判断。除了这些还有就是涉及了坐标空间的变换,特别是多层级话,子元素是相对父素的坐标空间,所以元素一般有局部坐标和屏幕坐标,矩阵计算本身就耗时,减少矩阵的变换计算频率也很关键。另外:事件的捕捉Mapbox地图框架在事件上面一个很优秀的方案。就是它以元素ID和事件名来绑定。那在相应事件触发时。只需要在事件池里面,查找对应ID和事件的元素来处理,其它的不用管。说明:假如10万个元素,我只绑定了一个叫:A的。我只需要判断A是否与点击区域相交就行了。这个也不是所有场景适合

目录

下面是完整代码


主要测试 canvas和webgl绘制2d平面圆的效率.

随机生成20万个均匀分布的圆,圆的颜色随机

每个测试100次,取平均值时长

下面每种测试数据都是通过不同手段,最终绘制圆:

  • 性能测试-每个图创建一个路径,耗时:376豪秒(每个圆创建一条路径,填充颜色) 
  • 性能测试-颜色分组绘制,并且只创建一个路径,耗时:160豪秒(一条路径只能填充一种颜色,所以要分组)
  • 性能测试-createPattern,耗时:450.27豪秒 (比如复杂的图形绘制太麻烦,可以只绘制一遍,通过个来设置)
  • 性能测试-drawImage,耗时:394.15豪秒(利有一个隐藏canvas绘制图形再转图片,最后利用drawImage填充,对于复杂图形这种位图填是很快的,不需要扫描图形计算填充区域,)
  • 性能测试-zrender,耗时:113.59豪秒 (这个时间是进行全量更新,不然会更快,这个框架渲染只对修改过的元素进行了包盒围裁剪更新。所以会很快)
  • 性能测试-pixijs,耗时:77.97豪秒 (第一次并不快,我用的graphics对象绘制的,所以有缺点就是是无法查找对应的元素修改,pixijs也支持自定义shader着色器编程)
  • 性能测试-threejs,耗时:44.69豪秒(把所有位置放到一个顶点集合里面,再利用Points 和纹理图生成圆,类似精灵图,如果用几何对象实例的话,会挂掉)

下面是完整代码

   var addExample = createModuleExample('vue3')

        var testData = {}
        function consoleTestData() {
            Object.keys(testData).forEach(key => {
                let time = testData[key].reduce((a, b) => a + b, 0) / testData[key].length
                console.log(key + ',耗时:' + time + '豪秒')
            })
        }
        function startEndTime() {
            let time = Date.now()
            return function () {
                return (Date.now() - time)
            }
        }
        function testFnExecuTime(name, fn) {
            let data = testData[name] = []
            return function () {
                let end = startEndTime()
                fn.apply(this, arguments)
                let time = end()
                //  console.log(name + ',耗时:' + time + '豪秒')
                data.push(time)
                return time;
            }
        }
        let EventEmitter = function (context) {
            this.context = context || this
            var events = []
            var currentEvents = []
            Object.defineProperty(this, 'events', {
                get() {
                    return events
                },
                set() {
                    throw '不能修改这个属性'
                }
            })
            this.emit = function (...args) {
                let fns = currentEvents = events
                for (let i = 0, len = fns.length; i < len; i++) {
                    if (fns[i].callback.apply(this.context, args) === false) {
                        break
                    }
                }
                return this
            }
            this.remove = function (callback) {
                if (events === currentEvents) {
                    events = currentEvents.slice()
                }
                let index = events.findIndex(d => d.callback == callback)
                if (index !== -1) {
                    events.splice(index, 1)
                }
                return this
            }
            this.add = function (callback, priority = 0) {
                if (events === currentEvents) {
                    events = currentEvents.slice()
                }
                let index = events.length
                for (let i = 0; i < events.length; i++) {
                    if (priority > events[i].priority) {
                        index = i
                        break
                    }
                }
                events.splice(index, 0, { callback: callback, priority: priority })
                return this
            }
        }

        function createCanvasRenderer(opts = {}) {
            let { devicePixelRatio, renderType, width, height, container } = {
                devicePixelRatio: 1,
                renderType: 'canvas',
                width: 500,
                height: 500,
                ...opts
            }
            let el = document.createElement('canvas')
            let viewWidth = el.width = width * devicePixelRatio
            let viewHeight = el.height = height * devicePixelRatio
            if (devicePixelRatio > 1) {
                el.style.width = width + 'px'
                el.style.height = height + 'px'
            }
            let ctx = el.getContext(renderType === 'canvas' ? '2d' : 'webgl')
            container.appendChild(el)

            let lastTime = 0, delta, current
            let instance = {
                ctx: ctx,
                width: viewWidth,
                height: viewHeight,
                init() { },
                clear() {
                    ctx.clearRect(0, 0, viewWidth, viewHeight)
                },
                save() {
                    ctx.save()
                },
                restore() {
                    ctx.restore()
                },
                beginPath() {
                    ctx.beginPath()
                },
                arc(x, y, radius, start, end, counterclockwise) {
                    ctx.arc(x, y, radius, start, end, counterclockwise)
                },
                fill() {
                    ctx.fill()
                },
                startDraw() {
                    this.clear()
                    this.save()
                },
                draw() {

                },
                afterDraw() {
                    this.restore()
                }
            }
            let webglInstance = {
                ctx: ctx,
                width: viewWidth,
                height: viewHeight,
                regl: null,
                init() {
                    this.regl = createREGL({
                        canvas: el,
                    })
                    this._drawCircle = this.regl({
                        vert: `
                            precision mediump float;
                            attribute vec2 position;
                            attribute vec3 color;
                            mat2 mat3 modelMatrix;
                            varying vec3 fragColor;
                            void main(){
                                gl_Position=modelMatrix*vec4(position,0,1.0);
                                fragColor=color;
                            }
                        `,
                        frag: `
                            varying vec3 fragColor;
                            void main(){
                                gl_FragColor=vec4(fragColor,1.0);
                            }
                        `,
                        attributes: {
                            position: regl.prop('position'),
                            posicolortion: regl.prop('color')
                        },
                        uniforms: {
                            modelMatrix: regl.prop('modelMatrix')
                        }
                    })
                },
                points: [],
                fillStyle: null,
                beginPath() {
                    this.points.length = 0
                },
                drawCircle(x, y, r) {
                    this._drawCircle({

                    })
                },
                startDraw() {

                },
                draw() {

                },
                afterDraw() {

                }
            }
            let _instance = renderType === 'canvas' ? instance : webglInstance
            _instance.init()
            let hooks = {
                startDraw: (new EventEmitter(_instance)).add(_instance.startDraw),
                draw: new EventEmitter(_instance).add(_instance.draw),
                afterDraw: new EventEmitter(_instance).add(_instance.afterDraw)
            }
            let rendererAnimation = function (fn) {
                let lastTime = 0, delta;
                let animate = function (current) {
                    if (!lastTime) {
                        lastTime = current
                        window.requestAnimationFrame(animate)
                        return
                    }
                    delta = current - lastTime
                    lastTime = current
                    rendererAnimation.draw(delta)
                    rendererAnimation.currentAnimateId = window.requestAnimationFrame(animate)
                }
                hooks.draw.add(fn)
                window.requestAnimationFrame(animate)
                return function () {
                    hooks.draw.remove(fn)
                }
            }
            rendererAnimation.draw = function (delta) {
                hooks.startDraw.emit(delta)
                hooks.draw.emit(delta)
                hooks.afterDraw.emit(delta)
            }
            rendererAnimation.currentAnimateId = null
            rendererAnimation.hooks = hooks
            rendererAnimation.stop = function () {
                if (rendererAnimation.currentAnimateId) {
                    cancelAnimationFrame(rendererAnimation.currentAnimateId)
                    rendererAnimation.currentAnimateId = null
                }
            }
            return rendererAnimation
        }
        function createCanvasTexture(color, radius) {
            var temp = document.createElement('canvas');
            var size = radius * 2
            temp.width = size;
            temp.height = size;
            var tctx = temp.getContext('2d');
            tctx.beginPath();
            tctx.fillStyle = color;
            tctx.arc(size / 2, size / 2, radius, 0, Math.PI * 2);
            tctx.fill();
            return temp;
        }

        function randomInt(min, max) {
            return Math.floor(Math.random() * (max - min) + min)
        }
        function buildRandomPoints(count, bounds) {
            let minX = bounds[0], minY = bounds[1], maxX = bounds[2], maxY = bounds[3]
            let points = new Array(count)
            let colors = ['#ff0000', '#00ff00', '#0000ff']
            let colorsHex = [0xff0000, 0x00ff00, 0x0000ff]
            for (let i = 0; i < count; i++) {
                let x = randomInt(minX, maxX)
                let y = randomInt(minY, maxY)
                let colorIndex = randomInt(0, 3)
                points[i] = {
                    x,
                    y,
                    r: randomInt(3, 6),
                    color: colors[colorIndex],
                    color2: colorsHex[colorIndex]
                }
            }
            return points
        }
        function buildUniformPoints(count, bounds, raduis = 5, padding = 2) {
            let minX = bounds[0], minY = bounds[1], maxX = bounds[2], maxY = bounds[3]
            let width = maxX - minX, height = maxY - minY
            let diameter = raduis * 2
            let colCount = Math.floor(width / diameter)
            let rowCount = Math.floor(height / diameter)
            let points = new Array(count)
            let colors = ['red', 'green', 'blue']
            let colorsHex = [0xff0000, 0x00ff00, 0x0000ff]

            let x = 0, y = 0;
            for (let i = 0; i < count; i++) {
                let c = i % colCount
                let r = Math.floor((i * diameter) / width)
                let colorIndex = randomInt(0, 3)
                points[i] = {
                    id: i,
                    x: c * diameter + raduis,
                    y: r * diameter + raduis,
                    r: raduis,
                    color: colors[colorIndex],
                    color2: colorsHex[colorIndex]
                }
            }
            return points
        }
        var viewWidth = 1500, viewHeight = 1500
        let testPoints = buildUniformPoints(200000, [0, 0, viewWidth, viewHeight])
        let PI2 = Math.PI * 2


        addExample("性能测试-canvas", function ({ gui }) {
            let { toRaw, ref, unref, provide, inject, getCurrentInstance, reactive, shallowReactive, computed, watchEffect, watch, onBeforeMount, onMounted, onBeforeUpdated, onUpdated, onBeforeUnmount, onUnmounted, toRef, toRefs } = Vue;
            return {
                template: `
`, setup(props, ctx) { let container = ref(); onMounted(() => { let render = createCanvasRenderer({ container: container.value, renderType: "canvas", width: viewWidth, height: viewHeight }) render.hooks.draw.add(testFnExecuTime('性能测试-每个图创建一个路径', function () { let ctx = this.ctx; testPoints.forEach((point) => { ctx.beginPath() ctx.fillStyle = point.color; ctx.arc(point.x, point.y, point.r, 0, PI2) ctx.fill() }) })) //render.draw() addGuiScheme(gui, { source: { refresh() { render.draw() }, test100() { let count = 100; while (count-- > 0) { render.draw() } } } }) }) return { main: container } } } }) addExample("性能测试-canvas-one-beginpath", function ({ gui }) { let { toRaw, ref, unref, provide, inject, getCurrentInstance, reactive, shallowReactive, computed, watchEffect, watch, onBeforeMount, onMounted, onBeforeUpdated, onUpdated, onBeforeUnmount, onUnmounted, toRef, toRefs } = Vue; return { template: `
`, setup(props, ctx) { let container = ref(); onMounted(() => { let render = createCanvasRenderer({ container: container.value, renderType: "canvas", width: viewWidth, height: viewHeight }) let colorGroup = _.groupBy(testPoints, d => d.color) // 相同颜色在一个路径下 render.hooks.draw.add(testFnExecuTime('性能测试-颜色分组只创建一个路径', function () { let ctx = this.ctx; Object.keys(colorGroup).forEach((color, index) => { let points = colorGroup[color] ctx.beginPath() ctx.fillStyle = color points.forEach((point) => { ctx.moveTo(point.x, point.y) ctx.arc(point.x, point.y, point.r, 0, PI2) }) ctx.fill() }) })) //render.draw() addGuiScheme(gui, { source: { refresh() { render.draw() }, test100() { let count = 100; while (count-- > 0) { render.draw() } } } }) }) return { main: container } } } }) addExample("性能测试-canvas-createPattern", function ({ gui }) { let { toRaw, ref, unref, provide, inject, getCurrentInstance, reactive, shallowReactive, computed, watchEffect, watch, onBeforeMount, onMounted, onBeforeUpdated, onUpdated, onBeforeUnmount, onUnmounted, toRef, toRefs } = Vue; return { template: `
`, setup(props, ctx) { let container = ref(); onMounted(() => { let render = createCanvasRenderer({ container: container.value, renderType: "canvas", width: viewWidth, height: viewHeight }) function createCirce(color, r) { var p = document.createElement('canvas') p.width = r * 2 p.height = r * 2; var pctx = p.getContext('2d') pctx.fillStyle = color pctx.arc(r, r, r, 0, PI2, false) pctx.fill() // no-repeat repeat repeat-x repeat-y // 以左上角为坐标起始点 var pattern = pctx.createPattern(p, 'no-repeat'); return pattern } let colorCircle = {}, len = testPoints.length; let p = new Promise((resolve) => { let next = () => { len-- if (len <= 0) { resolve() } } testPoints.forEach(point => { if (!colorCircle[point.color]) { colorCircle[point.color] = createCirce(point.color, point.r) next() return } next() }) }) render.hooks.draw.add(testFnExecuTime('性能测试-createPattern', function () { let ctx = this.ctx; // ctx.translate(100,100) // ctx.rect(0,0,100,100) // ctx.fillStyle=colorCircle['red'] // ctx.fill() // ctx.stroke() testPoints.forEach((point) => { ctx.beginPath() ctx.setTransform(1, 0, 0, 1, point.x, point.y) ctx.fillStyle = colorCircle[point.color] //ctx.moveTo(point.x,point.y) ctx.rect(0, 0, point.r * 2, point.r * 2) ctx.fill() }) ctx.stroke() })) p.then(() => { // render.draw() addGuiScheme(gui, { source: { refresh() { render.draw() }, test100() { let count = 100; while (count-- > 0) { render.draw() } } } }) }) }) return { main: container } } } }) addExample("性能测试-canvas-drawImage", function ({ gui }) { let { toRaw, ref, unref, provide, inject, getCurrentInstance, reactive, shallowReactive, computed, watchEffect, watch, onBeforeMount, onMounted, onBeforeUpdated, onUpdated, onBeforeUnmount, onUnmounted, toRef, toRefs } = Vue; return { template: `
`, setup(props, ctx) { let container = ref(); onMounted(() => { let render = createCanvasRenderer({ container: container.value, renderType: "canvas", width: viewWidth, height: viewHeight }) function createCirce(color, r) { var p = document.createElement('canvas') p.width = r * 2 p.height = r * 2; var pctx = p.getContext('2d') pctx.fillStyle = color pctx.arc(r, r, r, 0, PI2, false) pctx.fill() return new Promise((resolve) => { let img = new Image() img.onload = () => { resolve(img) } img.src = p.toDataURL('image/png') }) } let colorCircle = {}, len = testPoints.length; let p = new Promise((resolve) => { let next = () => { len-- if (len <= 0) { resolve() } } testPoints.forEach(point => { if (!colorCircle[point.color]) { colorCircle[point.color] = true; createCirce(point.color, point.r).then((img) => { colorCircle[point.color] = img next() }, () => { next() }) return } next() }) }) render.hooks.draw.add(testFnExecuTime('性能测试-drawImage', function () { let ctx = this.ctx; testPoints.forEach((point) => { // ctx.beginPath() // ctx.fillStyle=point.color; ctx.drawImage(colorCircle[point.color], point.x, point.y) //ctx.fill() }) })) p.then(() => { addGuiScheme(gui, { source: { refresh() { render.draw() }, test100() { let count = 100; while (count-- > 0) { render.draw() } } } }) }) }) return { main: container } } } }) addExample("性能测试-threejs", function ({ gui }) { let { toRaw, ref, unref, provide, inject, getCurrentInstance, reactive, shallowReactive, computed, watchEffect, watch, onBeforeMount, onMounted, onBeforeUpdated, onUpdated, onBeforeUnmount, onUnmounted, toRef, toRefs } = Vue; return { template: `
`, setup(props, ctx) { let container = ref(); onMounted(() => { let renderer = new THREE.WebGLRenderer({ }) let scene = new THREE.Scene() let camera = new THREE.OrthographicCamera(0, viewWidth, 0, viewHeight, 1, 1000) camera.position.set(0, 2, 100) camera.lookAt(0, 0, 0) renderer.clearColor(0xddd) renderer.setSize(viewWidth, viewHeight) container.value.appendChild(renderer.domElement) let colorGroup = _.groupBy(testPoints, d => d.color) let cacheGem={} var draw = testFnExecuTime('性能测试-three', function () { scene.clear() Object.keys(colorGroup).forEach((color, index) => { let testpoints = colorGroup[color] const path = new THREE.Path(); var vertices = []; testpoints.forEach((point) => { vertices.push(point.x, point.y, 1); // path.arc(point.x,point.y,point.r,0,PI2) }) vertices = new THREE.Float32BufferAttribute(vertices, 3) const points = path.getPoints(); const geometry = new THREE.BufferGeometry() geometry.setAttribute('position', vertices) // geometry.setFromPoints(points); var sprite = new THREE.CanvasTexture(createCanvasTexture(color, testpoints[0].r)) const material = new THREE.PointsMaterial({ color: testpoints[0].color2, map: sprite, size: testpoints[0].r * 2, sizeAttenuation: false, alphaTest: 0.5, transparent: true }); var mesh = new THREE.Points(geometry, material); mesh.name='mesh' scene.add(mesh) }) renderer.render(scene, camera) }) addGuiScheme(gui, { source: { refresh() { draw() }, test100() { let count = 100; while (count-- > 0) { draw() } } } }) }) return { main: container } } } }) addExample("性能测试-pixijs", function ({ gui }) { let { toRaw, ref, unref, provide, inject, getCurrentInstance, reactive, shallowReactive, computed, watchEffect, watch, onBeforeMount, onMounted, onBeforeUpdated, onUpdated, onBeforeUnmount, onUnmounted, toRef, toRefs } = Vue; return { template: `
`, setup(props, ctx) { let container = ref(); onMounted(() => { let app = new PIXI.Application({ width: viewWidth, height: viewHeight, autoStart: false }) container.value.appendChild(app.view) let colorGroup = _.groupBy(testPoints, d => d.color) let g = new PIXI.Graphics() testPoints.forEach((point) => { g.beginFill(point.color2) g.drawCircle(point.x, point.y, point.r) g.endFill() }) app.stage.addChild(g) var draw = testFnExecuTime('性能测试-pixijs', function () { app.render() }) addGuiScheme(gui, { source: { refresh() { draw() }, test100() { let count = 100; while (count-- > 0) { draw() } } } }) }) return { main: container } } } }) addExample("性能测试-zrender", function ({ gui }) { let { toRaw, ref, unref, provide, inject, getCurrentInstance, reactive, shallowReactive, computed, watchEffect, watch, onBeforeMount, onMounted, onBeforeUpdated, onUpdated, onBeforeUnmount, onUnmounted, toRef, toRefs } = Vue; return { template: `
`, setup(props, ctx) { let container = ref(); onMounted(() => { let render = window.render = zrender.init(container.value, { width: viewWidth, height: viewHeight, devicePixelRatio: 1, useDirtyRect: false }) let g = new zrender.Group() render.add(g) let colorGroup = _.groupBy(testPoints, d => d.color) var first = false var draw = testFnExecuTime('性能测试-zrender', function () { if (first) { g.eachChild((point) => { point.setStyle({ fill: '#' + Math.random().toString(16).substr(2, 6) }) }) render.refresh() return } first = true testPoints.forEach((point) => { let circle = new zrender.Circle({ shape: { cx: point.x, cy: point.y, r: point.r, }, style: { fill: point.color } }) g.add(circle) }) }) addGuiScheme(gui, { source: { refresh() { draw() }, test100() { let count = 100; while (count-- > 0) { draw() } } } }) }) return { main: container } } } })

你可能感兴趣的:(threejs,webgl,canvas,html5,javascript)