From cebde38caae1c8c55d47d92fb5c32ec60d7b9c70 Mon Sep 17 00:00:00 2001 From: Francesco Luzzana Date: Mon, 4 Apr 2022 03:09:46 +0200 Subject: [PATCH] input-knobs and json modules --- .gitignore | 18 + modular/__init__.py | 41 ++ modular/module.py | 23 ++ modular/prefix.py | 14 + modular/static/css/style.css | 63 +++ modular/static/js/input-knobs.js | 375 ++++++++++++++++++ modular/templates/module.html | 43 ++ modules/A long pad_2022-04-04_03-06-22.json | 1 + modules/AHahah_2022-04-04_03-07-08.json | 1 + modules/a short stab_2022-04-04_03-06-41.json | 1 + modules/af_2022-04-04_02-59-27.json | 1 + setup.py | 10 + 12 files changed, 591 insertions(+) create mode 100644 .gitignore create mode 100644 modular/__init__.py create mode 100644 modular/module.py create mode 100644 modular/prefix.py create mode 100644 modular/static/css/style.css create mode 100644 modular/static/js/input-knobs.js create mode 100644 modular/templates/module.html create mode 100644 modules/A long pad_2022-04-04_03-06-22.json create mode 100644 modules/AHahah_2022-04-04_03-07-08.json create mode 100644 modules/a short stab_2022-04-04_03-06-41.json create mode 100644 modules/af_2022-04-04_02-59-27.json create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4092bfb --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +venv/ + +*.pyc +__pycache__/ +.ipynb_checkpoints + + + +.pytest_cache/ +.coverage +htmlcov/ + +dist/ +build/ +*.egg-info/ + +.env +.vscode \ No newline at end of file diff --git a/modular/__init__.py b/modular/__init__.py new file mode 100644 index 0000000..4fc6a7c --- /dev/null +++ b/modular/__init__.py @@ -0,0 +1,41 @@ +import os +from flask import Flask, send_from_directory +from . import prefix + + +def create_app(test_config=None): + # Create and configure the Flask App + app = Flask(__name__, instance_relative_config=True) + app.config.from_mapping( + SECRET_KEY="dev", + ) + + if test_config is None: + # load the instance config, if it exists, when not testing + app.config.from_pyfile("config.py", silent=True) + else: + # load the test config if passed in + app.config.from_mapping(test_config) + + # ensure the instance folder exists + try: + os.makedirs(os.environ.get('MODULES', 'modules')) + except OSError: + pass + + @app.route("/favicon.ico") + def favicon(): + return send_from_directory( + os.path.join(app.root_path, "static"), + "favicon.ico", + mimetype="image/vnd.microsoft.icon", + ) + + from . import module + app.register_blueprint(module.bp) + + app.wsgi_app = prefix.PrefixMiddleware( + app.wsgi_app, prefix=os.environ.get("URL_PREFIX", "") + ) + + return app diff --git a/modular/module.py b/modular/module.py new file mode 100644 index 0000000..3e85a22 --- /dev/null +++ b/modular/module.py @@ -0,0 +1,23 @@ +from flask import Blueprint, render_template, request +from datetime import datetime +import json +import os + +bp = Blueprint('module', __name__, url_prefix="/") + + +@bp.route('/', methods=['GET', 'POST']) +def module(): + if request.method == 'POST': + preset = {} + for key in request.form.keys(): + preset[key] = request.form.get(key) + name = preset.get('name', '') + filename = f"{name}_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.json" + with open(os.path.join(os.environ.get('MODULES'), filename), 'w') as f: + f.write(json.dumps(preset)) + + modules = [os.path.splitext(filename)[0] + for filename in os.listdir(os.environ.get('MODULES'))] + + return render_template('module.html', modules=modules) diff --git a/modular/prefix.py b/modular/prefix.py new file mode 100644 index 0000000..a94e608 --- /dev/null +++ b/modular/prefix.py @@ -0,0 +1,14 @@ +class PrefixMiddleware(object): + def __init__(self, app, prefix=""): + self.app = app + self.prefix = prefix + + def __call__(self, environ, start_response): + + if environ["PATH_INFO"].startswith(self.prefix): + environ["PATH_INFO"] = environ["PATH_INFO"][len(self.prefix) :] + environ["SCRIPT_NAME"] = self.prefix + return self.app(environ, start_response) + else: + start_response("404", [("Content-Type", "text/plain")]) + return ["This url does not belong to the app.".encode()] diff --git a/modular/static/css/style.css b/modular/static/css/style.css new file mode 100644 index 0000000..c41034d --- /dev/null +++ b/modular/static/css/style.css @@ -0,0 +1,63 @@ +* { + box-sizing: border-box; + font-family: 'Courier New', Courier, monospace; +} + +:root { + --radius: 2px; +} + +.module { + + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + display: flex; + justify-content: flex-start; + align-items: start; + width: 480px; + height: 240px; + border-radius: var(--radius); + + padding: 32px; + border: 1px solid currentColor; + + margin: 0 auto; +} + +.control { + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.control label { + margin-top: 8px; +} + +.control + .control { + margin-left: 8px; +} + +.module input[type='submit'] { + position: absolute; + right: 32px; + bottom: 32px; + border: 1px solid currentColor; + background: none; + border-radius: var(--radius); + padding: 4px 8px; +} + +.module #name { + position: absolute; + bottom: 32px; + left: 32px; + padding: 4px 8px; + border-radius: var(--radius); + border: 1px solid currentColor; +} \ No newline at end of file diff --git a/modular/static/js/input-knobs.js b/modular/static/js/input-knobs.js new file mode 100644 index 0000000..814422c --- /dev/null +++ b/modular/static/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 || "#f00"; + 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/modular/templates/module.html b/modular/templates/module.html new file mode 100644 index 0000000..8fb7c78 --- /dev/null +++ b/modular/templates/module.html @@ -0,0 +1,43 @@ + + + + + + + Module test + + + + + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + diff --git a/modules/A long pad_2022-04-04_03-06-22.json b/modules/A long pad_2022-04-04_03-06-22.json new file mode 100644 index 0000000..632b0a7 --- /dev/null +++ b/modules/A long pad_2022-04-04_03-06-22.json @@ -0,0 +1 @@ +{"attack": "62", "decay": "17", "sustain": "78", "release": "100", "name": "A long pad"} \ No newline at end of file diff --git a/modules/AHahah_2022-04-04_03-07-08.json b/modules/AHahah_2022-04-04_03-07-08.json new file mode 100644 index 0000000..2408ae4 --- /dev/null +++ b/modules/AHahah_2022-04-04_03-07-08.json @@ -0,0 +1 @@ +{"attack": "50", "decay": "50", "sustain": "50", "release": "50", "name": "AHahah"} \ No newline at end of file diff --git a/modules/a short stab_2022-04-04_03-06-41.json b/modules/a short stab_2022-04-04_03-06-41.json new file mode 100644 index 0000000..933d491 --- /dev/null +++ b/modules/a short stab_2022-04-04_03-06-41.json @@ -0,0 +1 @@ +{"attack": "0", "decay": "0", "sustain": "14", "release": "20", "name": "a short stab"} \ No newline at end of file diff --git a/modules/af_2022-04-04_02-59-27.json b/modules/af_2022-04-04_02-59-27.json new file mode 100644 index 0000000..84396ee --- /dev/null +++ b/modules/af_2022-04-04_02-59-27.json @@ -0,0 +1 @@ +{"attack": "76", "decay": "50", "sustain": "31", "release": "73", "name": "af"} \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f10f367 --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +from setuptools import find_packages, setup + +setup( + name="modcms", + version="1.0.0", + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=["flask", "python-dotenv"], +)