1/* 2Plugin: 3D Tag Sphere 3Version: 0.1 4Author: Ian George 5Website: http://www.iangeorge.net 6Tools: Emacs, js2-mode 7Tested on: IE6, IE7, IE8, Firefox 3.6 Linux, Firefox 3.5 Windows, Chrome Linux / Windows 8Requirements: Optional jquery.mousewheel for zooming 9 10Description: 3d tag cloud, rotates with the mouse and zooms in and out. Fixed camera position to simplify calculations 11 12Known issues: 13 Breaks badly on IE5.5 14 Looks a bit rough on page load 15 16TODO: 17 Performance is horrible when more than one instance is present in the page 18 Would be much quicker if instead of recalculating all values on mouse move a global phi/theta value pair is stored and the values calculated on-the-fly in Draw() 19 20Options: 21 Option default Comments 22 -------------------------------------------------- 23 zoom 75 Initial zoom level 24 min_zoom 25 25 max_zoom 120 26 zoom_factor 2 Speed of zoom by the mouse wheel 27 rotate_factor 0.45 In degrees, the amount that the sphere rotates. Negative values reverse the direction. 28 fps 10 Defines the (target) number of times the animation will be updated per second 29 centrex 250 Horizontal rotation centre in the container <div> 30 centrey 250 Vertical rotation centre in the container <div> 31 min_font_size 12 32 max_font_size 32 33 font_units 'px' 34 random_points 50 Adds some random points on to the sphere to enhance the effect 35 foreground_colour #fff Accepted colour formats are #333 #0A0A0A and rgb(n,n,n) 36 background_colour rgb(0,0,0) Named colours may not work 37 38Usage: 39 Vanilla: 40 $('.tags').tagcloud(); 41 42 Centered in a 200 x 200 container: 43 $('.tags').tagcloud({centrex:100,centrey:100}); 44 45 With a different update speed 46 $('.selector').tagcloud({fps:24}); 47 48Markup: 49 Must be an unordered list in a div with links in the list items. 50 rel="[number]" is optional but necessary for ranking by font-size. 51 <div class="tags"> 52 <ul> 53 <li><a href="#" rel="20">link 1</a></li> 54 <li><a href="#" rel="20">link 2</a></li> 55 <li><a href="#" rel="20">link 3</a></li> 56 <li><a href="#" rel="20">link 4</a></li> 57 <li><a href="#" rel="20">link 5</a></li> 58 </ul> 59 */ 60 61(function($){ 62 63 // jquery plugin hook 64 $.fn.tagcloud = function(options){ 65 66 // overwrite defaults with user-specified 67 var opts = $.extend($.fn.tagcloud.defaults, options); 68 opts.drawing_interval = 1/(opts.fps/1000); 69 70 //create a new class for every matching element 71 $(this).each(function(){ 72 $('ul', $(this)).css('display', 'none'); 73 new TagCloudClass($(this), opts); 74 }); 75 return this; 76 }; 77 78 79 //default values for setup 80 $.fn.tagcloud.defaults = { 81 zoom: 90, 82 max_zoom: 120, 83 min_zoom: 25, 84 zoom_factor: 2, //multiplication factor for wheel delta 85 rotate_factor: -0.45, // multiplication factor for rotation 86 fps: 10, // frames per second 87 centrex: 250, // set centre of display 88 centrey: 250, 89 min_font_size: 12, //font limits and units 90 max_font_size: 32, 91 font_units: 'px', 92 random_points: 0 93 }; 94 95 var TagCloudClass = function(el, options){ 96 $(el).css('position', 'relative'); 97 98 // general values 99 var eyez = -500; 100 101 // set rotation (in this case, 5degrees) 102 var rad = Math.PI/180; 103 var global_cos = Math.cos(0); 104 105 var hex = new Array("0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"); 106 107 // per-instance values 108 var dirty = true; 109 var container = $(el); 110 var id_stub = 'tc_' + $(el).attr('id') + "_"; 111 var opts = options; 112 var zoom = opts.zoom; 113 var depth; 114 var lastx = 0; 115 var lasty = 0; 116 var points = []; 117 points['data'] = []; 118 var drawing_interval; 119 var cmx = options.centrex; 120 var cmy = options.centrey;/* 121 var bg_colour, fg_colour; 122 if (options.background_colour){ 123 bg_colour = parsecolour(options.background_colour); 124 }else{ 125 bg_colour = parsecolour($(el).css('background-color')); 126 } 127 if (options.foreground_colour){ 128 fg_colour = parsecolour(options.foreground_colour); 129 }else{ 130 fg_colour = parsecolour($(el).css('color')); 131 }*/ 132 133 function parsecolour(colour){ 134 function parse_rgb_colour(colour){ 135 rgb = colour.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); 136 if(rgb.length > 3){ 137 return {"r" : parseInt(rgb[1]), "g": parseInt(rgb[2]), "b" : parseInt(rgb[3])}; 138 }else{ 139 return {"r":0, "g":0, "b":0}; 140 } 141 } 142 function parse_hex_colour(colour){ 143 var r = 0, g = 0, b = 0; 144 if(colour.length > 4) 145 { 146 r = parseInt(colour.substr(1,2), 16); 147 g = parseInt(colour.substr(3,2), 16); 148 b = parseInt(colour.substr(5,2), 16); 149 } 150 else 151 { 152 r = parseInt(colour.substr(1,1)+colour.substr(1,1), 16); 153 g = parseInt(colour.substr(2,1)+colour.substr(2,1), 16); 154 b = parseInt(colour.substr(3,1)+colour.substr(3,1), 16); 155 } 156 return {"r" : r, "g" : g, "b" : b}; 157 } 158 159 if(colour.substr(0, 1) === '#') 160 { 161 return parse_hex_colour(colour); 162 } 163 else if (colour.substr(0,3) === 'rgb') 164 { 165 return parse_rgb_colour(colour); 166 } 167 else{ 168 //somehow we've got a plain old string as a colour 169 if(window.console != undefined) 170 console.log("unable to parse:'" + colour + "' please ensure background and foreground colors for the container are set as hex values"); 171 return null; 172 } 173 } 174 /* 175 function getcolour(num){ 176 if(num>256){num=256;} 177 if(num<0){num=0;} 178 179 var r = getshade(bg_colour.r, fg_colour.r, num); 180 var g = getshade(bg_colour.g, fg_colour.g, num); 181 var b = getshade(bg_colour.b, fg_colour.b, num); 182 183 var ret = "rgb(" + r + ", "+ g + ", " + b + ")"; 184 return ret; 185 }*/ 186 187 function getshade(lbound, ubound, dist){ 188 var scope = ubound - lbound; 189 var dist_percent = scope / 100; 190 var shade = Math.round(lbound + (dist * dist_percent)); 191 return shade; 192 } 193 194 195 //drawing and rotation... 196 function rotx(s, c){ 197 for(var p in points.data) 198 { 199 var temp = s * points.data[p].y + c * points.data[p].z; 200 points.data[p].y = c * points.data[p].y - s * points.data[p].z; 201 points.data[p].z = temp; 202 } 203 } 204 205 function roty(s, c){ 206 for(var p in points.data){ 207 var temp = - s * points.data[p].x + c * points.data[p].z; 208 points.data[p].x = c * points.data[p].x + s * points.data[p].z; 209 points.data[p].z = temp; 210 } 211 } 212 213 function rotz(s, c){ 214 for(var p in points.data) { 215 var temp = s * points.data[p].x + c * points.data[p].y; 216 points.data[p].x = c * points.data[p].x - s * points.data[p].y; 217 points.data[p].y = temp; 218 } 219 } 220 221 function zoomed(by){ 222 zoom += by * opts.zoom_factor; 223 224 if (zoom > opts.max_zoom) { 225 zoom = opts.max_zoom; 226 } 227 if (zoom < opts.min_zoom) { 228 zoom = opts.min_zoom; 229 } 230 231 depth = -(zoom * (eyez - opts.max_zoom) / 100) + eyez; 232 } 233 234 function moved(mx,my){ 235 if(mx>lastx){ 236 var angle = mx - lastx; 237 var sin = -Math.sin(angle*options.rotate_factor*rad); 238 var cos = Math.cos(angle*options.rotate_factor*rad); 239 roty(sin, cos); 240 } 241 if(mx<lastx){ 242 var angle = lastx - mx; 243 var sin = Math.sin(angle*options.rotate_factor*rad); 244 var cos = Math.cos(angle*options.rotate_factor*rad); 245 roty(sin, cos); 246 } 247 if(my>lasty){ 248 var angle = my - lasty; 249 var sin = Math.sin(angle*options.rotate_factor*rad); 250 var cos = Math.cos(angle*options.rotate_factor*rad); 251 rotx(sin, cos); 252 } 253 if(my<lasty){ 254 var angle = lasty - my; 255 var sin = -Math.sin(angle*options.rotate_factor*rad); 256 var cos = Math.cos(angle*options.rotate_factor*rad); 257 rotx(sin, cos); 258 } 259 260 lastx = mx; 261 lasty = my; 262 dirty = true; 263 } 264 265 function draw(){ 266 // calculate 2D coordinates 267 if(dirty){ 268 var smallz = 10000; var bigz = -10000; 269 for(var r_p in points.data){ 270 if(points.data[r_p].z < smallz){smallz = points.data[r_p].z;} 271 if(points.data[r_p].z > bigz){bigz = points.data[r_p].z;} 272 } 273 var minz = Math.min(smallz, bigz); 274 var maxz = Math.max(smallz, bigz); 275 var diffz = maxz - minz; 276 277 for(var s_p in points.data){ 278 //normalise depth 279 var u = (depth - eyez)/(points.data[s_p].z - eyez); 280 281 // calculate normalised grey value 282 var dist = Math.round(((maxz - points.data[s_p].z)/diffz) * 100); 283 //var dist_colour = getcolour(dist); 284 //set new 2d positions for the data 285 //$('#'+points.data[s_p].id + ' a', container).css('color',dist_colour); 286 $('#'+points.data[s_p].id, container).css('z-index',dist); 287 $('#'+points.data[s_p].id, container).css('left', u * points.data[s_p].x + cmx - points.data[s_p].cwidth); 288 $('#'+points.data[s_p].id, container).css('top', u * points.data[s_p].y + cmy); 289 } 290 dirty = false;
291 } 292 } 293 294 // number of elements we're adding and placeholders for range values 295 points.count = $('li a', container).length; 296 points.largest = 1; 297 points.smallest = 0; 298 299 300 // Run through each li > a in the container and create an absolutely-positioned div in its place 301 // Also need to create a data structure to keep state between calls to draw() 302 // Data structure is as follows: 303 // 304 // points{ 305 // 'count':0, //Total number of points 306 // 'largest':0, //largest 'size' value 307 // 'smallest':0, //Smallest 'size' value 308 // 'data':{ 309 // 'id':"", //HTML id for element 310 // 'size':0, //Size (from rel attribute on <a>) 311 // 'theta':0.0, //Angle on sphere (used to calculate initial cartesian position) 312 // 'phi': 0.0, //Angle on sphere (used to calculate initial cartesian position) 313 // 'x':0.0, //Cartesian position in 3d space 314 // 'y':0.0, //Cartesian position in 3d space 315 // 'z':0.0, //Cartesian position in 3d space 316 // } 317 // } 318 $('li a', container).each(function(idx, val){ 319 320 var sz = parseInt($(this).attr('rel')); 321 if(sz == 0) 322 sz = 1; 323 324 points.data[idx] = { 325 id:id_stub + idx, 326 size:sz 327 }; 328 329 // plot the points on a sphere 330 // from: http://www.math.niu.edu/~rusin/known-math/97/spherefaq 331 // for k=1 to N do 332 // h = -1 + 2*(k-1)/(N-1) 333 // theta[k] = arccos(h) 334 // if k=1 or k=N then phi[k] = 0 335 // else phi[k] = (phi[k-1] + 3.6/sqrt(N*(1-h^2))) mod (2*pi) 336 // endfor 337 338 // In Cartesian coordinates the required point on a sphere of radius 1 is 339 // (cos(theta)*sin(phi), sin(theta)*sin(phi), cos(phi)) 340 341 var h = -1 + 2*(idx)/(points.count-1); 342 points.data[idx].theta = Math.acos(h); 343 if(idx == 0 || idx == points.count-1){ 344 points.data[idx].phi = 0; 345 } 346 else{ 347 points.data[idx].phi = (points.data[idx-1].phi + 3.6/Math.sqrt(points.count*(1-Math.pow(h,2)))) % (2 * Math.PI); 348 } 349 350 points.data[idx].x = Math.cos(points.data[idx].phi) * Math.sin(points.data[idx].theta) * (cmx/2); 351 points.data[idx].y = Math.sin(points.data[idx].phi) * Math.sin(points.data[idx].theta) * (cmy/2); 352 points.data[idx].z = Math.cos(points.data[idx].theta) * (cmx/2); 353 354 if(sz > points.largest) points.largest = sz; 355 if(sz < points.smallest) points.smallest = sz; 356 357 container.append('<div id="'+ id_stub + idx +'" class="point" style="position:absolute;display:none;"><a href=' + $(this).attr('href') + '>' + $(this).html() + '</a></div>'); 358 }); 359 360 //if required to do so (by opts.random_points being > 0) we need to generate some random points on the sphere 361 //bit cheezy, but can make more sparse data sets look a bit more believable 362 if(opts.random_points > 0){ 363 for(b=0; b<opts.random_points; b++){ 364 points.count++; 365 points.data[points.count] = { 366 id:id_stub + points.count, 367 size:1 368 }; 369 points.data[points.count].theta = Math.random() * 2 * Math.PI; 370 points.data[points.count].phi = Math.random() * 2 * Math.PI; 371 points.data[points.count].x = Math.cos(points.data[points.count].phi) * Math.sin(points.data[points.count].theta) * (cmx/2); 372 points.data[points.count].y = Math.sin(points.data[points.count].phi) * Math.sin(points.data[points.count].theta) * (cmy/2);
373 points.data[points.count].z = Math.cos(points.data[points.count].theta) * (cmx/2); 374 container.append('<div id="'+ id_stub + points.count +'" class="point" style="position:absolute;"><a>.</a></div>'); 375 } 376 } 377 378 //tag size and font size ranges 379 var sz_range = points.largest - points.smallest + 1; 380 var sz_n_range = opts.max_font_size - opts.min_font_size + 1; 381 382 //set font size to normalised tag size 383 for(var p in points.data){ 384 var sz = points.data[p].size; 385 var sz_n = parseInt((sz / sz_range) * sz_n_range) + opts.min_font_size; 386 if(!$('#' + points.data[p].id, container).hasClass('background')){ 387 $('#' + points.data[p].id, container).css('font-size', sz_n); 388 } 389 //store element width / 2 so we can centre the text around the point later. 390 points.data[p].cwidth = $('#' + points.data[p].id, container).width()/2; 391 } 392 // bin original html 393 $('ul', container).remove(); 394 395 //set up initial view 396 depth = -(zoom * (eyez - opts.max_zoom) / 100) + eyez; 397 moved(cmx, cmy); 398 draw(); 399 400 //show all the points 401 $('.point',container).css('display','inherit'); 402 403 404 //call draw every so often 405 drawing_interval = setInterval(draw, opts.drawing_interval); 406 407 //events to change position of items 408 container.mousemove(function(evt){ 409 moved(evt.clientX, evt.clientY); 410 }); 411 container.mousewheel(function(evt, delta){ 412 zoomed(delta); 413 evt.preventDefault(); 414 return false; 415 }); 416 417 }; 418 419 })(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.