基于HTML5 Canvas绘制的支持手势缩放的室内地图

项目github链接:https://github.com/licaomeng/canvas-zoom
(欢迎小伙伴们Star我的开源项目)

你是否有过这样的经历,在大型的商圈、商场中傻傻找不到路。嗯,室内地图就这样应运而生了。百度地图、高德地图等都提供了室内地图的功能,高德地图最近还把室内地图的API开放了。室内地图的导航、定位功能一定是未来几年非常有前途的一件事。本文提供了一种基于HTML5 Canvas绘制室内地图的方案,更重要的是可以支持手势的缩放。先来看看室内地图的效果gif动图吧:
基于HTML5 Canvas绘制的支持手势缩放的室内地图_第1张图片           
怎么样?是不是感觉非常炫酷?下面就来分析一下这个开源项目:

地图绘制

关于地图的绘制,这里采用的是HTML5的Canvas绘制的。这是HTML5非常重要的一个特性,这使得我们绘制一些图形变得非常的方便。该项目是将地图数据专门放到一个mapinfo.js文件里面的,使用一个数组进行保存,如下:

var ALLMAPINFO = [[
{
    title: 'Toilet',
    x: 0,
    y: 0,
    width: 171,
    height: 283,
    color: "rgba(76, 181, 216, 0.2)",
    textcolor: "black",
    bordercolor: "rgba(76, 181, 216, 1)",
    imageurl: 'images/Toilet.png',
},
......

最后遍历这些数据,用一个方法将地图绘制出来。

function DrawBlock(x, y, width, height, text, backgroudcolor, textcolor, bordercolor, imageurl) {
    var canvas = document.getElementById("indoormap");
    var context2D = canvas.getContext("2d");
    var textsize = width * height / (1000 * zoomScale * zoomScale);
    context2D.fillStyle = backgroudcolor;
    context2D.fillRect(x, y, width, height);
    context2D.strokeStyle = bordercolor;
    context2D.lineWidth = 0.8;
    context2D.strokeRect(x, y, width, height);
    context2D.fillStyle = textcolor;
    if (textsize > 15) {
        context2D.font = 20 * zoomScale + "pt Microsoft YaHei";
        if (imageurl != "") {
            var image = new Image();
            image.src = imageurl;
            context2D.drawImage(image, x + width / 7, y + height / 2 - 25 * zoomScale, 30 * zoomScale, 30 * zoomScale);
        }
    } else {
        context2D.font = 0 + "pt Microsoft YaHei";
    }
    context2D.fillText(text, x + width / 7 + 40 * zoomScale, y + height / 2);
}

手势缩放

这个室内地图的关键是如何进行缩放,该功能的实现主要依赖canvas-zoom.js,先把代码贴在这里:

/* ================================= canvas-zoom - v0.1 http://github.com/licaomeng/canvas-zoom (c) 2015 Caomeng LI This code may be freely distributed under the Apache License ================================= */

(function () {
    var root = this; // global object
    var CanvasZoom = function (options) {
        if (!options || !options.canvas) {
            throw 'CanvasZoom constructor: missing arguments canvas';
        }

        this.canvas = options.canvas;
        this.canvas.width = this.canvas.clientWidth;
        this.canvas.height = this.canvas.clientHeight;
        this.context = this.canvas.getContext('2d');

        this.desktop = options.desktop || false; // non touch events

        this.scaleAdaption = 1;

        var indoormap = document.getElementById("indoormap");
        var pageWidth = parseInt(indoormap.getAttribute("width"));
        var pageHeight = parseInt(indoormap.getAttribute("height"));
        currentWidth = document.documentElement.clientWidth;
        currentHeight = document.documentElement.clientHeight;

        var offsetX = 0;
        var offsetY = 0;
        if (pageWidth < pageHeight) {//canvas.width < canvas.height
            this.scaleAdaption = currentHeight / pageHeight;
            if (pageWidth * this.scaleAdaption > currentWidth) {
                this.scaleAdaption = this.scaleAdaption * (currentWidth / (this.scaleAdaption * pageWidth));
            }
        } else {//canvas.width >= canvas.height
            this.scaleAdaption = currentWidth / pageWidth;
            if (pageHeight * this.scaleAdaption > currentHeight) {
                this.scaleAdaption = this.scaleAdaption * (currentHeight / (this.scaleAdaption * pageHeight));
            }
        }

        indoormap.setAttribute("width", pageWidth * this.scaleAdaption);
        indoormap.setAttribute("height", pageHeight * this.scaleAdaption);

        this.positionAdaption = {
            x: (parseInt(currentWidth) - parseInt(indoormap.getAttribute("width"))) / 2,
            y: (parseInt(currentHeight) - parseInt(indoormap.getAttribute("height"))) / 2
        };

        indoormap.setAttribute("width", currentWidth);
        indoormap.setAttribute("height", currentHeight);

        this.position = {
            x: 0,
            y: 0
        };

        this.scale = {
            x: 1,
            y: 1
        };

        this.focusPointer = {
            x: 0,
            y: 0
        }

        this.lastZoomScale = null;
        this.lastX = null;
        this.lastY = null;

        this.mdown = false; // desktop drag

        this.init = false;
        this.checkRequestAnimationFrame();
        requestAnimationFrame(this.animate.bind(this));

        this.setEventListeners();
    };

    CanvasZoom.prototype = {
        animate: function () {
            // set scale such as image cover all the canvas
            if (!this.init) {
                var scaleRatio = null;
                if (this.canvas.clientWidth > this.canvas.clientHeight) {
                    scaleRatio = this.scale.x;
                } else {
                    scaleRatio = this.scale.y;
                }
                this.scale.x = scaleRatio;
                this.scale.y = scaleRatio;
                this.init = true;
            }
            // 清空Canvas
            this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
            // 绘制地图方法
            DrawMapInfo(this.scale.x * this.scaleAdaption, this.scale.y * this.scaleAdaption, this.position.x + this.positionAdaption.x, this.position.y + this.positionAdaption.y);
            // 每隔一段时间进行一次刷新
            requestAnimationFrame(this.animate.bind(this));
        },

        gesturePinchZoom: function (event) {
            var zoom = false;
            if (event.targetTouches.length >= 2) {
                // 获得屏幕上的第一个点
                var p1 = event.targetTouches[0];
                // 获得屏幕上的第二个点
                var p2 = event.targetTouches[1];
                // 屏幕上两个点的中点坐标,也就是我们缩放时的缩放中点
                this.focusPointer.x = (p1.pageX + p2.pageX) / 2;
                this.focusPointer.y = (p1.pageY + p2.pageY) / 2;
                // zoomScale是此时刻屏幕上两点之间的距离
                var zoomScale = Math.sqrt(Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2));
                if (this.lastZoomScale) {
                    // zoom是此时刻与上一时刻屏幕上两点之间的距离的差
                    zoom = zoomScale - this.lastZoomScale;
                }
                // 此时刻屏幕上两点之间距离成为下一时刻屏幕上两点之间距离
                this.lastZoomScale = zoomScale;
            }
            // 最后返回的这个zoom将成为缩放比例的依据,这里是除以400得到的值+1作为缩放的倍数
            return zoom;
        },

        doZoom: function (zoom) {
            if (!zoom)
                return;
            // new scale
            var currentScale = this.scale.x;
            var newScale = this.scale.x + zoom / 400;

            if (newScale > 1) {
                if (newScale > 2.5) {
                    newScale = 2.5;
                } else {
                    newScale = this.scale.x + zoom / 400;
                }
            } else {
                newScale = 1;
            }
            this.scale.x = newScale;
            this.scale.y = newScale;

            var deltaScale = newScale - currentScale;
            var currentWidth = (this.canvas.width * this.scale.x);
            var currentHeight = (this.canvas.height * this.scale.y);
            var deltaWidth = this.canvas.width * deltaScale;
            var deltaHeight = this.canvas.height * deltaScale;
            var canvasmiddleX = this.focusPointer.x;
            var canvasmiddleY = this.focusPointer.y;
            var xonmap = (-this.position.x) + canvasmiddleX;
            var yonmap = (-this.position.y) + canvasmiddleY;
            var coefX = -xonmap / (currentWidth);
            var coefY = -yonmap / (currentHeight);
            var newPosX = this.position.x + deltaWidth * coefX;
            var newPosY = this.position.y + deltaHeight * coefY;
            // edges cases
            var newWidth = currentWidth + deltaWidth;
            var newHeight = currentHeight + deltaHeight;
            if (newWidth < this.canvas.clientWidth)
                return;
            if (newPosX > 0) {
                newPosX = 0;
            }
            if (newPosX + newWidth < this.canvas.clientWidth) {
                newPosX = this.canvas.clientWidth - newWidth;
            }

            if (newHeight < this.canvas.clientHeight)
                return;
            if (newPosY > 0) {
                newPosY = 0;
            }
            if (newPosY + newHeight < this.canvas.clientHeight) {
                newPosY = this.canvas.clientHeight - newHeight;
            }

            // finally affectations
            this.scale.x = newScale;
            this.scale.y = newScale;
            this.position.x = newPosX;
            this.position.y = newPosY;
        },

        doMove: function (relativeX, relativeY) {
            if (this.lastX && this.lastY) {
                var deltaX = relativeX - this.lastX;
                var deltaY = relativeY - this.lastY;

                var currentWidth = (this.canvas.clientWidth * this.scale.x);
                var currentHeight = (this.canvas.clientHeight * this.scale.y);

                this.position.x += deltaX;
                this.position.y += deltaY;

                // edge cases
                if (this.position.x > 0) {
                    this.position.x = 0;
                } else if (this.position.x + currentWidth < this.canvas.clientWidth) {
                    this.position.x = this.canvas.width - currentWidth;
                }
                if (this.position.y > 0) {
                    this.position.y = 0;
                } else if (this.position.y + currentHeight < this.canvas.clientHeight) {
                    this.position.y = this.canvas.height - currentHeight;
                }
            }
            this.lastX = relativeX;
            this.lastY = relativeY;
        },

        setEventListeners: function () {
            // touch
            this.canvas.addEventListener('touchstart', function (e) {
                this.lastX = null;
                this.lastY = null;
                this.lastZoomScale = null;
            }.bind(this));

            this.canvas.addEventListener('touchmove', function (e) {
                e.preventDefault();

                if (e.targetTouches.length == 2) { // pinch
                    this.doZoom(this.gesturePinchZoom(e));
                } else if (e.targetTouches.length == 1) {// move
                    var relativeX = e.targetTouches[0].pageX - this.canvas.getBoundingClientRect().left;
                    var relativeY = e.targetTouches[0].pageY - this.canvas.getBoundingClientRect().top;

                    this.doMove(relativeX, relativeY);
                }
            }.bind(this));

            if (this.desktop) {
                // keyboard+mouse
                window.addEventListener('keyup', function (e) {
                    if (e.keyCode == 187 || e.keyCode == 61) { // +
                        this.doZoom(15);
                    } else if (e.keyCode == 54) {// -
                        this.doZoom(-15);
                    }
                }.bind(this));

                window.addEventListener('mousedown', function (e) {
                    this.mdown = true;
                    this.lastX = null;
                    this.lastY = null;
                }.bind(this));

                window.addEventListener('mouseup', function (e) {
                    this.mdown = false;
                }.bind(this));

                window.addEventListener('mousemove', function (e) {
                    var relativeX = e.pageX - this.canvas.getBoundingClientRect().left;
                    var relativeY = e.pageY - this.canvas.getBoundingClientRect().top;

                    if (e.target == this.canvas && this.mdown) {
                        this.doMove(relativeX, relativeY);
                    }

                    if (relativeX <= 0 || relativeX >= this.canvas.clientWidth || relativeY <= 0 || relativeY >= this.canvas.clientHeight) {
                        this.mdown = false;
                    }
                }.bind(this));
            }
        },

        checkRequestAnimationFrame: function () {
            var lastTime = 0;
            var vendors = ['ms', 'moz', 'webkit', 'o'];
            for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
                window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
                window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame']
                        || window[vendors[x] + 'CancelRequestAnimationFrame'];
            }

            if (!window.requestAnimationFrame) {
                window.requestAnimationFrame = function (callback, element) {
                    var currTime = new Date().getTime();
                    var timeToCall = Math.max(0, 16 - (currTime - lastTime));
                    var id = window.setTimeout(function () {
                        callback(currTime + timeToCall);
                    }, timeToCall);
                    lastTime = currTime + timeToCall;
                    return id;
                };
            }

            if (!window.cancelAnimationFrame) {
                window.cancelAnimationFrame = function (id) {
                    clearTimeout(id);
                };
            }
        }
    };

    root.CanvasZoom = CanvasZoom;
}).call(this);

其中,最核心的两个方法主要是animategesturePinchZoom,它们分别负责地图的刷新重绘以及缩放的实现。

跨平台移植

代码的大致情况就是这样,如果想要把我们这些前端的HTML5, js移植到移动端就需要借助于Cordova(前身是PhoneGap),在提供的github项目中,给出了Android版的,iOS版的可能还需要一段时间,不过都是借助于Cordova进行打包而已。

屏幕自适应

移动端应用自适应不同手机屏幕尺寸、分辨率,这是一个既老生常谈又常论常新的话题。关于屏幕自适应,项目结合了两种做法

第一阶段:

之前的做法是利用HTML5 的viewport进行屏幕自适应的:

function setViewport() {
    if ((navigator.userAgent.toLowerCase().indexOf("android") != -1) || (navigator.userAgent.toLowerCase().indexOf("iphone") != -1)) {
        var scale = curwidth / pagewidth;
        var vPort = "width=" + pagewidth + ", maximum-scale=" + scale + ", minimum-scale=" + scale + ", initial-scale=" + scale + ", user-scalable=yes";
        document.getElementById("viewport_map").setAttribute("content", vPort);
    }
}

这样做能够很容易达到我们的目的,但是问题还是很明显的,经过这种方法的自适应处理,相当于在我们的浏览器中直接将页面放大缩小,这样Canvas就会失真变得模糊不清。

第二阶段:

之后改良的做法,有一部分和之前的做法是相似的,虽然没有直接粗暴的往viewport里面添加值。但是同样都要获取(当前设备的宽度(高度)像素/页面本身的宽度(高度)像素),这个就是我们缩放页面的倍数,这个倍数可能大于1,也可能小于1。那么在页面初始加载的时候所有元素都要乘上这个倍数,这样我们在不同的设备尺寸、分辨率上看到的样式就是统一的。

控件特效

页面上面三个控件都是使用CSS3绘制出来的,按下还有disabled状态都是用CSS实现的。包括上面的图标也都是通过CSS3 的@font-face实现的(可以参考我的上一篇博文:利用CSS3 @font-face使用图标字体)。关于控件特效的整体实现,请参考github上的项目源码。

你可能感兴趣的:(canvas,html5,屏幕适配,手势缩放,室内地图)