基于球坐标的3D webex-ball.
小组里有一个小小的研究, 用前端技术画一个新的webex-ball
(什么是webex-ball ? webex-ball 是 webex meeting 启动时的一个icon, 旋转用于显示当前的状态, 如下)
PIC-1
以前我们的webex-ball, 是横着旋转的. 这个时候, LD们想改了. 想来个现在流行的 "注水式" 的进度读取方式, 如下:
PIC-2
话说其实最推荐的处理, 当然是用 3d-max, 画一个这样的球, 然后渲染出来成png, 然后只需要用js 完成拼接动作, 即可了.
但是, 这样就谈不上太多的前端技术了 (大家都在想, 只用css split 能够完成的活, 怎么能够体现出我们的价值呢)
于是, 很多复杂的方案出来了.
有arlen (刘国文) 的div 嵌套遮盖的
PIC-3
PIC-4
PIC-5
有dh 的css3 画的
http://www.jslab.org.cn/?tag=WebExBall&page=2
有利用几个div互相遮盖, 嵌套的 (同事们的处理).
{
1. 最底层是个完成的 ball a
2. ball a上面是个div, 盖住ball a
3. 椭圆形在div上面
4. 最上面是一个半透明的 ball b
满的过程: 就是 div的height从ball的 高度变到0,顶部和ball的 顶部对齐, 慢慢露出ball a.
椭圆形的width 从 0 -> ball width -> 0, 位置跟着移动
}
以上的都有不可避免的缺陷, 即: 不够真实. 因为无法更具体的描绘出他的变化(看椭圆切面可得之). 所以, 如果想让他真实, 还真得把他放到比较真实的情况下.
所以, 用3d 来构建是最好不过了.
我以前写了一个JS 的渲染模式, 这个时候刚刚好可以拿来用了(以前在w3c.Hangzhou 上分享过).
但是, 我看了下后, 发现他的代码量太多拉 (我用的 直角坐标系), 他是一套完整的系统, 需要有
1. 世界坐标系.
2. 摄像机坐标系.
3. 物体坐标系.
必须把这3者都初始化, 才能够得到一个真实的世界.
仅仅用在一个球上, 显然是, 有些 大材小用, 或者太复杂了.
然后, 想起了一位仁兄(ONEBOYS, http://www.cssass.com/blog/index.php), 曾经也弄过这个, 并且和他有过讨论, 他的Demo 代码很省, 再次翻到的时候, 发现他用的是球坐标体系... 果然代码省了 (球坐标和直角坐标 各有用途, 这方面没有太多的具体可比, 关键看用途)
不多说, 看代码.
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset='gbk' /> 5 <title>WebexBall-test-3</title> 6 </head> 7 <body> 8 <div style="overflow:hidden;"> 9 <button id="btn-0"> 10 从0到100 11 </button> 12 <button id="btn-1"> 13 从100到0 14 </button> 15 <span>从</span> 16 <input type="text" maxlength="3" id="inp-0" value="0" /> 17 <span>到</span> 18 <input type="text" maxlength="3" id="inp-1" value="10" /> 19 <button id="btn-2"> 20 开始 21 </button> 22 </div> 23 <canvas id="canvas" width="800" height="800"></canvas> 24 <script type="text/javascript" > 25 function g(e){ 26 return document.getElementById(e); 27 } 28 var ctx = g('canvas').getContext('2d'); 29 ctx.scale(100,100); 30 ctx.lineWidth=0.002; 31 ctx.lineJoin = "round"; 32 var Ball = function(x,y,r){ 33 this.theta = 0; //球坐标转角 34 this.eleva = 0; //球坐标仰角 35 this.radius= r || 1; //球坐标半径 36 this.pos={ //球坐标原点 37 x:x || 2, 38 y:y || 2 39 }; 40 this.co={ //二维上的投影坐标 41 x:0, 42 y:0 43 }; 44 /* 45 this.col={ //颜色 46 r:255, 47 g:255, 48 b:255, 49 a:0.6 50 }; 51 */ 52 this.init=function(){ 53 ctx.translate(this.pos.x,this.pos.y); 54 this.theta = 1.57; 55 this.eleva = 0.32; 56 this.preview(0); 57 }; 58 this.init(); 59 }; 60 /* 球坐标系转平面直角坐标系 */ 61 Ball.prototype.iso=function(a, b, r){ 62 var x, y, z; 63 x = r * Math.cos(a + this.theta) * Math.sin(b); 64 y = r * Math.sin(a + this.theta) * Math.sin(b); 65 z = r * Math.cos(b); 66 var fact = (y * Math.cos(this.eleva) + z * Math.sin(this.eleva) + 8) / 8; 67 y = y*Math.sin(this.eleva) + z*Math.cos(this.eleva); 68 x *= fact; 69 y *= fact; 70 return { 71 x: x, 72 y: y 73 }; 74 } 75 Ball.prototype.preview = function(n, u){ 76 ctx.clearRect(-2,-2,4,4); 77 var that = this, arr = this.sphere(); 78 79 function fillColorBall(arr, I, bgLeft, bgRight, U){ 80 var aMid = [], 81 i, 82 ni; 83 for(i = -1, ni = arr.length - 1; i++ < ni;){ 84 var _ = arr[i][I]; 85 U && console.log(_) 86 if(i == 0){ 87 ctx.beginPath(); 88 ctx.moveTo(_[0], _[1]); 89 }else{ 90 if(i === ni){ 91 ctx.lineTo(aMid[0], aMid[1]); 92 ctx.closePath(); 93 ctx.fillStyle = bgRight; 94 ctx.fill(); 95 }else{ 96 ctx.lineTo(arr[i + 1][I][0], arr[i + 1][I][1]); 97 if(i === Math.floor(ni / 2)){ 98 aMid = [arr[i][I][0], arr[i][I][1]]; 99 ctx.lineTo(_[0], _[1]); 100 ctx.closePath(); 101 ctx.fillStyle = bgLeft; 102 ctx.fill(); 103 ctx.beginPath(); 104 ctx.moveTo(aMid[0], aMid[1]); 105 } 106 } 107 } 108 } 109 110 } 111 function fillAlphaBall(arr, I, bgAll){ 112 var i, 113 ni; 114 for(i = -1, ni = arr.length - 1; i++ < ni;){ 115 var _ = arr[i][I]; 116 if(i === 0){ 117 ctx.beginPath(); 118 ctx.moveTo(_[0], _[1]); 119 }else{ 120 if(i === ni){ 121 ctx.lineTo(_[0], _[1]); 122 ctx.closePath(); 123 ctx.fillStyle = bgAll; 124 ctx.fill(); 125 }else{ 126 ctx.lineTo(arr[i + 1][I][0], arr[i + 1][I][1]); 127 } 128 } 129 } 130 } 131 function fillSection(arr){ 132 var i, 133 ni, j, nj, 134 A = arr; 135 for(i = -1, ni = A[0].length - 1; i++ < ni;){ 136 if(i > Math.floor(ni / 2)){ 137 break; 138 } 139 for(j = -1, nj = A.length - 1; j++ < nj;){ 140 var _ = A[j][i]; 141 if(j === 0){ 142 ctx.beginPath(); 143 ctx.moveTo(_[0], _[1]); 144 }else{ 145 if(j > Math.floor(nj / 2)){ 146 ctx.fillStyle = 'rgba(91, 91, 91, '+ (0.2 * i / ni) +')'; 147 ctx.lineTo(_[0], _[1]); 148 ctx.closePath(); 149 ctx.fill(); 150 break; 151 }else{ 152 ctx.lineTo(A[j + 1][i][0], A[j + 1][i][1]); 153 } 154 } 155 } 156 } 157 } 158 var i, j, ni, nj, _n = !n ? 0 : n; 159 for(i = arr[0].length, ni = 0, l = i - 1, d = i / 4, getI = function(z){ 160 return Math.floor(z - d > 0 ? z - d : l - z); 161 }; i-- > ni;){ 162 if(i < _n){ 163 fillColorBall(arr, i, 'rgba(12, 12, 255, 0.15)', 'rgba(5, 207, 5, 0.15)'); 164 }else{ 165 fillAlphaBall(arr, i, 'rgba(239, 239, 239, 0.85)'); 166 } 167 } 168 fillColorBall(arr, _n, 'rgba(98, 98, 255, 0.85)', 'rgba(102, 226, 102, 0.85)', u || null); 169 //fillSection(arr); 170 }; 171 Ball.prototype.sphere = function(){ 172 var a, b, step = 64, arr = []; 173 for(a = 0; a < 2 * Math.PI; a += Math.PI / step){ 174 var _arr = []; 175 for(b = 0; b < 2 * Math.PI; b += Math.PI / step){ 176 //if(b == 0 || b * 100 >> 0 == Math.PI * 100 >> 0 || b * 100 >> 0 == 2 * Math.PI * 100 >> 0) continue; /* 排除一些仰角(接近)为0/PI/2PI的点. */ 177 this.co = this.iso(0,0,0); 178 //ctx.moveTo(this.co.x, this.co.y); 179 this.co = this.iso(a,b,this.radius); 180 //ctx.lineTo(this.co.x, this.co.y); 181 _arr.push([this.co.x, this.co.y]); 182 } 183 arr.push(_arr); 184 } 185 return arr; 186 }; 187 var webexBall = new Ball(2,2,1); 188 189 var hadBegin = false, 190 btn_0 = g('btn-0'), 191 btn_1 = g('btn-1'), 192 btn_2 = g('btn-2'), 193 inp_0 = g('inp-0'), 194 inp_1 = g('inp-1'); 195 function checkNum(n, id){ 196 if(!/^\d+$/.test(n)){ 197 n = 0; 198 }else if(n < 0){ 199 n = 0; 200 }else if(n > 100){ 201 n = 100; 202 } 203 return id.value = +n; 204 } 205 function transfor100(n){ 206 return Math.floor(n * 64 / 100); 207 } 208 function fNtM(n, m){ 209 if(hadBegin || (n === m)){ 210 return; 211 } 212 hadBegin = true; 213 n = checkNum(n, inp_0); 214 m = checkNum(m, inp_1); 215 var r, f0 = n < m ? function(n, m){ 216 return n >= m; 217 } : function(n, m){ 218 return n <= m; 219 }, f1 = n < m ? function(n){ 220 return n + 1; 221 } : function(n){ 222 return n - 1; 223 }; 224 r = setInterval(function(){ 225 webexBall.preview(transfor100(n)); 226 n = f1(n); 227 if(f0(n, m)){ 228 clearInterval(r); 229 hadBegin = false; 230 return; 231 } 232 }, 30); 233 } 234 btn_0.onclick = function(){ 235 fNtM(0, 100); 236 }; 237 btn_1.onclick = function(){ 238 fNtM(100, 0); 239 }; 240 btn_2.onclick = function(){ 241 fNtM(inp_0.value, inp_1.value); 242 } 243 </script> 244 </body> 245 </html>
这里他完成了一个不失真的 灌水的球.
当然, 用这个也有些不完整, 毕竟我们还是没处理好高光等等部分 (在模拟)
未来如果有时间, 我还是会用直角坐标系再尝试下. 至于渲染, 对于js来说, 估计还是有些麻烦了. 要做的工作也满多.