├── .eslintrc ├── README.md ├── hacks └── leakySensor.js ├── hardware ├── BiggerMotor.js ├── Button.js ├── Doors.js ├── Elevator.js ├── LightButton.js ├── Motor.js ├── Panel.js └── ProximitySensor.js ├── software ├── Action.js ├── Controller.js ├── EventTarget.js └── SignalTarget.js ├── the-doors ├── index.html ├── the-doors.css └── the-doors.js ├── the-elevator ├── index.html ├── the-elevator.css └── the-elevator.js └── the-light-button ├── index.html ├── the-light-button.css └── the-light-button.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "globals": { 4 | "Set": true, 5 | "WeakMap": true 6 | }, 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "no-case-declarations": 0, 12 | "no-fallthrough": 0 13 | } 14 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A JavaScript Elevator 2 | ===================== 3 | 4 | An introduction to programming for the Internet of Things through a metaphoric code explained through [a blog posts](https://medium.com/@WebReflection/a-javascript-elevator-9b55e1d4acc8). 5 | 6 | ### Demos 7 | 8 | * [the doors](https://webreflection.github.io/elevator/the-doors/) of a lift have at least a motor and a sensor (unless it chops people that are passing through). Doors can close or open, and everything is asynchronous and cancelable (unless you want to chop people that are passing through). 9 | * [the light button](https://webreflection.github.io/elevator/the-light-button/) does exactly what a button does, but it has an extra `switch(state)` method to switch on or off its internal light. It's important to understand that pressing the button, does not necessarily mean switching on the light. Remember, an elevator is a list of hardware driven by a controller, not a set of stand-alone modules with a proper purpose. 10 | * [the elevator](https://webreflection.github.io/elevator/the-elevator/) with the most basic, queue based, controller implementation. Click as many buttons you want from the floor (left) or from the elevator (right) and see it moving. 11 | 12 | ### Requirements 13 | 14 | You need a modern browser compatible with ECMAScript 2015 modules (ESM). 15 | 16 | Chrome, Web, or Safari are right now tested and supported. 17 | -------------------------------------------------------------------------------- /hacks/leakySensor.js: -------------------------------------------------------------------------------- 1 | // make the target class available 2 | import Doors from '../hardware/Doors.js'; 3 | 4 | // override WeakMap#set 5 | const set = WeakMap.prototype.set; 6 | Object.defineProperty( 7 | WeakMap.prototype, 8 | 'set', 9 | {value(self, data) { 10 | if (self instanceof Doors) { 11 | leak = data.sensor; 12 | } 13 | return set.call(this, self, data); 14 | }} 15 | ); 16 | 17 | // and exports the leaky sensor 18 | export let leak; 19 | -------------------------------------------------------------------------------- /hardware/BiggerMotor.js: -------------------------------------------------------------------------------- 1 | import Motor from './Motor.js'; 2 | 3 | // an Elevator Motor is just a Motor 4 | // with more power. In this case just more speed. 5 | export default class BiggerMotor extends Motor { 6 | constructor() { 7 | super().speed = 0.05; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /hardware/Button.js: -------------------------------------------------------------------------------- 1 | import SignalTarget from '../software/SignalTarget.js'; 2 | 3 | // the most basic Button ever does one thing and one thing only: 4 | // when you press it, it sends a signal that it's being pressed 5 | export default class Button extends SignalTarget { 6 | constructor(symbol) { 7 | super(); 8 | this.symbol = symbol; 9 | } 10 | 11 | // while in the Internet of Things world you'll have 12 | // a physical way to press such button, we need a 13 | // synthetic alternative to simulate actual pressing. 14 | press() { this.signal('press'); } 15 | } 16 | -------------------------------------------------------------------------------- /hardware/Doors.js: -------------------------------------------------------------------------------- 1 | import SignalTarget from '../software/SignalTarget.js'; 2 | 3 | // modern elevator doors have at least one motor 4 | import Motor from './Motor.js'; 5 | // and one sensor to avoid chopping people while doors close 6 | import ProximitySensor from './ProximitySensor.js'; 7 | 8 | // private properties are handled by this WeakMap 9 | const privates = new WeakMap; 10 | 11 | export default class Doors extends SignalTarget { 12 | 13 | // doors can be either closed or opened 14 | static get CLOSED() { return 0; } 15 | static get OPENED() { return 1; } 16 | 17 | constructor() { 18 | super(); 19 | // setup motor and sensor for these doors 20 | const motor = new Motor; 21 | const sensor = new ProximitySensor; 22 | // update doors position while motor is rotating 23 | motor.on('rotating', this); 24 | // react on proximity sensor signals 25 | sensor.on('proximity', this); 26 | // these are internal features nobody else 27 | // should handle, granting doors functionality integrity. 28 | privates.set(this, {motor, sensor, status: Doors.CLOSED}); 29 | } 30 | 31 | // a read only property to know if doors are opened or closed 32 | get status() { 33 | return privates.get(this).status; 34 | } 35 | 36 | // a public API to open doors 37 | open() { 38 | const info = privates.get(this); 39 | // if already opened, nothing happens 40 | if (info.status === Doors.OPENED) return; 41 | // otherwise move doors through the motor 42 | info.motor.rotate(1); 43 | } 44 | 45 | // same API goes for closing doors 46 | close() { 47 | const info = privates.get(this); 48 | // if laready closed, nothing happens 49 | if (info.status === Doors.CLOSED) return; 50 | // otherwise be sure nobody gets chopped while closing 51 | info.sensor.activate(); 52 | // and start moving doors through the motor 53 | info.motor.rotate(-1); 54 | } 55 | 56 | // whenever the sensor signals proximity 57 | // the doors.open() method is invoked 58 | onproximity() { this.open(); } 59 | 60 | // while the motor is rotaing 61 | onrotating(event) { 62 | const info = privates.get(this); 63 | // update the status between 0 and 1 64 | info.status = Math.max( 65 | Doors.CLOSED, 66 | Math.min( 67 | Doors.OPENED, 68 | info.status + event.detail 69 | ) 70 | ); 71 | // signal to a visual panels or other hardware 72 | // that doors are moving 73 | this.signal('moving'); 74 | // and if status is now opened or closed 75 | switch (info.status) { 76 | case Doors.CLOSED: 77 | case Doors.OPENED: 78 | // stop the sensor 79 | info.sensor.deactivate(); 80 | // and stop the motor 81 | info.motor.stop(); 82 | // notify whatever needs it that doors status changed 83 | // this is the equivalent of a `.then(...)` after 84 | // doors have finished opening or closing 85 | this.signal('changed'); 86 | break; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /hardware/Elevator.js: -------------------------------------------------------------------------------- 1 | import SignalTarget from '../software/SignalTarget.js'; 2 | 3 | // just like its doors, an elevator moves through a motor 4 | import BiggerMotor from './BiggerMotor.js'; 5 | 6 | // private properties are handled by this WeakMap 7 | const privates = new WeakMap; 8 | 9 | export default class Elevator extends SignalTarget { 10 | 11 | constructor() { 12 | super(); 13 | 14 | // setup the motor 15 | const motor = new BiggerMotor; 16 | motor.on('rotating', this); 17 | 18 | // the level is used as distance between 0 19 | // and the next floor to reach 20 | privates.set(this, {motor, level: 0, status: 0}); 21 | } 22 | 23 | // a read only property to know where is the elevator 24 | get status() { 25 | return privates.get(this).status; 26 | } 27 | 28 | // instead of open/close, an elevator 29 | // simply reaches a new level 30 | reach(level) { 31 | const info = privates.get(this); 32 | const rotation = level > info.level ? 1 : -1; 33 | info.level = level; 34 | info.motor.rotate(rotation); 35 | } 36 | 37 | // while moving, calculate when it's time to stop. 38 | // note: this is not how it actually works for real 39 | onrotating(event) { 40 | const info = privates.get(this); 41 | info.status += event.detail; 42 | // and whenever it reached the previous or next floor 43 | if ( 44 | (event.detail < 0 && info.status <= info.level) || 45 | (event.detail > 0 && info.status >= info.level) 46 | ) { 47 | // stop the motor 48 | info.motor.stop(); 49 | // reset status 50 | info.status = Math.round(info.status); 51 | // signal last precise movement 52 | this.signal('moving'); 53 | // and also signal the floor change 54 | this.signal('changed'); 55 | } else { 56 | // just signal that it's moving 57 | this.signal('moving'); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /hardware/LightButton.js: -------------------------------------------------------------------------------- 1 | import Button from './Button.js'; 2 | 3 | // a LightButton is exactly what it says: 4 | // a button with also a light connected to it 5 | export default class LightButton extends Button { 6 | 7 | // the light can be "on" or "off" 8 | static get ON() { return 'on'; } 9 | static get OFF() { return 'off'; } 10 | 11 | // the state is very simple: 12 | // either the button is on or off (default) 13 | constructor(symbol) { 14 | super(symbol).state = LightButton.OFF; 15 | } 16 | 17 | // the light switch is independent of the press signal 18 | // indeed the button can be switched on without pressing actions 19 | switch(state) { 20 | switch(state) { 21 | case LightButton.ON: 22 | case LightButton.OFF: 23 | // same state won't do anything 24 | if (this.state === state) return; 25 | // updated state will signal changes 26 | this.state = state; 27 | this.signal('light' + state); 28 | break; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /hardware/Motor.js: -------------------------------------------------------------------------------- 1 | import SignalTarget from '../software/SignalTarget.js'; 2 | 3 | // an elevator doors motor does literally 2 things: 4 | // it opens doors or it closes them. 5 | export default class Motor extends SignalTarget { 6 | constructor() { 7 | super(); 8 | // the rotating is used like a flag 9 | // based on the fact intervals are always 10 | // integers different from zero 11 | this.rotating = 0; 12 | // every motor has also different voltage, 13 | // power, or speed, which is in this demo 14 | // the most important for this motor to work. 15 | this.speed = 0.01; 16 | } 17 | 18 | // rotate is a public feature/API of the motor 19 | rotate(direction) { 20 | // it stops if it was moving already 21 | if (this.rotating) this.stop(); 22 | // and it starts rotating in a direction 23 | // which is either clockwise or anticlockwise 24 | this.rotating = setInterval( 25 | // per each rotation, a signal is sent 26 | rotateTheMotor, 27 | // 60 frames per seconds 28 | 1000/60, 29 | // to this motor 30 | this, 31 | // and with a direction higher or lower than 0 32 | direction < 0 ? -this.speed : +this.speed 33 | ); 34 | } 35 | 36 | // a motor should be able to stop at any given time 37 | stop() { 38 | clearInterval(this.rotating); 39 | this.rotating = 0; 40 | } 41 | } 42 | 43 | // and signaling *may* pass through the speed 44 | // even if it could be retrieved by the motor type itself 45 | function rotateTheMotor(motor, speed) { 46 | motor.signal('rotating', speed); 47 | } 48 | -------------------------------------------------------------------------------- /hardware/Panel.js: -------------------------------------------------------------------------------- 1 | import SignalTarget from '../software/SignalTarget.js'; 2 | 3 | // a panel is a convenient intermediate layer that could 4 | // group one or more buttons together and be mounted 5 | // either externally or internally the elevator 6 | export default class Panel extends SignalTarget { 7 | 8 | constructor(buttons) { 9 | super(); 10 | // each button signal will trigger the panel 11 | this.buttons = buttons.map(asPressEvent, this); 12 | } 13 | 14 | // delegate the press signal to whoever is listening 15 | onpress(event) { 16 | this.signal('press', event.currentTarget); 17 | } 18 | 19 | } 20 | 21 | // helper function to map every button 22 | function asPressEvent(button) { 23 | return button.on('press', this); 24 | } 25 | -------------------------------------------------------------------------------- /hardware/ProximitySensor.js: -------------------------------------------------------------------------------- 1 | import SignalTarget from '../software/SignalTarget.js'; 2 | 3 | // when active, a proximity sensor is capable of signaling 4 | // whenever someone, or something, is around 5 | export default class ProximitySensor extends SignalTarget { 6 | 7 | // a sensor can be either active or inactive (default) 8 | constructor() { super().active = false; } 9 | 10 | activate() { this.active = true; } 11 | deactivate() { this.active = false; } 12 | 13 | // once an object gets closer, the sensor signals its proximity 14 | // in the real world, a proximity sensor might send more data 15 | // like, as example, how far is the detected object. 16 | detect() { if (this.active) this.signal('proximity'); } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /software/Action.js: -------------------------------------------------------------------------------- 1 | // every button has a meaning 2 | const symbols = { 3 | ALARM: '🔔', 4 | CLOSE_DOORS: '▷│◁', 5 | OPEN_DOORS: '◁│▷', 6 | BASEMENT_FLOOR: 'S', 7 | GROUND_FLOOR: 'G', 8 | FIRST_FLOOR: '1', 9 | SECOND_FLOOR: '2', 10 | THIRD_FLOOR: '3' 11 | // ... and so on ... 12 | }; 13 | 14 | // and every meaning could be a floor 15 | const proto = { 16 | asFloor(symbol) { 17 | switch (symbol) { 18 | // floor simply cover a range, from 0 to X 19 | case symbols.BASEMENT_FLOOR: return 0; 20 | case symbols.GROUND_FLOOR: return 1; 21 | case symbols.FIRST_FLOOR: return 2; 22 | case symbols.SECOND_FLOOR: return 3; 23 | case symbols.THIRD_FLOOR: return 4; 24 | default: return -1; 25 | } 26 | }, 27 | asSymbol(floor) { 28 | for (const key in this) { 29 | if (this.asFloor(this[key]) === floor) { 30 | return this[key]; 31 | } 32 | } 33 | } 34 | }; 35 | 36 | export default Object.freeze(Object.setPrototypeOf(symbols, proto)); 37 | -------------------------------------------------------------------------------- /software/Controller.js: -------------------------------------------------------------------------------- 1 | import Action from './Action.js'; 2 | import SignalTarget from './SignalTarget.js'; 3 | 4 | import Doors from '../hardware/Doors.js'; 5 | import LightButton from '../hardware/LightButton.js'; 6 | 7 | // private properties are handled by this WeakMap 8 | const privates = new WeakMap; 9 | 10 | // trap utilities to avoid external 11 | // interferences/polyfills/pollution 12 | const {assign, freeze} = Object; 13 | 14 | // a controller is in charge of orchestrating 15 | // the elevator functionality as a whole. 16 | export default class Controller extends SignalTarget { 17 | 18 | constructor(elevator, doors, panels) { 19 | super(); 20 | 21 | // let's assume by contract doorsPanel 22 | // is mandatory at index 0 23 | const floor = Math.min.apply( 24 | Math, 25 | panels[0].buttons 26 | .filter(isFloor) 27 | .map(Action.asFloor) 28 | ); 29 | 30 | // store private variables 31 | privates.set(this, { 32 | doors, 33 | elevator, 34 | panels, 35 | queue: [], 36 | state: {floor, moving: false}, 37 | timer: 0 38 | }); 39 | 40 | // listen to all panels buttons 41 | panels.forEach(addPanelPress, this); 42 | 43 | // listen to doors too and propagate events 44 | doors 45 | .on('changed', this) 46 | .on('changed', propagate(this, 'doors')) 47 | .on('moving', propagate(this, 'doors')); 48 | 49 | // same goes for the elevator 50 | elevator 51 | .on('changed', this) 52 | .on('changed', propagate(this, 'elevator')) 53 | .on('moving', propagate(this, 'elevator')); 54 | 55 | } 56 | 57 | // a state is exposed as read-only (unique) object 58 | get state() { 59 | return freeze(assign({}, privates.get(this).state)); 60 | } 61 | 62 | // every event should eventually clear the timer 63 | // that scheduled doors closing and next action 64 | // this is why handleEvent is redefined, 65 | // to intercept all possible registered events. 66 | handleEvent(event) { 67 | const info = privates.get(this); 68 | 69 | // clear any waiting timer 70 | if (info.timer) { 71 | clearTimeout(info.timer); 72 | info.timer = 0; 73 | } 74 | 75 | // then analyze the action 76 | switch (event.type) { 77 | case 'press': 78 | this.onButtonPress(event); 79 | break; 80 | case 'changed': 81 | if (event.currentTarget === info.doors) { 82 | this.onDoorsChanged(event); 83 | } else { 84 | this.onElevatorChanged(event); 85 | } 86 | break; 87 | } 88 | } 89 | 90 | onButtonPress(event) { 91 | const button = event.detail; 92 | const info = privates.get(this); 93 | // if the button is about changing level/floor 94 | if (isFloor(button)) { 95 | // find out which one 96 | const floor = Action.asFloor(button.symbol); 97 | // be sure it's not already in the queue 98 | if (!info.queue.includes(floor)) { 99 | // in such case push it through 100 | info.queue.push(floor); 101 | // light eah button related to this floor on 102 | switchButton(info.panels, button.symbol, LightButton.ON); 103 | // verify doors state 104 | switch (info.doors.status) { 105 | // if opened, close them and let the event follow up 106 | case Doors.OPENED: 107 | info.doors.close(); 108 | break; 109 | // if the lift has closed doors 110 | case Doors.CLOSED: 111 | // and the elevator is not moving 112 | if (!info.state.moving) { 113 | // signal a doors change to trigger doors close logic 114 | info.doors.signal('changed'); 115 | } 116 | break; 117 | } 118 | } 119 | } else { 120 | // turn on the light for an instant 121 | if (button instanceof LightButton) { 122 | button.switch(LightButton.ON); 123 | setTimeout(() => button.switch(LightButton.OFF), 300); 124 | } 125 | // find out what to do 126 | switch (button.symbol) { 127 | case Action.ALARM: 128 | alert('ALARM ALARM'); 129 | break; 130 | // if it's about opening doors 131 | case Action.OPEN_DOORS: 132 | // in case these are already opened 133 | if (info.doors.status === Doors.OPENED) { 134 | // prepare for the next action, if any 135 | prepareNextAction(info); 136 | } 137 | // otherwise if the elevator is not moving 138 | else if (!info.state.moving) { 139 | // ask to open doors 140 | info.doors.open(); 141 | } 142 | break; 143 | // if it's about clsing doors 144 | case Action.CLOSE_DOORS: 145 | // just invoke it and let the rest 146 | // of the events flow 147 | info.doors.close(); 148 | break; 149 | } 150 | } 151 | } 152 | 153 | onDoorsChanged(event) { 154 | const doors = event.currentTarget; 155 | const info = privates.get(this); 156 | switch (doors.status) { 157 | case Doors.OPENED: 158 | prepareNextAction(info); 159 | break; 160 | case Doors.CLOSED: 161 | if (info.queue.length) { 162 | const level = info.queue[0]; 163 | if (level === Action.asFloor(info.state.floor.symbol)) { 164 | info.queue.shift(); 165 | info.doors.open(); 166 | } else { 167 | info.state.moving = true; 168 | info.elevator.reach(level); 169 | } 170 | } 171 | break; 172 | } 173 | } 174 | 175 | onElevatorChanged(event) { 176 | const info = privates.get(this); 177 | info.state.moving = false; 178 | info.state.floor = info.queue.shift(); 179 | switchButton( 180 | info.panels, 181 | Action.asSymbol(info.state.floor), 182 | LightButton.OFF 183 | ); 184 | info.doors.open(); 185 | } 186 | 187 | } 188 | 189 | function addPanelPress(panel) { 190 | // attach all panel events to this controller 191 | panel.on('press', this); 192 | } 193 | 194 | function isFloor(button) { 195 | // true if a button is associated to a floor 196 | return Action.asFloor(button.symbol) !== -1; 197 | } 198 | 199 | function prepareNextAction(info) { 200 | // if there is something to do 201 | if (info.queue.length) { 202 | // setup a timer to do it once doors are closed 203 | info.timer = setTimeout(() => info.doors.close(), 1000); 204 | } 205 | } 206 | 207 | function propagate(controller, prefix) { 208 | // re-signal events for UI sake. Pass parts around too 209 | return event => { 210 | controller.signal(`${prefix}:${event.type}`, event.currentTarget); 211 | }; 212 | } 213 | 214 | // find every button of every panel 215 | // that is related to a certain symbol 216 | // and switch its state on or off 217 | function switchButton(panels, symbol, state) { 218 | for (const panel of panels) { 219 | for (const button of panel.buttons) { 220 | if (button.symbol === symbol) { 221 | if (button instanceof LightButton) { 222 | button.switch(state); 223 | } 224 | } 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /software/EventTarget.js: -------------------------------------------------------------------------------- 1 | import EventTarget from 'https://unpkg.com/event-target@latest/esm/index.js'; 2 | 3 | // based on DOM specifications, 4 | // EventTarget can be used everywhere, 5 | // not just for browser related tasks. 6 | export default EventTarget; 7 | -------------------------------------------------------------------------------- /software/SignalTarget.js: -------------------------------------------------------------------------------- 1 | import EventTarget from './EventTarget.js'; 2 | 3 | // since we don't like to write long method names and 4 | // we also don't like to write new CustomEvent(type, {detail}) 5 | // every single time, we can lightly wrap the EventTarget 6 | // making more easy to deal with. 7 | export default class SignalTarget extends EventTarget { 8 | 9 | // a well known `obj.on(type, callback)` shortcut 10 | on(...args) { 11 | this.addEventListener(...args); 12 | return this; 13 | } 14 | 15 | // a way to handle events that does not need bindings all over 16 | // https://medium.com/@WebReflection/dom-handleevent-a-cross-platform-standard-since-year-2000-5bf17287fd38 17 | handleEvent(event) { this[`on${event.type}`](event); } 18 | 19 | // and a mechanical "signal" to notify any hardware 20 | // that is listening to the current instance. 21 | signal(signal, detail) { 22 | this.dispatchEvent(new CustomEvent(signal, {detail})); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /the-doors/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A JavaScript Elevator: The Doors 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /the-doors/the-doors.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | overflow: hidden; 6 | } 7 | 8 | body { 9 | width: 100vw; 10 | height: 100vh; 11 | background: #111; 12 | text-align: center; 13 | } 14 | 15 | body > * { 16 | position: absolute; 17 | } 18 | 19 | .sensor { 20 | top: 0; 21 | left: 50%; 22 | width: 20vh; 23 | height: 100vh; 24 | margin-left: -10vh; 25 | } 26 | 27 | progress { 28 | top: 4px; 29 | left: 50%; 30 | width: 80px; 31 | height: 8px; 32 | margin-left: -40px; 33 | } 34 | 35 | .panel { 36 | top: 30%; 37 | right: 25%; 38 | } 39 | 40 | .panel button { 41 | outline: none; 42 | display: block; 43 | margin: 50% auto; 44 | font-size: 1.2em; 45 | height: 2.4em; 46 | border-radius: 1em; 47 | border: 2px solid #000; 48 | background-color: #fff; 49 | } 50 | 51 | .left, .right { 52 | top: 0; 53 | width: 50%; 54 | height: 100%; 55 | background: #333; 56 | } 57 | 58 | .left { 59 | left: 0; 60 | } 61 | 62 | .right { 63 | right: 0; 64 | } -------------------------------------------------------------------------------- /the-doors/the-doors.js: -------------------------------------------------------------------------------- 1 | import {render, html} from 'https://unpkg.com/uhtml?module'; 2 | import Doors from '../hardware/Doors.js'; 3 | import {leak} from '../hacks/leakySensor.js'; 4 | import Button from '../hardware/Button.js'; 5 | 6 | document.addEventListener( 7 | 'DOMContentLoaded', 8 | () => { 9 | // there are two doors 10 | const doors = new Doors; 11 | // (with a leaky sensor !!!) 12 | const sensor = leak; 13 | // that communicates when it detects movements 14 | sensor.on('proximity', () => console.log('movement detected')); 15 | 16 | // doors are controlled by two buttons 17 | const opener = new Button('⇤⇥'); 18 | const closer = new Button('⇥⇤'); 19 | 20 | // each button does one thing: 21 | // it sends a signal when you press it 22 | opener.on('press', openDoors); 23 | closer.on('press', closeDoors); 24 | 25 | // whenever doors finish opening or closing 26 | // inform the used about the status 27 | doors.on('changed', () => { 28 | switch (doors.status) { 29 | case Doors.OPENED: 30 | console.log('doors opened'); 31 | break; 32 | case Doors.CLOSED: 33 | console.log('doors closed'); 34 | break; 35 | } 36 | }); 37 | 38 | // while doors are opening or closing 39 | // update any visual indicator (i.e. progress) 40 | doors.on('moving', update); 41 | update(); 42 | 43 | // to update the view, simply render into the body 44 | function update() { 45 | render(document.body, html` 46 |
47 |
48 |
49 | 50 |
51 | 52 | 53 |
54 | `); 55 | } 56 | 57 | // when DOM buttons are clicked, mechanic buttons are pressed 58 | function pressOpener() { opener.press(); } 59 | function pressCloser() { closer.press(); } 60 | 61 | // doors can either open or close 62 | function openDoors() { doors.open(); } 63 | function closeDoors() { 64 | doors.close(); 65 | // easter egg: "the fly" 66 | // ~10% of the times doors 67 | // will re-open again by themselves 68 | // through the leaky sensor 69 | if (Math.random() < .1) { 70 | setTimeout(() => { 71 | console.log('a fly passed by'); 72 | detect(); 73 | }, 500); 74 | } 75 | } 76 | 77 | // simulating the (leaky) proximity sensor detection 78 | function detect() { 79 | sensor.detect(); 80 | } 81 | }, 82 | // setup on DOMContentLoaded only once 83 | // if the event gets dispatched again nothing will happen 84 | // a safer elevator for modern browsers 85 | {once: true} 86 | ); -------------------------------------------------------------------------------- /the-elevator/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A JavaScript Elevator: The Elevator 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /the-elevator/the-elevator.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | background: #000; 5 | overflow: hidden; 6 | font-family: sans-serif; 7 | } 8 | 9 | body:before { 10 | content: ' '; 11 | position: absolute; 12 | bottom: 0; 13 | height: 20%; 14 | width: 100%; 15 | background: #222; 16 | z-index: 1; 17 | } 18 | 19 | .light-button { 20 | width: 40px; 21 | height: 40px; 22 | padding: 0; 23 | text-align: center; 24 | outline: none; 25 | display: block; 26 | font-size: 1.2em; 27 | font-weight: bold; 28 | border-radius: 1em; 29 | border: 2px solid #333; 30 | color: #333; 31 | background-color: #666; 32 | z-index: 3; 33 | } 34 | 35 | .light-button.on { 36 | color: #00F; 37 | border-color: #00F; 38 | } 39 | 40 | .external .light-button { 41 | margin-top: 3%; 42 | margin-left: 2%; 43 | } 44 | 45 | .internal { 46 | position: absolute; 47 | top: 0; 48 | right: 0; 49 | } 50 | 51 | .internal .light-button { 52 | margin-top: 10%; 53 | margin-right: 4px; 54 | float: left; 55 | } 56 | 57 | .internal .light-button:nth-child(3), 58 | .internal .light-button:nth-child(5), 59 | .internal .light-button:nth-child(6), 60 | .internal .light-button:nth-child(8) { 61 | clear: left; 62 | } 63 | 64 | .internal .light-button:nth-child(5), 65 | .internal .light-button:nth-child(8) { 66 | margin-left: 24px; 67 | } 68 | 69 | .internal .light-button:nth-child(6), 70 | .internal .light-button:nth-child(7) { 71 | font-size: 8px; 72 | 73 | } 74 | 75 | .elevator:after { 76 | content: ' '; 77 | display: block; 78 | position: absolute; 79 | height: 100vh; 80 | top: -100vh; 81 | left: 50%; 82 | margin-left: -4px; 83 | width: 8px; 84 | background: #222; 85 | } 86 | .elevator:before { 87 | position: absolute; 88 | display: block; 89 | content: attr(data-floor); 90 | width: 100%; 91 | height: 100%; 92 | color: #000; 93 | text-align: center; 94 | font-weight: bold; 95 | font-size: 3em; 96 | } 97 | .elevator { 98 | position: absolute; 99 | bottom: 0; 100 | left: 50%; 101 | width: 8%; 102 | height: 20%; 103 | min-width: 60px; 104 | margin-left: -30px; 105 | background-color: #333; 106 | z-index: 2; 107 | } 108 | 109 | .elevator .left, 110 | .elevator .right { 111 | content: ' '; 112 | position: absolute; 113 | display: block; 114 | width: 50%; 115 | height: 100%; 116 | background-color: #666; 117 | } 118 | 119 | .elevator .left { 120 | left: 0; 121 | } 122 | .elevator .right { 123 | right: 0; 124 | } -------------------------------------------------------------------------------- /the-elevator/the-elevator.js: -------------------------------------------------------------------------------- 1 | import {render, html} from 'https://unpkg.com/uhtml?module'; 2 | 3 | // software 4 | import Action from '../software/Action.js'; 5 | import Controller from '../software/Controller.js'; 6 | 7 | // hardware 8 | import LightButton from '../hardware/LightButton.js'; 9 | import Doors from '../hardware/Doors.js'; 10 | import Elevator from '../hardware/Elevator.js'; 11 | import Panel from '../hardware/Panel.js'; 12 | 13 | document.addEventListener( 14 | 'DOMContentLoaded', 15 | () => { 16 | 17 | // the controller handles doors 18 | const doors = new Doors; 19 | 20 | // an internal panel (inside the elevator) 21 | const internalPanel = new Panel([ 22 | new LightButton(Action.THIRD_FLOOR), 23 | new LightButton(Action.SECOND_FLOOR), 24 | new LightButton(Action.FIRST_FLOOR), 25 | new LightButton(Action.GROUND_FLOOR), 26 | new LightButton(Action.BASEMENT_FLOOR), 27 | new LightButton(Action.OPEN_DOORS), new LightButton(Action.CLOSE_DOORS), 28 | new LightButton(Action.ALARM) 29 | ]); 30 | 31 | // as well as every other external panel 32 | const panels = [ 33 | // assuming by contract internalPanel 34 | // is mandatory at index 0 (to simplify the demo) 35 | internalPanel, 36 | // then we have at least a panel per floor 37 | new Panel([new LightButton(Action.THIRD_FLOOR)]), 38 | new Panel([new LightButton(Action.SECOND_FLOOR)]), 39 | new Panel([new LightButton(Action.FIRST_FLOOR)]), 40 | new Panel([new LightButton(Action.GROUND_FLOOR)]), 41 | new Panel([new LightButton(Action.BASEMENT_FLOOR)]) 42 | ]; 43 | 44 | // the controller handles an elevator too 45 | const elevator = new Elevator; 46 | 47 | // all together 48 | const controller = new Controller(elevator, doors, panels); 49 | 50 | // render the scenario: 51 | // on the left, panels per each floor 52 | // in the middle, the elevator 53 | // on the right, the internal panel 54 | render(document.body, html` 55 |
56 | ${panels.slice(1).map(createPanel)} 57 |
58 |
59 |
60 |
61 |
62 |
63 | ${createPanel(panels[0])} 64 |
65 | `); 66 | 67 | // let's setup the UI for demo purpose 68 | const elevatorUI = document.body.querySelector('.elevator'); 69 | 70 | // setup the elevator movement 71 | controller 72 | .on('elevator:moving', event => { 73 | // event.detail.status goes from 0 (basement) to top building floor 74 | const bottom = innerHeight * event.detail.status / (panels.length - 1); 75 | elevatorUI.style.bottom = `${bottom}px`; 76 | }) 77 | .on('elevator:changed', event => { 78 | // update the internal panel with the current floor 79 | elevatorUI.dataset.floor = 80 | Action.asSymbol(event.currentTarget.state.floor); 81 | }); 82 | 83 | // setup doors movement 84 | setupDoors(controller, { 85 | left: elevatorUI.querySelector('.left'), 86 | right: elevatorUI.querySelector('.right') 87 | }); 88 | 89 | // it's all setup 🎉 let's move to the ground floor 90 | internalPanel.buttons[3].press(); 91 | 92 | // define a button that reacts through the hardware 93 | // changing class per each light switch 94 | function createButton(button) { 95 | const press = () => button.press(); 96 | const update = lightClass => html.for(button)` 97 | 103 | `; 104 | button.on('lighton', () => update('on')); 105 | button.on('lightoff', () => update('off')); 106 | return update('off'); 107 | } 108 | 109 | // define a panel with a list of one or more buttons 110 | function createPanel(panel) { 111 | return html.for(panel)` 112 |
${ 113 | panel.buttons.map(createButton) 114 | }
115 | `; 116 | } 117 | 118 | // setup doors, opening and closing together 119 | function setupDoors(controller, doors) { 120 | controller.on('doors:moving', event => { 121 | // event.detail.status goes from 0 to 1, usable as percentage 122 | const position = -(doors.left.offsetWidth * event.detail.status); 123 | doors.left.style.left = `${position}px`; 124 | doors.right.style.right = `${position}px`; 125 | }); 126 | } 127 | }, 128 | 129 | // setup on DOMContentLoaded only once 130 | // if the event gets dispatched again nothing will happen 131 | // a safer elevator for modern browsers 132 | {once: true} 133 | ); -------------------------------------------------------------------------------- /the-light-button/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A JavaScript Elevator: The Light Button 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /the-light-button/the-light-button.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | overflow: hidden; 6 | } 7 | 8 | body { 9 | width: 100vw; 10 | height: 100vh; 11 | background: #333; 12 | } 13 | 14 | .light-button { 15 | padding: 0; 16 | text-align: center; 17 | position: absolute; 18 | top: 50%; 19 | left: 50%; 20 | outline: none; 21 | display: block; 22 | font-size: 1.2em; 23 | width: 2.4em; 24 | height: 2.4em; 25 | margin-top: -1.2em; 26 | margin-left: -1.2em; 27 | border-radius: 1em; 28 | border: 2px solid #000; 29 | color: #555; 30 | background-color: #fff; 31 | } 32 | 33 | .light-button.on { 34 | color: #000; 35 | background-color: #ffa; 36 | } 37 | -------------------------------------------------------------------------------- /the-light-button/the-light-button.js: -------------------------------------------------------------------------------- 1 | import {render, html} from 'https://unpkg.com/uhtml?module'; 2 | import LightButton from '../hardware/LightButton.js'; 3 | 4 | document.addEventListener( 5 | 'DOMContentLoaded', 6 | () => { 7 | 8 | // there is one button 9 | const button = new LightButton('G'); 10 | 11 | // whenever it switches on or off, it sends a signal 12 | button.on('lighton', () => update('on')); 13 | button.on('lightoff', () => update('off')); 14 | 15 | // by default, the button has the light switched off 16 | update('off'); 17 | 18 | function update(lightClass) { 19 | render(document.body, html` 20 | 24 | `); 25 | } 26 | 27 | // this function is in charge of switching 28 | // the button light on or off 29 | function toggleLight() { 30 | switch (button.state) { 31 | case LightButton.ON: 32 | button.switch(LightButton.OFF); 33 | console.log('light off'); 34 | break; 35 | case LightButton.OFF: 36 | button.switch(LightButton.ON); 37 | console.log('light on'); 38 | break; 39 | } 40 | } 41 | }, 42 | // setup on DOMContentLoaded only once 43 | // if the event gets dispatched again nothing will happen 44 | // a safer elevator for modern browsers 45 | {once: true} 46 | ); --------------------------------------------------------------------------------