1/*!jQuery Knob*/
2/**
3 * Downward compatible, touchable dial
4 *
5 * Version: 1.2.0 (15/07/2012)
6 * Requires: jQuery v1.7+
7 *
8 * Copyright (c) 2012 Anthony Terrien
9 * Under MIT and GPL licenses:
10 * http://www.opensource.org/licenses/mit-license.php
11 * http://www.gnu.org/licenses/gpl.html
12 *
13 * Thanks to vor, eskimoblood, spiffistan, FabrizioC
14 */
15(function($) {
16
17 /**
18 * Kontrol library
19 */
20 "use strict";
21
22 /**
23 * Definition of globals and core
24 */
25 var k = {}, // kontrol
26 max = Math.max,
27 min = Math.min;
28
29 k.c = {};
30 k.c.d = $(document);
31 k.c.t = function (e) {
32 return e.originalEvent.touches.length - 1;
33 };
34
35 /**
36 * Kontrol Object
37 *
38 * Definition of an abstract UI control
39 *
40 * Each concrete component must call this one.
41 * <code>
42 * k.o.call(this);
43 * </code>
44 */
45 k.o = function () {
46 var s = this;
47
48 this.o = null; // array of options
49 this.$ = null; // jQuery wrapped element
50 this.i = null; // mixed HTMLInputElement or array of HTMLInputElement
51 this.g = null; // 2D graphics context for 'pre-rendering'
52 this.v = null; // value ; mixed array or integer
53 this.cv = null; // change value ; not commited value
54 this.x = 0; // canvas x position
55 this.y = 0; // canvas y position
56 this.$c = null; // jQuery canvas element
57 this.c = null; // rendered canvas context
58 this.t = 0; // touches index
59 this.isInit = false;
60 this.fgColor = null; // main color
61 this.pColor = null; // previous color
62 this.dH = null; // draw hook
63 this.cH = null; // change hook
64 this.eH = null; // cancel hook
65 this.rH = null; // release hook
66
67 this.run = function () {
68 var cf = function (e, conf) {
69 var k;
70 for (k in conf) {
71 s.o[k] = conf[k];
72 }
73 s.init();
74 s._configure()
75 ._draw();
76 };
77
78 if(this.$.data('kontroled')) return;
79 this.$.data('kontroled', true);
80
81 this.extend();
82 this.o = $.extend(
83 {
84 // Config
85 min : this.$.data('min') || 0,
86 max : this.$.data('max') || 100,
87 stopper : true,
88 readOnly : this.$.data('readonly'),
89
90 // UI
91 cursor : (this.$.data('cursor') === true && 30)
92 || this.$.data('cursor')
93 || 0,
94 thickness : this.$.data('thickness') || 0.35,
95 lineCap : this.$.data('linecap') || 'butt',
96 width : this.$.data('width') || 200,
97 height : this.$.data('height') || 200,
98 displayInput : this.$.data('displayinput') == null || this.$.data('displayinput'),
99 displayPrevious : this.$.data('displayprevious'),
100 fgColor : this.$.data('fgcolor') || '#87CEEB',
101 inputColor: this.$.data('inputcolor') || this.$.data('fgcolor') || '#87CEEB',
102 inline : false,
103 step : this.$.data('step') || 1,
104
105 // Hooks
106 draw : null, // function () {}
107 change : null, // function (value) {}
108 cancel : null, // function () {}
109 release : null // function (value) {}
110 }, this.o
111 );
112
113 // routing value
114 if(this.$.is('fieldset')) {
115
116 // fieldset = array of integer
117 this.v = {};
118 this.i = this.$.find('input')
119 this.i.each(function(k) {
120 var $this = $(this);
121 s.i[k] = $this;
122 s.v[k] = $this.val();
123
124 $this.bind(
125 'change'
126 , function () {
127 var val = {};
128 val[k] = $this.val();
129 s.val(val);
130 }
131 );
132 });
133 this.$.find('legend').remove();
134
135 } else {
136 // input = integer
137 this.i = this.$;
138 this.v = this.$.val();
139 (this.v == '') && (this.v = this.o.min);
140
141 this.$.bind(
142 'change'
143 , function () {
144 s.val(s._validate(s.$.val()));
145 }
146 );
147 }
148
149 (!this.o.displayInput) && this.$.hide();
150
151 this.$c = $('<canvas width="' +
152 this.o.width + 'px" height="' +
153 this.o.height + 'px"></canvas>');
154 this.c = this.$c[0].getContext("2d");
155
156 this.$
157 .wrap($('<div style="' + (this.o.inline ? 'display:inline;' : '') +
158 'width:' + this.o.width + 'px;height:' +
159 this.o.height + 'px;"></div>'))
160 .before(this.$c);
161
162 if (this.v instanceof Object) {
163 this.cv = {};
164 this.copy(this.v, this.cv);
165 } else {
166 this.cv = this.v;
167 }
168
169 this.$
170 .bind("configure", cf)
171 .parent()
172 .bind("configure", cf);
173
174 this._listen()
175 ._configure()
176 ._xy()
177 .init();
178
179 this.isInit = true;
180
181 this._draw();
182
183 return this;
184 };
185
186 this._draw = function () {
187
188 // canvas pre-rendering
189 var d = true,
190 c = document.createElement('canvas');
191
192 c.width = s.o.width;
193 c.height = s.o.height;
194 s.g = c.getContext('2d');
195
196 s.clear();
197
198 s.dH
199 && (d = s.dH());
200
201 (d !== false) && s.draw();
202
203 s.c.drawImage(c, 0, 0);
204 c = null;
205 };
206
207 this._touch = function (e) {
208
209 var touchMove = function (e) {
210
211 var v = s.xy2val(
212 e.originalEvent.touches[s.t].pageX,
213 e.originalEvent.touches[s.t].pageY
214 );
215
216 if (v == s.cv) return;
217
218 if (
219 s.cH
220 && (s.cH(v) === false)
221 ) return;
222
223
224 s.change(s._validate(v));
225 s._draw();
226 };
227
228 // get touches index
229 this.t = k.c.t(e);
230
231 // First touch
232 touchMove(e);
233
234 // Touch events listeners
235 k.c.d
236 .bind("touchmove.k", touchMove)
237 .bind(
238 "touchend.k"
239 , function () {
240 k.c.d.unbind('touchmove.k touchend.k');
241
242 if (
243 s.rH
244 && (s.rH(s.cv) === false)
245 ) return;
246
247 s.val(s.cv);
248 }
249 );
250
251 return this;
252 };
253
254 this._mouse = function (e) {
255
256 var mouseMove = function (e) {
257 var v = s.xy2val(e.pageX, e.pageY);
258 if (v == s.cv) return;
259
260 if (
261 s.cH
262 && (s.cH(v) === false)
263 ) return;
264
265 s.change(s._validate(v));
266 s._draw();
267 };
268
269 // First click
270 mouseMove(e);
271
272 // Mouse events listeners
273 k.c.d
274 .bind("mousemove.k", mouseMove)
275 .bind(
276 // Escape key cancel current change
277 "keyup.k"
278 , function (e) {
279 if (e.keyCode === 27) {
280 k.c.d.unbind("mouseup.k mousemove.k keyup.k");
281
282 if (
283 s.eH
284 && (s.eH() === false)
285 ) return;
286
287 s.cancel();
288 }
289 }
290 )
291 .bind(
292 "mouseup.k"
293 , function (e) {
294 k.c.d.unbind('mousemove.k mouseup.k keyup.k');
295
296 if (
297 s.rH
298 && (s.rH(s.cv) === false)
299 ) return;
300
301 s.val(s.cv);
302 }
303 );
304
305 return this;
306 };
307
308 this._xy = function () {
309 var o = this.$c.offset();
310 this.x = o.left;
311 this.y = o.top;
312 return this;
313 };
314
315 this._listen = function () {
316
317 if (!this.o.readOnly) {
318 this.$c
319 .bind(
320 "mousedown"
321 , function (e) {
322 e.preventDefault();
323 s._xy()._mouse(e);
324 }
325 )
326 .bind(
327 "touchstart"
328 , function (e) {
329 e.preventDefault();
330 s._xy()._touch(e);
331 }
332 );
333 this.listen();
334 } else {
335 this.$.attr('readonly', 'readonly');
336 }
337
338 return this;
339 };
340
341 this._configure = function () {
342
343 // Hooks
344 if (this.o.draw) this.dH = this.o.draw;
345 if (this.o.change) this.cH = this.o.change;
346 if (this.o.cancel) this.eH = this.o.cancel;
347 if (this.o.release) this.rH = this.o.release;
348
349 if (this.o.displayPrevious) {
350 this.pColor = this.h2rgba(this.o.fgColor, "0.4");
351 this.fgColor = this.h2rgba(this.o.fgColor, "0.6");
352 } else {
353 this.fgColor = this.o.fgColor;
354 }
355
356 return this;
357 };
358
359 this._clear = function () {
360 this.$c[0].width = this.$c[0].width;
361 };
362
363 this._validate = function(v) {
364 return (~~ (((v < 0) ? -0.5 : 0.5) + (v/this.o.step))) * this.o.step;
365 };
366
367 // Abstract methods
368 this.listen = function () {}; // on start, one time
369 this.extend = function () {}; // each time configure triggered
370 this.init = function () {}; // each time configure triggered
371 this.change = function (v) {}; // on change
372 this.val = function (v) {}; // on release
373 this.xy2val = function (x, y) {}; //
374 this.draw = function () {}; // on change / on release
375 this.clear = function () { this._clear(); };
376
377 // Utils
378 this.h2rgba = function (h, a) {
379 var rgb;
380 h = h.substring(1,7)
381 rgb = [parseInt(h.substring(0,2),16)
382 ,parseInt(h.substring(2,4),16)
383 ,parseInt(h.substring(4,6),16)];
384 return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + a + ")";
385 };
386
387 this.copy = function (f, t) {
388 for (var i in f) { t[i] = f[i]; }
389 };
390 };
391
392
393 /**
394 * k.Dial
395 */
396 k.Dial = function () {
397 k.o.call(this);
398
399 this.startAngle = null;
400 this.xy = null;
401 this.radius = null;
402 this.lineWidth = null;
403 this.cursorExt = null;
404 this.w2 = null;
405 this.PI2 = 2*Math.PI;
406
407 this.extend = function () {
408 this.o = $.extend(
409 {
410 bgColor : this.$.data('bgcolor') || '#EEEEEE',
411 angleOffset : this.$.data('angleoffset') || 0,
412 angleArc : this.$.data('anglearc') || 360,
413 inline : true
414 }, this.o
415 );
416 };
417
418 this.val = function (v) {
419 if (null != v) {
420 this.cv = this.o.stopper ? max(min(v, this.o.max), this.o.min) : v;
421 this.v = this.cv;
422 this.$.val(this.v);
423 this._draw();
424 } else {
425 return this.v;
426 }
427 };
428
429 this.xy2val = function (x, y) {
430 var a, ret;
431
432 a = Math.atan2(
433 x - (this.x + this.w2)
434 , - (y - this.y - this.w2)
435 ) - this.angleOffset;
436
437 if(this.angleArc != this.PI2 && (a < 0) && (a > -0.5)) {
438 // if isset angleArc option, set to min if .5 under min
439 a = 0;
440 } else if (a < 0) {
441 a += this.PI2;
442 }
443
444 ret = ~~ (0.5 + (a * (this.o.max - this.o.min) / this.angleArc))
445 + this.o.min;
446
447 this.o.stopper
448 && (ret = max(min(ret, this.o.max), this.o.min));
449
450 return ret;
451 };
452
453 this.listen = function () {
454 // bind MouseWheel
455 var s = this,
456 mw = function (e) {
457 e.preventDefault();
458 var ori = e.originalEvent
459 ,deltaX = ori.detail || ori.wheelDeltaX
460 ,deltaY = ori.detail || ori.wheelDeltaY
461 ,v = parseInt(s.$.val()) + (deltaX>0 || deltaY>0 ? s.o.step : deltaX<0 || deltaY<0 ? -s.o.step : 0);
462
463 if (
464 s.cH
465 && (s.cH(v) === false)
466 ) return;
467
468 s.val(v);
469 }
470 , kval, to, m = 1, kv = {37:-s.o.step, 38:s.o.step, 39:s.o.step, 40:-s.o.step};
471
472 this.$
473 .bind(
474 "keydown"
475 ,function (e) {
476 var kc = e.keyCode;
477
478 // numpad support
479 if(kc >= 96 && kc <= 105) {
480 kc = e.keyCode = kc - 48;
481 }
482
483 kval = parseInt(String.fromCharCode(kc));
484
485 if (isNaN(kval)) {
486
487 (kc !== 13) // enter
488 && (kc !== 8) // bs
489 && (kc !== 9) // tab
490 && (kc !== 189) // -
491 && e.preventDefault();
492
493 // arrows
494 if ($.inArray(kc,[37,38,39,40]) > -1) {
495 e.preventDefault();
496
497 var v = parseInt(s.$.val()) + kv[kc] * m;
498
499 s.o.stopper
500 && (v = max(min(v, s.o.max), s.o.min));
501
502 s.change(v);
503 s._draw();
504
505 // long time keydown speed-up
506 to = window.setTimeout(
507 function () { m*=2; }
508 ,30
509 );
510 }
511 }
512 }
513 )
514 .bind(
515 "keyup"
516 ,function (e) {
517 if (isNaN(kval)) {
518 if (to) {
519 window.clearTimeout(to);
520 to = null;
521 m = 1;
522 s.val(s.$.val());
523 }
524 } else {
525 // kval postcond
526 (s.$.val() > s.o.max && s.$.val(s.o.max))
527 || (s.$.val() < s.o.min && s.$.val(s.o.min));
528 }
529
530 }
531 );
532
533 this.$c.bind("mousewheel DOMMouseScroll", mw);
534 this.$.bind("mousewheel DOMMouseScroll", mw)
535 };
536
537 this.init = function () {
538
539 if (
540 this.v < this.o.min
541 || this.v > this.o.max
542 ) this.v = this.o.min;
543
544 this.$.val(this.v);
545 this.w2 = this.o.width / 2;
546 this.cursorExt = this.o.cursor / 100;
547 this.xy = this.w2;
548 this.lineWidth = this.xy * this.o.thickness;
549 this.lineCap = this.o.lineCap;
550 this.radius = this.xy - this.lineWidth / 2;
551
552 this.o.angleOffset
553 && (this.o.angleOffset = isNaN(this.o.angleOffset) ? 0 : this.o.angleOffset);
554
555 this.o.angleArc
556 && (this.o.angleArc = isNaN(this.o.angleArc) ? this.PI2 : this.o.angleArc);
557
558 // deg to rad
559 this.angleOffset = this.o.angleOffset * Math.PI / 180;
560 this.angleArc = this.o.angleArc * Math.PI / 180;
561
562 // compute start and end angles
563 this.startAngle = 1.5 * Math.PI + this.angleOffset;
564 this.endAngle = 1.5 * Math.PI + this.angleOffset + this.angleArc;
565
566 var s = max(
567 String(Math.abs(this.o.max)).length
568 , String(Math.abs(this.o.min)).length
569 , 2
570 ) + 2;
571
572 this.o.displayInput
573 && this.i.css({
574 'width' : ((this.o.width / 2 + 4) >> 0) + 'px'
575 ,'height' : ((this.o.width / 3) >> 0) + 'px'
576 ,'position' : 'absolute'
577 ,'vertical-align' : 'middle'
578 ,'margin-top' : ((this.o.width / 3) >> 0) + 'px'
579 ,'margin-left' : '-' + ((this.o.width * 3 / 4 + 2) >> 0) + 'px'
580 ,'border' : 0
581 ,'background' : 'none'
582 ,'font' : 'bold ' + ((this.o.width / s) >> 0) + 'px Arial'
583 ,'text-align' : 'center'
584 ,'color' : this.o.inputColor || this.o.fgColor
585 ,'padding' : '0px'
586 ,'-webkit-appearance': 'none'
587 })
588 || this.i.css({
589 'width' : '0px'
590 ,'visibility' : 'hidden'
591 });
592 };
593
594 this.change = function (v) {
595 this.cv = v;
596 this.$.val(v);
597 };
598
599 this.angle = function (v) {
600 return (v - this.o.min) * this.angleArc / (this.o.max - this.o.min);
601 };
602
603 this.draw = function () {
604
605 var c = this.g, // context
606 a = this.angle(this.cv) // Angle
607 , sat = this.startAngle // Start angle
608 , eat = sat + a // End angle
609 , sa, ea // Previous angles
610 , r = 1;
611
612 c.lineWidth = this.lineWidth;
613
614 c.lineCap = this.lineCap;
615
616 this.o.cursor
617 && (sat = eat - this.cursorExt)
618 && (eat = eat + this.cursorExt);
619
620 c.beginPath();
621 c.strokeStyle = this.o.bgColor;
622 c.arc(this.xy, this.xy, this.radius, this.endAngle - 0.00001, this.startAngle + 0.00001, true);
623 c.stroke();
624
625 if (this.o.displayPrevious) {
626 ea = this.startAngle + this.angle(this.v);
627 sa = this.startAngle;
628 this.o.cursor
629 && (sa = ea - this.cursorExt)
630 && (ea = ea + this.cursorExt);
631
632 c.beginPath();
633 c.strokeStyle = this.pColor;
634 c.arc(this.xy, this.xy, this.radius, sa, ea, false);
635 c.stroke();
636 r = (this.cv == this.v);
637 }
638
639 c.beginPath();
640 c.strokeStyle = r ? this.o.fgColor : this.fgColor ;
641 c.arc(this.xy, this.xy, this.radius, sat, eat, false);
642 c.stroke();
643 };
644
645 this.cancel = function () {
646 this.val(this.v);
647 };
648 };
649
650 $.fn.dial = $.fn.knob = function (o) {
651 return this.each(
652 function () {
653 var d = new k.Dial();
654 d.o = o;
655 d.$ = $(this);
656 d.run();
657 }
658 ).parent();
659 };
660
661})(jQuery);