换了新工作,也确定了我未来数据可视化的发展方向。新年第一篇博客,又逢春运,这篇技术文章就来交给大家如何做一个酷炫的迁徙图(支持移动哦)。(求star 代码点这里

  迁徙图的制作思路分为静态的元素和变换的动画。其中动画是围绕着静态的元素变换,所以我们首要的任务就是如何绘制静态的元素。

  仔细看一下,静态的元素分为弧线(Arc)、弧线端点的箭头(Marker),动画部分主要是弧线终点向脉冲波一样的圆(Pulse),以及像流星一样的动态小箭头和弧线的光晕,这两个我们放在一起成为Spark。我们可以看到Spark主要在弧线上运动,如果你仔细观察一下会发现终点处点头的指向也是朝着终点处的切线方向,所以我们把主要的任务放在如何根据两个点绘制一段弧线。

  我们要绘制这段弧线就要知道圆心和半径,而数学定理告诉我们的是三点定圆,过两个点的圆有无数个,所以我们只能找一个比较合适的圆。

  所以现在的问题变成了已知两点pointF和pointT,求一个合适的圆心pointC (xc, yc);

  根据pointF和pointT所以我们能够确定一条直线,他的斜率 kt =(yt - yf)/ (xt - xf);

  根据PointF和pointT我们能够计算出他们的中点pointH=(m, n); m = (xt - xf) / 2, n = (yt - yf) / 2;

  经过两点的圆一定在他们两点的中垂线上,而直线的中垂线斜率kl与直线的斜率kt存在数学关系:kl * kt = -1;

  把我们的参数全部套入这个公式可得:

  ((yc - n)/ (xc - m)) * ((yt - yf)/ (xt - xf)) = -1;

  接着变换一下:

  (yc - n) / (xc - m) = -(xt - xf) / (yt - yf);

  去掉碍事的负号:

  (yc - n) / (xc - m) = (xt - xf) / (yf - yt);

  再变换一下:

  (yc - n)/ (xt - xf) = (xc - m) / (yf - yt) = factor;

  到此我们得到:

  yc - n = (xt - xf) * factor;

  xc - m = (yf - yt) * factor;

  这两行公式中都各存在两个位置参数(yc、factor) 和 (xc、factor);所以只要找到一个合适的factor就能够得到合适的圆心进而得到半径起始角和终止角以及半径。有了这些那么Marker的指向、Spark的轨迹都可以确定了。

  现在需要做的是把上述过程转换为代码:

var Arc = (function() {
var A = function(options) {
var startX = options.startX,
startY = options.startY,
endX = options.endX,
endY = options.endY; //两点之间的圆有多个,通过两点及半径便可以定出两个圆,根据需要选取其中一个圆
var L = Math.sqrt(Math.pow(startX - endX, 2) + Math.pow(startY - endY, 2));
var m = (startX + endX) / 2; // 横轴中点
var n = (startY + endY) / 2; // 纵轴中点
var factor = 1.5; var centerX = (startY - endY) * factor + m;
var centerY = (endX - startX) * factor + n; var radius = Math.sqrt(Math.pow(L / 2, 2) + Math.pow(L * factor, 2));
var startAngle = Math.atan2(startY - centerY, startX - centerX);
var endAngle = Math.atan2(endY - centerY, endX - centerX); // this.L = L;
this.startX = startX;
this.startY = startY;
this.endX = endX;
this.endY = endY;
this.centerX = centerX;
this.centerY = centerY;
this.startAngle = startAngle;
this.endAngle = endAngle;
this.startLabel = options && options.labels && options.labels[0],
this.endLabel = options && options.labels && options.labels[1],
this.radius = radius;
this.lineWidth = options.width || 1;
this.strokeStyle = options.color || '#000';
this.shadowBlur = options.shadowBlur;
}; A.prototype.draw = function(context) {
context.save();
context.lineWidth = this.lineWidth;
context.strokeStyle = this.strokeStyle;
context.shadowColor = this.strokeStyle;
context.shadowBlur = this.shadowBlur || 2; context.beginPath();
context.arc(this.centerX, this.centerY, this.radius, this.startAngle, this.endAngle, false);
context.stroke();
context.restore(); context.save();
context.fillStyle = this.strokeStyle;
context.font = "15px sans-serif";
if (this.startLabel) {
context.fillText(this.startLabel, x, y);
}
if (this.endLabel) {
context.fillText(this.endLabel, x, y);
}
context.restore();
}; return A;
})();

  

  理解了上述过程,我们就已经成功了一半。下一步的重点就是动画的绘制。关于动画首先要了解requestAnimationFrame,不知道的小伙伴要去恶补一下啦。好啦,言归正传。Spark动画分为两部分,一部拖尾效果,一部分是弧线光晕效果,当你第一次打开时候会发现,弧线光晕会随着小箭头运动,到达终点后光晕停止运动,剩下小箭头自己运动。由于每个圆的大小不一致,我们需要在每次动画过程中控制光晕和小箭头的位置。

   由于他们是在圆弧上运动,所以我们只要每次计算出他们新的弧度就可以了,弧度的步长可以这样来制定:每走过20像素所转过的弧度就是各个Spark的步长啦。

  所以factor = 20 / radius;

  每次绘制时,光晕与小箭头的弧度位置为:

var endAngle = this.endAngle;
// 匀速
var angle = this.trailAngle + this.factor;

  弧度确定之后我们就能得到小箭头的位置,但是目前并不能得到小箭头的方向。根据canvas中角度的特点,再由简单的几何知识,可以得到小箭头的旋转方向应该为:rotation = angle + Math.PI / 2;

  

  目前为止我们解决了Spark动画中的两大问题,剩下了最后一个:拖尾效果。看起来由粗到细这段就是拖尾效果。

  

  实际上为了保证在移动端的性能,本次实例中并没有明显的拖尾。但拖尾还是一个比较常见的特效,所以我们需要把它掌握。拖尾效果一般是对一个元素进行多次复制,并线性的渐变这队影元素的大小宽度以及颜色的透明度在达到由粗到细由大到小颜色有深变浅的效果。那么每次绘制时候都需要知道这队影元素每个的位置,每个的线宽以及每个的颜色,根据上面讨论的元素位置需要根据弧度来确定。我们说过他们的位置是渐变的,渐变的步长可以这样指定,假设从头到尾的弧长为80,那么每个影元素的之间的间隔为:

this.deltaAngle = (80 / Math.min(this.radius, 400)) / this.tailPointsCount;

  由此便可绘制出拖尾效果:

// 拖尾效果
var count = this.tailPointsCount;
for (var i = 0; i < count; i++) {
var arcColor = utils.calculateColor(this.strokeStyle, 0.3-0.3/count*i);
var tailLineWidth = 5;
if (this.trailAngle - this.deltaAngle * i > this.startAngle) {
this.drawArc(context, arcColor,
tailLineWidth - tailLineWidth / count * i,
this.trailAngle - this.deltaAngle * i,
this.trailAngle
);
}
}

  所以整个Spark的代码如下:

var Spark = (function() {
var S = function(options) {
var startX = options.startX,
startY = options.startY,
endX = options.endX,
endY = options.endY; //两点之间的圆有多个,通过两点及半径便可以定出两个圆,根据需要选取其中一个圆
var L = Math.sqrt(Math.pow(startX - endX, 2) + Math.pow(startY - endY, 2));
var m = (startX + endX) / 2; // 横轴中点
var n = (startY + endY) / 2; // 纵轴中点
var factor = 1.5; var centerX = (startY - endY) * factor + m;
var centerY = (endX - startX) * factor + n; var radius = Math.sqrt(Math.pow(L / 2, 2) + Math.pow(L * factor, 2));
var startAngle = Math.atan2(startY - centerY, startX - centerX);
var endAngle = Math.atan2(endY - centerY, endX - centerX); // 保证Spark的弧度不超过Math.PI
if (startAngle * endAngle < 0) {
if (startAngle < 0) {
startAngle += Math.PI * 2;
endAngle += Math.PI * 2;
} else {
endAngle += Math.PI * 2;
}
} this.tailPointsCount = 5; // 拖尾点数
this.centerX = centerX;
this.centerY = centerY;
this.startAngle = startAngle;
this.endAngle = endAngle;
this.radius = radius;
this.lineWidth = options.width || 5;
this.strokeStyle = options.color || '#000';
this.factor = 2 / this.radius;
this.deltaAngle = (80 / Math.min(this.radius, 400)) / this.tailPointsCount;
this.trailAngle = this.startAngle;
this.arcAngle = this.startAngle; this.animateBlur = true; this.marker = new Marker({
x: 50,
y:80,
rotation: 50 * Math.PI / 180,
style: 'arrow',
color: 'rgb(255, 255, 255)',
size: 2,
borderWidth: 0,
borderColor: this.strokeStyle
});
}; S.prototype.drawArc = function(context, strokeColor, lineWidth, startAngle, endAngle) {
context.save();
context.lineWidth = lineWidth;
// context.lineWidth = 5;
context.strokeStyle = strokeColor;
context.shadowColor = this.strokeStyle;
// context.shadowBlur = 5;
context.lineCap = "round";
context.beginPath();
context.arc(this.centerX, this.centerY, this.radius, startAngle, endAngle, false);
context.stroke();
context.restore();
}; S.prototype.draw = function(context) {
var endAngle = this.endAngle;
// 匀速
var angle = this.trailAngle + (endAngle - this.startAngle) * this.factor;
var strokeColor = this.strokeStyle;
if (this.animateBlur) {
this.arcAngle = angle;
}
this.trailAngle = angle;
strokeColor = utils.calculateColor(strokeColor, 0.1); this.drawArc(context, strokeColor, this.lineWidth, this.startAngle, this.arcAngle); // 拖尾效果
var count = this.tailPointsCount;
for (var i = 0; i < count; i++) {
var arcColor = utils.calculateColor(this.strokeStyle, 0.3-0.3/count*i);
var tailLineWidth = 5;
if (this.trailAngle - this.deltaAngle * i > this.startAngle) {
this.drawArc(context, arcColor,
tailLineWidth - tailLineWidth / count * i,
this.trailAngle - this.deltaAngle * i,
this.trailAngle
);
}
} context.save();
context.translate(this.centerX, this.centerY);
this.marker.x = Math.cos(this.trailAngle) * this.radius;
this.marker.y = Math.sin(this.trailAngle) * this.radius;
this.marker.rotation = this.trailAngle + Math.PI / 2;
this.marker.draw(context);
context.restore(); if ((endAngle - this.trailAngle) * 180 / Math.PI < 0.5) {
this.trailAngle = this.startAngle;
this.animateBlur = false;
}
}; return S;
})();

Spark源码

  到目前为止,迁徙图中主要的技术难点就已经讲完了。但如何把它放到地图上,这个问题我们将在下篇文章中讨论。

手把手教你DIY一个春运迁徙图(一)的更多相关文章

  1. iOS回顾笔记(05) -- 手把手教你封装一个广告轮播图框架

    html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...

  2. R数据分析:跟随top期刊手把手教你做一个临床预测模型

    临床预测模型也是大家比较感兴趣的,今天就带着大家看一篇临床预测模型的文章,并且用一个例子给大家过一遍做法. 这篇文章来自护理领域顶级期刊的文章,文章名在下面 Ballesta-Castillejos ...

  3. 只有20行Javascript代码!手把手教你写一个页面模板引擎

    http://www.toobug.net/article/how_to_design_front_end_template_engine.html http://barretlee.com/webs ...

  4. PWA入门:手把手教你制作一个PWA应用

    摘要: PWA图文教程 原文:PWA入门:手把手教你制作一个PWA应用 作者:MudOnTire Fundebug经授权转载,版权归原作者所有. 简介 Web前端的同学是否想过学习app开发,以弥补自 ...

  5. 用Python手把手教你搭一个Transformer!

    来源商业新知网,原标题:百闻不如一码!手把手教你用Python搭一个Transformer 与基于RNN的方法相比,Transformer 不需要循环,主要是由Attention 机制组成,因而可以充 ...

  6. 手把手教你画一个 逼格满满圆形水波纹loadingview Android

    才没有完结呢o( ̄︶ ̄)n .大家好,这里是番外篇. 拜读了爱哥的博客,又学到不少东西.爱哥曾经说过: 要站在巨人的丁丁上. 那么今天,我们就站在爱哥的丁丁上来学习制作一款自定义view(开个玩笑,爱 ...

  7. 手把手教你DIY尼康ML-L3红外遥控器

    项目介绍 ML-L3是用于尼康部分型号相机的无线红外遥控器,可以通过红外方式来控制快门的释放,支持B门拍摄.官方售价100RMB左右,山寨版售价10RMB左右.虽然也能实现基本的遥控功能,但是功能还是 ...

  8. 手把手教你做一个python+matplotlib的炫酷的数据可视化动图

    1.效果图 2.注意: 上述资料是虚拟的,为了学习制作动图,构建的. 仅供学习, 不是真实数据,请别误传. 当自己需要对真实数据进行可视化时,可进行适当修改. 3.代码: #第1步:导出模块,固定 i ...

  9. 手把手教你画AndroidK线分时图及指标

    先废话一下:来到公司之前.项目是由外包公司做的,面试初,没有接触过分时图k线这块,认为好难,我能搞定不.可是一段时间之后,发现之前做的那是一片稀烂,可是这货是主功能啊.迟早的自己操刀,痛下决心,开搞, ...

随机推荐

  1. 理解Java机制最受欢迎的8幅图

    原文链接:  Top 8 Diagrams for Understanding Java 翻译人员: 铁锚 翻译时间: 2013年10月29日 世间总是一图胜过千万言! 下面的8幅图来自于 Progr ...

  2. 如何开发Android Wear应用程序

    Android Wear是连接安卓手机和可穿戴产品的一个平台.自从今年上半年发布以来,Android Wear获得了大量关注,既有来自消费者的关注,也有来自开发商的关注,后者希望自己的应用程序已经准备 ...

  3. Church encoding

    In mathematics, Church encoding is a means of representing data and operators in the lambda calculus ...

  4. Change ugly fonts in Firefox (KDE)

    When you use KDE the fonts in Firefox are ugly. This is the fix: Open with Kate: /home/yourusername/ ...

  5. Hive 创建和生成Rcfile 和SequenceFile格式的表

    rcfile格式表需要从原始的textfile 文件格式表导出数据并导入到新建好的rcfile格式表里 转: HIVE RCFile高效存储结构 创建rcfile 格式的表: CREATE TABLE ...

  6. git上解决代码冲突

    1.切换到master: git co master 2.拉最新代码:git pull origin master 3.删掉多余符号 4.切换到提交的分支:git br Txxxx 5.合并:git  ...

  7. [Angular 2] Template property syntax

    This lesson covers using the [input] syntax to change an element property such as “hidden” or “conte ...

  8. rsyslog安装

    http://www.rsyslog.com/downloads/download-other/ http://www.rsyslog.com/download/stable-download/pag ...

  9. MIUI6&7桌面角标开源代码简介

    MIUI6&7桌面角标开源代码简介 MIUI6&7上重新设计了桌面app图标的角标显示,基本规则如下: 一.基本介绍 1.默认的情况 当app 向通知栏发送了一条通知 (通知不带进度条 ...

  10. instanceof 和 构造函数

    1. intanceof 运算符 instanceof 运算符返回一个布尔值,表示指定对象是否为某个构造函数的实例. instanceof左边是实例对象 右边是构造函数.它的运算实质是检查右边构建函数 ...