image.png

    1. <!DOCTYPE html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
    6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    7. <title>svg力导向图动画-可以修改小球数量</title>
    8. <style>
    9. svg {
    10. border: 1px solid #eee;
    11. }
    12. </style>
    13. </head>
    14. <body>
    15. <svg xmlns="http://www.w3.org/2000/svg" viewBox="-400 -400 800 800" width="800" height="800">
    16. <path id="links" fill="none" stroke="#eeeeee" />
    17. <text id="text" fill="white"></text>
    18. </svg>
    19. <script>
    20. //辅助类 Vector: 向量==矢量,有方向的标量;
    21. (function () {
    22. function Vector(x, y) {
    23. this.x = x || 0;
    24. this.y = y || 0;
    25. }
    26. Vector.prototype = {
    27. constructor: Vector,
    28. square: function () {
    29. return this.x ** 2 + this.y ** 2;
    30. },
    31. length: function () {
    32. return Math.sqrt(this.square());
    33. },
    34. add: function (q) {
    35. return new Vector(this.x + q.x, this.y + q.y)
    36. },
    37. minus: function (q) {
    38. return new Vector(this.x - q.x, this.y - q.y)
    39. },
    40. multiply: function (scale) {
    41. return new Vector(this.x * scale, this.y * scale)
    42. },
    43. //标准化: 乘以一个方向向量,相当于length在某向量上的投影
    44. normalize: function (length) {
    45. if (length == undefined) {
    46. length = 1;
    47. }
    48. return this.multiply(length / this.length());
    49. }
    50. };
    51. Vector.fromPoints = function (p1, p2) { //函数可以有自己的方法
    52. return new Vector(p2.x - p1.x, p2.y - p1.y);
    53. };
    54. window.Vector = Vector;
    55. })();
    56. //end of Vector
    57. // 力导图: 近乎弹簧的互作,近排斥,远吸引;
    58. var XML_NS = "http://www.w3.org/2000/svg";
    59. var k = 0.1; //弹性系数,弹簧变形的距离和力之间的系数 F=k*delta; 尝试0.05比较好
    60. //建立点,简化为等质量的点,可以人一多个点
    61. //todo map()函数
    62. var points = "a,b,c,d,e,f,g,h".split(",").map(function (name, index, arr) {
    63. return {
    64. name: name,
    65. color: 'hsl(' + (360 / arr.length * index) + ', 100%, 60%)',
    66. }
    67. });
    68. var relation = 300; //互作为0的距离;
    69. var svg = document.querySelector('svg'), text = document.querySelector('text');
    70. var Vector = window.Vector;
    71. function random(min, max) {
    72. return Math.round(min + (max - min) * Math.random());
    73. }
    74. // todo forEach()函数
    75. points.forEach(function (point) {
    76. //console.log(point) //{name: "a", color: "hsl(0, 100%, 60%)"}
    77. var circle = document.createElementNS(XML_NS, 'circle'); //这个namespace写错是不能显示的!
    78. //init position
    79. var x = random(-300, 300), y = random(-300, 300); //调整到屏幕中心区域
    80. circle.setAttribute('cx', x);
    81. circle.setAttribute('cy', y);
    82. circle.setAttribute('r', "10");
    83. circle.setAttribute('fill', point.color);
    84. //绑定
    85. point.circle = circle;
    86. point.s = new Vector(x, y);//初始化位移
    87. point.v = new Vector();//初始没有速度
    88. point.a = new Vector();//初始没有加速度
    89. //加入到dom
    90. svg.appendChild(circle);
    91. });
    92. //获取上一帧的时间
    93. var lastFrameTime = +new Date();
    94. //更新当前帧
    95. var w = 0
    96. var links = document.getElementById('links');
    97. function update() {
    98. var frameTime = +new Date();//拿到当前帧的时间
    99. var t = frameTime - lastFrameTime;//两帧时间差
    100. //animation begin
    101. //对时间差(单位是ms)进行缩放,
    102. t /= 100; //缩放到1/5s为单位; 物理学单位是m,屏幕单位是px,尝试出来的换算关系;
    103. //更新点的位移、速度、加速度、力等
    104. points.forEach(function (pa) {
    105. //console.log(pa)
    106. var f = new Vector(); //定义一个空矢量,表示力
    107. //1.计算合力
    108. points.forEach(function (pb) {
    109. //两两互作,排除掉自己和自己
    110. if (pa.name == pb.name) return;
    111. //console.log(w,'两两',pa.name, pb.name)
    112. var x = Vector.fromPoints(pa.s, pb.s);//点之间的距离
    113. var delta = x.length() - relation;//自然长度之外的 弹性变量
    114. //求合力 f=k*x
    115. f = f.add(x.normalize(delta * k)); //在x方向上,力(delta*k) 的投影的长度
    116. });
    117. //2.计算加速度 a=F/m; 简化质量都是m=1
    118. pa.a = f;
    119. //3. 计算速度 vt=v0+a*t;
    120. //如果没有能量损耗,系统将会一直运动下去。此处假设速度每次损耗2%,设置为0.98
    121. pa.v = pa.v.add(pa.a.multiply(t)).multiply(0.98); //bug: 0.999时停止的慢,但是全部小球的质心不固定!
    122. //4. 计算位置 st=s0+v*t
    123. pa.s = pa.s.add(pa.v.multiply(t));
    124. //设置到dom上
    125. pa.circle.setAttribute('cx', pa.s.x);
    126. pa.circle.setAttribute('cy', pa.s.y);
    127. });
    128. //前面计算
    129. //接着,根据计算结果,更新画面
    130. render();
    131. //for debug only;
    132. w += 1;
    133. if (w % 100 == 0) {
    134. console.log(' >>> steps=', w);
    135. }
    136. //2000步差不多正好,可以停止了,
    137. //也可以设置其他停止条件,比如全部小球的变动小于某个值的时候,需要记录上一个小球的位置
    138. if (w > 2000) {
    139. return;
    140. }
    141. //animation end
    142. lastFrameTime = frameTime;//当前帧的时间已经成历史
    143. window.requestAnimationFrame(update);//更新画面
    144. }
    145. //渲染
    146. function render() {
    147. //画点和点之间的连线
    148. var linkPath = [];
    149. points.forEach(function (pa) {
    150. var sa = pa.s;
    151. points.forEach(function (pb) {
    152. if (pa.name == pb.name) return;
    153. var sb = pb.s;
    154. linkPath = linkPath.concat([
    155. "M", sa.x, sa.y,
    156. 'L', sb.x, sb.y,
    157. ]);
    158. });
    159. });
    160. //绘制连线
    161. links.setAttribute('d', linkPath.join(' '));
    162. //为小球添加文字
    163. text.innerHTML = "";
    164. function n2str(n) {
    165. return new String(n);
    166. }
    167. points.forEach(function (p) {
    168. var tspan = document.createElementNS(XML_NS, 'tspan');
    169. tspan.setAttribute('x', n2str(p.s.x - 5))
    170. tspan.setAttribute('y', n2str(p.s.y + 5))
    171. tspan.innerHTML = p.name;
    172. text.appendChild(tspan);
    173. });
    174. svg.append(text);//重新把文字放到最后,也就是顶层
    175. }
    176. window.requestAnimationFrame(update);
    177. </script>
    178. </body>
    179. </html>