commit c47fd5c7224d9658fd95d580678df3652668854f Author: Francesco Luzzana Date: Sun Oct 30 17:56:41 2022 +0100 sunday birthday card diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f83818 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +## HBS 2022 + +![the svg panel](hbs.svg) + +Just a birthday card that you need to draw yourself, powered by [panel.js](https://git.xpub.nl/kamo/panel) and [spaghetti.js](https://git.xpub.nl/kamo/spaghetti) diff --git a/hbs.svg b/hbs.svg new file mode 100644 index 0000000..e148df4 --- /dev/null +++ b/hbs.svg @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..45f5868 --- /dev/null +++ b/index.html @@ -0,0 +1,667 @@ + + + + + + + HBS + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
Happy birthday amount:
+ + diff --git a/js/input-knobs.js b/js/input-knobs.js new file mode 100644 index 0000000..e4486f9 --- /dev/null +++ b/js/input-knobs.js @@ -0,0 +1,375 @@ +window.addEventListener("load", () => { + let op = window.inputKnobsOptions || {}; + op.knobWidth = op.knobWidth || op.knobDiameter || 64; + op.knobHeight = op.knobHeight || op.knobDiameter || 64; + op.sliderWidth = op.sliderWidth || op.sliderDiameter || 128; + op.sliderHeight = op.sliderHeight || op.sliderDiameter || 20; + op.switchWidth = op.switchWidth || op.switchDiameter || 24; + op.switchHeight = op.switchHeight || op.switchDiameter || 24; + op.fgcolor = op.fgcolor || "#fff"; + op.bgcolor = op.bgcolor || "#000"; + op.knobMode = op.knobMode || "linear"; + op.sliderMode = op.sliderMode || "relative"; + let styles = document.createElement("style"); + styles.innerHTML = `input[type=range].input-knob,input[type=range].input-slider{ + -webkit-appearance:none; + -moz-appearance:none; + border:none; + box-sizing:border-box; + overflow:hidden; + background-repeat:no-repeat; + background-size:100% 100%; + background-position:0px 0%; + background-color:transparent; + touch-action:none; +} +input[type=range].input-knob{ + width:${op.knobWidth}px; height:${op.knobHeight}px; +} +input[type=range].input-slider{ + width:${op.sliderWidth}px; height:${op.sliderHeight}px; +} +input[type=range].input-knob::-webkit-slider-thumb,input[type=range].input-slider::-webkit-slider-thumb{ + -webkit-appearance:none; + opacity:0; +} +input[type=range].input-knob::-moz-range-thumb,input[type=range].input-slider::-moz-range-thumb{ + -moz-appearance:none; + height:0; + border:none; +} +input[type=range].input-knob::-moz-range-track,input[type=range].input-slider::-moz-range-track{ + -moz-appearance:none; + height:0; + border:none; +} +input[type=checkbox].input-switch,input[type=radio].input-switch { + width:${op.switchWidth}px; + height:${op.switchHeight}px; + -webkit-appearance:none; + -moz-appearance:none; + background-size:100% 200%; + background-position:0% 0%; + background-repeat:no-repeat; + border:none; + border-radius:0; + background-color:transparent; +} +input[type=checkbox].input-switch:checked,input[type=radio].input-switch:checked { + background-position:0% 100%; +}`; + document.head.appendChild(styles); + let makeKnobFrames = (fr, fg, bg) => { + let r = ` + + +`; + for (let i = 1; i < fr; ++i) + r += ``; + return r + ""; + }; + let makeHSliderFrames = (fr, fg, bg, w, h) => { + let r = ` + +`; + for (let i = 0; i < fr; ++i) { + r += ``; + r += ``; + } + return r + ""; + }; + let makeVSliderFrames = (fr, fg, bg, w, h) => { + let r = ` + +`; + for (let i = 0; i < fr; ++i) { + r += ``; + r += ``; + } + return r + ""; + }; + let initSwitches = (el) => { + let w, h, d, fg, bg; + if (el.inputKnobs) return; + el.inputKnobs = {}; + el.refresh = () => { + let src = el.getAttribute("data-src"); + d = +el.getAttribute("data-diameter"); + let st = document.defaultView.getComputedStyle(el, null); + w = parseFloat(el.getAttribute("data-width") || d || st.width); + h = parseFloat(el.getAttribute("data-height") || d || st.height); + bg = el.getAttribute("data-bgcolor") || op.bgcolor; + fg = el.getAttribute("data-fgcolor") || op.fgcolor; + el.style.width = w + "px"; + el.style.height = h + "px"; + if (src) el.style.backgroundImage = "url(" + src + ")"; + else { + let minwh = Math.min(w, h); + let svg = ` + + +`; + el.style.backgroundImage = "url(data:image/svg+xml;base64," + btoa(svg) + ")"; + } + }; + el.refresh(); + }; + let initKnobs = (el) => { + let w, h, d, fg, bg; + if (el.inputKnobs) { + el.redraw(); + return; + } + let ik = (el.inputKnobs = {}); + el.refresh = () => { + d = +el.getAttribute("data-diameter"); + let st = document.defaultView.getComputedStyle(el, null); + w = parseFloat(el.getAttribute("data-width") || d || st.width); + h = parseFloat(el.getAttribute("data-height") || d || st.height); + bg = el.getAttribute("data-bgcolor") || op.bgcolor; + fg = el.getAttribute("data-fgcolor") || op.fgcolor; + ik.sensex = ik.sensey = 200; + if (el.className.indexOf("input-knob") >= 0) ik.itype = "k"; + else { + if (w >= h) { + ik.itype = "h"; + ik.sensex = w - h; + ik.sensey = Infinity; + el.style.backgroundSize = "auto 100%"; + } else { + ik.itype = "v"; + ik.sensex = Infinity; + ik.sensey = h - w; + el.style.backgroundSize = "100% auto"; + } + } + el.style.width = w + "px"; + el.style.height = h + "px"; + ik.frameheight = h; + let src = el.getAttribute("data-src"); + if (src) { + el.style.backgroundImage = `url(${src})`; + let sp = +el.getAttribute("data-sprites"); + if (sp) ik.sprites = sp; + else ik.sprites = 0; + if (ik.sprites >= 1) el.style.backgroundSize = `100% ${(ik.sprites + 1) * 100}%`; + else if (ik.itype != "k") { + el.style.backgroundColor = bg; + el.style.borderRadius = Math.min(w, h) * 0.25 + "px"; + } + } else { + let svg; + switch (ik.itype) { + case "k": + svg = makeKnobFrames(101, fg, bg); + break; + case "h": + svg = makeHSliderFrames(101, fg, bg, w, h); + break; + case "v": + svg = makeVSliderFrames(101, fg, bg, w, h); + break; + } + ik.sprites = 100; + el.style.backgroundImage = "url(data:image/svg+xml;base64," + btoa(svg) + ")"; + el.style.backgroundSize = `100% ${(ik.sprites + 1) * 100}%`; + } + ik.valrange = { + min: +el.min, + max: el.max == "" ? 100 : +el.max, + step: el.step == "" ? 1 : +el.step, + }; + el.redraw(true); + }; + el.setValue = (v) => { + v = + Math.round((v - ik.valrange.min) / ik.valrange.step) * ik.valrange.step + + ik.valrange.min; + if (v < ik.valrange.min) v = ik.valrange.min; + if (v > ik.valrange.max) v = ik.valrange.max; + el.value = v; + if (el.value != ik.oldvalue) { + el.setAttribute("value", el.value); + el.redraw(); + let event = document.createEvent("HTMLEvents"); + event.initEvent("input", false, true); + el.dispatchEvent(event); + ik.oldvalue = el.value; + } + }; + ik.pointerdown = (ev) => { + el.focus(); + const evorg = ev; + if (ev.touches) ev = ev.touches[0]; + let rc = el.getBoundingClientRect(); + let cx = (rc.left + rc.right) * 0.5, + cy = (rc.top + rc.bottom) * 0.5; + let dx = ev.clientX, + dy = ev.clientY; + let da = Math.atan2(ev.clientX - cx, cy - ev.clientY); + if (ik.itype == "k" && op.knobMode == "circularabs") { + dv = + ik.valrange.min + + ((da / Math.PI) * 0.75 + 0.5) * (ik.valrange.max - ik.valrange.min); + el.setValue(dv); + } + if (ik.itype != "k" && op.sliderMode == "abs") { + dv = + (ik.valrange.min + ik.valrange.max) * 0.5 + + ((dx - cx) / ik.sensex - (dy - cy) / ik.sensey) * + (ik.valrange.max - ik.valrange.min); + el.setValue(dv); + } + ik.dragfrom = { + x: ev.clientX, + y: ev.clientY, + a: Math.atan2(ev.clientX - cx, cy - ev.clientY), + v: +el.value, + }; + document.addEventListener("mousemove", ik.pointermove); + document.addEventListener("mouseup", ik.pointerup); + document.addEventListener("touchmove", ik.pointermove); + document.addEventListener("touchend", ik.pointerup); + document.addEventListener("touchcancel", ik.pointerup); + document.addEventListener("touchstart", ik.preventScroll); + evorg.preventDefault(); + evorg.stopPropagation(); + }; + ik.pointermove = (ev) => { + let dv; + let rc = el.getBoundingClientRect(); + let cx = (rc.left + rc.right) * 0.5, + cy = (rc.top + rc.bottom) * 0.5; + if (ev.touches) ev = ev.touches[0]; + let dx = ev.clientX - ik.dragfrom.x, + dy = ev.clientY - ik.dragfrom.y; + let da = Math.atan2(ev.clientX - cx, cy - ev.clientY); + switch (ik.itype) { + case "k": + switch (op.knobMode) { + case "linear": + dv = + (dx / ik.sensex - dy / ik.sensey) * + (ik.valrange.max - ik.valrange.min); + if (ev.shiftKey) dv *= 0.2; + el.setValue(ik.dragfrom.v + dv); + break; + case "circularabs": + if (!ev.shiftKey) { + dv = + ik.valrange.min + + ((da / Math.PI) * 0.75 + 0.5) * + (ik.valrange.max - ik.valrange.min); + el.setValue(dv); + break; + } + case "circularrel": + if (da > ik.dragfrom.a + Math.PI) da -= Math.PI * 2; + if (da < ik.dragfrom.a - Math.PI) da += Math.PI * 2; + da -= ik.dragfrom.a; + dv = (da / Math.PI / 1.5) * (ik.valrange.max - ik.valrange.min); + if (ev.shiftKey) dv *= 0.2; + el.setValue(ik.dragfrom.v + dv); + } + break; + case "h": + case "v": + dv = (dx / ik.sensex - dy / ik.sensey) * (ik.valrange.max - ik.valrange.min); + if (ev.shiftKey) dv *= 0.2; + el.setValue(ik.dragfrom.v + dv); + break; + } + }; + ik.pointerup = () => { + document.removeEventListener("mousemove", ik.pointermove); + document.removeEventListener("touchmove", ik.pointermove); + document.removeEventListener("mouseup", ik.pointerup); + document.removeEventListener("touchend", ik.pointerup); + document.removeEventListener("touchcancel", ik.pointerup); + document.removeEventListener("touchstart", ik.preventScroll); + let event = document.createEvent("HTMLEvents"); + event.initEvent("change", false, true); + el.dispatchEvent(event); + }; + ik.preventScroll = (ev) => { + ev.preventDefault(); + }; + ik.keydown = () => { + el.redraw(); + }; + ik.wheel = (ev) => { + let delta = ev.deltaY > 0 ? -ik.valrange.step : ik.valrange.step; + if (!ev.shiftKey) delta *= 5; + el.setValue(+el.value + delta); + ev.preventDefault(); + ev.stopPropagation(); + }; + el.redraw = (f) => { + if (f || ik.valueold != el.value) { + let v = (el.value - ik.valrange.min) / (ik.valrange.max - ik.valrange.min); + if (ik.sprites >= 1) + el.style.backgroundPosition = + "0px " + -((v * ik.sprites) | 0) * ik.frameheight + "px"; + else { + switch (ik.itype) { + case "k": + el.style.transform = "rotate(" + (270 * v - 135) + "deg)"; + break; + case "h": + el.style.backgroundPosition = (w - h) * v + "px 0px"; + break; + case "v": + el.style.backgroundPosition = "0px " + (h - w) * (1 - v) + "px"; + break; + } + } + ik.valueold = el.value; + } + }; + el.refresh(); + el.redraw(true); + el.addEventListener("keydown", ik.keydown); + el.addEventListener("mousedown", ik.pointerdown); + el.addEventListener("touchstart", ik.pointerdown); + el.addEventListener("wheel", ik.wheel); + }; + let refreshque = () => { + let elem = document.querySelectorAll("input.input-knob,input.input-slider"); + for (let i = 0; i < elem.length; ++i) procque.push([initKnobs, elem[i]]); + elem = document.querySelectorAll( + "input[type=checkbox].input-switch,input[type=radio].input-switch" + ); + for (let i = 0; i < elem.length; ++i) { + procque.push([initSwitches, elem[i]]); + } + }; + let procque = []; + refreshque(); + setInterval(() => { + for (let i = 0; procque.length > 0 && i < 8; ++i) { + let q = procque.shift(); + q[0](q[1]); + } + if (procque.length <= 0) refreshque(); + }, 50); +}); diff --git a/js/new-panel.js b/js/new-panel.js new file mode 100644 index 0000000..6b1d70a --- /dev/null +++ b/js/new-panel.js @@ -0,0 +1,23 @@ +const container = document.querySelector("#container"); +const svg = document.querySelector("#hbs"); +const card = document.querySelector("#card"); + +const cardWords = []; +let lastWord = ""; + +let panel = new Panel(svg.outerHTML, container); + +window.addEventListener("onCableStart", (e) => { + let currentWord = e.detail.value().trim(); + if (currentWord == lastWord) return; + cardWords.push(currentWord); + lastWord = currentWord; + card.innerHTML = cardWords.join(" "); +}); + +window.addEventListener("onCableEnd", (e) => { + currentWord = e.detail.value().trim(); + cardWords.push(currentWord); + lastWord = currentWord; + card.innerHTML = cardWords.join(" "); +}); diff --git a/js/panel.js b/js/panel.js new file mode 100644 index 0000000..480f184 --- /dev/null +++ b/js/panel.js @@ -0,0 +1,331 @@ +class Cable { + start = ""; + end = ""; + strPath = ""; + color = "tomato"; + strokeWidth = 3; + bufferSize = 5; + buffer = []; + path = null; + + constructor(start) { + this.start = start; + // this.randomColor(); + } + + randomColor() { + const hue = Math.floor(Math.random() * 360); + const saturation = Math.floor(50 + Math.random() * (50 + 1)) + "%"; + const lightness = "75%"; + this.color = "hsl(" + hue + ", " + saturation + ", " + lightness + ")"; + } + + createCable(position) { + this.path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + + this.path.setAttribute("fill", "none"); + + this.path.setAttribute("stroke", this.color); + this.path.setAttribute("stroke-width", this.strokeWidth); + this.path.setAttribute("stroke-linecap", "round"); + + this.buffer = []; + this.appendToBuffer(position); + this.strPath = "M" + position.x + " " + position.y; + this.path.setAttribute("d", this.strPath); + } + + appendToBuffer(position) { + this.buffer.push(position); + while (this.buffer.length > this.bufferSize) { + this.buffer.shift(); + } + } + + getAveragePoint(offset) { + // Calculate the average point, starting at offset in the buffer + let len = this.buffer.length; + if (len % 2 === 1 || len >= this.bufferSize) { + let totalX = 0; + let totalY = 0; + let position, i; + let count = 0; + for (i = offset; i < len; i++) { + count++; + position = this.buffer[i]; + totalX += position.x; + totalY += position.y; + } + return { + x: totalX / count, + y: totalY / count, + }; + } + return null; + } + + updatePath() { + let position = this.getAveragePoint(0); + if (position) { + // Get the smoothed part of the path that will not change + this.strPath += " L" + position.x + " " + position.y; + + // Get the last part of the path (close to the current mouse position) + // This part will change if the mouse moves again + var tmpPath = ""; + for (var offset = 2; offset < this.buffer.length; offset += 2) { + position = this.getAveragePoint(offset); + tmpPath += " L" + position.x + " " + position.y; + } + this.path.setAttribute("d", this.strPath + tmpPath); + } + } +} + +class Panel { + width = 0; + height = 0; + + svg = null; + container = null; + containerBoundingClient = null; + + model = {}; + params = []; + sockets = []; + preset = {}; + + cable = null; + cables = null; + + constructor(svg, container, preset = {}, debug = false) { + this.svg = this.htmlToElement(svg); + this.container = container; + this.container.style.position = "relative"; + this.containerRect = container.getBoundingClientRect(); + this.preset = { ...preset }; + this.debug = debug; + + while (container.firstChild) { + container.removeChild(container.lastChild); + } + container.appendChild(this.svg); + + this.setSize(); + this.createParams(); + + this.createSockets(); + this.createCables(); + + this.svg.addEventListener("mousedown", (e) => this.startCable(e)); + this.svg.addEventListener("mousemove", (e) => this.drawCable(e)); + this.svg.addEventListener("mouseup", (e) => this.endCable(e)); + // this.svg.addEventListener("mouseleave", (e) => this.endCable(e)); + } + + htmlToElement(string) { + var template = document.createElement("template"); + string = string.trim(); // Never return a text node of whitespace as the result + template.innerHTML = string; + return template.content.firstChild; + } + + setSize() { + this.width = this.svg.getAttribute("width"); + this.height = this.svg.getAttribute("height"); + this.container.style.width = this.width + "px"; + this.container.style.height = this.height + "px"; + } + + createParams() { + // this.svg.querySelector("#params").style.visibility = "hidden"; + this.params = this.svg.querySelectorAll("#params [fill='#FF0000']"); + this.model.params = []; + + for (const param of this.params) { + let rect = param.getBoundingClientRect(); + + let control = document.createElement("input"); + control.setAttribute("type", "range"); + control.setAttribute("data-width", rect.width); + control.setAttribute("data-height", rect.height); + + control.setAttribute("data-fgcolor", "white"); + control.classList.add("input-knob"); + control.style.position = "absolute"; + control.style.left = rect.left - this.containerRect.left + "px"; + control.style.top = rect.top - this.containerRect.top + "px"; + + control.setAttribute("name", param.id); + + if (this.preset.hasOwnProperty(param.id)) { + control.value = this.preset[param.id]; + } + + this.container.appendChild(control); + + this.model.params.push(param.id); + + if (this.debug) { + let label = document.createElement("label"); + label.setAttribute("for", param.id); + label.innerHTML = param.id; + + label.style.position = "absolute"; + label.style.left = rect.left - this.containerRect.left + "px"; + label.style.top = rect.top + rect.height + "px"; + label.style.fontSize = "1rem"; + label.style.backgroundColor = "red"; + label.style.color = "white"; + + this.container.appendChild(label); + } + } + let group = this.svg.querySelector("#params"); + if (group) group.style.display = "none"; + } + + createSockets() { + // this.svg.querySelector("#sockets").style.visibility = "hidden"; + this.sockets = this.svg.querySelectorAll("#sockets [fill='#00FF00']"); + this.model.sockets = []; + for (const socket of this.sockets) { + let rect = socket.getBoundingClientRect(); + + let input = document.createElement("input"); + input.setAttribute("name", socket.id); + input.classList.add("socket"); + + input.style.position = "absolute"; + input.style.left = rect.left - this.containerRect.left + "px"; + input.style.top = rect.top - this.containerRect.top + "px"; + input.style.width = rect.width + "px"; + input.style.height = rect.height + "px"; + + input.style.border = "none"; + input.style.background = "none"; + input.style.opacity = 0; + + if (this.debug) { + input.style.border = "1px solid #00FF00"; + input.style.opacity = 1; + } + + input.style.cursor = "alias"; + + input.addEventListener("mouseup", (e) => this.endCable(e)); + + input.addEventListener("mousedown", (e) => { + e.preventDefault(); + this.startCable(e); + return false; + }); + + this.container.appendChild(input); + + this.model.sockets.push(socket.id); + } + + let group = this.svg.querySelector("#sockets"); + if (group) group.style.display = "none"; + } + + createCables() { + let cables = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + + cables.classList.add("cables"); + cables.setAttribute("xmlns", "http://www.w3.org/2000/svg"); + cables.setAttribute("width", this.width); + cables.setAttribute("height", this.height); + cables.setAttribute("viewBox", `0 0 ${this.width} ${this.height}`); + cables.setAttribute("fill", "none"); + cables.style.pointerEvents = "none"; + cables.style.position = "absolute"; + cables.style.left = 0; + cables.style.top = 0; + cables.style.width = this.width; + cables.style.height = this.height; + + this.cables = cables; + this.container.appendChild(this.cables); + } + + startCable(event) { + let position = this.getMousePosition(event); + let socket = event.target; + + if (socket && socket.classList.contains("socket")) { + this.cable = new Cable(socket.getAttribute("name")); + this.cable.createCable(position); + let cable = this.cable.path; + cable.style.pointerEvents = "stroke"; + + cable.addEventListener("click", (e) => { + cable.remove(); + }); + + this.cables.appendChild(this.cable.path); + + let onCableStart = new CustomEvent("onCableStart", { + bubbles: true, + detail: { + value: () => socket.getAttribute("name"), + }, + }); + + dispatchEvent(onCableStart); + + for (const input of this.container.querySelectorAll(".input-knob")) { + input.style.pointerEvents = "none"; + } + } + } + + drawCable(event) { + if (this.cable) { + this.cable.appendToBuffer(this.getMousePosition(event)); + this.cable.updatePath(); + } + } + + endCable(event) { + if (this.cable) { + let socket = event.target; + + if (socket && socket.classList.contains("socket")) { + this.cable.end = socket.getAttribute("name"); + + let startSocket = document.querySelector(`.socket[name="${this.cable.start}"]`); + + let values = (startSocket.getAttribute("value") || "") + .split(",") + .map((entry) => entry.trim()); + values.push(this.cable.end); + startSocket.setAttribute("value", values.filter((value) => value != "").join()); + + let onCableEnd = new CustomEvent("onCableEnd", { + bubbles: true, + detail: { value: () => socket.getAttribute("name") }, + }); + dispatchEvent(onCableEnd); + + // TODO: log cable + } else { + this.cable.path.remove(); + } + + for (const input of this.container.querySelectorAll(".input-knob")) { + input.style.pointerEvents = "all"; + } + + this.cable = null; + } + } + + getMousePosition(e) { + return { + x: e.pageX - this.containerRect.left, + y: e.pageY - this.containerRect.top, + }; + } +}