Web based virtual Nixie Tube Clock
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

546 lines
15 KiB

  1. /****************************************************************************
  2. Virtual nixie tube display, clock & calculator DHTML components
  3. v 1.05, 20080214a
  4. (c) 2007-08 Cestmir Hybl, cestmir.hybl@nustep.net
  5. http://cestmir.freeside.sk/projects/dhtml-nixie-display
  6. license: free for non-commercial use, copyright must be preserved
  7. ****************************************************************************/
  8. /* NixieDisplay */
  9. // public class NixieDisplay
  10. function NixieDisplay()
  11. {
  12. // public
  13. this.id = 'nixie';
  14. this.elContainer = null;
  15. this.charCount = 10;
  16. this.autoDecimalPoint = true; // automatically extracts decimal point index in setText() call
  17. this.align = 'left'; // alignment of text via setText() call
  18. this.afterUpdate = null; // after display update callback
  19. this.charWidth = 62;
  20. this.charHeight = 150;
  21. this.charGapWidth = 0;
  22. this.extraGapsWidths = [];
  23. this.createCharElements = true;
  24. this.text = '';
  25. this.decimalPoint = -1;
  26. this.urlCharsetImage = 'nixie/zm1082_l1_09bdm_62x150_8b.png';
  27. this.charMap = {
  28. 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9,
  29. '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, ' ': 10, '-': 11,
  30. 'default': 10
  31. };
  32. // maps displayable chars onto glyph matrix indexes
  33. // protected
  34. function _drawChar(index)
  35. {
  36. var el = document.getElementById(this.id + '_d' + index);
  37. var charIndex = this.charMap[this.text.charAt(index)];
  38. if (!charIndex && charIndex !== 0)
  39. charIndex = this.charMap['default'];
  40. var x = - (charIndex * this.charWidth);
  41. var y = (index === this.decimalPoint ? - this.charHeight : 0);
  42. el.style.backgroundPosition = x + 'px ' + y + 'px';
  43. }
  44. this._drawChar = _drawChar;
  45. // Shows given string on display
  46. // public
  47. function setText(text, updateDecimalPoint)
  48. {
  49. // force string type
  50. this.text = text + '';
  51. // extract decimal point
  52. updateDecimalPoint = (typeof(updateDecimalPoint) != 'undefined' ? updateDecimalPoint : this.autoDecimalPoint);
  53. if (updateDecimalPoint) {
  54. var i = this.text.indexOf('.');
  55. if (i >= 0) {
  56. this.decimalPoint = i - 1;
  57. // alert(this.decimalPoint);
  58. this.text = this.text.substr(0, i) + this.text.substr(i + 1);
  59. } else
  60. this.decimalPoint = -1;
  61. }
  62. // pad up to display width (from left/right acording to this.align)
  63. if (this.text.length < this.charCount) {
  64. var pad = '';
  65. var padWidth = this.charCount - this.text.length;
  66. for (var i = 0; i < padWidth; i++)
  67. pad += ' ';
  68. if (this.align == 'left')
  69. this.text = this.text + pad;
  70. else {
  71. if (this.decimalPoint >= 0)
  72. this.decimalPoint += padWidth;
  73. this.text = pad + this.text;
  74. }
  75. }
  76. if (this.text.length > this.charCount)
  77. this.text = this.text.substr(0, this.charCount);
  78. // draw chars
  79. for (var i = 0; i < this.text.length; i++) {
  80. this._drawChar(i);
  81. }
  82. if (this.afterUpdate)
  83. this.afterUpdate(this);
  84. }
  85. this.setText = setText;
  86. // Sets char at given display position
  87. // public
  88. function setChar(index, chr)
  89. {
  90. // alert(chr);
  91. this.text = this.text.substring(0, index) + chr + this.text.substring(index + 1);
  92. this.setText(this.text, false);
  93. }
  94. this.setChar = setChar;
  95. function setDecimalPoint(index)
  96. {
  97. var oldDecimalPoint = this.decimalPoint;
  98. this.decimalPoint = ((!index && index !== 0) ? -1 : index);
  99. if (oldDecimalPoint != this.decimalPoint) {
  100. if (oldDecimalPoint >= 0)
  101. this._drawChar(oldDecimalPoint);
  102. if (this.decimalPoint >= 0)
  103. this._drawChar(this.decimalPoint);
  104. }
  105. }
  106. this.setDecimalPoint = setDecimalPoint;
  107. // Clears display - fills all positions with given char (space by default).
  108. // public
  109. function clear(chr)
  110. {
  111. chr = (typeof(chr) == 'undefined' ? ' ' : chr);
  112. this.text = '';
  113. for (var i = 0; i < this.charCount; i++)
  114. this.text += chr;
  115. this.decimalPoint = -1;
  116. this.setText(this.text);
  117. }
  118. this.clear = clear;
  119. // Shifts display contents left or right
  120. // public
  121. function shift(direction, step)
  122. {
  123. step = (!step && step !== 0 ? 1 : step);
  124. direction = (!direction ? 'left' : direction);
  125. if (this.decimalPoint >= 0) {
  126. this.decimalPoint += (direction == 'left' ? - step : + step);
  127. if (this.decimalPoint >= this.charCount)
  128. this.decimalPoint = -1;
  129. }
  130. if (direction == 'left')
  131. this.text = this.text.substr(step) + ' '; // @todo padding for step != +/-1
  132. else if (direction == 'right')
  133. this.text = ' ' + this.text.substr(0, this.text.length - 1); // @todo padding for step != +/-1
  134. this.setText(this.text, false);
  135. }
  136. this.shift = shift;
  137. // public
  138. function init()
  139. {
  140. if (!this.elContainer) {
  141. this.elContainer = document.getElementById(this.id);
  142. if (!this.elContainer)
  143. throw "Container element '" + this.id + "' not found";
  144. }
  145. this.elContainer.style.position = 'relative';
  146. if (this.createCharElements) {
  147. var totalWidth = 0;
  148. for (var i = 0; i < this.charCount; i++) {
  149. var charWidthIncludingGap = (this.charWidth + this.charGapWidth);
  150. var elId = this.id + '_d' + i;
  151. var el0 = document.getElementById(elId);
  152. var el = (el0 ? el0 : document.createElement('div'));
  153. el.id = this.id + '_d' + i;
  154. el.className = 'digit d' + i;
  155. el.style.position = 'absolute';
  156. el.style.left = totalWidth + 'px';
  157. el.style.width = this.charWidth + 'px';
  158. el.style.height = this.charHeight + 'px';
  159. el.style.background = 'url(' + this.urlCharsetImage + ')';
  160. if (!el.parentNode)
  161. this.elContainer.appendChild(el);
  162. totalWidth += charWidthIncludingGap + (this.extraGapsWidths[i] ? this.extraGapsWidths[i] : 0);
  163. }
  164. this.elContainer.style.width = totalWidth + 'px';
  165. this.elContainer.style.height = this.charHeight + 'px';
  166. }
  167. if (this.text)
  168. this.setText(this.text)
  169. else
  170. this.clear();
  171. }
  172. this.init = init;
  173. }
  174. /* NixieClock */
  175. // public class NixieClock : NixieDisplay
  176. function NixieClock()
  177. {
  178. // public
  179. // private
  180. this.lastSeconds = -1;
  181. // Show current time on "display"
  182. // public
  183. function showCurrentTime(refreshAfterChangeOnly)
  184. {
  185. var d = new Date();
  186. var s = d.getSeconds();
  187. if (refreshAfterChangeOnly && s == this.lastSeconds)
  188. return;
  189. else
  190. this.lastSeconds = s;
  191. var h = d.getHours();
  192. var m = d.getMinutes();
  193. var digits = '';
  194. digits += (h / 10) | 0;
  195. digits += h % 10;
  196. digits += (m / 10) | 0;
  197. digits += m % 10;
  198. digits += (s / 10) | 0;
  199. digits += s % 10;
  200. this.setText(digits);
  201. }
  202. this.showCurrentTime = showCurrentTime;
  203. // Run clock (via scheduling a periodic callback to showCurrentTime())
  204. // public
  205. function run()
  206. {
  207. if (!this.elContainer)
  208. this.init();
  209. var __nixieClock = this;
  210. window.setInterval(function() { __nixieClock.showCurrentTime(true); }, 100);
  211. }
  212. this.run = run;
  213. this.ancestor = NixieDisplay;
  214. this.ancestor();
  215. this.charCount = 6;
  216. this.extraGapsWidths[1] = 20;
  217. this.extraGapsWidths[3] = 20;
  218. }
  219. /* NixieCalculator */
  220. // @todo rounding of rightmost digit
  221. // public class NixieCalculator : NixieDisplay
  222. function NixieCalculator()
  223. {
  224. // public
  225. this.id = 'nixieCalc';
  226. this.digitCount = 13;
  227. this.display = new NixieDisplay();
  228. // private
  229. this.operandStack = [];
  230. this.newValueAtNextChar = false;
  231. this.fullPrecisionValue = 0;
  232. // private
  233. function push(value)
  234. {
  235. this.operandStack[this.operandStack.length] = value; // JS50 compatible .push()
  236. }
  237. this.push = push;
  238. // private
  239. function pop()
  240. {
  241. if (!this.operandStack.length)
  242. return null;
  243. var v = this.operandStack[this.operandStack.length - 1];
  244. this.operandStack = this.operandStack.slice(0, this.operandStack.length - 1); // JS50 compatible .pop()
  245. return v;
  246. }
  247. this.pop = pop;
  248. // public
  249. function getValue()
  250. {
  251. if (this.fullPrecisionValue !== null)
  252. return this.fullPrecisionValue;
  253. var v = this.display.text;
  254. // insert decimal point
  255. if (this.display.decimalPoint >= 0 && this.display.decimalPoint < this.digitCount - 1)
  256. v = v.substr(0, this.display.decimalPoint + 1) + '.' + v.substr(this.display.decimalPoint + 1);
  257. // remove padding spaces
  258. var i = 0;
  259. while (i < v.length && v.charAt(i) == ' ')
  260. i++;
  261. v = v.substr(i);
  262. // convert to number
  263. v = parseFloat(v);
  264. return v;
  265. }
  266. this.getValue = getValue;
  267. // public
  268. function setValue(v)
  269. {
  270. if (typeof(v) != 'number')
  271. v = parseFloat(v);
  272. if (isNaN(v) || v > this.maxNumber || v < -this.maxNumber)
  273. this.error();
  274. else {
  275. this.fullPrecisionValue = v;
  276. if (v.toFixed) {
  277. // force fixed-point notation (JS5.5+)
  278. var s = (v >= 0 ? ' ' : '') + v.toFixed(1);
  279. s = s.substring(0, s.length - 2);
  280. // (s now contains string with integer part of value, prefixed by either ' ' or '-')
  281. v = v.toFixed(this.digitCount - s.length); // to fixed point + round rightmost digit
  282. } else {
  283. v = v.toString();
  284. if (v.toLowerCase().indexOf('e') >= 0) {
  285. // we won't handle exp notation in JS<5.5
  286. this.error();
  287. return;
  288. }
  289. }
  290. if (v !== '0') {
  291. if (v.charAt(0) != '-')
  292. v = ' ' + v;
  293. var c = this.digitCount + (v.indexOf('.') >= 0 ? 1 : 0);
  294. if (v.length > c)
  295. v = v.substr(0, c);
  296. v = v.replace(/^(.{1,}?)\.?0+$/g, '$1'); // strip zero's from right
  297. }
  298. this.display.setText(v);
  299. }
  300. }
  301. this.setValue = setValue;
  302. // private
  303. function eval(v1, o, v2)
  304. {
  305. try {
  306. switch (o) {
  307. case '+':
  308. return v1 + v2;
  309. case '-':
  310. return v1 - v2;
  311. case '*':
  312. return v1 * v2;
  313. case '/':
  314. return v1 / v2;
  315. case '^':
  316. return Math.pow(v1, v2);
  317. case 'sqrt':
  318. return Math.sqrt(v1);
  319. case 'sqr':
  320. return v1 * v1;
  321. default:
  322. throw "Unsupported operand: '" + o + "'";
  323. }
  324. } catch(e) {
  325. this.error();
  326. }
  327. }
  328. this.eval = eval;
  329. // public
  330. function error()
  331. {
  332. var s= '';
  333. for (var i = 0; i < this.digitCount; i++)
  334. s += '-';
  335. this.operandStack = [];
  336. this.newValueAtNextChar = true;
  337. this.fullPrecisionValue = null;
  338. this.display.setText(s);
  339. }
  340. this.error = error;
  341. // public
  342. function clear()
  343. {
  344. this.display.clear();
  345. this.setValue(0);
  346. this.operandStack = [];
  347. this.fullPrecisionValue = null;
  348. }
  349. this.clear = clear;
  350. // public
  351. function keyDown(event0)
  352. {
  353. var e = (event0 ? event0 : event);
  354. var k = e.keyCode;
  355. var cancelEvent = true;
  356. if (k == 8) {
  357. // backspace
  358. if (this.display.text.charAt(this.digitCount - 2) == ' ')
  359. this.display.setChar(this.digitCount - 1, '0');
  360. else
  361. this.display.shift('right');
  362. this.fullPrecisionValue = null;
  363. } else if (k == 27) {
  364. // escape
  365. this.clear();
  366. } else
  367. cancelEvent = false;
  368. return !cancelEvent;
  369. }
  370. this.keyDown = keyDown;
  371. // public
  372. function keyPress(event0)
  373. {
  374. var e = (event0 ? event0 : event);
  375. var k = (e.keyCode ? e.keyCode : e.which); // IE: .keyCode, FF: .which
  376. var chr = String.fromCharCode(k);
  377. var cancelEvent = true;
  378. var newValueAtThisChar = this.newValueAtNextChar;
  379. this.newValueAtNextChar = true;
  380. if (chr >= '0' && chr <= '9') {
  381. this.fullPrecisionValue = null;
  382. if (newValueAtThisChar) {
  383. this.display.clear();
  384. }
  385. if (this.display.text.charAt(1) == ' ' || this.display.text.charAt(1) == '-') {
  386. if (this.display.text.charAt(this.digitCount - 1) == '0' && this.display.text.charAt(this.digitCount - 2) == ' ' && this.display.decimalPoint < 0)
  387. ;
  388. else
  389. this.display.shift('left');
  390. this.display.setChar(this.digitCount - 1, chr);
  391. }
  392. this.newValueAtNextChar = false;
  393. }
  394. else if (chr == '.' || chr == ',') {
  395. this.fullPrecisionValue = null;
  396. if (newValueAtThisChar)
  397. this.display.setText(0);
  398. if (this.display.decimalPoint < 0)
  399. this.display.setDecimalPoint(this.digitCount - 1);
  400. this.newValueAtNextChar = false;
  401. }
  402. else if (chr == '+' || chr == '-' || chr == '*' || chr == '/' || chr == '^') {
  403. if (this.operandStack.length > 2)
  404. // cancel repeated evaluation
  405. this.operandStack = [];
  406. if (this.operandStack.length == 2) {
  407. // previous expression without explicit '=', evaluate it
  408. this.setValue(this.eval(this.operandStack[this.operandStack.length - 2], this.operandStack[this.operandStack.length - 1], this.getValue()));
  409. this.operandStack = [];
  410. }
  411. // push left operand
  412. this.push(this.getValue());
  413. // push operator
  414. this.push(chr);
  415. }
  416. else if (chr == 'm' || chr == 'M') {
  417. this.setValue(- this.getValue());
  418. this.newValueAtNextChar = false;
  419. }
  420. else if (chr == 'p' || chr == 'P') {
  421. this.setValue(Math.PI);
  422. }
  423. else if (chr == 'q') {
  424. this.setValue(this.eval(this.getValue(), 'sqrt', null));
  425. }
  426. else if (chr == 'Q') {
  427. this.setValue(this.eval(this.getValue(), 'sqr', null));
  428. }
  429. else if (k == 13 || chr == '=') {
  430. if (this.operandStack.length >= 2) {
  431. if (this.operandStack.length <= 2)
  432. // push right operand
  433. this.push(this.getValue())
  434. else
  435. // repeated evaluation (e.g. [1] [+] [1] [=] [=] ...), replace left operand with current display value
  436. this.operandStack[this.operandStack.length - 3] = this.getValue();
  437. // alert(this.operandStack);
  438. var result = this.eval(this.operandStack[this.operandStack.length - 3], this.operandStack[this.operandStack.length - 2], this.operandStack[this.operandStack.length - 1]);
  439. this.setValue(result);
  440. // this.operandStack = [];
  441. }
  442. }
  443. // else if (k == 27) { // Fix: chromium: Esc not handled in keyPress(), http://code.google.com/p/chromium/issues/detail?id=12744
  444. // // escape
  445. // this.clear();
  446. // }
  447. else {
  448. cancelEvent = false;
  449. this.newValueAtNextChar = false;
  450. }
  451. return !cancelEvent;
  452. }
  453. this.keyPress = keyPress;
  454. // public
  455. function init()
  456. {
  457. this.display.id = this.id;
  458. this.display.charCount = this.digitCount;
  459. this.display.align = 'right';
  460. this.display.init();
  461. this.display.setText('0');
  462. this.maxNumber = Math.pow(10, this.digitCount - 1) - 1;
  463. }
  464. this.init = init;
  465. // assign default glyph matrix
  466. this.display.urlCharsetImage = 'nixie/zm1080_l2_09bdm_45x75_8b.png';
  467. this.display.charWidth = 45;
  468. this.display.charHeight = 75;
  469. this.display.charGapWidth = 5;
  470. }