Skip to content

Instantly share code, notes, and snippets.

@rcombs

rcombs/stdin Secret

Created Apr 1, 2021
Embed
What would you like to do?
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ 79:
/***/ ((module) => {
module.exports = {
name: 'Beep',
alt: 'Check check check ... chhecck chhecck chhecck ... check check check',
url: '/2445',
width: 740,
height: 448,
apiEndpoint: '/2445/morse',
}
/***/ }),
/***/ 369:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const comic_1 = __webpack_require__(79);
const SEND_DELAY = 3000;
const DOT_LENGTH = 150;
const HUD_DELAY = 3000;
const IDLE_DELAY = 30 * 1000;
function clickTimesToMorse(clickTimes) {
return clickTimes
.map((t) => {
if (t < 0) {
return Math.abs(t) > DOT_LENGTH * 3 ? ' ' : '';
}
else {
return t > DOT_LENGTH * 3 ? '-' : '.';
}
})
.join('')
.replace(/^ /, ''); // Strip a preceding space
}
class Client {
escape(t) {
return t.replace(/ /g, '_');
}
say(morse) {
return __awaiter(this, void 0, void 0, function* () {
const url = [
comic_1.apiEndpoint,
'/.../',
this.stateId ? this.escape(this.stateId) + '/' : '',
this.escape(morse),
].join('');
const resp = yield fetch(url);
if (!resp.ok) {
throw new Error(`Unexpected response ${resp}`);
}
const data = yield resp.text();
const [state, ...respMorse] = data.split(' / ');
this.stateId = state.trim();
return respMorse.join(' / ').trim();
});
}
open(key) {
const morseKey = window.morse.encode(key);
window.open(`${comic_1.apiEndpoint}/.-./${this.escape(morseKey)}`);
}
}
class Speaker {
constructor(parentEl) {
this.TARGET_GAIN = 0.2;
this.isEnabled = false;
this.ctx = null;
this.gainNode = null;
this.parentEl = parentEl;
this.el = document.createElement('button');
this.el.setAttribute('style', `
position: absolute;
bottom: 6px;
right: 6px;
background: none;
border: none;
font-size: 20px;
line-height: 20px;
text-align: center;
width: 30px;
height: 30px;
padding: 2px;
box-sizing: content-box;
opacity: 0;
filter: grayscale(1);
transition: all 5s linear;
`);
this.el.addEventListener('click', () => {
if (this.isEnabled) {
this.disable();
}
else {
this.enable();
}
});
this.parentEl.appendChild(this.el);
this.update();
}
show() {
this.el.style.opacity = '.2';
}
update() {
this.el.innerText = this.isEnabled ? '🔊' : '🔇';
this.el.title = this.isEnabled ? 'Mute sound' : 'Enable sound';
}
enable() {
this.isEnabled = true;
this.update();
}
disable() {
this.isEnabled = false;
this.off();
this.update();
}
on() {
if (!this.isEnabled) {
return;
}
if (!this.ctx) {
this.ctx = new AudioContext();
const oscNode = this.ctx.createOscillator();
this.gainNode = this.ctx.createGain();
oscNode.type = 'sine';
oscNode.frequency.value = 440;
oscNode.connect(this.gainNode);
this.gainNode.gain.value = 0;
this.gainNode.connect(this.ctx.destination);
oscNode.start();
}
this.gainNode.gain.cancelScheduledValues(this.ctx.currentTime);
this.gainNode.gain.linearRampToValueAtTime(this.TARGET_GAIN, this.ctx.currentTime + 0.01);
}
off() {
if (!this.ctx) {
return;
}
this.gainNode.gain.linearRampToValueAtTime(0, this.ctx.currentTime + DOT_LENGTH / 1000 / 2);
}
}
class MorseHUD {
constructor(parentEl) {
this.HUD_LENGTH = 6;
this.HUD_CHAR_WIDTH = 50;
this.HUD_CHAR_SIZE = 7;
this.HUD_ANIM_DURATION = DOT_LENGTH;
this.HUD_FAST_ANIM_DURATION = DOT_LENGTH / 2;
this.parentEl = parentEl;
this.el = document.createElement('div');
this.el.setAttribute('style', `
position: absolute;
bottom: 50px;
width: ${this.HUD_LENGTH * this.HUD_CHAR_WIDTH}px;
height: ${this.HUD_CHAR_WIDTH}px;
left: 50%;
opacity: 0;
transform: translateX(-50%);
transition: all 5s linear;
pointer-events: none;
`);
this.parentEl.appendChild(this.el);
this.lastMorse = '';
this.elStack = [];
this.shown = false;
}
show() {
this.el.style.opacity = '1';
}
update(morse) {
const lastMorse = this.lastMorse;
this.lastMorse = morse;
if (morse.length === 0) {
const oldStack = this.elStack;
this.elStack = [];
for (const el of oldStack) {
el.style.opacity = '0';
el.style.transitionDuration = `${this.HUD_ANIM_DURATION * 2}ms`;
}
setTimeout(() => {
for (const el of oldStack) {
el.parentElement.removeChild(el);
}
}, this.HUD_ANIM_DURATION);
return;
}
const newCount = morse.length - lastMorse.length;
for (let i = 0; i < newCount; i++) {
const newBoxEl = document.createElement('div');
newBoxEl.setAttribute('style', `
position: absolute;
right: 0;
display: flex;
align-items: center;
justify-content: center;
width: ${this.HUD_CHAR_WIDTH}px;
opacity: 0;
transition: all ${this.HUD_ANIM_DURATION}ms ease-out;
transform: translateY(7px);
`);
const newCharEl = document.createElement('div');
newCharEl.setAttribute('style', `
background: #999;
transition: all ${this.HUD_FAST_ANIM_DURATION}ms ease-out;
height: ${this.HUD_CHAR_SIZE}px;
`);
newBoxEl.appendChild(newCharEl);
this.el.appendChild(newBoxEl);
this.el.offsetTop; // Force layout to set transition start values
this.elStack.unshift(newBoxEl);
}
for (const [idx, boxEl] of this.elStack.entries()) {
const char = morse[morse.length - 1 - idx];
const charEl = boxEl.children[0];
if (char === '.') {
charEl.style.width = `${this.HUD_CHAR_SIZE}px`;
charEl.style.borderRadius = `${this.HUD_CHAR_SIZE}px`;
charEl.style.opacity = '1';
}
else if (char === '-') {
charEl.style.width = `${Math.floor(3.5 * this.HUD_CHAR_SIZE)}px`;
charEl.style.borderRadius = '2px';
charEl.style.opacity = '1';
}
else {
charEl.style.opacity = '0';
}
boxEl.style.opacity = '1';
const x = -idx * this.HUD_CHAR_WIDTH;
boxEl.style.transform = `translateX(${Math.floor(x)}px)`;
}
while (this.elStack.length > this.HUD_LENGTH) {
const oldBoxEl = this.elStack.pop();
oldBoxEl.style.opacity = '0';
setTimeout(() => {
this.el.removeChild(oldBoxEl);
}, this.HUD_ANIM_DURATION);
}
}
}
class Comic {
constructor(el) {
this.el = el;
this.hud = new MorseHUD(el);
this.speaker = new Speaker(this.el);
this.isHUDShown = false;
this.hasInteracted = false;
this.sendTimeout = null;
this.updateTimeout = null;
this.client = new Client();
this.clickTimes = [];
this.playbackDelay = 250;
}
start() {
const { el } = this;
const inputEl = el.querySelector('input');
const labelEl = el.querySelector('label');
let keyHeld = false;
this.lastOff = Date.now();
this.lastOn = 0;
const setOn = (isOn) => {
inputEl.checked = isOn;
if (isOn) {
this.lastOn = Date.now();
this.speaker.on();
}
else {
this.lastOff = Date.now();
this.speaker.off();
}
this.clickTimes.push(this.lastOff - this.lastOn);
};
const handleDown = (ev) => {
ev.preventDefault();
clearTimeout(this.playTimeout);
clearTimeout(this.sendTimeout);
setOn(true);
this.hasInteracted = true;
this.showHUD();
this.updateHUD();
};
const handleUp = (ev) => {
// Since this is hooked up to global event handlers, we must ensure the checkbox is actually being held.
if (this.lastOn < this.lastOff) {
return;
}
ev.preventDefault();
setOn(false);
this.updateHUD();
this.interpretClicks();
inputEl.focus();
};
labelEl.addEventListener('mousedown', handleDown);
labelEl.addEventListener('touchstart', handleDown);
labelEl.addEventListener('keydown', (ev) => {
if (!this.el.contains(ev.target)) {
return;
}
if (!keyHeld && (ev.key === ' ' || ev.key === 'Enter')) {
keyHeld = true;
handleDown(ev);
}
});
window.addEventListener('mouseup', handleUp);
window.addEventListener('touchend', handleUp);
window.addEventListener('keyup', (ev) => {
if (ev.key === ' ' || ev.key === 'Enter') {
keyHeld = false;
handleUp(ev);
}
});
labelEl.addEventListener('click', (ev) => {
ev.preventDefault();
});
this.printIntro();
setTimeout(() => {
if (!this.hasInteracted) {
this.playback(window.morse.encode('CQ'));
}
}, IDLE_DELAY);
}
printIntro() {
const lines = [];
lines.push(' MORSE CODE ');
lines.push('------------');
for (const [char, code] of window.morse.table) {
lines.push(` ${char} [${code}]`);
}
console.log(lines.join('\n'));
}
showHUD() {
if (!this.isHUDShown) {
this.isHUDShown = true;
setTimeout(() => {
this.hud.show();
}, HUD_DELAY);
setTimeout(() => {
this.speaker.show();
}, HUD_DELAY * 2);
}
}
updateHUD() {
// Display as if the user took a final action now.
let clickTimes;
const now = Date.now();
if (this.lastOn > this.lastOff) {
clickTimes = [...this.clickTimes, now - this.lastOn + 1];
}
else {
clickTimes = [...this.clickTimes, this.lastOff - now - 1];
}
let morse = clickTimesToMorse(clickTimes);
if (this.lastOff > this.lastOn) {
// Add a blank space for the next character if not holding down.
morse += ' ';
}
this.hud.update(morse);
clearTimeout(this.updateTimeout);
this.updateTimeout = window.setTimeout(() => {
this.updateHUD();
}, DOT_LENGTH);
}
interpretClicks() {
const morse = clickTimesToMorse(this.clickTimes);
clearTimeout(this.sendTimeout);
this.sendTimeout = window.setTimeout(() => {
this.send(morse);
}, this.impatient ? DOT_LENGTH * 7 : SEND_DELAY);
}
send(morse) {
return __awaiter(this, void 0, void 0, function* () {
this.hasInteracted = true;
const newClickTimes = [];
this.clickTimes = newClickTimes;
this.hud.update('');
const text = window.morse.decode(morse);
console.log(`Said: [${morse}] "${text}"`);
this.handleCommand(text);
const responseMorse = yield this.client.say(morse);
if (this.clickTimes != newClickTimes || this.clickTimes.length) {
// If the user has input anything since, disregard this response.
return;
}
this.playback(responseMorse);
});
}
playback(morse) {
const text = window.morse.decode(morse);
if (this.impatient) {
console.log(`Received: [${window.morse.encode(text)}] "${text}"`);
}
this.handleAction(text);
const inputEl = this.el.querySelector('input');
const delays = [];
for (const c of morse) {
if (c === '.') {
delays.push([true, this.playbackDelay]);
delays.push([false, this.playbackDelay]);
}
else if (c === '-') {
delays.push([true, this.playbackDelay * 3]);
delays.push([false, this.playbackDelay]);
}
else if (c === ' ') {
delays.push([false, this.playbackDelay * 3]);
}
}
delays.push([false, this.playbackDelay * 7]);
let idx = 0;
let hasPrinted = false;
const tick = () => {
const [isOn, delay] = delays[idx];
inputEl.checked = isOn;
if (isOn) {
this.speaker.on();
}
else {
this.speaker.off();
}
if (!this.impatient && idx === delays.length - 1 && !hasPrinted) {
console.log(`Received: [${window.morse.encode(text)}] "${text}"`);
hasPrinted = true;
}
idx = (idx + 1) % delays.length;
clearTimeout(this.playTimeout);
this.playTimeout = window.setTimeout(tick, delay);
};
tick();
}
handleCommand(text) {
if (text === 'BEEP') {
this.speaker.enable();
}
else if (text === 'MUTE' || text === 'QUIET') {
this.speaker.disable();
}
else if (text === 'QRS') {
this.playbackDelay = Math.min(500, this.playbackDelay * 1.2);
}
else if (text === 'QRQ') {
this.playbackDelay = Math.max(50, this.playbackDelay * (1 / 1.2));
}
}
handleAction(text) {
if (text.startsWith('//')) {
this.client.open(text.substr(2));
}
}
}
exports.default = Comic;
/***/ }),
/***/ 607:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.BeepComicGlobal = void 0;
const comic_1 = __importDefault(__webpack_require__(79));
const Comic_1 = __importDefault(__webpack_require__(369));
class BeepComicGlobal {
constructor() {
this.comicEl = null;
}
draw(comicEl) {
comicEl.setAttribute('style', `
position: relative;
display: inline-flex;
width: ${comic_1.default.width}px;
height: ${comic_1.default.height}px;
box-sizing: border-box;
border: 2px solid black;
`);
comicEl.innerHTML = `
<label style="display: flex; width: 100%; height: 100%; align-items: center; justify-content: center">
<input type="checkbox" style="outline: none">
</label>
`;
comicEl.title = comic_1.default.alt;
this.comicEl = comicEl;
}
}
exports.BeepComicGlobal = BeepComicGlobal;
function main() {
const { comicEl } = window.BeepComic;
const comic = new Comic_1.default(comicEl);
comic.start();
window.BeepComic.send = (morse) => {
comic.send(morse);
};
window.BeepComic.hurryUp = () => {
comic.impatient = true;
return "Ok fine. But you're like, totally breaking the immersion.";
};
}
document.addEventListener('DOMContentLoaded', main);
window.BeepComic = new BeepComicGlobal();
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module is referenced by other modules so it can't be inlined
/******/ var __webpack_exports__ = __webpack_require__(607);
/******/
/******/ })()
;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment