jquery.knob.js 23 KB

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