项目github链接:https://github.com/licaomeng/canvas-zoom
(欢迎小伙伴们Star我的开源项目)
你是否有过这样的经历,在大型的商圈、商场中傻傻找不到路。嗯,室内地图就这样应运而生了。百度地图、高德地图等都提供了室内地图的功能,高德地图最近还把室内地图的API开放了。室内地图的导航、定位功能一定是未来几年非常有前途的一件事。本文提供了一种基于HTML5 Canvas绘制室内地图的方案,更重要的是可以支持手势的缩放。先来看看室内地图的效果gif动图吧:
怎么样?是不是感觉非常炫酷?下面就来分析一下这个开源项目:
关于地图的绘制,这里采用的是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);
其中,最核心的两个方法主要是animate
和gesturePinchZoom
,它们分别负责地图的刷新重绘以及缩放的实现。
代码的大致情况就是这样,如果想要把我们这些前端的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上的项目源码。