Utils
Tabletop = require('tabletop')
function googleSpreadsheet(id) { return new Promise((callback, reject) => { Tabletop.init({ key: "https://docs.google.com/spreadsheets/d/" + id + "/edit", callback, simpleSheet: false, postProcess: row => { for (let key in row) { const value = row[key]; if (typeof value == "string" && value.indexOf(",") != -1) { const f = parseFloat( value .replace(",", ".") .replace("€", "") .replace("\u00A0", "") // non-breaking space ); if (!isNaN(f)) row[key] = f; } } } }); }); }
storedValue = function(name, options = {}) { const storage = window[(options.session ? "session" : "local") + "Storage"]; const form = html`<form>`; const type = options.hidden ? "password" : "text"; const input = html`<input type="${type}">`; input.style.marginRight = "5px"; form.appendChild(input); const submit = html`<input type="submit">`; submit.value = options.submit || "Save"; submit.style.backgroundColor = "#ccc"; submit.style.margin = "0"; submit.style.padding = "2px 10px"; submit.style.borderRadius = "0 5px 5px 0"; input.style.font = submit.style.font = "17px/20px -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica,helvetica neue,ubuntu,roboto,noto,segoe ui,arial,sans-serif"; form.appendChild(submit); const value = storage.getItem(name); if (value !== null) { form.value = input.value = value; } submit.addEventListener("click", e => { const value = input.value; storage.setItem(name, value); form.value = value; form.dispatchEvent(new CustomEvent("input")); e.preventDefault(); }); return form; }
corsFetch = url => fetch(`https://cors-anywhere.herokuapp.com/${url}`)
promisify = function(fn, ...args) { return new Promise((resolve, reject) => { fn(...args, function(err, data) { if (err) reject(err); resolve(data); }); }); }
classDiagram = { const yumlDiagram = await require("https://bundle.run/yuml-diagram@1.0.8"); return yuml => { const svgDoc = new yumlDiagram().processYumlDocument( `// {type:class}\n${yuml}` ); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(svgDoc, "text/xml"); return svg`${xmlDoc.getElementsByTagName("svg")[0].outerHTML}`; }; }
Logger = () => { const container = DOM.element("div"); const ta = DOM.element("textarea"); Object.assign(ta.style, { width: "100%", height: "200px", fontFamily: "monospace", fontSize: "13px" }); ta.readOnly = true; container.appendChild(ta); Object.assign(container.style, { width: width + "px" }); container.value = { log: txt => { ta.value += txt + "\n"; }, reset: () => { ta.value = ""; } }; return container; }
createImageBitmap = async function(blob) { return new Promise((resolve, reject) => { let img = document.createElement("img"); img.addEventListener("load", function() { resolve(this); }); img.src = URL.createObjectURL(blob); }); }
getImageData = async function(blob) { const bitmap = await createImageBitmap(blob); const [width, height] = [bitmap.width, bitmap.height]; // an intermediate "buffer" 2D context is necessary const ctx = DOM.context2d(width, height, 1); ctx.drawImage(bitmap, 0, 0); return ctx.getImageData(0, 0, width, height); }
decodeImage = (imageData, cb, [stepX, stepY] = [1, 1]) => { const { width, height, data } = imageData; const stepIx = stepX * 4; const stepIy = (stepY - 1) * 4; for (let index = 0, j = 0; j < height; j += stepY, index += stepIy) for (let i = 0; i < width; i += stepX, index += stepIx) cb([i, j], data.slice(index, index + 3)); }
resizeImageData = (imageData, sx, sy, sWidth, sHeight, dWidth, dHeight) => { const scontext = DOM.context2d(sWidth + sx, sHeight + sy, 1); scontext.putImageData(imageData, 0, 0, sx, sy, sWidth + sx, sHeight + sy); const context = DOM.context2d(dWidth, dHeight, 1); context.drawImage( scontext.canvas, sx, sy, sWidth, sHeight, 0, 0, dWidth, dHeight ); return context.getImageData(0, 0, dWidth, dHeight); }
canvas2D = function(data, size, [min, max] = [0, 255], palette) { const I = new ImageData(size, size); const D = I.data; const N = data.length; const factor1 = N / size; const factor2 = 255 / (max - min); let value, rgb; const isRgb = Array.isArray(data[0][0]); for (let y = size - 1, index = 0; y >= 0; y--) { for (let x = 0; x < size; x++, index += 4) { value = data[(x * factor1) | 0][(y * factor1) | 0]; if (isRgb) { rgb = value; } else { value = ((value - min) * factor2) | 0; rgb = palette ? palette[value] : [value, value, value]; } D.set([...rgb, 255], index); } } return I; }
viewofCanvas = function(W, H) { const context = DOM.context2d(W, H, 1); const canvas = context.canvas; context.fillStyle = "hsl(0,0%,80%)"; context.fillRect(0, 0, W, H); return { context, canvas, dispatch: function(value) { canvas.value = value; canvas.dispatchEvent(new CustomEvent("input")); } }; }
zoomAndPan = function(element, cb) { element.addEventListener("wheel", e => { cb(0, 0, e.deltaY); e.preventDefault(); }); element.addEventListener("mousedown", e => { let [x0, y0] = [e.layerX, e.layerY]; const mm = e => { const [x, y] = [e.layerX, e.layerY]; cb(x - x0, y - y0, 0); [x0, y0] = [x, y]; }; const mu = e => { element.removeEventListener("mousemove", mm); element.removeEventListener("mouseup", mu); element.removeEventListener("mouseleave", mu); }; element.addEventListener("mousemove", mm); element.addEventListener("mouseup", mu); element.addEventListener("mouseleave", mu); e.preventDefault(); }); }
image2canvas = (imageData, width) => { const context = DOM.context2d(imageData.width, imageData.height, 1); context.putImageData(imageData, 0, 0); if (!width || imageData.width <= width) return context.canvas; const height = (imageData.height * width) / imageData.width; const contextr = DOM.context2d(width, height, 1); contextr.drawImage(context.canvas, 0, 0, width, height); return contextr.canvas; }
rgb2hsl = ([r, g, b]) => { const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h, s, l = (max + min) / 2; if (max == min) { h = s = 0; // achromatic } else { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); if (max == r) h = (g - b) / d + (g < b ? 6 : 0); else if (max == g) h = (b - r) / d + 2; else h = (r - g) / d + 4; h /= 6; } return [h, s, l]; }
rgb2hsv = ([r, g, b]) => { const [min, max] = [Math.min(r, g, b), Math.max(r, g, b)]; let h, s, v = max; const diff = max - min; const diffc = c => (max - c) / 6 / diff + 0.5; if (diff == 0) { h = s = 0; } else { s = diff / max; let [rr, gg, bb] = [diffc(r), diffc(g), diffc(b)]; if (r === max) h = bb - gg; else if (g === max) h = 1 / 3 + rr - bb; else if (b === max) h = 2 / 3 + gg - rr; if (h < 0) h += 1; else if (h > 1) h -= 1; } return [h, s, v]; }
colorGradient = ([r, g, b], nb) => { const [rf, gf, bf] = [r / nb, g / nb, b / nb]; return [...Array(nb)] .map((_, i) => [(i * rf) | 0, (i * gf) | 0, (i * bf) | 0]); }
interpolateRGB = ([r1, g1, b1], [r2, g2, b2], r) => [ (r1 + r * (r2 - r1)) | 0, (g1 + r * (g2 - g1)) | 0, (b1 + r * (b2 - b1)) | 0 ]
interpolatePalette = (palette, color, r) => palette.map(c => interpolateRGB(c, color, r))
separator = function() { return html`<div style="height: 16px">` }
floor = function(n, dec) { const pow = 10**dec; return Math.floor(n*pow)/pow; }
minstRand0 = function(seed) { var rndval = seed; const mod = 2**31-1; return function() { rndval = rndval * 16807 % mod; return (rndval - 1) / (mod - 1); } }
Range = bounds => { let [min, max] = bounds || [+Infinity, -Infinity]; return { bounds: () => [min, max], expand: value => { [min, max] = [Math.min(min, value), Math.max(max, value)]; return this; }, constrain: value => Math.max(min, Math.min(max, value)) }; }
MultiRange = dimensions => { const ranges = [...Array(dimensions)].map(_ => Range()); const r = { bounds: () => ranges.map(r => r.bounds()), expand: (...points) => { points.forEach(coords => { ranges.forEach((r, i) => { r.expand(coords[i]); }); }); return r; }, constrain: (...points) => points.map(coords => ranges.map((r, i) => r.constrain(coords[i]))) }; return r; }
fillArray = (array, values) => { const N = array.length; const n = values.length; for (let index = 0; index < N; index += n) array.set(values, index); return array; }
import {slider} from "@jashkenas/inputs"
sliders = function(params, filter) { const form = DOM.element('form'); var value = {}; for (let name in params) { let input = slider(params[name]); let div = input.querySelector('div'); if (div) div.style.marginTop = '10px'; let update = function() { value[name] = input.value; form.value = filter ? filter(value) : value; }; input.addEventListener('input', update); update(); form.appendChild(input); } return form; }