| @@ -0,0 +1,115 @@ | ||
| var audio = new Audio(); | ||
| var data = []; | ||
| var theme; | ||
| var compile; | ||
| var wave = new RIFFWAVE(); | ||
| audio.loop = true; | ||
|
|
||
| const getQParam = (param) => | ||
| decodeURI(atob(new URL(window.location).searchParams.get(param))); | ||
|
|
||
| const setQParam = (param, val) => { | ||
| let url = new URL(window.location); | ||
| url.searchParams.set(param, encodeURI(btoa(val))); | ||
| window.history.pushState({ path: url.href }, "", url.href); | ||
| }; | ||
|
|
||
| window.onload = () => { | ||
| theme = new Theme(); | ||
| theme.install(document.body); | ||
| theme.start(); | ||
|
|
||
| Module.onRuntimeInitialized = (_) => { | ||
| window.read = () => { | ||
| tableFrews = document.getElementById("freqs").value; | ||
| tableData = document.getElementById("index").value; | ||
| waveformsData = document.getElementById("waveforms").value; | ||
| vec = Module.gen(tableFrews, tableData, waveformsData); | ||
|
|
||
| for (var i = 0; i < vec.size(); i++) { | ||
| // TODO: figure out correct type over wasm->js embind | ||
| data[i] = vec.get(i); | ||
| } | ||
|
|
||
| wave.header.bitsPerSample = 16; | ||
| wave.header.sampleRate = 44100; | ||
| wave.header.numChannels = 1; | ||
| wave.Make(data); | ||
| var continuePlaying; | ||
| if (!audio.paused) { | ||
| continuePlaying = true; | ||
| audio.pause(); | ||
| } | ||
| audio.src = wave.dataURI; | ||
| if (continuePlaying) audio.play(); | ||
| }; | ||
| init(); | ||
| }; | ||
|
|
||
| window.setFreqs = (e) => { | ||
| setQParam("frequencies", e.value); | ||
| read(); | ||
| }; | ||
|
|
||
| window.init = () => { | ||
| let f = (document.getElementById("freqs").value = getQParam("frequencies")), | ||
| i = (document.getElementById("dslIndex").value = getQParam("index")), | ||
| w = (document.getElementById("dslWaveforms").value = getQParam( | ||
| "waveforms" | ||
| )); | ||
| compile(i, "index"); | ||
| compile(f, "waveforms"); | ||
| draw(); | ||
| }; | ||
|
|
||
| window.compile = (txt, destination) => { | ||
| setQParam(destination, txt); | ||
| let seq = wave_dsl.parser.parse__GT_js(txt); | ||
| document.getElementById(destination).value = seq.join(" "); | ||
| read(); | ||
| }; | ||
| }; | ||
|
|
||
| audio.onpause = () => { | ||
| document.getElementById("playButton").className = "off"; | ||
| }; | ||
|
|
||
| const play = (e) => { | ||
| e.className = !audio.paused ? "off" : ""; | ||
| if (!audio.paused) { | ||
| audio.pause(); | ||
| } else { | ||
| audio.play(); | ||
| } | ||
| }; | ||
|
|
||
| const loopToggle = (e) => { | ||
| audio.loop = !audio.loop; | ||
| e.className = audio.loop ? "" : "off"; | ||
| }; | ||
|
|
||
| const dataURItoBlob = (dataURI) => { | ||
| // convert base64/URLEncoded data component to raw binary data held in a string | ||
| var byteString; | ||
| if (dataURI.split(",")[0].indexOf("base64") >= 0) | ||
| byteString = atob(dataURI.split(",")[1]); | ||
| else byteString = unescape(dataURI.split(",")[1]); | ||
|
|
||
| // separate out the mime component | ||
| let mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0]; | ||
|
|
||
| // write the bytes of the string to a typed array | ||
| var ia = new Uint8Array(byteString.length); | ||
| for (var i = 0; i < byteString.length; i++) { | ||
| ia[i] = byteString.charCodeAt(i); | ||
| } | ||
|
|
||
| return new Blob([ia], { type: mimeString }); | ||
| }; | ||
|
|
||
| const download = () => saveAs(dataURItoBlob(wave.dataURI), "segmod.wav"); | ||
|
|
||
| const share = () => { | ||
| navigator.clipboard.writeText(window.location.href); | ||
| alert("link copied to clipboard!"); | ||
| }; |
| @@ -0,0 +1,43 @@ | ||
| var ctx = document.getElementById("scope").getContext("2d"); | ||
|
|
||
| function hexToRGB(hex, alpha) { | ||
| var r = parseInt(hex.slice(1, 3), 16), | ||
| g = parseInt(hex.slice(3, 5), 16), | ||
| b = parseInt(hex.slice(5, 7), 16); | ||
|
|
||
| if (alpha) { | ||
| return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")"; | ||
| } else { | ||
| return "rgb(" + r + ", " + g + ", " + b + ")"; | ||
| } | ||
| } | ||
|
|
||
| function draw() { | ||
| var width = ctx.canvas.width; | ||
| var height = ctx.canvas.height; | ||
| var scaling = 30 / 32768; //(height + 32768) / (32768 * 2); | ||
| var timeData = data; | ||
| var risingEdge = 0; | ||
| var edgeThreshold = 5; | ||
|
|
||
| ctx.fillStyle = hexToRGB(theme.active.background); | ||
| ctx.fillRect(0, 0, width, height); | ||
|
|
||
| ctx.lineWidth = 4; | ||
| ctx.strokeStyle = hexToRGB(theme.active.f_high); | ||
| ctx.beginPath(); | ||
|
|
||
| // No buffer overrun protection | ||
| while (timeData[risingEdge++] - 16000 > 0 && risingEdge <= width); | ||
| if (risingEdge >= width) risingEdge = 0; | ||
|
|
||
| while (timeData[risingEdge++] - 16000 < edgeThreshold && risingEdge <= width); | ||
| if (risingEdge >= width) risingEdge = 0; | ||
|
|
||
| for (var x = risingEdge; x < timeData.length && x - risingEdge < width; x++) | ||
| ctx.lineTo(x - risingEdge, (height - timeData[x] * scaling) / 2); | ||
|
|
||
| ctx.stroke(); | ||
|
|
||
| requestAnimationFrame(draw); | ||
| } |
| @@ -0,0 +1,125 @@ | ||
| :root { | ||
| --font-fam: JetBrainsMono, monospace; | ||
| --line-height: 30px; | ||
| --font-size: 14px; | ||
| --line-width: 1px; | ||
| } | ||
|
|
||
| @font-face { | ||
| font-family: JetBrainsMono; | ||
| src: url(vendors/JetBrainsMono-Regular-40b288a985bc90e34d3260d92b8f4515.woff2) | ||
| format("woff2"), | ||
| url(vendors/JetBrainsMono-Regular-edaecf9876afc052ec1a241d9443d594.woff) | ||
| format("woff"), | ||
| url(vendors/JetBrainsMono-Regular-6664e867457f3dc3d7ee961775261e80.ttf) | ||
| format("truetype"); | ||
| font-weight: 400; | ||
| font-style: normal; | ||
| } | ||
|
|
||
| body { | ||
| font-family: var(--font-fam); | ||
| font-size: var(--font-size); | ||
| line-height: var(--line-height); | ||
| } | ||
|
|
||
| * { | ||
| padding: 0; | ||
| margin: 0; | ||
| box-sizing: border-box; | ||
| text-rendering: geometricPrecision; | ||
| } | ||
|
|
||
| ::selection { | ||
| background: var(--f_med); | ||
| } | ||
|
|
||
| input, | ||
| textarea, | ||
| button { | ||
| font-family: var(--font-fam); | ||
| font-size: var(--font-size); | ||
| color: var(--f_high); | ||
| background-color: var(--background); | ||
| border: none; | ||
| line-height: var(--line-height); | ||
| resize: none; | ||
| height: 100%; | ||
| caret-color: var(--f_med); | ||
| outline: none; | ||
| } | ||
|
|
||
| h1 { | ||
| font-size: var(--font-size); | ||
| font-weight: normal; | ||
| min-height: var(--line-height); | ||
| color: var(--f_med); | ||
| text-transform: lowercase; | ||
| line-height: var(--line-height); | ||
| position: relative; | ||
| } | ||
| h1 span { | ||
| background-color: var(--background); | ||
| z-index: 3; | ||
| position: relative; | ||
| padding-right: 10px; | ||
| } | ||
|
|
||
| h1:after { | ||
| content: ""; | ||
| width: calc(100% - 30px); | ||
| margin-left: 10px; | ||
| border-bottom: solid var(--line-width) var(--f_low); | ||
| z-index: 1; | ||
| position: absolute; | ||
| left: 0; | ||
| top: 50%; | ||
| } | ||
|
|
||
| .col:last-child h1:after { | ||
| width: 100%; | ||
| } | ||
|
|
||
| button { | ||
| margin-right: 10px; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| button.off { | ||
| color: var(--f_low); | ||
| } | ||
|
|
||
| .container { | ||
| display: flex; | ||
| flex-direction: column; | ||
| height: 70vh; | ||
| max-width: 70rem; | ||
| transform: translate(25%, 25%); | ||
| } | ||
|
|
||
| .row { | ||
| display: flex; | ||
| flex-direction: row; | ||
| flex: auto; | ||
| } | ||
| .col { | ||
| display: flex; | ||
| flex-direction: column; | ||
| flex: 1; | ||
| } | ||
|
|
||
| .col:not(:last-child) > *:nth-child(2n) { | ||
| border-right: solid var(--line-width) var(--f_low); | ||
| padding-right: 10xp; | ||
| margin-right: 5px; | ||
| height: 100%; | ||
| } | ||
|
|
||
| .col:not(:first-child) > * { | ||
| padding-left: 10px; | ||
| } | ||
|
|
||
| canvas { | ||
| width: 100%; | ||
| height: 30px; | ||
| } |
| @@ -0,0 +1,197 @@ | ||
| /* Blob.js | ||
| * A Blob implementation. | ||
| * 2014-07-24 | ||
| * | ||
| * By Eli Grey, http://eligrey.com | ||
| * By Devin Samarin, https://github.com/dsamarin | ||
| * License: X11/MIT | ||
| * See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md | ||
| */ | ||
|
|
||
| /*global self, unescape */ | ||
| /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, | ||
| plusplus: true */ | ||
|
|
||
| /*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ | ||
|
|
||
| (function (view) { | ||
| "use strict"; | ||
|
|
||
| view.URL = view.URL || view.webkitURL; | ||
|
|
||
| if (view.Blob && view.URL) { | ||
| try { | ||
| new Blob; | ||
| return; | ||
| } catch (e) {} | ||
| } | ||
|
|
||
| // Internally we use a BlobBuilder implementation to base Blob off of | ||
| // in order to support older browsers that only have BlobBuilder | ||
| var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) { | ||
| var | ||
| get_class = function(object) { | ||
| return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; | ||
| } | ||
| , FakeBlobBuilder = function BlobBuilder() { | ||
| this.data = []; | ||
| } | ||
| , FakeBlob = function Blob(data, type, encoding) { | ||
| this.data = data; | ||
| this.size = data.length; | ||
| this.type = type; | ||
| this.encoding = encoding; | ||
| } | ||
| , FBB_proto = FakeBlobBuilder.prototype | ||
| , FB_proto = FakeBlob.prototype | ||
| , FileReaderSync = view.FileReaderSync | ||
| , FileException = function(type) { | ||
| this.code = this[this.name = type]; | ||
| } | ||
| , file_ex_codes = ( | ||
| "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " | ||
| + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR" | ||
| ).split(" ") | ||
| , file_ex_code = file_ex_codes.length | ||
| , real_URL = view.URL || view.webkitURL || view | ||
| , real_create_object_URL = real_URL.createObjectURL | ||
| , real_revoke_object_URL = real_URL.revokeObjectURL | ||
| , URL = real_URL | ||
| , btoa = view.btoa | ||
| , atob = view.atob | ||
|
|
||
| , ArrayBuffer = view.ArrayBuffer | ||
| , Uint8Array = view.Uint8Array | ||
|
|
||
| , origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/ | ||
| ; | ||
| FakeBlob.fake = FB_proto.fake = true; | ||
| while (file_ex_code--) { | ||
| FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1; | ||
| } | ||
| // Polyfill URL | ||
| if (!real_URL.createObjectURL) { | ||
| URL = view.URL = function(uri) { | ||
| var | ||
| uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a") | ||
| , uri_origin | ||
| ; | ||
| uri_info.href = uri; | ||
| if (!("origin" in uri_info)) { | ||
| if (uri_info.protocol.toLowerCase() === "data:") { | ||
| uri_info.origin = null; | ||
| } else { | ||
| uri_origin = uri.match(origin); | ||
| uri_info.origin = uri_origin && uri_origin[1]; | ||
| } | ||
| } | ||
| return uri_info; | ||
| }; | ||
| } | ||
| URL.createObjectURL = function(blob) { | ||
| var | ||
| type = blob.type | ||
| , data_URI_header | ||
| ; | ||
| if (type === null) { | ||
| type = "application/octet-stream"; | ||
| } | ||
| if (blob instanceof FakeBlob) { | ||
| data_URI_header = "data:" + type; | ||
| if (blob.encoding === "base64") { | ||
| return data_URI_header + ";base64," + blob.data; | ||
| } else if (blob.encoding === "URI") { | ||
| return data_URI_header + "," + decodeURIComponent(blob.data); | ||
| } if (btoa) { | ||
| return data_URI_header + ";base64," + btoa(blob.data); | ||
| } else { | ||
| return data_URI_header + "," + encodeURIComponent(blob.data); | ||
| } | ||
| } else if (real_create_object_URL) { | ||
| return real_create_object_URL.call(real_URL, blob); | ||
| } | ||
| }; | ||
| URL.revokeObjectURL = function(object_URL) { | ||
| if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) { | ||
| real_revoke_object_URL.call(real_URL, object_URL); | ||
| } | ||
| }; | ||
| FBB_proto.append = function(data/*, endings*/) { | ||
| var bb = this.data; | ||
| // decode data to a binary string | ||
| if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) { | ||
| var | ||
| str = "" | ||
| , buf = new Uint8Array(data) | ||
| , i = 0 | ||
| , buf_len = buf.length | ||
| ; | ||
| for (; i < buf_len; i++) { | ||
| str += String.fromCharCode(buf[i]); | ||
| } | ||
| bb.push(str); | ||
| } else if (get_class(data) === "Blob" || get_class(data) === "File") { | ||
| if (FileReaderSync) { | ||
| var fr = new FileReaderSync; | ||
| bb.push(fr.readAsBinaryString(data)); | ||
| } else { | ||
| // async FileReader won't work as BlobBuilder is sync | ||
| throw new FileException("NOT_READABLE_ERR"); | ||
| } | ||
| } else if (data instanceof FakeBlob) { | ||
| if (data.encoding === "base64" && atob) { | ||
| bb.push(atob(data.data)); | ||
| } else if (data.encoding === "URI") { | ||
| bb.push(decodeURIComponent(data.data)); | ||
| } else if (data.encoding === "raw") { | ||
| bb.push(data.data); | ||
| } | ||
| } else { | ||
| if (typeof data !== "string") { | ||
| data += ""; // convert unsupported types to strings | ||
| } | ||
| // decode UTF-16 to binary string | ||
| bb.push(unescape(encodeURIComponent(data))); | ||
| } | ||
| }; | ||
| FBB_proto.getBlob = function(type) { | ||
| if (!arguments.length) { | ||
| type = null; | ||
| } | ||
| return new FakeBlob(this.data.join(""), type, "raw"); | ||
| }; | ||
| FBB_proto.toString = function() { | ||
| return "[object BlobBuilder]"; | ||
| }; | ||
| FB_proto.slice = function(start, end, type) { | ||
| var args = arguments.length; | ||
| if (args < 3) { | ||
| type = null; | ||
| } | ||
| return new FakeBlob( | ||
| this.data.slice(start, args > 1 ? end : this.data.length) | ||
| , type | ||
| , this.encoding | ||
| ); | ||
| }; | ||
| FB_proto.toString = function() { | ||
| return "[object Blob]"; | ||
| }; | ||
| FB_proto.close = function() { | ||
| this.size = 0; | ||
| delete this.data; | ||
| }; | ||
| return FakeBlobBuilder; | ||
| }(view)); | ||
|
|
||
| view.Blob = function(blobParts, options) { | ||
| var type = options ? (options.type || "") : ""; | ||
| var builder = new BlobBuilder(); | ||
| if (blobParts) { | ||
| for (var i = 0, len = blobParts.length; i < len; i++) { | ||
| builder.append(blobParts[i]); | ||
| } | ||
| } | ||
| return builder.getBlob(type); | ||
| }; | ||
| }(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this)); |
| @@ -0,0 +1,131 @@ | ||
| /* | ||
| * RIFFWAVE.js v0.03 - Audio encoder for HTML5 <audio> elements. | ||
| * Copyleft 2011 by Pedro Ladaria <pedro.ladaria at Gmail dot com> | ||
| * | ||
| * Public Domain | ||
| * | ||
| * Changelog: | ||
| * | ||
| * 0.01 - First release | ||
| * 0.02 - New faster base64 encoding | ||
| * 0.03 - Support for 16bit samples | ||
| * | ||
| * Notes: | ||
| * | ||
| * 8 bit data is unsigned: 0..255 | ||
| * 16 bit data is signed: −32,768..32,767 | ||
| * | ||
| */ | ||
|
|
||
| var FastBase64 = { | ||
|
|
||
| chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", | ||
| encLookup: [], | ||
|
|
||
| Init: function() { | ||
| for (var i=0; i<4096; i++) { | ||
| this.encLookup[i] = this.chars[i >> 6] + this.chars[i & 0x3F]; | ||
| } | ||
| }, | ||
|
|
||
| Encode: function(src) { | ||
| var len = src.length; | ||
| var dst = ''; | ||
| var i = 0; | ||
| while (len > 2) { | ||
| n = (src[i] << 16) | (src[i+1]<<8) | src[i+2]; | ||
| dst+= this.encLookup[n >> 12] + this.encLookup[n & 0xFFF]; | ||
| len-= 3; | ||
| i+= 3; | ||
| } | ||
| if (len > 0) { | ||
| var n1= (src[i] & 0xFC) >> 2; | ||
| var n2= (src[i] & 0x03) << 4; | ||
| if (len > 1) n2 |= (src[++i] & 0xF0) >> 4; | ||
| dst+= this.chars[n1]; | ||
| dst+= this.chars[n2]; | ||
| if (len == 2) { | ||
| var n3= (src[i++] & 0x0F) << 2; | ||
| n3 |= (src[i] & 0xC0) >> 6; | ||
| dst+= this.chars[n3]; | ||
| } | ||
| if (len == 1) dst+= '='; | ||
| dst+= '='; | ||
| } | ||
| return dst; | ||
| } // end Encode | ||
|
|
||
| } | ||
|
|
||
| FastBase64.Init(); | ||
|
|
||
| var RIFFWAVE = function(data) { | ||
|
|
||
| this.data = []; // Array containing audio samples | ||
| this.wav = []; // Array containing the generated wave file | ||
| this.dataURI = ''; // http://en.wikipedia.org/wiki/Data_URI_scheme | ||
|
|
||
| this.header = { // OFFS SIZE NOTES | ||
| chunkId : [0x52,0x49,0x46,0x46], // 0 4 "RIFF" = 0x52494646 | ||
| chunkSize : 0, // 4 4 36+SubChunk2Size = 4+(8+SubChunk1Size)+(8+SubChunk2Size) | ||
| format : [0x57,0x41,0x56,0x45], // 8 4 "WAVE" = 0x57415645 | ||
| subChunk1Id : [0x66,0x6d,0x74,0x20], // 12 4 "fmt " = 0x666d7420 | ||
| subChunk1Size: 16, // 16 4 16 for PCM | ||
| audioFormat : 1, // 20 2 PCM = 1 | ||
| numChannels : 1, // 22 2 Mono = 1, Stereo = 2... | ||
| sampleRate : 8000, // 24 4 8000, 44100... | ||
| byteRate : 0, // 28 4 SampleRate*NumChannels*BitsPerSample/8 | ||
| blockAlign : 0, // 32 2 NumChannels*BitsPerSample/8 | ||
| bitsPerSample: 8, // 34 2 8 bits = 8, 16 bits = 16 | ||
| subChunk2Id : [0x64,0x61,0x74,0x61], // 36 4 "data" = 0x64617461 | ||
| subChunk2Size: 0 // 40 4 data size = NumSamples*NumChannels*BitsPerSample/8 | ||
| }; | ||
|
|
||
| function u32ToArray(i) { | ||
| return [i&0xFF, (i>>8)&0xFF, (i>>16)&0xFF, (i>>24)&0xFF]; | ||
| } | ||
|
|
||
| function u16ToArray(i) { | ||
| return [i&0xFF, (i>>8)&0xFF]; | ||
| } | ||
|
|
||
| function split16bitArray(data) { | ||
| var r = []; | ||
| var j = 0; | ||
| var len = data.length; | ||
| for (var i=0; i<len; i++) { | ||
| r[j++] = data[i] & 0xFF; | ||
| r[j++] = (data[i]>>8) & 0xFF; | ||
| } | ||
| return r; | ||
| } | ||
|
|
||
| this.Make = function(data) { | ||
| if (data instanceof Array) this.data = data; | ||
| this.header.blockAlign = (this.header.numChannels * this.header.bitsPerSample) >> 3; | ||
| this.header.byteRate = this.header.blockAlign * this.sampleRate; | ||
| this.header.subChunk2Size = this.data.length * (this.header.bitsPerSample >> 3); | ||
| this.header.chunkSize = 36 + this.header.subChunk2Size; | ||
|
|
||
| this.wav = this.header.chunkId.concat( | ||
| u32ToArray(this.header.chunkSize), | ||
| this.header.format, | ||
| this.header.subChunk1Id, | ||
| u32ToArray(this.header.subChunk1Size), | ||
| u16ToArray(this.header.audioFormat), | ||
| u16ToArray(this.header.numChannels), | ||
| u32ToArray(this.header.sampleRate), | ||
| u32ToArray(this.header.byteRate), | ||
| u16ToArray(this.header.blockAlign), | ||
| u16ToArray(this.header.bitsPerSample), | ||
| this.header.subChunk2Id, | ||
| u32ToArray(this.header.subChunk2Size), | ||
| (this.header.bitsPerSample == 16) ? split16bitArray(this.data) : this.data | ||
| ); | ||
| this.dataURI = 'data:audio/wav;base64,'+FastBase64.Encode(this.wav); | ||
| }; | ||
|
|
||
| if (data instanceof Array) this.Make(data); | ||
|
|
||
| }; // end RIFFWAVE | ||
|
|
| @@ -0,0 +1,19 @@ | ||
| body { background:var(--background) !important; color:var(--f_med); } | ||
| .bg { background:var(--background) !important } | ||
| .f_high { color:var(--f_high) !important; stroke:var(--f_high) !important } | ||
| .f_med { color:var(--f_med) !important ; stroke:var(--f_med) !important } | ||
| .f_low { color:var(--f_low) !important ; stroke:var(--f_low) !important } | ||
| .f_inv { color:var(--f_inv) !important ; stroke:var(--f_inv) !important } | ||
| .b_high { background:var(--b_high) !important; fill:var(--b_high) !important } | ||
| .b_med { background:var(--b_med) !important ; fill:var(--b_med) !important } | ||
| .b_low { background:var(--b_low) !important ; fill:var(--b_low) !important } | ||
| .b_inv { background:var(--b_inv) !important ; fill:var(--b_inv) !important } | ||
|
|
||
| #debug { color:var(--f_high); } | ||
|
|
||
| #dis_f_high { background: var(--f_high) !important } | ||
| #dis_f_med { background: var(--f_med) !important } | ||
| #dis_f_low { background: var(--f_low) !important } | ||
| #dis_b_high { background: var(--b_high) !important } | ||
| #dis_b_med { background: var(--b_med) !important } | ||
| #dis_b_low { background: var(--b_low) !important } |
| @@ -0,0 +1,170 @@ | ||
| 'use strict' | ||
|
|
||
| /* global localStorage */ | ||
| /* global FileReader */ | ||
| /* global DOMParser */ | ||
|
|
||
| function Theme (client) { | ||
| this.el = document.createElement('style') | ||
| this.el.type = 'text/css' | ||
|
|
||
| this.active = {} | ||
| this.default = { | ||
| background: "#111111", | ||
| f_high: "#efefef", | ||
| f_med: "#ff4444", | ||
| f_low: "#333333", | ||
| f_inv: "#000000", | ||
| b_high: "#666666", | ||
| b_med: "#444444", | ||
| b_low: "#222222", | ||
| b_inv: "#ff4444" | ||
| } | ||
|
|
||
| // Callbacks | ||
| this.onLoad = () => {} | ||
|
|
||
| this.install = (host = document.body) => { | ||
| window.addEventListener('dragover', this.drag) | ||
| window.addEventListener('drop', this.drop) | ||
| host.appendChild(this.el) | ||
| } | ||
|
|
||
| this.start = () => { | ||
| console.log('Theme', 'Starting..') | ||
| if (isJson(localStorage.theme)) { | ||
| const storage = JSON.parse(localStorage.theme) | ||
| if (isValid(storage)) { | ||
| console.log('Theme', 'Loading theme in localStorage..') | ||
| this.load(storage) | ||
| return | ||
| } | ||
| } | ||
| this.load(this.default) | ||
| } | ||
|
|
||
| this.open = () => { | ||
| console.log('Theme', 'Open theme..') | ||
| const input = document.createElement('input') | ||
| input.type = 'file' | ||
| input.onchange = (e) => { | ||
| this.read(e.target.files[0], this.load) | ||
| } | ||
| input.click() | ||
| } | ||
|
|
||
| this.load = (data) => { | ||
| const theme = this.parse(data) | ||
| if (!isValid(theme)) { console.warn('Theme', 'Invalid format'); return } | ||
| console.log('Theme', 'Loaded theme!') | ||
| this.el.innerHTML = `:root { | ||
| --background: ${theme.background}; | ||
| --f_high: ${theme.f_high}; | ||
| --f_med: ${theme.f_med}; | ||
| --f_low: ${theme.f_low}; | ||
| --f_inv: ${theme.f_inv}; | ||
| --b_high: ${theme.b_high}; | ||
| --b_med: ${theme.b_med}; | ||
| --b_low: ${theme.b_low}; | ||
| --b_inv: ${theme.b_inv}; | ||
| }` | ||
| localStorage.setItem('theme', JSON.stringify(theme)) | ||
| this.active = theme | ||
| if (this.onLoad) { | ||
| this.onLoad(data) | ||
| } | ||
| } | ||
|
|
||
| this.reset = () => { | ||
| this.load(this.default) | ||
| } | ||
|
|
||
| this.set = (key, val) => { | ||
| if (!val) { return } | ||
| const hex = (`${val}`.substr(0, 1) !== '#' ? '#' : '') + `${val}` | ||
| if (!isColor(hex)) { console.warn('Theme', `${hex} is not a valid color.`); return } | ||
| this.active[key] = hex | ||
| } | ||
|
|
||
| this.get = (key) => { | ||
| return this.active[key] | ||
| } | ||
|
|
||
| this.parse = (any) => { | ||
| if (isValid(any)) { return any } | ||
| if (isJson(any)) { return JSON.parse(any) } | ||
| if (isHtml(any)) { return extract(any) } | ||
| } | ||
|
|
||
| // Drag | ||
|
|
||
| this.drag = (e) => { | ||
| e.stopPropagation() | ||
| e.preventDefault() | ||
| e.dataTransfer.dropEffect = 'copy' | ||
| } | ||
|
|
||
| this.drop = (e) => { | ||
| e.preventDefault() | ||
| const file = e.dataTransfer.files[0] | ||
| if (file.name.indexOf('.svg') > -1) { | ||
| this.read(file, this.load) | ||
| } | ||
| e.stopPropagation() | ||
| } | ||
|
|
||
| this.read = (file, callback) => { | ||
| const reader = new FileReader() | ||
| reader.onload = (event) => { | ||
| callback(event.target.result) | ||
| } | ||
| reader.readAsText(file, 'UTF-8') | ||
| } | ||
|
|
||
| // Helpers | ||
|
|
||
| function extract (xml) { | ||
| const svg = new DOMParser().parseFromString(xml, 'text/xml') | ||
| try { | ||
| return { | ||
| background: svg.getElementById('background').getAttribute('fill'), | ||
| f_high: svg.getElementById('f_high').getAttribute('fill'), | ||
| f_med: svg.getElementById('f_med').getAttribute('fill'), | ||
| f_low: svg.getElementById('f_low').getAttribute('fill'), | ||
| f_inv: svg.getElementById('f_inv').getAttribute('fill'), | ||
| b_high: svg.getElementById('b_high').getAttribute('fill'), | ||
| b_med: svg.getElementById('b_med').getAttribute('fill'), | ||
| b_low: svg.getElementById('b_low').getAttribute('fill'), | ||
| b_inv: svg.getElementById('b_inv').getAttribute('fill') | ||
| } | ||
| } catch (err) { | ||
| console.warn('Theme', 'Incomplete SVG Theme', err) | ||
| } | ||
| } | ||
|
|
||
| function isValid (json) { | ||
| if (!json) { return false } | ||
| if (!json.background || !isColor(json.background)) { return false } | ||
| if (!json.f_high || !isColor(json.f_high)) { return false } | ||
| if (!json.f_med || !isColor(json.f_med)) { return false } | ||
| if (!json.f_low || !isColor(json.f_low)) { return false } | ||
| if (!json.f_inv || !isColor(json.f_inv)) { return false } | ||
| if (!json.b_high || !isColor(json.b_high)) { return false } | ||
| if (!json.b_med || !isColor(json.b_med)) { return false } | ||
| if (!json.b_low || !isColor(json.b_low)) { return false } | ||
| if (!json.b_inv || !isColor(json.b_inv)) { return false } | ||
| return true | ||
| } | ||
|
|
||
| function isColor (hex) { | ||
| return /^#([0-9A-F]{3}){1,2}$/i.test(hex) | ||
| } | ||
|
|
||
| function isJson (text) { | ||
| try { JSON.parse(text); return true } catch (error) { return false } | ||
| } | ||
|
|
||
| function isHtml (text) { | ||
| try { new DOMParser().parseFromString(text, 'text/xml'); return true } catch (error) { return false } | ||
| } | ||
| } |