Simon Hunt

GUI -- Experimental Transform.

Change-Id: Iebf31a32707f2736b22102b6d4620db3489fe252
1 +<!DOCTYPE html>
2 +<!--
3 + Testing transformations for transitioning between overhead and
4 + perspective projections of two layers.
5 +
6 + @author Simon Hunt
7 + -->
8 +<html>
9 +<head>
10 + <meta charset="utf-8">
11 + <title>Layer Transformations</title>
12 +
13 + <script src="../tp/d3.js"></script>
14 + <script src="../tp/topojson.v1.min.js"></script>
15 + <script src="../tp/jquery-2.1.1.min.js"></script>
16 +
17 + <style>
18 + html,
19 + body {
20 + background-color: #ccc;
21 + font-family: Arial, Helvetica, sans-serif;
22 + font-size: 9pt;
23 + }
24 +
25 + svg {
26 + position: absolute;
27 + background-color: #fff;
28 + top: 30px;
29 + left: 60px;
30 + }
31 +
32 + svg text {
33 + font-size: 3pt;
34 + }
35 +
36 + </style>
37 +</head>
38 +<body>
39 + <svg width="1000px" height="600px" viewBox="0 0 160 120"></svg>
40 +
41 + <script>
42 + (function (){
43 +
44 + // Configuration...
45 + var w = 160,
46 + h = 120,
47 + time = 1500;
48 +
49 + var pktData = [
50 + [20,60,'a'],
51 + [60,20,'b'],
52 + [100,20,'c'],
53 + [140,60,'d'],
54 + [100,100,'e'],
55 + [60,100,'f'],
56 + [20,20,'w'],
57 + [140,20,'x'],
58 + [20,100,'y'],
59 + [140,100,'z']
60 + ],
61 + optData = [
62 + [40,40,'p'],
63 + [120,40,'q'],
64 + [120,80,'r'],
65 + [40,80,'s'],
66 + [20,20,'j'],
67 + [140,20,'k'],
68 + [20,100,'l'],
69 + [140,100,'m']
70 + ],
71 + linkData = [
72 + ['a','p'],
73 + ['p','b'],
74 + ['b','c'],
75 + ['c','q'],
76 + ['q','d'],
77 + ['d','r'],
78 + ['r','e'],
79 + ['e','f'],
80 + ['f','s'],
81 + ['s','a'],
82 + ['s','q'],
83 + ['p','r'],
84 + ['b','f'],
85 + ['c','e'],
86 + ['w','j'],
87 + ['x','k'],
88 + ['z','m'],
89 + ['y','l']
90 + ];
91 +
92 + // Transform parameters
93 + var tf = {
94 + tt: -.7, // x skew y factor
95 + xsk: -35, // x skew angle
96 + ysc: 0.5, // y scale
97 + ytr: 50, // y translate
98 + pad: 5
99 + },
100 + rectFill = {
101 + pkt: 'rgba(130,130,170,0.3)',
102 + opt: 'rgba(170,130,170,0.3)'
103 + };
104 +
105 + // Internal state...
106 + var nodes = [],
107 + links = [],
108 + overhead = true,
109 + xffn;
110 +
111 + // D3/DOM magic...
112 + var svg = d3.select('svg'),
113 + nodeG,
114 + linkG,
115 + node,
116 + link,
117 + force,
118 + pktLayer,
119 + optLayer;
120 +
121 +
122 + // General functions ...
123 + function isF(f) {
124 + return $.isFunction(f) ? f : null;
125 + }
126 +
127 + function translate(x,y) {
128 + return 'translate(' + x + ',' + y + ')';
129 + }
130 +
131 + function scale(x,y) {
132 + return 'scale(' + x + ',' + y + ')';
133 + }
134 + function skewX(x) {
135 + return 'skewX(' + x + ')';
136 + }
137 +
138 +
139 + // Key Bindings...
140 + var keyHandler = {
141 + T: transform
142 + };
143 +
144 + function whatKey(code) {
145 + switch (code) {
146 + case 13: return 'enter';
147 + case 16: return 'shift';
148 + case 17: return 'ctrl';
149 + case 18: return 'alt';
150 + case 27: return 'esc';
151 + case 32: return 'space';
152 + case 37: return 'leftArrow';
153 + case 38: return 'upArrow';
154 + case 39: return 'rightArrow';
155 + case 40: return 'downArrow';
156 + case 91: return 'cmdLeft';
157 + case 93: return 'cmdRight';
158 + case 187: return 'equals';
159 + case 189: return 'dash';
160 + case 191: return 'slash';
161 + default:
162 + if ((code >= 48 && code <= 57) ||
163 + (code >= 65 && code <= 90)) {
164 + return String.fromCharCode(code);
165 + } else if (code >= 112 && code <= 123) {
166 + return 'F' + (code - 111);
167 + }
168 + return '.';
169 + }
170 + }
171 +
172 + function keyIn() {
173 + var event = d3.event,
174 + keyCode = event.keyCode,
175 + key = whatKey(keyCode),
176 + fn = isF(keyHandler[key]);
177 + if (fn) {
178 + fn(key, keyCode, event);
179 + }
180 + }
181 +
182 + // Key events....
183 + function transform() {
184 + overhead = !overhead;
185 + if (overhead) {
186 + toOverhead();
187 + } else {
188 + toOblique();
189 + }
190 + }
191 +
192 + function toOverhead() {
193 + xffn = null;
194 + hidePlane(pktLayer);
195 + hidePlane(optLayer);
196 + transitionNodes();
197 + }
198 +
199 + function padBox(box, p) {
200 + box.x -= p;
201 + box.y -= p;
202 + box.width += p*2;
203 + box.height += p*2;
204 + }
205 +
206 + function toOblique() {
207 + var box = nodeG.node().getBBox();
208 + padBox(box, tf.pad);
209 +
210 + xffn = function (xy, dir) {
211 + var x = xy.x + xy.y*tf.tt,
212 + y = xy.y*tf.ysc + tf.ysc*tf.ytr*dir;
213 + return { x: x, y: y};
214 + };
215 +
216 + showPlane(pktLayer, box, -1);
217 + showPlane(optLayer, box, 1);
218 + transitionNodes();
219 + }
220 +
221 + function transitionNodes() {
222 + // note: turn off force layout while transitioning.. if it is on
223 +// force.stop();
224 +
225 + if (xffn) {
226 + nodes.forEach(function (d) {
227 + var dir = d.type === 'pkt' ? -1 : 1,
228 + oldxy = {x: d.x, y: d.y},
229 + coords = xffn(oldxy, dir);
230 + d.oldxy = oldxy;
231 + d.x = coords.x;
232 + d.y = coords.y;
233 + });
234 + } else {
235 + nodes.forEach(function (d) {
236 + d.x = d.oldxy.x;
237 + d.y = d.oldxy.y;
238 + delete d.oldxy;
239 + });
240 + }
241 +
242 + nodeG.selectAll('.node')
243 + .transition()
244 + .duration(time)
245 + .attr({
246 + transform: function (d) {
247 + return translate(d.x, d.y);
248 + }
249 + });
250 +
251 + linkG.selectAll('.link')
252 + .transition()
253 + .duration(time)
254 + .attr({
255 + x1: function (d) { return d.source.x; },
256 + y1: function (d) { return d.source.y; },
257 + x2: function (d) { return d.target.x; },
258 + y2: function (d) { return d.target.y; }
259 + });
260 + }
261 +
262 + function showPlane(layer, box, dir) {
263 + layer.select('rect')
264 + .attr(box)
265 + .attr('opacity', 0)
266 + .transition()
267 + .duration(time)
268 + .attr('opacity', 1)
269 + .attr('transform', obliqueXform(dir));
270 + }
271 +
272 + function hidePlane(layer) {
273 + var rect = layer.select('rect');
274 + rect.transition()
275 + .duration(time)
276 + .attr('opacity', 0)
277 + .attr('transform', overheadXform());
278 +
279 + }
280 +
281 + function obliqueXform(dir) {
282 + return scale(1, tf.ysc) + translate(0, dir * tf.ytr) + skewX(tf.xsk);
283 + }
284 +
285 +
286 + function overheadXform() {
287 + return skewX(0) + translate(0,0) + scale(1,1);
288 + }
289 +
290 + // Nodes and Links...
291 + function prepareNodes() {
292 + var hw = w/2,
293 + hh = h/2;
294 +
295 + function addNode(t, d) {
296 + nodes.push({
297 + type: t,
298 + x: d[0] - hw,
299 + y: d[1] - hh,
300 + id: d[2],
301 + fixed: true
302 + });
303 + }
304 +
305 + optData.forEach(function (d) {
306 + addNode('opt', d);
307 + });
308 + pktData.forEach(function (d) {
309 + addNode('pkt', d);
310 + });
311 + }
312 +
313 + function findNode(id) {
314 + for (var i=0,n=nodes.length; i<n; i++) {
315 + if (nodes[i].id === id) {
316 + return nodes[i];
317 + }
318 + }
319 + return null;
320 + }
321 +
322 + function prepareLinks() {
323 + linkData.forEach(function (d) {
324 + var src = d[0],
325 + dst = d[1];
326 + links.push({
327 + id: src + '-' + dst,
328 + source: findNode(src),
329 + target: findNode(dst)
330 + });
331 + });
332 +
333 + }
334 +
335 + function updateNodes() {
336 + node = nodeG.selectAll('.node')
337 + .data(nodes, function (d) { return d.id; });
338 +
339 + var entering = node.enter()
340 + .append('g').attr({
341 + id: function (d) { return d.id; },
342 + 'class': function (d) { return 'node ' + d.type; }
343 + });
344 +
345 + entering.each(function (d) {
346 + var el = d3.select(this);
347 + d.el = el;
348 +
349 + el.append('rect').attr({
350 + width: 5,
351 + height: 5,
352 + fill: function (d) {
353 + return d.type === 'pkt' ? '#669' : '#969';
354 + },
355 + rx: 1,
356 + transform: 'translate(-2.5,-2.5)'
357 + });
358 + el.append('text')
359 + .text(d.id)
360 + .attr({
361 + dy: '0.9em',
362 + 'text-anchor': 'middle',
363 + transform: 'translate(0,-2.5)',
364 + fill: 'white'
365 + });
366 + });
367 + }
368 +
369 + function updateLinks() {
370 + link = linkG.selectAll('.link')
371 + .data(links, function (d) { return d.id; });
372 +
373 + var entering = link.enter()
374 + .append('line').attr({
375 + id: function (d) { return d.id; },
376 + class: 'link',
377 + stroke: '#888',
378 + 'stroke-width': 0.4,
379 + opacity: 0.7
380 + });
381 +
382 + entering.each(function (d) {
383 + d.el = d3.select(this);
384 +
385 + });
386 + }
387 +
388 + function update() {
389 + updateNodes();
390 + updateLinks();
391 + }
392 +
393 + var ntick = 0;
394 + function tick() {
395 + console.log('tick ' + (++ntick));
396 + node.attr({
397 + transform: function (d) { return translate(d.x, d.y); }
398 + });
399 +
400 + link.attr({
401 + x1: function (d) { return d.source.x; },
402 + y1: function (d) { return d.source.y; },
403 + x2: function (d) { return d.target.x; },
404 + y2: function (d) { return d.target.y; }
405 + });
406 + }
407 +
408 + function setOrigin(/*varargs*/) {
409 + var i, n, g;
410 + for (i= 0,n=arguments.length; i< n; i++) {
411 + g = arguments[i];
412 + g.attr('transform', translate(w/2, h/2));
413 + }
414 + }
415 +
416 + function initLayers() {
417 + optLayer.attr('class', 'layer').append('rect')
418 + .attr('fill', rectFill.opt);
419 + pktLayer.attr('class', 'layer').append('rect')
420 + .attr('fill', rectFill.pkt);
421 + }
422 +
423 + function init() {
424 + svg.append('text')
425 + .text('Press the "T" key....')
426 + .attr({ dy: '1.2em', fill: '#999'})
427 + .style('font-size', '2.4pt')
428 + .style('font-style', 'italic');
429 +
430 + optLayer = svg.append('g').attr('id', 'optLayer');
431 + pktLayer = svg.append('g').attr('id', 'pktLayer');
432 + linkG = svg.append('g').attr('id', 'links');
433 + nodeG = svg.append('g').attr('id', 'nodes');
434 +
435 + setOrigin(optLayer, pktLayer, linkG, nodeG);
436 +
437 + node = nodeG.selectAll('.node');
438 + link = linkG.selectAll('.link');
439 +
440 + initLayers();
441 + prepareNodes();
442 + prepareLinks();
443 +
444 + force = d3.layout.force()
445 + .size([w,h])
446 + .nodes(nodes)
447 + .links(links)
448 + .gravity(0.4)
449 + .friction(0.7)
450 + .on('tick', tick);
451 + update();
452 + tick();
453 + d3.select('body').on('keydown', keyIn);
454 + }
455 +
456 + init();
457 + })();
458 + </script>
459 +</body>
460 +</html>