(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.Windy = factory()); }(this, (function () { 'use strict'; var tool = { merge: function merge(settings, defaults) { Object.keys(settings).forEach(function (key) { defaults[key] = settings[key]; }); }, //计算两点间距离 getDistance: function getDistance(p1, p2) { return Math.sqrt((p1[0] - p2[0]) * (p1[0] - p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1])); }, //判断点是否在线段上 containStroke: function containStroke(x0, y0, x1, y1, lineWidth, x, y) { if (lineWidth === 0) { return false; } var _l = lineWidth; var _a = 0; var _b = x0; // Quick reject if (y > y0 + _l && y > y1 + _l || y < y0 - _l && y < y1 - _l || x > x0 + _l && x > x1 + _l || x < x0 - _l && x < x1 - _l) { return false; } if (x0 !== x1) { _a = (y0 - y1) / (x0 - x1); _b = (x0 * y1 - x1 * y0) / (x0 - x1); } else { return Math.abs(x - x0) <= _l / 2; } var tmp = _a * x - y + _b; var _s = tmp * tmp / (_a * _a + 1); return _s <= _l / 2 * _l / 2; } }; var resolutionScale = function (context) { var devicePixelRatio = window.devicePixelRatio || 1; context.canvas.width = context.canvas.width * devicePixelRatio; context.canvas.height = context.canvas.height * devicePixelRatio; context.canvas.style.width = context.canvas.width / devicePixelRatio + 'px'; context.canvas.style.height = context.canvas.height / devicePixelRatio + 'px'; context.scale(devicePixelRatio, devicePixelRatio); }; var global = typeof window === 'undefined' ? {} : window; var requestAnimationFrame = global.requestAnimationFrame || global.mozRequestAnimationFrame || global.webkitRequestAnimationFrame || global.msRequestAnimationFrame || function (callback) { return global.setTimeout(callback, 1000 / 60); }; var cancelAnimationFrame = global.cancelAnimationFrame || global.mozCancelAnimationFrame || global.webkitCancelAnimationFrame || global.msCancelAnimationFrame || function (id) { clearTimeout(id); }; var Windy = function Windy(map, userOptions) { var self = this; self.map = map; //默认参数 var options = { MAX_PARTICLE_AGE: 100, FRAME_RATE: 20, PARTICLE_MULTIPLIER: 8, size: 0.8, color: 'rgba(71,160,233,0.8)', isShowLine: true }; self.init(userOptions, options); //全局变量 var baseCtx = self.baseCtx = self.options.canvas.getContext("2d"); self.baseCtx.lineWidth = 0.8; self.baseCtx.fillStyle = 'rgba(0,0,0,.9)'; }; Windy.prototype.init = function (setting, defaults) { //合并参数 tool.merge(setting, defaults); this.options = defaults; }; Windy.prototype.start = function () { var self = this; self.stop(); self.adjustSize(); self.addLine(); self.interpolateGrid(); // if (self.options.isShowLine) { // self.drawline(); // } // self.createParticle(); // self.animate(); // self.drawParticle(); }; Windy.prototype.stop = function () { var self = this; self.baseCtx.clearRect(0, 0, self.width, self.height); if (self.vector) self.vector.release(); if (self.animationId) { cancelAnimationFrame(self.animationId); } if (self.timer) { clearTimeout(self.timer); } }; Windy.prototype.adjustSize = function () { var width = this.map.width; var height = this.map.height; this.baseCtx.canvas.width = width; this.baseCtx.canvas.height = height; resolutionScale(this.baseCtx); this.width = this.baseCtx.canvas.width; this.height = this.baseCtx.canvas.height; }; Windy.prototype.addLine = function () { var options = this.options; var roadLines = this.roadLines = [], dataset = this.options.data; dataset.forEach(function (line, i) { roadLines.push(new Line({ points: line, color: 'rgba(38,173,133)' })); }); }; Windy.prototype.drawline = function () { var self = this; var baseCtx = self.baseCtx; if (!baseCtx) { return; } var roadLines = self.roadLines; roadLines.forEach(function (line) { line.drawPath(baseCtx, self.map, self.options); }); }; Windy.prototype.createParticle = function () { var self = this; var poly = self.roadLines[0].path; var options = this.options; var particles = this.particles = []; var particleCount = Math.round(this.width * options.PARTICLE_MULTIPLIER); for (var i = 0; i < particleCount; i++) { var x = Math.floor(Math.random() * self.width), y = Math.floor(Math.random() * self.height); if (self.rayCasting({ x: x, y: y }, poly) === 'out') { continue; } if (this.columns[x] == undefined) { continue; } var vector = self.vector(x, y); vector.x = x; vector.y = y; vector.age = Math.floor(Math.random() * options.MAX_PARTICLE_AGE); vector.color = '#fff'; particles.push(vector); } }; Windy.prototype.animate = function () { var self = this, options = this.options, particles = self.particles; function evolve() { particles.forEach(function (particle, i) { if (particle.age > options.MAX_PARTICLE_AGE) { particle = self.vector.generate({ age: 0 }); particles.splice(i, 1, particle); } var x = particle.x; var y = particle.y; var v = self.vector(x, y); if (v) { var xe = x - v.speed * Math.sin(Math.PI / 180 * (180 - v.angle)); var ye = y - v.speed * Math.cos(Math.PI / 180 * (180 - v.angle)); var nextPoint = self.vector(xe, ye); if (nextPoint) { particle.xe = xe; particle.ye = ye; } else { var newParticle = self.vector.generate({ age: Math.floor(Math.random() * options.MAX_PARTICLE_AGE) }); particles.splice(i, 1, newParticle); } } else { particle.age = options.MAX_PARTICLE_AGE; } particle.age += 1; }); } function render() { var context = self.baseCtx; context.fillStyle = "rgba(0,0,0,0.97)"; var prev = context.globalCompositeOperation; context.globalCompositeOperation = "destination-in"; context.fillRect(0, 0, self.width, self.height); context.globalCompositeOperation = prev; context.beginPath(); context.strokeStyle = '#fff'; particles.forEach(function (particle, i) { context.moveTo(particle.x, particle.y); context.lineTo(particle.xe, particle.ye); particle.x = particle.xe; particle.y = particle.ye; }); context.stroke(); } (function frame() { try { self.timer = setTimeout(function () { requestAnimationFrame(frame); evolve(); render(); }, 1000 / options.FRAME_RATE); } catch (e) { console.error(e); } })(); }; Windy.prototype.drawParticle = function () { var self = this; var baseCtx = self.baseCtx; if (!baseCtx) { return; } var particles = self.particles; particles.forEach(function (particle, i) { particle.draw(baseCtx); }); }; Windy.prototype.interpolateGrid = function () { var self = this; var columns = this.columns = []; var x = 0; function interpolateColumn(x) { var column = []; for (var y = 0; y < self.height; y += 2) { var wind = interpolate(x, y); column[y + 1] = column[y] = wind; } columns[x + 1] = columns[x] = column; } var points = self.roadLines[0].points.splice(0, 90); function interpolate(x, y) { var angle0 = 0, angle1 = 0, speed0 = 5, speed1 = 5, wind = {}; points.forEach(function (p) { angle0 += p.angle * 1.0 / ((y - p.y) * (y - p.y) + (x - p.x) * (x - p.x)); angle1 += 1.0 / ((y - p.y) * (y - p.y) + (x - p.x) * (x - p.x)); speed0 += p.speed * 1.0 / ((y - p.y) * (y - p.y) + (x - p.x) * (x - p.x)); speed1 += 1.0 / ((y - p.y) * (y - p.y) + (x - p.x) * (x - p.x)); if (angle1 != 0) { wind.angle = angle0 / angle1; } if (speed1 != 0) { wind.speed = speed0 / speed1; } }); return wind; } (function batchInterpolate() { var start = Date.now(); while (x < self.width) { interpolateColumn(x); x += 2; if (Date.now() - start > 1000) { setTimeout(batchInterpolate, 25); return; } } self.createField(); })(); }; Windy.prototype.createField = function () { var self = this; var columns = this.columns; function vector(x, y) { var column = columns[Math.floor(x)]; return column && column[Math.floor(y)]; } vector.release = function () { self.columns = []; }; vector.generate = function (o) { var x = Math.floor(Math.random() * self.width); var y = Math.floor(Math.random() * self.height); o.x = x; o.y = y; return o; }; this.vector = vector; if (this.options.isShowLine) { this.drawline(); } this.createParticle(); this.animate(); }; Windy.prototype.rayCasting = function (p, poly) { var px = p.x, py = p.y, flag = false; for (var i = 0, l = poly.length, j = l - 1; i < l; j = i, i++) { var sx = poly[i].x, sy = poly[i].y, tx = poly[j].x, ty = poly[j].y; // 点与多边形顶点重合 if (sx === px && sy === py || tx === px && ty === py) { return 'in'; } // 判断线段两端点是否在射线两侧 if (sy < py && ty >= py || sy >= py && ty < py) { // 线段上与射线 Y 坐标相同的点的 X 坐标 var x = sx + (py - sy) * (tx - sx) / (ty - sy); // 点在多边形的边上 if (x === px) { return 'in'; } // 射线穿过多边形的边界 if (x > px) { flag = !flag; } } } // 射线穿过多边形边界的次数为奇数时点在多边形内 return flag ? 'in' : 'out'; }; function Line(options) { this.points = options.points || []; this.color = options.color || '#ffff00'; } Line.prototype.getPointList = function (map) { var path = this.path = [], points = this.points; if (points && points.length > 0) { points.forEach(function (p) { var _screePoint = map.toScreen(p); // if (_screePoint.x < 0 || _screePoint.y < 0) { // return; // } var pixel = map.toScreen(p); path.push({ x: pixel.x, y: pixel.y }); }); this.maxAge = path.length; } return path; }; Line.prototype.drawPath = function (context, map) { var pointList = this.path || this.getPointList(map); context.beginPath(); context.lineWidth = 1; context.strokeStyle = this.color; context.moveTo(pointList[0].x, pointList[0].y); for (var i = 0, len = pointList.length; i < len; i++) { context.lineTo(pointList[i].x, pointList[i].y); } context.stroke(); }; return Windy; })));