/*********************************************************************** * retro-220/webUI B220PanelUtil.js ************************************************************************ * Copyright (c) 2017, Paul Kimpel. * Licensed under the MIT License, see * http://www.opensource.org/licenses/mit-license.php ************************************************************************ * JavaScript object definition for the Burroughs 220 Emulator Control * Panel utility constructors: * NeonLamp * NeonLampBox * ColoredLamp * ToggleSwitch * ThreeWaySwitch * OrganSwitch * BlackControlKnob * PanelRegister ************************************************************************ * 2017-01-01 P.Kimpel * Original version, from retro-205 D205PanelUtil.js. ***********************************************************************/ /*********************************************************************** * Panel Neon Lamp * ***********************************************************************/ function NeonLamp(parent, x, y, id) { /* Constructor for the neon lamp objects used within panels. x & y are the coordinates of the lamp within its containing element; id is the DOM id */ this.state = 0; // current lamp state, 0=off this.topCaptionDiv = null; // optional top caption element this.bottomCaptionDiv = null; // optional bottom caption element // visible DOM element this.element = document.createElement("div"); this.element.id = id; this.element.className = NeonLamp.lampClass; if (x !== null) { this.element.style.left = x.toString() + "px"; } if (y !== null) { this.element.style.top = y.toString() + "px"; } if (parent) { parent.appendChild(this.element); } } /**************************************/ NeonLamp.topCaptionClass = "neonLampTopCaption"; NeonLamp.bottomCaptionClass = "neonLampBottomCaption"; NeonLamp.lampClass = "neonLamp"; NeonLamp.litClass = "neonLamp neonLit"; NeonLamp.lampLevels = 6; NeonLamp.levelClass = [ // css class names for the lamp levels NeonLamp.lampClass, NeonLamp.litClass + "1", NeonLamp.litClass + "2", NeonLamp.litClass + "3", NeonLamp.litClass + "4", NeonLamp.litClass + "5", NeonLamp.litClass]; /**************************************/ NeonLamp.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) { /* Sets an event handler whenever the image element is clicked */ this.element.addEventListener(eventName, handler, useCapture); }; /**************************************/ NeonLamp.prototype.set = function set(state) { /* Changes the visible state of the lamp according to the value of "state", 0-1 */ var newState = Math.max(Math.min(Math.round(state*NeonLamp.lampLevels + 0.4999), NeonLamp.lampLevels), 0); if (this.state ^ newState) { // the state has changed this.state = newState; this.element.className = NeonLamp.levelClass[newState]; } }; /**************************************/ NeonLamp.prototype.flip = function flip() { /* Complements the visible state of the lamp */ this.set(1.0 - this.state/NeonLamp.lampLevels); }; /**************************************/ NeonLamp.prototype.setCaption = function setCaption(caption, atBottom) { /* Establishes an optional caption at the top or bottom of a single lamp. Returns the caption element */ var e = (atBottom ? this.bottomCaptionDiv : this.topCaptionDiv); if (e) { e.textContent = caption; } else { e = document.createElement("div"); if (atBottom) { this.bottomCaptionDiv = e; e.className = NeonLamp.bottomCaptionClass; } else { this.topCaptionDiv = e; e.className = NeonLamp.topCaptionClass; } e.appendChild(document.createTextNode(caption)); this.element.appendChild(e); } return e; }; /*********************************************************************** * Panel Neon Lamp Box * ***********************************************************************/ function NeonLampBox(parent, x, y, id, caption) { /* Constructor for the neon lamp-in-a-box objects used within panels. x & y are the coordinates of the box within its containing element; id is the DOM id */ // visible DOM element this.element = document.createElement("div"); this.element.id = id; this.element.className = NeonLampBox.lampBoxClass; if (x !== null) { this.element.style.left = x.toString() + "px"; } if (y !== null) { this.element.style.top = y.toString() + "px"; } this.lamp = new NeonLamp(this.element, 3, 3, id + "_Lamp"); this.button = document.createElement("div"); this.button.id = id + "_LampBtn"; this.button.className = NeonLampBox.lampButtonClass; this.button.textContent = caption; this.element.appendChild(this.button); if (parent) { parent.appendChild(this.element); } } /**************************************/ NeonLampBox.lampBoxClass = "lampBox"; NeonLampBox.lampButtonClass = "lampButton"; /**************************************/ NeonLampBox.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) { /* Sets an event handler whenever the image element is clicked */ this.element.addEventListener(eventName, handler, useCapture); }; /**************************************/ NeonLampBox.prototype.set = function set(state) { /* Changes the visible state of the lamp according to the value of "state", 0-1 */ this.lamp.set(state); }; /**************************************/ NeonLampBox.prototype.flip = function flip() { /* Complements the visible state of the lamp */ this.lamp.flip(); }; /**************************************/ NeonLampBox.prototype.setCaption = function setCaption(caption) { /* Establishes an optional caption on the button for a single lamp */ this.button.textContent = caption; }; /*********************************************************************** * Panel Colored Lamp * ***********************************************************************/ function ColoredLamp(parent, x, y, id, offClass, onClass) { /* Constructor for the colored lamp objects used within panels. x & y are the coordinates of the lamp within its containing element; id is the DOM id */ this.state = 0; // current lamp state, 0=off this.topCaptionDiv = null; // optional top caption element this.bottomCaptionDiv = null; // optional bottom caption element this.lampClass = offClass; // css styling for an "off" lamp this.litClass = // css styling for an "on" lamp offClass + " " + onClass; this.levelClass = [ // css class names for the lamp levels offClass, this.litClass + "1", this.litClass + "2", this.litClass + "3", this.litClass + "4", this.litClass + "5", this.litClass]; // visible DOM element this.element = document.createElement("div"); this.element.id = id; this.element.className = offClass; if (x !== null) { this.element.style.left = x.toString() + "px"; } if (y !== null) { this.element.style.top = y.toString() + "px"; } if (parent) { parent.appendChild(this.element); } } /**************************************/ ColoredLamp.lampLevels = 6; ColoredLamp.topCaptionClass = "coloredLampTopCaption"; ColoredLamp.bottomCaptionClass = "coloredLampBottomCaption"; /**************************************/ ColoredLamp.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) { /* Sets an event handler whenever the image element is clicked */ this.element.addEventListener(eventName, handler, useCapture); }; /**************************************/ ColoredLamp.prototype.set = function set(state) { /* Changes the visible state of the lamp according to the value of "state", 0-1 */ var newState = Math.max(Math.min(Math.round(state*ColoredLamp.lampLevels + 0.4999), ColoredLamp.lampLevels), 0); if (this.state != newState) { // the state has changed this.state = newState; this.element.className = this.levelClass[newState]; } }; /**************************************/ ColoredLamp.prototype.flip = function flip() { /* Complements the visible state of the lamp */ this.set(ColoredLamp.lampLevels - this.state); }; /**************************************/ ColoredLamp.prototype.setCaption = function setCaption(caption, atBottom) { /* Establishes an optional caption at the top or bottom of a single lamp. Returns the caption element */ var e = (atBottom ? this.bottomCaptionDiv : this.topCaptionDiv); if (e) { e.textContent = caption; } else { e = document.createElement("div"); if (atBottom) { this.bottomCaptionDiv = e; e.className = ColoredLamp.bottomCaptionClass; } else { this.topCaptionDiv = e; e.className = ColoredLamp.topCaptionClass; } e.appendChild(document.createTextNode(caption)); this.element.appendChild(e); } return e; }; /*********************************************************************** * Panel Toggle Switch * ***********************************************************************/ function ToggleSwitch(parent, x, y, id, offImage, onImage) { /* Constructor for the toggle switch objects used within panels. x & y are the coordinates of the switch within its containing element; id is the DOM id */ this.state = 0; // current switch state, 0=off this.topCaptionDiv = null; // optional top caption element this.bottomCaptionDiv = null; // optional bottom caption element this.offImage = offImage; // image used for the off state this.onImage = onImage; // image used for the on state // visible DOM element this.element = document.createElement("img"); this.element.id = id; this.element.src = offImage; if (x !== null) { this.element.style.left = x.toString() + "px"; } if (y !== null) { this.element.style.top = y.toString() + "px"; } if (parent) { parent.appendChild(this.element); } } /**************************************/ ToggleSwitch.topCaptionClass = "toggleSwitchTopCaption"; ToggleSwitch.bottomCaptionClass = "toggleSwitchBottomCaption"; /**************************************/ ToggleSwitch.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) { /* Sets an event handler whenever the image element is clicked */ this.element.addEventListener(eventName, handler, useCapture); }; /**************************************/ ToggleSwitch.prototype.set = function set(state) { /* Changes the visible state of the switch according to the low-order bit of "state" */ var newState = state & 1; if (this.state ^ newState) { // the state has changed this.state = newState; this.element.src = (newState ? this.onImage : this.offImage); } }; /**************************************/ ToggleSwitch.prototype.flip = function flip() { /* Complements the visible state of the switch */ var newState = this.state ^ 1; this.state = newState; this.element.src = (newState ? this.onImage : this.offImage); }; /**************************************/ ToggleSwitch.prototype.setCaption = function setCaption(caption, atBottom) { /* Establishes an optional caption at the top or bottom of a single switch. Returns the caption element */ var e = (atBottom ? this.bottomCaptionDiv : this.topCaptionDiv); if (e) { e.textContent = caption; } else { e = document.createElement("div"); if (atBottom) { this.bottomCaptionDiv = e; e.className = ToggleSwitch.bottomCaptionClass; } else { this.topCaptionDiv = e; e.className = ToggleSwitch.topCaptionClass; } e.appendChild(document.createTextNode(caption)); this.element.appendChild(e); } return e; }; /*********************************************************************** * Panel Three-way Toggle Switch * ***********************************************************************/ function ThreeWaySwitch(parent, x, y, id, offImage, onImage1, onImage2) { /* Constructor for the three-way toggle switch objects used within panels. x & y are the coordinates of the switch within its containing element; id is the DOM id */ this.state = 0; // current switch state, 0=off, 1=down, 2=up this.topCaptionDiv = null; // optional top caption element this.bottomCaptionDiv = null; // optional bottom caption element this.offImage = offImage; // image used for the off state this.onImage1 = onImage1; // image used for the lower on state this.onImage2 = onImage2; // image used for the upper on state // visible DOM element this.element = document.createElement("img"); this.element.id = id; this.element.src = offImage; if (x !== null) { this.element.style.left = x.toString() + "px"; } if (y !== null) { this.element.style.top = y.toString() + "px"; } if (parent) { parent.appendChild(this.element); } } /**************************************/ ThreeWaySwitch.topCaptionClass = "ToggleSwitchTopCaption"; ThreeWaySwitch.bottomCaptionClass = "ToggleSwitchBottomCaption"; /**************************************/ ThreeWaySwitch.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) { /* Sets an event handler whenever the image element is clicked */ this.element.addEventListener(eventName, handler, useCapture); }; /**************************************/ ThreeWaySwitch.prototype.set = function set(state) { /* Changes the visible state of the switch according to the value of "state" */ if (this.state != state) { // the state has changed switch (state) { case 1: // down position this.state = 1; this.element.src = this.onImage1; break; case 2: // up position this.state = 2; this.element.src = this.onImage2; break; default: // middle position this.state = 0; this.element.src = this.offImage; break; } // switch state } }; /**************************************/ ThreeWaySwitch.prototype.flip = function flip() { /* Increments the visible state of the switch */ this.set(this.state+1); }; /**************************************/ ThreeWaySwitch.prototype.setCaption = function setCaption(caption, atBottom) { /* Establishes an optional caption at the top or bottom of a single switch. Returns the caption element */ var e = (atBottom ? this.bottomCaptionDiv : this.topCaptionDiv); if (e) { e.textContent = caption; } else { e = document.createElement("div"); if (atBottom) { this.bottomCaptionDiv = e; e.className = ThreeWaySwitch.bottomCaptionClass; } else { this.topCaptionDiv = e; e.className = ThreeWaySwitch.topCaptionClass; } e.appendChild(document.createTextNode(caption)); this.element.appendChild(e); } return e; }; /*********************************************************************** * Panel Organ Switch * ***********************************************************************/ function OrganSwitch(parent, x, y, id, offImage, onImage, momentary) { /* Constructor for the organ switch objects used within panels. x & y are the coordinates of the switch within its containing element; id is the DOM id */ this.state = 0; // current switch state, 0=off this.topCaptionDiv = null; // optional top caption element this.bottomCaptionDiv = null; // optional bottom caption element this.offImage = offImage; // image used for the off state this.onImage = onImage; // image used for the on state this.momentary = momentary || false;// true if the switch is only momentary-on // visible DOM element this.element = document.createElement("img"); this.element.id = id; this.element.src = offImage; if (x !== null) { this.element.style.left = x.toString() + "px"; } if (y !== null) { this.element.style.top = y.toString() + "px"; } if (parent) { parent.appendChild(this.element); } } /**************************************/ OrganSwitch.topCaptionClass = "OrganSwitchTopCaption"; OrganSwitch.bottomCaptionClass = "OrganSwitchBottomCaption"; OrganSwitch.momentaryPeriod = 200; // time for momentary switch to bounce back, ms /**************************************/ OrganSwitch.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) { /* Sets an event handler whenever the image element is clicked */ this.element.addEventListener(eventName, handler, useCapture); }; /**************************************/ OrganSwitch.prototype.set = function set(state) { /* Changes the visible state of the switch according to the low-order bit of "state" */ var newState = state & 1; if (this.state ^ newState) { // the state has changed this.state = newState; this.element.src = (newState ? this.onImage : this.offImage); if (this.momentary && newState) { setCallback(null, this, OrganSwitch.momentaryPeriod, this.set, 0); } } }; /**************************************/ OrganSwitch.prototype.flip = function flip() { /* Complements the visible state of the switch */ this.set(this.state ^ 1); }; /**************************************/ OrganSwitch.prototype.setCaption = function setCaption(caption, atBottom) { /* Establishes an optional caption at the top or bottom of a single switch. Returns the caption element */ var e = (atBottom ? this.bottomCaptionDiv : this.topCaptionDiv); if (e) { e.textContent = caption; } else { e = document.createElement("div"); if (atBottom) { this.bottomCaptionDiv = e; e.className = OrganSwitch.bottomCaptionClass; } else { this.topCaptionDiv = e; e.className = OrganSwitch.topCaptionClass; } e.appendChild(document.createTextNode(caption)); this.element.appendChild(e); } return e; }; /*********************************************************************** * Black Control Knob * ***********************************************************************/ function BlackControlKnob(parent, x, y, id, initial, positions) { /* Constructor for the black control knob objects used within panels. x & y are the coordinates of the knob within its containing element; id is the DOM id; initial is the 0-relative index indicating the default position of the switch; positions is an array indicating the angular position (in degrees, where 0 is straight up) of each of the knob's positions */ this.position = 0; // current knob position this.direction = 1; // rotate knob clockwise(1), counter-clockwise(-1) this.topCaptionDiv = null; // optional top caption element this.bottomCaptionDiv = null; // optional bottom caption element this.positions = positions; // array of knob position angles // visible DOM element this.element = document.createElement("canvas"); this.element.id = id; this.element.width = BlackControlKnob.size; this.element.height = BlackControlKnob.size; this.element.className = BlackControlKnob.className; if (x !== null) { this.element.style.left = x.toString() + "px"; } if (y !== null) { this.element.style.top = y.toString() + "px"; } if (parent) { parent.appendChild(this.element); } this.set(initial); // set to its initial position } /**************************************/ BlackControlKnob.topCaptionClass = "blackControlKnobTopCaption"; BlackControlKnob.bottomCaptionClass = "blackControlKnobBottomCaption"; BlackControlKnob.className = "blackControlKnob1"; BlackControlKnob.canvasColor = "transparent"; BlackControlKnob.size = 64; // width/height in pixels /**************************************/ BlackControlKnob.prototype.addEventListener = function addEventListener(eventName, handler, useCapture) { /* Sets an event handler whenever the canvas element is clicked */ this.element.addEventListener(eventName, handler, useCapture); }; /**************************************/ BlackControlKnob.prototype.set = function set(position) { /* Changes the visible state of the knob according to the position index */ var dc = this.element.getContext("2d"); var degrees = Math.PI/180; var fullCircle = 360*degrees; var halfSize = Math.floor(BlackControlKnob.size/2); var quarterSize = Math.floor(BlackControlKnob.size/4); var silverSkirt; if (position < 0) { this.position = 0; this.direction = 1; } else if (position < this.positions.length) { this.position = position; } else { this.position = this.positions.length-1; this.direction = -1; } dc.save(); dc.translate(halfSize+0.5, halfSize+0.5); // move origin to the center dc.fillStyle = BlackControlKnob.canvasColor;// fill in the panel background (aids antialiasing) dc.fillRect(-halfSize, -halfSize, BlackControlKnob.size, BlackControlKnob.size); silverSkirt = dc.createRadialGradient(0, 0, halfSize, 0, 0, quarterSize); silverSkirt.addColorStop(0.5, "#FFF"); silverSkirt.addColorStop(1, "#CCC"); dc.beginPath(); // draw the outer skirt of the knob dc.arc(0, 0, halfSize-1, 0, fullCircle, false); dc.fillStyle = silverSkirt; dc.fill(); dc.beginPath(); // draw the central knob dc.arc(0, 0, quarterSize, 0, fullCircle, false); dc.fillStyle = "#000"; dc.fill(); dc.beginPath(); // draw the inset on top of the knob dc.arc(0, 0, quarterSize-4, 0, fullCircle, false); dc.fillStyle = "#333"; dc.fill(); dc.save(); // draw the knob indicator dc.rotate(this.positions[this.position]*degrees); dc.beginPath(); dc.moveTo(0, 1-halfSize); dc.lineTo(-quarterSize/4, -halfSize+quarterSize/2); dc.lineTo(quarterSize/4, -halfSize+quarterSize/2); dc.closePath(); dc.fillStyle = "#000"; dc.fill(); dc.restore(); // undo the rotation dc.restore(); // undo the translation }; /**************************************/ BlackControlKnob.prototype.step = function step() { /* Steps the knob to its next position. If it is at the last position, steps it to the first position */ var position = this.position+this.direction; if (position < 0) { this.direction = 1; this.set(1); } else if (position < this.positions.length) { this.set(position); } else { this.direction = -1; this.set(this.positions.length-2); } }; /**************************************/ BlackControlKnob.prototype.setCaption = function setCaption(caption, atBottom) { /* Establishes an optional caption at the top or bottom of a single switch. Returns the caption element */ var e = (atBottom ? this.bottomCaptionDiv : this.topCaptionDiv); if (e) { e.textContent = caption; } else { e = document.createElement("div"); if (atBottom) { this.bottomCaptionDiv = e; e.className = BlackControlKnob.bottomCaptionClass; } else { this.topCaptionDiv = e; e.className = BlackControlKnob.topCaptionClass; } e.appendChild(document.createTextNode(caption)); this.element.appendChild(e); } return e; }; /*********************************************************************** * Panel Register * ***********************************************************************/ function PanelRegister(parent, bits, rows, idPrefix, caption) { /* Constructor for the register objects used within panels: parent:the DOM element (usually a
) within which the register will be built bits: number of bits in register rows: number of rows used to display the bit lamps caption:optional caption displayed at the bottom of the register */ var cols = Math.floor((bits+rows-1)/rows); var b; var cx; var cy; var e; this.element = parent; // containing element for the panel this.bits = bits; // number of bits in the register this.caption = caption || ""; // panel caption this.lastValue = 0; // prior register value this.lamps = new Array(bits); // bit lamps cx = cols*PanelRegister.hSpacing + PanelRegister.hOffset; for (b=0; b element */ var bar = document.createElement("div"); var e = ""; bar.style.position = "absolute"; bar.style.left = (this.xCoord(col) - (PanelRegister.hSpacing-PanelRegister.lampDiameter)/2 + 1).toString() + "px"; bar.style.width = "2px"; bar.style.top = this.yCoord(1).toString() + "px"; bar.style.height = (this.yCoord(rows+1) - this.yCoord(1)).toString() + "px"; if (styles) { for (e in styles) { bar.style[e] = styles[e]; } } this.element.appendChild(bar); return bar; }; /**************************************/ PanelRegister.prototype.drawBox = function drawBox(col, lamps, rows, leftStyle, rightStyle) { /* Creates a box centered around a specified group of lamps in a register. leftStyle and rightStyle specify the left and right borders of the box using standard CSS border syntax. Returns the box element */ var box = document.createElement("div"); box.style.position = "absolute"; box.style.left = (this.xCoord(col) - (PanelRegister.hSpacing-PanelRegister.lampDiameter)/2 + 1).toString() + "px"; box.style.width = (PanelRegister.hSpacing*lamps - 2).toString() + "px"; box.style.top = this.yCoord(1).toString() + "px"; box.style.height = (this.yCoord(rows+1) - this.yCoord(1)).toString() + "px"; box.style.borderLeft = leftStyle; box.style.borderRight = rightStyle; box.appendChild(document.createTextNode("\xA0")); this.element.appendChild(box); return box; }; /**************************************/ PanelRegister.prototype.setBoxCaption = function setBoxCaption(box, caption) { /* Establishes an optional caption for register lamp box. Returns the caption element */ var e = box.captionDiv; if (e) { e.textContent = caption; } else { box.captionDiv = e = document.createElement("div"); e.className = PanelRegister.boxCaptionClass; e.appendChild(document.createTextNode(caption)); box.appendChild(e); } return e; }; /**************************************/ PanelRegister.prototype.update = function update(value) { /* Update the register lamps from the value of the parameter. This routine compares the value of the register that was previously updated against the new one in an attempt to minimize the number of lamp flips that need to be done */ var bitBase = 0; var bitNr; var lastMask; var lastValue = this.lastValue; var thisMask; var thisValue = Math.floor(Math.abs(value)) % 0x100000000000; if (thisValue != lastValue) { this.lastValue = thisValue; // save it for next time do { // Loop through the masks 30 bits at a time so we can use Javascript bit ops bitNr = bitBase; lastMask = lastValue % 0x40000000; // get the low-order 30 bits lastValue = (lastValue-lastMask)/0x40000000; // shift the value right 30 bits thisMask = thisValue % 0x40000000; // ditto for the second value thisValue = (thisValue-thisMask)/0x40000000; lastMask ^= thisMask; // determine which bits have changed while (lastMask) { if (lastMask & 0x01) { this.lamps[bitNr].set(thisMask & 0x01); } if (++bitNr <= this.bits) { lastMask >>>= 1; thisMask >>>= 1; } else { thisValue = thisMask = 0; lastValue = lastMask = 0; break; // out of inner while loop } } bitBase += 30; } while (thisValue || lastValue); } }; /**************************************/ PanelRegister.prototype.updateLampGlow = function updateLampGlow(glow) { /* Update the register lamps from the bitwise intensity values in "glow" */ var bitNr; for (bitNr=this.bits-1; bitNr>=0; --bitNr) { this.lamps[bitNr].set(glow[bitNr]); } };