diff --git a/danijela.js b/danijela.js index 0aa3276..d26943d 100644 --- a/danijela.js +++ b/danijela.js @@ -5,10 +5,28 @@ const Quat = require('quaternion'); // Stevilke senzorjev const INDEX_GLAVA = 1 const INDEX_ROKA = 2 +const INDEX_BEKAP = 3 // CC kanali za midi mapping (kanal, CC kanal) -const senzorGlaveCCKanal = [176, 75] -const senzorRokeCCKanal = [176, 76] +const midiKanal = 176; + +const senzorCCKanal = { + 1: 75, + 2: 76, + 3: 77 +} + +const senzorCCMute = { + 1: 78, + 2: 79, + 3: 80 +} + +const senzorCCkalibracija = { + 1: 81, + 2: 82, + 3: 83 +} const DEBUG = { midi: true, @@ -16,42 +34,77 @@ const DEBUG = { } // Kalibracijsko izhodisce -let izhodisceQGlave = new Quat() -let izhodisceQRoke = new Quat() +const izhodisceQ = { + 1: new Quat(), + 2: new Quat(), + 3: new Quat() +} -let prejsnjiQGlave = new Quat() -let prejsnjiQRoke = new Quat() +const prejsnjiQ = { + 1: new Quat(), + 2: new Quat(), + 3: new Quat() +} -let eulerRotacijaRoke = [0, 0, 0] -let eulerRotacijaGlave = [0, 0, 0] +const eulerRotacija = { + 1: [0, 0, 0], + 2: [0, 0, 0], + 3: [0, 0, 0] +} -let senzorGlaveCC = 64 -let senzorRokeCC = 64 +const senzorCC = { + 1: 64, + 2: 64, + 3: 64 +} -let staroVrtenjeZmidi = 0; +let staroVrtenjeZMidi = { + 1: 0, + 2: 0, + 3: 0 +}; + +const muteMidi = { + 1: 0, + 2: 0, + 3: 0 +} // Midi controller input const mi = new midi.Input() mi.openVirtualPort("danijela-midi") mi.on('message', (deltaTime, msg) => { - if (msg[0] === senzorGlaveCCKanal[0] - && msg[1] === senzorGlaveCCKanal[1]) { - senzorGlaveCC = msg[2] - console.log(`senzor glave CC ${senzorGlaveCC}`) - } - if (msg[0] === senzorRokeCCKanal[0] - && msg[1] === senzorRokeCCKanal[1]) { - senzorRokeCC = msg[2] - console.log(`senzor roke CC ${senzorRokeCC}`) + if (msg[0] === midiKanal) { + if (Object.values(senzorCCKanal).includes(msg[1])) { + const s = Object.values(senzorCCKanal).indexOf(msg[1]) + 1 + if (!muteMidi[s]) { + console.log(`senzor ${s}: ${msg[2]}`) + } + } else if (Object.values(senzorCCMute).includes(msg[1])) { + const s = Object.values(senzorCCMute).indexOf(msg[1]) + 1 + if (msg[2]) { + console.log(`senzor UNMUTE ${s}`) + muteMidi[s] = false; + } else { + muteMidi[s] = true; + console.log(`senzor MUTE ${s}`) + } + } else if (Object.values(senzorCCkalibracija).includes(msg[1])) { + const s = Object.values(senzorCCkalibracija).indexOf(msg[1]) + 1 + izhodisceQ[s] = prejsnjiQ[s].conjugate() + console.log(`senzor KALIBRACIJA ${s}`) + } } // console.log(`midi in: ${msg} d: ${deltaTime}`); + /* if (senzorGlaveCC === 0 && senzorRokeCC === 0) { console.log('===== KALIBRACIJA! =====') let izhodisceQGlave = prejsnjiQGlave.conjugate() let izhodisceQRoke = prejsnjiQRoke.conjugate() } + */ }) // Razpon za pospeskomerje @@ -88,7 +141,7 @@ function posljiMidi(kanal, vrednost, jakost) { }) } function posljiMidiCC(kanal, vrednost) { - posljiMidi(176, kanal, vrednost); + posljiMidi(midiKanal, kanal, vrednost); } //const norm = (val, min, max) => Math.min(1, (val - min) / (max - min)) @@ -139,23 +192,23 @@ oscWS.on('message', ({ address, args }) => { if (addr == 'quaternion') { //console.log('IDX', index) // Izracunaj spremembo rotacije - const novQGlave = new Quat({w: args[0], x: args[1], y: args[2], z: args[3]}) - const novQGlaveC = novQGlave.mul(izhodisceQGlave) - const qWdGlave = novQGlaveC.div(prejsnjiQGlave) - prejsnjiQGlave = novQGlaveC; + const novQ = new Quat({w: args[0], x: args[1], y: args[2], z: args[3]}) + const novQC = novQ.mul(izhodisceQ[index]) + const qWd = novQC.div(prejsnjiQ[index]) + prejsnjiQ[index] = novQC[index]; - const eulerD = qWdGlave.toEuler() - eulerRotacijaGlave[0] += eulerD.roll * (senzorGlaveCC / 64); - eulerRotacijaGlave[1] += eulerD.pitch * (senzorGlaveCC / 64); - eulerRotacijaGlave[2] += eulerD.yaw * (senzorGlaveCC / 64); + const eulerD = qWd[index].toEuler() + eulerRotacija[index][0] += eulerD.roll * (senzorCC[index] / 64); + eulerRotacija[index][1] += eulerD.pitch * (senzorCC[index] / 64); + eulerRotacija[index][2] += eulerD.yaw * (senzorCC[index] / 64); - const vrtenjeX = Math.abs(Math.sin(eulerRotacijaGlave[0])); + const vrtenjeX = Math.abs(Math.sin(eulerRotacija[index][0])); /* posljiMidi(180, 20, Math.round(vrtenjeX * 127)); */ // Roka dol/gor - const vrtenjeY = Math.abs(Math.sin(eulerRotacijaGlave[1])); + const vrtenjeY = Math.abs(Math.sin(eulerRotacija[index][1])); //posljiMidi(180, 20, Math.round(vrtenjeY * 127)); //posljiMidi(181, 20, Math.round(vrtenjeY * 127)); @@ -163,29 +216,16 @@ oscWS.on('message', ({ address, args }) => { // Vrtenje levo/desno (stegnjena roka) //console.log(eulerRotacijaGlave) - const vrtenjeZ = normSin(eulerRotacijaGlave[2]) + const vrtenjeZ = normSin(eulerRotacija[index][2]) const vrtenjeZmidi = Math.round(vrtenjeZ * 127); //console.log('vrtenjeZ', vrtenjeZmidi) //console.log('cmp', vrtenjeZmidi, staroVrtenjeZmidi); - if ((vrtenjeZmidi !== staroVrtenjeZmidi) - && (senzorRokeCC !== 0)) { + if (vrtenjeZmidi !== staroVrtenjeZMidi[index] + && !muteMidi[index]) { posljiMidi(182, 20, vrtenjeZmidi); - staroVrtenjeZmidi = vrtenjeZmidi; + staroVrtenjeZMidi[index] = vrtenjeZmidi; } - - //console.log('VRTENJE', euler.map(x => x.toFixed(3))); - - /* - if (vrtenjeZ > 0.95 && !cakaj) { - console.log('POSLJI TON!'); - //posljiMidi(151, 60, 127); - cakaj = true; - setTimeout(function () { - cakaj = false; - }, 250); - } - */ } } }) diff --git a/webmidi/midipanel.html b/webmidi/midipanel.html new file mode 100644 index 0000000..5568502 --- /dev/null +++ b/webmidi/midipanel.html @@ -0,0 +1,66 @@ + + + + Gravitacija Perspektive Midi + + + + + +
+ + + + + + + + + + + + + + + +
+ diff --git a/webmidi/midipanel_files/bundle.js b/webmidi/midipanel_files/bundle.js new file mode 100644 index 0000000..faf6992 --- /dev/null +++ b/webmidi/midipanel_files/bundle.js @@ -0,0 +1,75 @@ +(()=>{"use strict";var e={305:(e,n,t)=>{t.d(n,{v:()=>_n});var o={};function r(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);n&&(o=o.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,o)}return t}function a(e){for(var n=1;nqe,set:()=>Fe});var c=new Map;function l(e){var n=c.get(e);return void 0===n&&(n=e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),c.set(e,n)),n}function d(e,n){var t=window.ShadyCSS;return t&&!t.nativeShadow?e(t):n}function u(e){return"<".concat(String(e.tagName).toLowerCase(),">")}var s="ActiveXObject"in window,f=Promise.resolve(),p=new WeakMap;function b(e){return(b="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var v=function(e){return e},h=function(e){if("object"!==b(e))throw TypeError("Assigned value must be an object: ".concat(b(e)));return e&&Object.freeze(e)};function m(e,n){var t=b(e),o=v;switch(t){case"string":o=String;break;case"number":o=Number;break;case"boolean":o=Boolean;break;case"function":e=(o=e)();break;case"object":e&&Object.freeze(e),o=h}return{get:function(n){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:e;return t},set:function(e,n,t){return o(n,t)},connect:"object"!==t&&"undefined"!==t?function(t,r,a){if(t[r]===e){var i=l(r);if(t.hasAttribute(i)){var c=t.getAttribute(i);t[r]=""===c&&o===Boolean||c}}return n&&n(t,r,a)}:n}}function g(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);n&&(o=o.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,o)}return t}function y(e){for(var n=1;n1&&void 0!==arguments[1]?arguments[1]:{};if("function"!=typeof e)throw TypeError("The first argument must be a function: ".concat(x(e)));var t=y({shadowRoot:!0},n),o={mode:"open"};return"object"===x(t.shadowRoot)&&Object.assign(o,t.shadowRoot),{get:function(n){var r=e(n),a=n;return t.shadowRoot&&(n.shadowRoot||n.attachShadow(o),a=n.shadowRoot),function(){return r(n,a),a}},observe:function(e,n){n()}}}(a):{get:a}:"object"!==i||null===a||Array.isArray(a)?m(a):{get:a.get||G,set:a.set||!a.get&&G||void 0,connect:a.connect,observe:a.observe},Object.defineProperty(e.prototype,o,{get:function(){return T(this,o,r.get)},set:r.set&&function(e){!function(e,n,t,o){var r=_(e,n),a=t(e,o,r.value);a!==r.value&&(r.checksum=0,r.state+=1,r.value=a,I(r))}(this,o,r.set,e)},enumerable:!0,configurable:"production"!==X.env.NODE_ENV}),r.observe&&t.unshift((function(e){return D(e,o,r.get,r.observe)})),r.connect&&t.push((function(e){return r.connect(e,o,(function(){C(e,o)}))}))}))}if("production"!==X.env.NODE_ENV){var Z=function e(n,t){t(n),Array.from(n.children).forEach((function(n){return e(n,t)})),n.shadowRoot&&Array.from(n.shadowRoot.children).forEach((function(n){return e(n,t)}))},K=new Map;H=function(e,n){K.size||f.then((function(){Z(document.body,(function(e){if(K.has(e.constructor)){var n=K.get(e.constructor);e.disconnectedCallback(),Object.keys(e.constructor.hybrids).forEach((function(t){C(e,t,e.constructor.hybrids[t]!==n[t])})),e.connectedCallback()}})),K.clear()})),K.set(e,n)}}var Q=new WeakMap;function ee(e,n){var t=W(n);if("object"!==t&&"function"!==t)throw TypeError("Second argument must be an object or a function: ".concat(t));if(null!==e){var o=window.customElements.get(e);if("function"===t)return o!==n?window.customElements.define(e,n):o;if(o){if(o.hybrids===n)return o;if("production"!==X.env.NODE_ENV&&o.hybrids){Object.keys(o.hybrids).forEach((function(e){delete o.prototype[e]}));var r=o.hybrids;return J(o,n),H(o,r),o}throw Error("Element '".concat(e,"' already defined"))}}var a=function(e){!function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),n&&B(e,n)}(a,e);var n,t,o,r=(n=a,function(){var e,t=q(n);if(F()){var o=q(this).constructor;e=Reflect.construct(t,arguments,o)}else e=t.apply(this,arguments);return L(this,e)});function a(){var e;!function(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this,a),e=r.call(this);for(var n=U.get(a),t=0;t1&&void 0!==arguments[1]?arguments[1]:new Set;return Array.isArray(e)?e.forEach((function(e){return n.add(e)})):null!==e&&"object"===pe(e)?Object.keys(e).forEach((function(t){return e[t]&&n.add(t)})):n.add(e),n}(t);be.set(n,r),r.forEach((function(e){n.classList.add(e),o.delete(e)})),o.forEach((function(e){n.classList.remove(e)}))}function he(e){return(he="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var me=new WeakMap;function ge(e,n,t){if(null===t||"object"!==he(t))throw TypeError("Style value must be an object in ".concat(u(n),":"),t);var o=me.get(n)||new Map,r=Object.keys(t).reduce((function(e,r){var a=l(r),i=t[r];return i||0===i?n.style.setProperty(a,i):n.style.removeProperty(a),e.set(a,i),o.delete(a),e}),new Map);o.forEach((function(e,t){n.style[t]=""})),me.set(n,r)}function ye(e,n,t){if("on"===n.substr(0,2))return o=n.substr(2),function(e,n,t,r){if(r){var a=fe.get(n);a&&n.removeEventListener(o,a.get(r),void 0!==r.options&&r.options)}if(t){if("function"!=typeof t)throw Error("Event listener must be a function: ".concat(se(t)));var i=fe.get(n);i||(i=new WeakMap,fe.set(n,i));var c=t.bind(null,e);i.set(t,c),n.addEventListener(o,c,void 0!==t.options&&t.options)}};var o;switch(e){case"class":return ve;case"style":return ge;default:return function(o,r,a){if(t||r instanceof SVGElement||!(n in r))if(!1===a||null==a)r.removeAttribute(e);else{var i=!0===a?"":String(a);r.setAttribute(e,i)}else r[n]!==a&&(r[n]=a)}}}function we(e,n){return function(e){if(Array.isArray(e))return e}(e)||function(e,n){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(e)){var t=[],o=!0,r=!1,a=void 0;try{for(var i,c=e[Symbol.iterator]();!(o=(i=c.next()).done)&&(t.push(i.value),!n||t.length!==n);o=!0);}catch(e){r=!0,a=e}finally{try{o||null==c.return||c.return()}finally{if(r)throw a}}return t}}(e,n)||function(e,n){if(e){if("string"==typeof e)return xe(e,n);var t=Object.prototype.toString.call(e).slice(8,-1);return"Object"===t&&e.constructor&&(t=e.constructor.name),"Map"===t||"Set"===t?Array.from(t):"Arguments"===t||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t)?xe(e,n):void 0}}(e,n)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function xe(e,n){(null==n||n>e.length)&&(n=e.length);for(var t=0,o=new Array(n);t0&&void 0!==arguments[0]?arguments[0]:0;return"{{h-".concat($e,"-").concat(e,"}}")},ke=Ne("(\\d+)"),_e=new RegExp("^".concat(ke,"$")),Oe=new RegExp(ke,"g"),Ie="--".concat($e,"--"),Me=new RegExp(Ie,"g"),je=new WeakMap,Te="object"===Se(window.ShadyDOM)&&window.ShadyDOM.inUse?function(e){var n;return{get currentNode(){return n},nextNode:function(){if(void 0===n)n=e.childNodes[0];else if(n.childNodes.length)n=n.childNodes[0];else if(n.nextSibling)n=n.nextSibling;else{var t=n.parentNode;for(n=t.nextSibling;!n&&t!==e;)t=t.parentNode,n=t.nextSibling}return!!n}}}:function(e){return document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT,null,!1)},Ae=document.createElement("div"),Ce=new Map;function De(e,n){var t=Ne(n),o=function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,t=(e=e.replace(/(^[\n\s\t ]+)|([\n\s\t ]+$)+/g,"")).indexOf("\n");if(t>-1){var o=0-n-2;for(t+=1;" "===e[t]&&t-1?"| ".concat(e,"\n--").concat("-".repeat(n)).concat("^".repeat(6)):"| ".concat(e)})).join("\n").replace(Oe,"${...}");return"".concat(o)}function Pe(e,n,t){var o=document.createElement("template"),r=[],a=function(e,n){var t=e.reduce((function(n,t,o){return 0===o?t:e.slice(o).join("").match(/^\s*<\/\s*(table|tr|thead|tbody|tfoot|colgroup)>/)?"".concat(n,"\x3c!--").concat(Ne(o-1),"--\x3e").concat(t):n+Ne(o-1)+t}),"");return n&&(t+="")),s?t.replace(/style\s*=\s*(["][^"]+["]|['][^']+[']|[^\s"'<>/]+)/g,(function(e){return"".concat(Ie).concat(e)})):t}(e,t);if(n&&(a="".concat(a,"")),s?o.innerHTML=a:(Ae.innerHTML=""),o.content.appendChild(Ae.children[0].content)),n){var i=o.content.firstChild;o.content.removeChild(i),Array.from(i.childNodes).forEach((function(e){return o.content.appendChild(e)}))}!function(e){for(var n,t=document.createNodeIterator(e,NodeFilter.SHOW_COMMENT,null,!1);n=t.nextNode();)_e.test(n.textContent)&&(n.parentNode.insertBefore(document.createTextNode(n.textContent),n),n.parentNode.removeChild(n))}(o.content);for(var c=Te(o.content),l=0,f=function(){var t=c.currentNode;if(t.nodeType===Node.TEXT_NODE){var o=t.textContent;if(!o.match(_e)){var a=o.match(Oe);if(a){var i=t;a.reduce((function(e,n){var t=we(e.pop().split(n),2),o=t[0],r=t[1];return o&&e.push(o),e.push(n),r&&e.push(r),e}),[o]).forEach((function(e,n){0===n?i.textContent=e:i=i.parentNode.insertBefore(document.createTextNode(e),i.nextSibling)}))}}var d=t.textContent.match(_e);d&&(s||(t.textContent=""),r[d[1]]=[l,ue])}else t.nodeType===Node.ELEMENT_NODE&&Array.from(t.attributes).forEach((function(o){var a=o.value.trim(),i=s?o.name.replace(Ie,""):o.name,c=a.match(_e);if(c){var d=e[c[1]].replace(/\s*=\s*['"]*$/g,"").split(/\s+/).pop();r[c[1]]=[l,ye(i,d,n)],t.removeAttribute(o.name)}else{var u=a.match(Oe);if(u){var f="attr__".concat(i);u.forEach((function(e,n){var t=we(e.match(_e),2)[1];r[t]=[l,function(t,o,r){var c=re(o,{});c[f]=(c[f]||a).replace(e,null==r?"":r),1!==u.length&&n+1!==u.length||(o.setAttribute(i,c[f]),c[f]=void 0)}]})),o.value="",s&&i!==o.name&&(t.removeAttribute(o.name),t.setAttribute(i,""))}}}));l+=1};c.nextNode();)f();return function(e,n,t,i){var c=re(n,{type:"function"});if(o!==c.template){(c.template||n.nodeType===Node.ELEMENT_NODE)&&ie(n),c.prevArgs=null;var l=document.importNode(function(e,n){return n?d((function(t){var o=je.get(e);o||(o=new Map,je.set(e,o));var r=o.get(n);if(!r){(r=document.createElement("template")).content.appendChild(e.content.cloneNode(!0)),o.set(n,r);var a=r.content.querySelectorAll("style");Array.from(a).forEach((function(e){for(var n=e.childNodes.length+1,t=0;t-1&&!customElements.get(m.tagName.toLowerCase()))throw Error("Missing ".concat(u(m)," element definition in ").concat(u(e)));for(;v&&v[0]===b;)h.push([m,v[1]]),v=p.shift();b+=1}if(n.nodeType===Node.TEXT_NODE){c.startNode=l.childNodes[0],c.endNode=l.childNodes[l.childNodes.length-1];for(var g=n,y=l.childNodes[0];y;)n.parentNode.insertBefore(y,g.nextSibling),g=y,y=l.childNodes[0]}else n.appendChild(l)}var w=n.adoptedStyleSheets;if(i){var x=!1;if((i=i.map((function(e){if(e instanceof CSSStyleSheet)return e;var n=Ce.get(e);return n||((n=new CSSStyleSheet).replaceSync(e),Ce.set(e,n)),n}))).length===w.length){x=!0;for(var S=0;S2&&void 0!==arguments[2]?arguments[2]:200;return function(o,r){var a;n&&(a=setTimeout((function(){a=void 0,requestAnimationFrame((function(){n(o,r)}))}),t)),Be.set(r,e),e.then((function(n){a&&clearTimeout(a),Be.get(r)===e&&(n(o,r),Be.set(r,null))}))}}var We=Ne(),Xe=Ne("svg"),He=/@import/,Ge=new Map,Ye=new WeakMap,Ue={define:function(e){return te(e),this},key:function(e){return this.id=e,this},style:function(){for(var e=arguments.length,n=new Array(e),t=0;t1&&void 0!==arguments[1]?arguments[1]:r,c=Ye.get(o),l=e.join(We);if(c){var d=c.join(We);(a=!!i.adoptedStyleSheets&&!He.test(d))||(l+=d)}t&&(l+=Xe);var u=Ge.get(l);u||(u=Pe(e,t,!a&&c),Ge.set(l,u)),u(r,i,n,a&&c)}),Ue)}function Ze(e){for(var n=arguments.length,t=new Array(n>1?n-1:0),o=1;o1?n-1:0),o=1;o{const a=`\n :host{\n flex-grow: ${e};\n margin: ${n}rem;\n display: flex;\n flex-direction: ${r};\n\n }\n div {\n display: flex;\n flex-direction: ${r};\n flex: 1 1;\n border-radius: var(--b-radius);\n border: ${t?"var(--b-width) solid":"none"}; \n border-color: ${o};\n\n }`;return Ze`
`.style(a)}};te("group-row",Ke);var Qe,en,nn,tn=Object.assign({},Ke);async function on(){try{return Qe||(Qe=await navigator.requestMIDIAccess()),Qe}catch(e){console.error("MIDI getAccess failed",e)}}function rn(e,n=1,t=0){if(!en)return;const o=nn>0?nn:n;un(e,"note"),un(t,"velocity"),dn(o),console.debug(`MIDI note off - note:${e}, velo:${t}, channel:${o}`),en.send([o-1+128,e,t])}function an(e,n=1,t=0){if(!en)return;const o=nn>0?nn:n;un(e,"CC"),un(t),dn(o),console.debug(`MIDI control - cc:${e}, val:${t}, channel:${o}`),en.send([o-1+176,e,t])}function cn(e,n=1,t=0,o=!1){if(!en)return;const r=nn>0?nn:n;try{const a=e.split(","),i=parseInt(a[0].trim()),c=parseInt(a[1].trim());if(un(i,"NRPN LSB"),un(c,"NRPN MSB"),dn(r),en.send([n-1+176,99,c]),en.send([n-1+176,98,i]),console.debug(`MIDI nrpn - lsb:${i}, msb:${c}, val:${t}, channel:${r}, highRes:${o}`),o){if(!Number.isInteger(t))throw"Bad value";const e=Math.floor(t/128),o=Math.floor(t%128);en.send([n-1+176,6,e]),en.send([n-1+176,38,o])}else un(t),en.send([n-1+176,6,t])}catch(n){console.warn("Malformed NRPN, it should be two integers separated by commas:",e,t)}}function ln(e,n=1,t=0){if(!en)return;const o=nn>0?nn:n;try{const n=e.split(","),r=parseInt(n[0].trim()),a=parseInt(n[1].trim());un(r,"Bank LSB"),un(a,"Bank MSB"),dn(o),console.debug(`MIDI prog change - lsb:${r}, msb:${a}, progNum:${t}, channel:${o}`),en.send([o-1+176,0,a]),en.send([o-1+176,32,r]),en.send([o-1+192,t])}catch(n){console.warn("Malformed program change number, it should be two integers separated by commas:",e,t)}}function dn(e){if(!(e>0&&e<=16&&Number.isInteger(e)))throw`MIDI channel '${e}' is invalid, should be an integer between 1 and 16`}function un(e,n="value"){if(!(e>=0&&e<=127&&Number.isInteger(e)))throw`MIDI value '${e}' for '${n}' is invalid, should be an integer between 0 and 127`}tn._direction="column",te("group-column",tn);const sn=window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1);function fn(e,n,t){return Math.min(Math.max(e,n),t)}function pn(e,n="",t="",o="",r="",a=""){let i=e.replaceAll("\\n","
");var c;return i=i.replaceAll(/%v([-+]\d+)?/g,((e,t)=>t?n+parseInt(t):n)),i=i.replaceAll("%h",o),i=i.replaceAll("%t",r),i=i.replaceAll("%n",a),i=i.replaceAll("%a",["C","C♯","D","D♯","E","F","F♯","G","G♯","A","A♯","B"][(c=a)%12]+(Math.floor(c/12)-1)),i=i.replaceAll("%p",t),i=i.replaceAll("%%","%"),i}function bn(e="",n="60"){return e.startsWith("#")?`${e}${n}`:`${function(e){var n={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",honeydew:"#f0fff0",hotpink:"#ff69b4","indianred ":"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgrey:"#d3d3d3",lightgreen:"#90ee90",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370d8",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#d87093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",rebeccapurple:"#663399",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"};return void 0!==n[e.toLowerCase()]?n[e.toLowerCase()]:"#ffffff"}(e)}${n}`}function vn(e,n,t){t!==Number.MIN_SAFE_INTEGER&&_n.restoreValues&&localStorage.setItem(`touchmidi.${sn}.${e}.${n}`,t)}function hn(e,n=""){n||(n=`${e.cc}${e.chan}${e.nrpn}`);const t=e.tagName.toLowerCase();return localStorage.getItem(`touchmidi.${sn}.${t}.${n}`)}function mn(){for(let[e,n]of Object.entries(localStorage))e.startsWith(`touchmidi.${sn}`)&&localStorage.removeItem(e)}const gn="#dialog {\n border: var(--b-width) solid;\n border-color: #11dd11;\n border-radius: var(--b-radius);\n position: absolute;\n top: 50%;\n left: 50%;\n padding: 1rem;\n transform: translate(-50%, -50%);\n z-index: 100;\n background-color: var(--bg);\n width: 80%;\n color: #dddddd;\n}\n\n#container {\n display: flex;\n flex-direction: column;\n}\n\n@media (orientation: landscape) {\n #container {\n flex-direction: row;\n }\n}\n\n.box {\n flex: 1;\n margin: 0.5rem;\n font-size: min(6vw, 1.5rem);\n}\n@media (orientation: landscape) {\n #container {\n flex-direction: row;\n }\n}\n\nlabel {\n font-size: min(5vw, 1.3rem);\n}\n\nINPUT[type='checkbox'] {\n width: min(5vw, 1.3rem);\n height: min(5vw, 1.3rem);\n border-radius: var(--b-radius);\n}\n\nselect {\n scrollbar-width: none;\n background-color: var(--bg);\n color: #bbbbbb;\n font-size: min(5vw, 1.3rem);\n width: 100%;\n overflow-y: auto;\n border: var(--b-width) solid;\n border-color: #222222;\n border-radius: var(--b-radius);\n margin: 0.2rem;\n outline: none;\n font-family: var(--font);\n}\n\n#start {\n border: var(--b-width) solid;\n border-radius: var(--b-radius);\n background-color: var(--bg);\n font-size: 2rem;\n font-family: var(--font);\n margin-top: 1rem;\n width: 100%;\n background-color: darkgreen;\n color: white;\n border-color: #11dd11;\n}\n\n.disabled {\n color: #444444 !important;\n background-color: #111111 !important;\n border-color: #666666 !important;\n pointer-events: none !important;\n}\n\n#title {\n text-align: right;\n display: flex;\n justify-content: flex-end;\n color: #666666;\n}\n\n#title a {\n color: #666666;\n text-decoration: none;\n}\n";let yn={deviceId:"output-1",globalChannel:1,restoreValues:!1};function wn(e){const n=e.shadowRoot.querySelector("#deviceList").value,t=parseInt(e.shadowRoot.querySelector("#channel").value)||0,o=e.shadowRoot.querySelector("#restoreValues").checked,r=e.shadowRoot.querySelector("#fullScreen").checked;if(!n)return;o||mn();const i=window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1);localStorage.setItem(`touchmidi.${i}.config`,JSON.stringify({deviceId:n,globalChannel:t,restoreValues:o})),function(e,n){var t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};e.dispatchEvent(new CustomEvent(n,a({bubbles:!1},t)))}(e,"config-done",{detail:{deviceId:n,globalChannel:t,restoreValues:o}}),r&&document.body.requestFullscreen(),document.getElementById("pageMask").style.display="none",document.body.removeChild(e)}function xn(e){e.shadowRoot.querySelector("#start").classList.remove("disabled")}function Sn(e,n){if(n.restoreValue)e.value=n.restoreValue;else{e._previousPos={x:n.x,y:n.y};var t=0;(t=e.horizontal?e.value+n.dx:e.value-n.dy)>e.max&&(t=e.max),t0&&rn(e.note,e.chan),e.cc>0&&e.valueOff&&an(e.cc,e.chan,e.valueOff),e.nrpn&&e.valueOff&&cn(e.nrpn,e.chan,e.valueOff,e.nrpnHires))}function $n(e,n){n.preventDefault(),e.toggle?e._pressed=!e._pressed:e._pressed=!0,e._pressed?(e.cc>0&&an(e.cc,e.chan,e.value),e.nrpn&&cn(e.nrpn,e.chan,e.value,e.nrpnHires),e.note>0&&function(e,n=1,t=127){if(!en)return;const o=nn>0?nn:n;un(e,"note"),un(t,"velocity"),dn(o),console.debug(`MIDI note on - note:${e}, velo:${t}, channel:${o}`),en.send([o-1+144,e,t])}(e.note,e.chan,e.velo),e.prog&&ln(e.prog,e.chan,e.value)):e.toggle&&(e.note>0&&rn(e.note,e.chan),e.cc>0&&e.valueOff&&an(e.cc,e.chan,e.valueOff),e.nrpn&&e.valueOff&&cn(e.nrpn,e.chan,e.valueOff,e.nrpnHires),e.prog&&e.valueOff&&ln(e.prog,e.chan,e.valueOff))}function Nn(e,n){n.preventDefault(),"inc"==n.target.id?e.value=e.value+1:e.value=e.value-1,e.value>e.max&&(e.value=e.max),e.value0&&an(e.cc,e.chan,e.value)}te("midi-config",{render:()=>{if(document.getElementById("pageMask").style.display="block",!Qe)return void alert("No MIDI access, something went wrong!\nYou should never see this error!");if(0==Qe.outputs.size)return Ze`
+
+ No MIDI output devices were found 😥

Please attach a MIDI device and try again +
+ +
`.style(gn);const e=Array.from(Array(16).keys()),n=window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1),t=localStorage.getItem(`touchmidi.${n}.config`);let o={};o=t?JSON.parse(t):yn;let r="disabled";return Qe.outputs.forEach((function(e,n){n==o.deviceId&&(r="enabled")})),Ze` +
+ Touch Midi v${"2.2.0"}   [[ GitHub ]] +
+
+ Select MIDI Device + +
+
+
+ MIDI Channels + +
+
+ + +
+ + +
+
+ +
+ `.style(gn)}}),te("midi-slider",{_previousPos:null,_width:30,_update:{observe:Sn},value:Number.MIN_SAFE_INTEGER,chan:1,cc:-1,nrpn:"",min:0,max:127,pitchBend:!1,colour:m("#ffffff"),label:"__unset__",horizontal:!1,labelScale:1,grow:1,render:({value:e,colour:n,label:t,min:o,max:r,cc:a,chan:i,horizontal:c,nrpn:l,pitchBend:d,_width:u,labelScale:s,grow:f})=>{const p=e;vn("midi-slider",`${a}${i}${l}`,e),e>r&&(e=r),e0&&p>Number.MIN_SAFE_INTEGER&&an(a,i,e),l&&p>Number.MIN_SAFE_INTEGER&&cn(l,i,e,r>127),d&&function(e=1,n=0){if(!en)return;const t=nn>0?nn:e;dn(t);const o=Math.floor(n/128),r=Math.floor(n%128);console.debug(`MIDI pitchbend - val_msb:${o}, val_lsb:${r}, channel:${t}`),en.send([t-1+224,o,r])}(i,e),Ze`
`.style(":host {\n flex: 1 1;\n display: flex;\n margin: var(--spacing);\n}\n\ndiv {\n font-size: 4vw;\n display: flex;\n flex: 1 1;\n border: var(--b-width) solid;\n border-radius: var(--b-radius);\n justify-content: center;\n align-items: center;\n text-align: center;\n line-height: 0.9em;\n cursor: pointer;\n overflow: hidden;\n user-select: none;\n}\n",v)}}),te("midi-encoder",{_previousPos:null,_width:30,_update:{observe:Sn},value:Number.MIN_SAFE_INTEGER,chan:1,cc:-1,nrpn:"",min:0,max:127,colour:m("#ffffff"),label:"__unset__",horizontal:!1,labelScale:1,grow:1,render:({value:e,colour:n,label:t,min:o,max:r,chan:a,cc:i,nrpn:c,_width:l,labelScale:d,grow:u})=>{const s=e;vn("midi-encoder",`${i}${a}${c}`,e),e>r&&(e=r),e0&&s>Number.MIN_SAFE_INTEGER&&an(i,a,e),c&&s>Number.MIN_SAFE_INTEGER&&cn(c,a,e,r>127);const b=`\n :host {\n flex-grow: ${u}\n }\n #label {\n color: ${n}; \n font-size:${.2*l*d}px\n }`;return Ze`
+ + + + +
+
`.style(":host {\n flex: 1 1;\n display: flex;\n margin: var(--spacing);\n}\n\nsvg {\n height: 100%;\n margin: 0 auto;\n}\n\ndiv {\n flex: auto;\n justify-content: center;\n display: flex;\n align-items: center;\n text-align: center;\n cursor: pointer;\n overflow: hidden;\n user-select: none;\n position: relative;\n}\n\n#label {\n font-size: 5vmin;\n position: absolute;\n z-index: 40;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n text-align: center;\n line-height: 0.9em;\n width: 100%;\n padding: 0.2rem;\n}\n",b)}}),te("midi-button",{_pressed:!1,_width:30,chan:1,cc:-1,nrpn:"",nrpnHires:!1,value:0,valueOff:0,velo:127,note:-1,prog:"",colour:m("#ffffff"),label:"__unset__",toggle:!1,labelScale:1,grow:1,render:({label:e,colour:n,_pressed:t,chan:o,cc:r,nrpn:a,prog:i,note:c,_width:l,labelScale:d,grow:u})=>{"__unset__"===e&&(c>0?e="%a":r>0?e="%t":a?e=a:i&&(e=i));const s=`\n :host {\n flex-grow: ${u}\n }\n button {\n background-color: ${t?`${bn(n)}`:"var(--bg)"};\n color: ${n};\n border-color: ${n};\n font-size: ${.3*l*d}px !important;\n }`;return Ze``.style(":host {\n flex: 1 1;\n display: flex;\n margin: var(--spacing);\n}\n\nbutton {\n font-family: var(--font);\n background-color: var(--bg);\n font-size: 5vw;\n display: flex;\n flex: 1 1;\n border: var(--b-width) solid;\n border-radius: var(--b-radius);\n justify-content: center;\n align-items: center;\n cursor: pointer;\n overflow: hidden;\n width: 1px;\n user-select: none;\n}\n",s)}}),te("midi-pad",{_previousPos:null,_width:30,_update:{observe:(e,n)=>{if(n.restoreValue)return void(e.value=n.restoreValue);e._previousPos={x:n.x,y:n.y};let t=e.valueX+n.dx,o=e.valueY-n.dy;t=fn(t,e.min,e.max),o=fn(o,e.min,e.max),e.valueX=Math.round(t),e.valueY=Math.round(o)}},valueX:Number.MIN_SAFE_INTEGER,valueY:Number.MIN_SAFE_INTEGER,chan:1,ccX:-1,ccY:-1,min:0,max:127,colour:m("#ffffff"),label:"__unset__",labelScale:1,grow:1,render:({valueX:e,valueY:n,colour:t,label:o,min:r,max:a,ccX:i,ccY:c,chan:l,nrpn:d,_width:u,labelScale:s,grow:f})=>{const p=e,b=n;vn("midi-pad",`${i}${l}${d}X`,e),vn("midi-pad",`${c}${l}${d}Y`,n),e=fn(e,r,a),n=fn(n,r,a);let v=Math.round((e-r)/(a-r)*100),h=Math.round((n-r)/(a-r)*100);"__unset__"===o&&(o="%v");const m=`\n :host {\n flex-grow: ${f}\n }\n div {\n color: ${t};\n border-color: ${t};\n }\n #label {\n font-size: ${.4*u*s}px;\n }\n #marker {\n background-color: ${bn(t)};\n border-color: ${t};\n bottom: calc(${h}% - ${.05*u}px);\n left: calc(${v}% - ${.05*u}px);\n width: ${.1*u}px;\n height: ${.1*u}px;\n }`;return i>0&&c>0&&p>Number.MIN_SAFE_INTEGER&&b>Number.MIN_SAFE_INTEGER&&(an(i,l,e),an(c,l,n)),Ze`
+
+ +
`.style(":host {\n flex: 1 1;\n display: flex;\n margin: var(--spacing);\n}\n\ndiv {\n font-size: 4vw;\n display: flex;\n flex: 1 1;\n border: var(--b-width) solid;\n border-radius: var(--b-radius);\n justify-content: center;\n align-items: center;\n text-align: center;\n line-height: 0.9em;\n cursor: pointer;\n overflow: hidden;\n width: 1px;\n user-select: none;\n position: relative;\n}\n\n#marker {\n width: 3vmax;\n height: 3vmax;\n border-radius: 50%;\n position: absolute;\n background-color: white;\n display: block;\n z-index: 10;\n border: 0.3vmax solid white;\n}\n\n#label {\n border: none;\n padding: 1rem;\n}\n",m)}}),te("midi-counter",{_width:30,chan:1,cc:-1,value:0,min:0,max:127,colour:m("#ffffff"),label:"__unset__",labelScale:1,grow:1,render:({colour:e,chan:n,cc:t,_width:o,labelScale:r,grow:a,value:i})=>{vn("midi-counter",`${t}${n}0`,i);const c=`\n :host {\n flex-grow: ${a}\n }\n button:active {\n background-color: ${bn(e)};\n }\n #label {\n font-size: ${.2*o*r}px !important;\n border-color: ${e};\n color: ${e};\n background-color: var(--bg);\n }\n button {\n color: ${e};\n border-color: ${e};\n font-size: ${.3*o*r}px !important;\n }`;return Ze`
+ + + +
`.style(":host {\n flex: 1 1;\n display: flex;\n margin: var(--spacing);\n}\n\ndiv {\n display: flex;\n flex-direction: row;\n flex: 1 1;\n position: relative;\n}\n\nbutton {\n font-family: var(--font);\n background-color: var(--bg);\n font-size: 5vw;\n flex: 1 1;\n border: var(--b-width) solid;\n border-radius: var(--b-radius);\n justify-content: center;\n align-items: center;\n cursor: pointer;\n overflow: hidden;\n user-select: none;\n}\n\n#label {\n position: absolute;\n background-color: var(--bg);\n height: 50%;\n text-align: center;\n width: 25%;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 4;\n font-size: 5vw;\n overflow: hidden;\n display: block;\n pointer-events: none;\n}\n",c)}}),te("midi-spacer",{grow:1,render:({grow:e})=>{const n=`\n :host{\n flex: ${e} 1;\n }\n div {\n flex ${e} 1;\n }`;return Ze`
`.style(n)}}),te("midi-label",{grow:1,render:({grow:e})=>{const n=`\n :host{\n flex: ${e} 1;\n }\n div {\n flex ${e} 1;\n font-size: 4vw;\n }`;return Ze`
`.style(n)}});let kn=[],_n={};function On(e){for(let n of e.changedTouches)kn[n.identifier]=n.target}function In(e){for(let n of e.changedTouches)Cn(n.target,n.screenX,n.screenY)}function Mn(e){for(let n of e.changedTouches)kn.splice(n.identifier,1)}function jn(e){kn[20]=e.target}function Tn(e){Cn(kn[20],e.screenX,e.screenY)}function An(){kn.splice(20,1)}function Cn(e,n,t){if(!e)return;let o=0,r=0;e._previousPos&&(o=n-e._previousPos.x,r=t-e._previousPos.y,o=fn(o,-6,6)/1,r=fn(r,-6,6)/1),e._update={dx:o,dy:r,x:n,y:t}}function Dn(e){e.preventDefault()}function Pn(e){if(!e.deviceId)return void alert("No MIDI device id, MIDI output will fail!");let n=!1;for(let t of Qe.outputs.values())t.id===e.deviceId&&(n=!0);if(n){if(function(e="output-1",n=0){en=e,nn=n}(Qe.outputs.get(e.deviceId),e.globalChannel||0),console.log(`MIDI configured for device '${e.deviceId}' and channel: ${e.globalChannel}`),e.restoreValues){console.log("Restoring saved widget values...");for(let e of document.body.querySelectorAll("midi-slider,midi-encoder,midi-pad,midi-counter"))if("midi-pad"==e.tagName.toLowerCase()){let n=hn(e,`${e.ccX}${e.chan}${e.nrpn}X`),t=hn(e,`${e.ccY}${e.chan}${e.nrpn}Y`);n&&(e.valueX=n),t&&(e.valueY=t)}else{let n=hn(e);n&&(e.value=n)}}}else alert(`The provided MIDI device id '${e.deviceId}' is invalid, MIDI output will fail!`)}function Ln(){for(let e of document.body.querySelectorAll("midi-slider,midi-encoder,midi-pad,midi-button,midi-counter"))e._width=e.clientWidth}window.addEventListener("load",(async()=>{await async function(){const e=document.createElement("div");e.id="pageMask",document.body.append(e);const n=document.createElement("style");n.textContent="@import url('https://fonts.googleapis.com/css2?family=PT+Sans&display=swap');\n\n:root {\n /* these variables are used everywhere */\n --bg: black; /* background colour */\n --b-radius: min(1.9vmin, 15px); /* border radius */\n --b-width: 0.3rem; /* border thickness */\n --spacing: 0.3rem; /* widget spacing */\n\n --font: 'PT Sans', sans-serif; /* Consolas, 'Courier New', monospace; */\n}\n\nhtml,\nbody {\n height: 100%;\n margin: 0;\n padding: 0;\n overflow-y: hidden;\n color: white;\n background-color: var(--bg);\n font-family: var(--font);\n touch-action: none;\n}\n\nbody {\n display: flex;\n flex-direction: column;\n}\n\n/* Used to hide the page behind the config dialog */\n#pageMask {\n background: rgba(0, 0, 0, 0.8);\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n display: none;\n}\n";let t=document.createElement("meta");t.setAttribute("name","viewport"),t.setAttribute("content","width=device-width, user-scalable=no, initial-scale=1.0"),document.getElementsByTagName("head")[0].appendChild(t);let o=document.querySelector("link[rel~='icon']");o||(o=document.createElement("link"),o.rel="icon",o.href="https://raw.githubusercontent.com/benc-uk/touchmidi/main/src/assets/favicon.png",document.getElementsByTagName("head")[0].appendChild(o)),document.head.append(n)}();const e=await on();if(e){for(let n of e.outputs.values())console.log(`MIDI device found --- deviceId: ${n.id}, name: ${n.name}`);null===new URLSearchParams(window.location.search).get("nomidi")&&function(e){const n=new URLSearchParams(window.location.search);if(n.has("channel")||n.has("device")||n.has("restore")){_n=yn,_n.globalChannel=n.has("channel")?parseInt(n.get("channel")):1,_n.deviceId=n.has("device")?n.get("device"):_n.deviceId,_n.restoreValues=n.has("restore")?"true"===n.get("restore"):_n.restoreValues,_n.restoreValues||mn();const e=window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1);return localStorage.setItem(`touchmidi.${e}.config`,JSON.stringify(_n)),void Pn(_n)}const t=document.createElement("midi-config");t.channelNames=void 0,t.addEventListener("config-done",(e=>{Pn(e.detail)})),document.body.appendChild(t)}(),window.addEventListener("resize",Ln),setTimeout(Ln,200),window.addEventListener("mousedown",jn,!1),window.addEventListener("mousemove",Tn,!1),window.addEventListener("mouseup",An,!1),window.addEventListener("touchstart",On,!1),window.addEventListener("touchmove",In,!1),window.addEventListener("touchend",Mn,!1),window.addEventListener("dblclick",Dn,!1),window.addEventListener("contextmenu",Dn,!1)}else document.body.innerHTML='
\n

Failed to get MIDI access 😯

\n
This is likely because your browser doesn\'t support MIDI or permissions were not granted.\n
Also ensure you load the page from a https:// or file:// URL origin.

Try again using Chrome or Edge
'}))}},n={};function t(o){if(n[o])return n[o].exports;var r=n[o]={exports:{}};return e[o](r,r.exports,t),r.exports}t.d=(e,n)=>{for(var o in n)t.o(n,o)&&!t.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:n[o]})},t.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t(305)})(); \ No newline at end of file diff --git a/webmidi/midipanel_files/ruffle.js b/webmidi/midipanel_files/ruffle.js new file mode 100644 index 0000000..ee89abe --- /dev/null +++ b/webmidi/midipanel_files/ruffle.js @@ -0,0 +1,4282 @@ +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ({ + +/***/ 899: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +module.exports = __webpack_require__.p + "f33720e624651f91f06b.wasm"; + +/***/ }), + +/***/ 878: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +module.exports = __webpack_require__.p + "570910818fe53bf73aa8.wasm"; + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // 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] = { +/******/ id: moduleId, +/******/ loaded: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.loaded = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = __webpack_modules__; +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/ensure chunk */ +/******/ (() => { +/******/ __webpack_require__.f = {}; +/******/ // This file contains only the entry chunk. +/******/ // The chunk loading function for additional chunks +/******/ __webpack_require__.e = (chunkId) => { +/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => { +/******/ __webpack_require__.f[key](chunkId, promises); +/******/ return promises; +/******/ }, [])); +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/get javascript chunk filename */ +/******/ (() => { +/******/ // This function allow to reference async chunks +/******/ __webpack_require__.u = (chunkId) => { +/******/ // return url for filenames based on template +/******/ return "" + chunkId + ".js"; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/global */ +/******/ (() => { +/******/ __webpack_require__.g = (function() { +/******/ if (typeof globalThis === 'object') return globalThis; +/******/ try { +/******/ return this || new Function('return this')(); +/******/ } catch (e) { +/******/ if (typeof window === 'object') return window; +/******/ } +/******/ })(); +/******/ })(); +/******/ +/******/ /* webpack/runtime/harmony module decorator */ +/******/ (() => { +/******/ __webpack_require__.hmd = (module) => { +/******/ module = Object.create(module); +/******/ if (!module.children) module.children = []; +/******/ Object.defineProperty(module, 'exports', { +/******/ enumerable: true, +/******/ set: () => { +/******/ throw new Error('ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: ' + module.id); +/******/ } +/******/ }); +/******/ return module; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/load script */ +/******/ (() => { +/******/ var inProgress = {}; +/******/ var dataWebpackPrefix = "ruffle-extension:"; +/******/ // loadScript function to load a script via script tag +/******/ __webpack_require__.l = (url, done, key, chunkId) => { +/******/ if(inProgress[url]) { inProgress[url].push(done); return; } +/******/ var script, needAttach; +/******/ if(key !== undefined) { +/******/ var scripts = document.getElementsByTagName("script"); +/******/ for(var i = 0; i < scripts.length; i++) { +/******/ var s = scripts[i]; +/******/ if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; } +/******/ } +/******/ } +/******/ if(!script) { +/******/ needAttach = true; +/******/ script = document.createElement('script'); +/******/ +/******/ script.charset = 'utf-8'; +/******/ script.timeout = 120; +/******/ if (__webpack_require__.nc) { +/******/ script.setAttribute("nonce", __webpack_require__.nc); +/******/ } +/******/ script.setAttribute("data-webpack", dataWebpackPrefix + key); +/******/ script.src = url; +/******/ } +/******/ inProgress[url] = [done]; +/******/ var onScriptComplete = (prev, event) => { +/******/ // avoid mem leaks in IE. +/******/ script.onerror = script.onload = null; +/******/ clearTimeout(timeout); +/******/ var doneFns = inProgress[url]; +/******/ delete inProgress[url]; +/******/ script.parentNode && script.parentNode.removeChild(script); +/******/ doneFns && doneFns.forEach((fn) => (fn(event))); +/******/ if(prev) return prev(event); +/******/ } +/******/ var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000); +/******/ script.onerror = onScriptComplete.bind(null, script.onerror); +/******/ script.onload = onScriptComplete.bind(null, script.onload); +/******/ needAttach && document.head.appendChild(script); +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/publicPath */ +/******/ (() => { +/******/ __webpack_require__.p = ""; +/******/ })(); +/******/ +/******/ /* webpack/runtime/jsonp chunk loading */ +/******/ (() => { +/******/ __webpack_require__.b = document.baseURI || self.location.href; +/******/ +/******/ // object to store loaded and loading chunks +/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched +/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded +/******/ var installedChunks = { +/******/ 492: 0 +/******/ }; +/******/ +/******/ __webpack_require__.f.j = (chunkId, promises) => { +/******/ // JSONP chunk loading for javascript +/******/ var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined; +/******/ if(installedChunkData !== 0) { // 0 means "already installed". +/******/ +/******/ // a Promise means "currently loading". +/******/ if(installedChunkData) { +/******/ promises.push(installedChunkData[2]); +/******/ } else { +/******/ if(true) { // all chunks have JS +/******/ // setup Promise in chunk cache +/******/ var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject])); +/******/ promises.push(installedChunkData[2] = promise); +/******/ +/******/ // start chunk loading +/******/ var url = __webpack_require__.p + __webpack_require__.u(chunkId); +/******/ // create error before stack unwound to get useful stacktrace later +/******/ var error = new Error(); +/******/ var loadingEnded = (event) => { +/******/ if(__webpack_require__.o(installedChunks, chunkId)) { +/******/ installedChunkData = installedChunks[chunkId]; +/******/ if(installedChunkData !== 0) installedChunks[chunkId] = undefined; +/******/ if(installedChunkData) { +/******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type); +/******/ var realSrc = event && event.target && event.target.src; +/******/ error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'; +/******/ error.name = 'ChunkLoadError'; +/******/ error.type = errorType; +/******/ error.request = realSrc; +/******/ installedChunkData[1](error); +/******/ } +/******/ } +/******/ }; +/******/ __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId); +/******/ } else installedChunks[chunkId] = 0; +/******/ } +/******/ } +/******/ }; +/******/ +/******/ // no prefetching +/******/ +/******/ // no preloaded +/******/ +/******/ // no HMR +/******/ +/******/ // no HMR manifest +/******/ +/******/ // no on chunks loaded +/******/ +/******/ // install a JSONP callback for chunk loading +/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => { +/******/ var [chunkIds, moreModules, runtime] = data; +/******/ // add "moreModules" to the modules object, +/******/ // then flag all "chunkIds" as loaded and fire callback +/******/ var moduleId, chunkId, i = 0; +/******/ if(chunkIds.some((id) => (installedChunks[id] !== 0))) { +/******/ for(moduleId in moreModules) { +/******/ if(__webpack_require__.o(moreModules, moduleId)) { +/******/ __webpack_require__.m[moduleId] = moreModules[moduleId]; +/******/ } +/******/ } +/******/ if(runtime) var result = runtime(__webpack_require__); +/******/ } +/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data); +/******/ for(;i < chunkIds.length; i++) { +/******/ chunkId = chunkIds[i]; +/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) { +/******/ installedChunks[chunkId][0](); +/******/ } +/******/ installedChunks[chunkId] = 0; +/******/ } +/******/ +/******/ } +/******/ +/******/ var chunkLoadingGlobal = self["webpackChunkruffle_extension"] = self["webpackChunkruffle_extension"] || []; +/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); +/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal)); +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. +(() => { + +;// CONCATENATED MODULE: ../core/dist/version.js +/** + * A representation of a semver 2 compliant version string + */ +class Version { + /** + * Construct a Version from specific components. + * + * If you wish to parse a string into a Version then please use [[fromSemver]]. + * + * @param major The major version component. + * @param minor The minor version component. + * @param patch The patch version component. + * @param prIdent A list of pre-release identifiers, if any + * @param buildIdent A list of build identifiers, if any + */ + constructor(major, minor, patch, prIdent, + // @ts-expect-error: Property 'buildIdent' is declared but its value is never read. + buildIdent) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.prIdent = prIdent; + this.buildIdent = buildIdent; + } + /** + * Construct a version from a semver 2 compliant string. + * + * This function is intended for use with semver 2 compliant strings. + * Malformed strings may still parse correctly, but this result is not + * guaranteed. + * + * @param versionString A semver 2.0.0 compliant version string + * @returns A version object + */ + static fromSemver(versionString) { + const buildSplit = versionString.split("+"), prSplit = buildSplit[0].split("-"), versionSplit = prSplit[0].split("."); + const major = parseInt(versionSplit[0], 10); + let minor = 0; + let patch = 0; + let prIdent = null; + let buildIdent = null; + if (versionSplit[1] !== undefined) { + minor = parseInt(versionSplit[1], 10); + } + if (versionSplit[2] !== undefined) { + patch = parseInt(versionSplit[2], 10); + } + if (prSplit[1] !== undefined) { + prIdent = prSplit[1].split("."); + } + if (buildSplit[1] !== undefined) { + buildIdent = buildSplit[1].split("."); + } + return new Version(major, minor, patch, prIdent, buildIdent); + } + /** + * Returns true if a given version is compatible with this one. + * + * Compatibility is defined as having the same nonzero major version + * number, or if both major versions are zero, the same nonzero minor + * version number, or if both minor versions are zero, the same nonzero + * patch version number. + * + * This implements the ^ operator in npm's semver package, with the + * exception of the prerelease exclusion rule. + * + * @param other The other version to test against + * @returns True if compatible + */ + isCompatibleWith(other) { + return ((this.major !== 0 && this.major === other.major) || + (this.major === 0 && + other.major === 0 && + this.minor !== 0 && + this.minor === other.minor) || + (this.major === 0 && + other.major === 0 && + this.minor === 0 && + other.minor === 0 && + this.patch !== 0 && + this.patch === other.patch)); + } + /** + * Returns true if this version has precedence over (is newer than) another + * version. + * + * Precedence is defined as in the Semver 2 spec. This implements the > + * operator in npm's semver package, with the exception of the prerelease + * exclusion rule. + * + * @param other The other version to test against + * @returns True if this version has precedence over the other one + */ + hasPrecedenceOver(other) { + if (this.major > other.major) { + return true; + } + else if (this.major < other.major) { + return false; + } + if (this.minor > other.minor) { + return true; + } + else if (this.minor < other.minor) { + return false; + } + if (this.patch > other.patch) { + return true; + } + else if (this.patch < other.patch) { + return false; + } + if (this.prIdent === null && other.prIdent !== null) { + return true; + } + else if (this.prIdent !== null && other.prIdent !== null) { + const isNumeric = /^[0-9]*$/; + for (let i = 0; i < this.prIdent.length && i < other.prIdent.length; i += 1) { + if (!isNumeric.test(this.prIdent[i]) && + isNumeric.test(other.prIdent[i])) { + return true; + } + else if (isNumeric.test(this.prIdent[i]) && + isNumeric.test(other.prIdent[i])) { + if (parseInt(this.prIdent[i], 10) > + parseInt(other.prIdent[i], 10)) { + return true; + } + else if (parseInt(this.prIdent[i], 10) < + parseInt(other.prIdent[i], 10)) { + return false; + } + } + else if (isNumeric.test(this.prIdent[i]) && + !isNumeric.test(other.prIdent[i])) { + return false; + } + else if (!isNumeric.test(this.prIdent[i]) && + !isNumeric.test(other.prIdent[i])) { + if (this.prIdent[i] > other.prIdent[i]) { + return true; + } + else if (this.prIdent[i] < other.prIdent[i]) { + return false; + } + } + } + return this.prIdent.length > other.prIdent.length; + } + return false; + } + /** + * Tests if a given version is equivalent to this one. + * + * Build and prerelease tags are ignored. + * + * @param other The other version to test against + * @returns True if the given version is equivalent + */ + isEqual(other) { + return (this.major === other.major && + this.minor === other.minor && + this.patch === other.patch); + } + /** + * Tests if a given version is stable or a compatible prerelease for this + * version. + * + * This implements the prerelease exclusion rule of NPM semver: a + * prerelease version can only pass this check if the major/minor/patch + * components of both versions are the same. Otherwise, the prerelease + * version always fails. + * + * @param other The other version to test against + * @returns True if the given version is either stable, or a + * prerelease in the same series as this one. + */ + isStableOrCompatiblePrerelease(other) { + if (other.prIdent === null) { + return true; + } + else { + return (this.major === other.major && + this.minor === other.minor && + this.patch === other.patch); + } + } +} + +;// CONCATENATED MODULE: ../core/dist/version-range.js + +/** + * Represents a set of version requirements. + */ +class VersionRange { + /** + * Constructs a range of versions as specified by the given requirements. + * + * If you wish to construct this object from a string representation, + * then use [[fromRequirementString]]. + * + * @param requirements Requirements to set this range by + */ + constructor(requirements) { + this.requirements = requirements; + } + /** + * Determine if a given version satisfies this range. + * + * @param fver A version object to test against. + * @returns Whether or not the given version matches this range + */ + satisfiedBy(fver) { + for (const requirement of this.requirements) { + let matches = true; + for (const { comparator, version } of requirement) { + matches = + matches && version.isStableOrCompatiblePrerelease(fver); + if (comparator === "" || comparator === "=") { + matches = matches && version.isEqual(fver); + } + else if (comparator === ">") { + matches = matches && fver.hasPrecedenceOver(version); + } + else if (comparator === ">=") { + matches = + matches && + (fver.hasPrecedenceOver(version) || + version.isEqual(fver)); + } + else if (comparator === "<") { + matches = matches && version.hasPrecedenceOver(fver); + } + else if (comparator === "<=") { + matches = + matches && + (version.hasPrecedenceOver(fver) || + version.isEqual(fver)); + } + else if (comparator === "^") { + matches = matches && version.isCompatibleWith(fver); + } + } + if (matches) { + return true; + } + } + return false; + } + /** + * Parse a requirement string into a version range. + * + * @param requirement The version requirements, consisting of a + * series of space-separated strings, each one being a semver version + * optionally prefixed by a comparator or a separator. + * + * Valid comparators are: + * - `""` or `"="`: Precisely this version + * - `">`": A version newer than this one + * - `">`=": A version newer or equal to this one + * - `"<"`: A version older than this one + * - `"<="`: A version older or equal to this one + * - `"^"`: A version that is compatible with this one + * + * A separator is `"||`" which splits the requirement string into + * left OR right. + * @returns A version range object. + */ + static fromRequirementString(requirement) { + const components = requirement.split(" "); + let set = []; + const requirements = []; + for (const component of components) { + if (component === "||") { + if (set.length > 0) { + requirements.push(set); + set = []; + } + } + else if (component.length > 0) { + const match = /[0-9]/.exec(component); + if (match) { + const comparator = component.slice(0, match.index).trim(); + const version = Version.fromSemver(component.slice(match.index).trim()); + set.push({ comparator, version }); + } + } + } + if (set.length > 0) { + requirements.push(set); + } + return new VersionRange(requirements); + } +} + +;// CONCATENATED MODULE: ../../node_modules/wasm-feature-detect/dist/esm/index.js +const bigInt=()=>(async e=>{try{return(await WebAssembly.instantiate(e)).instance.exports.b(BigInt(0))===BigInt(0)}catch(e){return!1}})(new Uint8Array([0,97,115,109,1,0,0,0,1,6,1,96,1,126,1,126,3,2,1,0,7,5,1,1,98,0,0,10,6,1,4,0,32,0,11])),bulkMemory=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,3,1,0,1,10,14,1,12,0,65,0,65,0,65,0,252,10,0,0,11])),exceptions=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,8,1,6,0,6,64,25,11,11])),extendedConst=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,5,3,1,0,1,11,9,1,0,65,1,65,2,106,11,0])),gc=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,10,2,95,1,125,0,96,0,1,107,0,3,2,1,1,10,12,1,10,0,67,0,0,0,0,251,7,0,11])),memory64=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,5,3,1,4,1])),multiValue=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,6,1,96,0,2,127,127,3,2,1,0,10,8,1,6,0,65,0,65,0,11])),mutableGlobals=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,2,8,1,1,97,1,98,3,127,1,6,6,1,127,1,65,0,11,7,5,1,1,97,3,1])),referenceTypes=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,7,1,5,0,208,112,26,11])),relaxedSimd=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,15,1,13,0,65,1,253,15,65,2,253,15,253,128,2,11])),saturatedFloatToInt=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,12,1,10,0,67,0,0,0,0,252,0,26,11])),signExtensions=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,8,1,6,0,65,0,192,26,11])),simd=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11])),streamingCompilation=()=>(async()=>"compileStreaming"in WebAssembly)(),tailCall=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,6,1,4,0,18,0,11])),threads=()=>(async e=>{try{return"undefined"!=typeof MessageChannel&&(new MessageChannel).port1.postMessage(new SharedArrayBuffer(1)),WebAssembly.validate(e)}catch(e){return!1}})(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,4,1,3,1,1,10,11,1,9,0,65,0,254,16,2,0,26,11])); + +;// CONCATENATED MODULE: ../core/dist/js-polyfills.js +/** + * Polyfills the `Array.prototype.reduce` method. + * + * Production steps of ECMA-262, Edition 5, 15.4.4.21 + * Reference: https://es5.github.io/#x15.4.4.21 + * https://tc39.github.io/ecma262/#sec-array.prototype.reduce + */ +function polyfillArrayPrototypeReduce() { + Object.defineProperty(Array.prototype, "reduce", { + value(...args) { + if (args.length === 0 && + window.Prototype && + window.Prototype.Version && + window.Prototype.Version < "1.6.1") { + // Off-spec: compatibility with prototype.js + return this.length > 1 ? this : this[0]; + } + const callback = args[0]; + if (this === null) { + throw new TypeError("Array.prototype.reduce called on null or undefined"); + } + if (typeof callback !== "function") { + throw new TypeError(`${callback} is not a function`); + } + const o = Object(this); + const len = o.length >>> 0; + let k = 0; + let value; + if (args.length >= 2) { + value = args[1]; + } + else { + while (k < len && !(k in o)) { + k++; + } + if (k >= len) { + throw new TypeError("Reduce of empty array with no initial value"); + } + value = o[k++]; + } + while (k < len) { + if (k in o) { + value = callback(value, o[k], k, o); + } + k++; + } + return value; + }, + }); +} +/** + * Polyfills the `Window` function. + */ +function polyfillWindow() { + if (typeof window.constructor !== "function" || + !isNativeFunction(window.constructor)) { + // Don't polyfill `Window` if `window.constructor` has been overridden. + return; + } + // @ts-expect-error: `Function not assignable to { new (): Window; prototype: Window; }` + window.Window = window.constructor; +} +/** + * Polyfills the `Reflect` object and members. + * + * This is a partial implementation, just enough to match our needs. + */ +function tryPolyfillReflect() { + if (window.Reflect === undefined || window.Reflect === null) { + // @ts-expect-error: {} indeed doesn't implement Reflect's interface. + window.Reflect = {}; + } + if (typeof Reflect.get !== "function") { + Object.defineProperty(Reflect, "get", { + value(target, key) { + return target[key]; + }, + }); + } + if (typeof Reflect.set !== "function") { + Object.defineProperty(Reflect, "set", { + value(target, key, value) { + target[key] = value; + }, + }); + } + if (typeof Reflect.has !== "function") { + Object.defineProperty(Reflect, "has", { + value(target, key) { + // @ts-expect-error: Type 'T' is not assignable to type 'object'. + return key in target; + }, + }); + } + if (typeof Reflect.ownKeys !== "function") { + Object.defineProperty(Reflect, "ownKeys", { + value(target) { + return [ + ...Object.getOwnPropertyNames(target), + ...Object.getOwnPropertySymbols(target), + ]; + }, + }); + } +} +/** + * Determines whether a function is native or not. + * + * @param func The function to test. + * @returns True if the function hasn't been overridden. + */ +// eslint-disable-next-line @typescript-eslint/ban-types +function isNativeFunction(func) { + const val = typeof Function.prototype.toString === "function" + ? Function.prototype.toString() + : null; + if (typeof val === "string" && val.indexOf("[native code]") >= 0) { + return (Function.prototype.toString.call(func).indexOf("[native code]") >= 0); + } + return false; +} +/** + * Checks and applies the polyfills to the current window, if needed. + */ +function setPolyfillsOnLoad() { + if (typeof Array.prototype.reduce !== "function" || + !isNativeFunction(Array.prototype.reduce)) { + // Some external libraries override the `Array.prototype.reduce` method in a way + // that causes Webpack to crash (#1507, #1865), so we need to override it again. + polyfillArrayPrototypeReduce(); + } + if (typeof Window !== "function" || !isNativeFunction(Window)) { + // Overriding the native `Window` function causes issues in wasm-bindgen, as a + // code like `window instanceof Window` will no longer work. + polyfillWindow(); + } + // Some pages override the native `Reflect` object, which causes various issues: + // 1- wasm-bindgen's stdlib may crash (#3173). + // 2- FlashVars may be ignored (#8537). + tryPolyfillReflect(); +} + +;// CONCATENATED MODULE: ../core/dist/public-path.js +// This must be in global scope because `document.currentScript` +// works only while the script is initially being processed. +let currentScriptURL = ""; +try { + if (document.currentScript !== undefined && + document.currentScript !== null && + "src" in document.currentScript && + document.currentScript.src !== "") { + let src = document.currentScript.src; + // CDNs allow omitting the filename. If it's omitted, append a slash to + // prevent the last component from being dropped. + if (!src.endsWith(".js") && !src.endsWith("/")) { + src += "/"; + } + currentScriptURL = new URL(".", src).href; + } +} +catch (e) { + console.warn("Unable to get currentScript URL"); +} +/** + * Attempt to discover the public path of the current Ruffle source. This can + * be used to configure Webpack. + * + * A global public path can be specified for all sources using the RufflePlayer + * config: + * + * ```js + * window.RufflePlayer.config.publicPath = "/dist/"; + * ``` + * + * If no such config is specified, then the parent path of where this script is + * hosted is assumed, which should be the correct default in most cases. + * + * @param config The `window.RufflePlayer.config` object. + * @returns The public path for the given source. + */ +function publicPath(config) { + // Default to the directory where this script resides. + let path = currentScriptURL; + if ("publicPath" in config && + config.publicPath !== null && + config.publicPath !== undefined) { + path = config.publicPath; + } + // Webpack expects the paths to end with a slash. + if (path !== "" && !path.endsWith("/")) { + path += "/"; + } + return path; +} + +;// CONCATENATED MODULE: ../core/dist/load-ruffle.js +/** + * Conditional ruffle loader + */ + + + +/** + * Load ruffle from an automatically-detected location. + * + * This function returns a new instance of Ruffle and downloads it every time. + * You should not use it directly; this module will memoize the resource + * download. + * + * @param config The `window.RufflePlayer.config` object. + * @param progressCallback The callback that will be run with Ruffle's download progress. + * @returns A ruffle constructor that may be used to create new Ruffle + * instances. + */ +async function fetchRuffle(config, progressCallback) { + // Apply some pure JavaScript polyfills to prevent conflicts with external + // libraries, if needed. + setPolyfillsOnLoad(); + // NOTE: Keep this list in sync with $RUSTFLAGS in the CI build config! + const extensionsSupported = (await Promise.all([ + bulkMemory(), + simd(), + saturatedFloatToInt(), + signExtensions(), + referenceTypes(), + ])).every(Boolean); + if (!extensionsSupported) { + console.log("Some WebAssembly extensions are NOT available, falling back to the vanilla WebAssembly module"); + } + __webpack_require__.p = publicPath(config); + // Note: The argument passed to import() has to be a simple string literal, + // otherwise some bundler will get confused and won't include the module? + const { default: init, Ruffle } = await (extensionsSupported + ? __webpack_require__.e(/* import() */ 339).then(__webpack_require__.bind(__webpack_require__, 339)) + : __webpack_require__.e(/* import() */ 159).then(__webpack_require__.bind(__webpack_require__, 159))); + let response; + const wasmUrl = extensionsSupported + ? new URL(/* asset import */ __webpack_require__(899), __webpack_require__.b) + : new URL(/* asset import */ __webpack_require__(878), __webpack_require__.b); + const wasmResponse = await fetch(wasmUrl); + if (progressCallback) { + const contentLength = wasmResponse.headers.get("content-length") || ""; + let bytesLoaded = 0; + // Use parseInt rather than Number so the empty string is coerced to NaN instead of 0 + const bytesTotal = parseInt(contentLength); + response = new Response(new ReadableStream({ + async start(controller) { + var _a; + const reader = (_a = wasmResponse.body) === null || _a === void 0 ? void 0 : _a.getReader(); + if (!reader) { + throw "Response had no body"; + } + progressCallback(bytesLoaded, bytesTotal); + for (;;) { + const { done, value } = await reader.read(); + if (done) { + break; + } + if (value === null || value === void 0 ? void 0 : value.byteLength) { + bytesLoaded += value === null || value === void 0 ? void 0 : value.byteLength; + } + controller.enqueue(value); + progressCallback(bytesLoaded, bytesTotal); + } + controller.close(); + }, + }), wasmResponse); + } + else { + response = wasmResponse; + } + await init(response); + return Ruffle; +} +let lastLoaded = null; +/** + * Obtain an instance of `Ruffle`. + * + * This function returns a promise which yields `Ruffle` asynchronously. + * + * @param config The `window.RufflePlayer.config` object. + * @param progressCallback The callback that will be run with Ruffle's download progress. + * @returns A ruffle constructor that may be used to create new Ruffle + * instances. + */ +function loadRuffle(config, progressCallback) { + if (lastLoaded === null) { + lastLoaded = fetchRuffle(config, progressCallback); + } + return lastLoaded; +} + +;// CONCATENATED MODULE: ../core/dist/shadow-template.js +/** + * The shadow template which is used to fill the actual Ruffle player element + * on the page. + */ +const ruffleShadowTemplate = document.createElement("template"); +ruffleShadowTemplate.innerHTML = ` + + + +
+
+
Click to unmute
+ +
+ + + +
    +`; + +;// CONCATENATED MODULE: ../core/dist/register-element.js +/** + * Number of times to try defining a custom element. + */ +const MAX_TRIES = 999; +/** + * A mapping between internal element IDs and DOM element IDs. + */ +const privateRegistry = {}; +/** + * Lookup a previously registered custom element. + * + * The returned object will have `name`, `class`, and `internal_name` + * properties listing the external name, implementing class, and internal name + * respectively. + * + * @param elementName The internal element name, previously used to + * register the element with the private registry. + * @returns The element data in the registry, or null if there is + * no such element name registered. + */ +function lookupElement(elementName) { + const data = privateRegistry[elementName]; + if (data !== undefined) { + return { + internalName: elementName, + name: data.name, + class: data.class, + }; + } + else { + return null; + } +} +/** + * Register a custom element. + * + * This function is designed to be tolerant of naming conflicts. If + * registration fails, we modify the name, and try again. As a result, this + * function returns the real element name to use. + * + * Calling this function multiple times will *not* register multiple elements. + * We store a private registry mapping internal element names to DOM names. + * Thus, the proper way to use this function is to call it every time you are + * about to work with custom element names. + * + * @param elementName The internal name of the element. + * @param elementClass The class of the element. + * + * You must call this function with the same class every time. + * @returns The actual element name. + * @throws Throws an error if two different elements were registered with the + * same internal name. + */ +function registerElement(elementName, elementClass) { + const registration = privateRegistry[elementName]; + if (registration !== undefined) { + if (registration.class !== elementClass) { + throw new Error("Internal naming conflict on " + elementName); + } + else { + return registration.name; + } + } + let tries = 0; + if (window.customElements !== undefined) { + while (tries < MAX_TRIES) { + let externalName = elementName; + if (tries > 0) { + externalName = externalName + "-" + tries; + } + if (window.customElements.get(externalName) !== undefined) { + tries += 1; + continue; + } + else { + window.customElements.define(externalName, elementClass); + } + privateRegistry[elementName] = { + class: elementClass, + name: externalName, + internalName: elementName, + }; + return externalName; + } + } + throw new Error("Failed to assign custom element " + elementName); +} + +;// CONCATENATED MODULE: ../core/dist/config.js +const DEFAULT_CONFIG = { + allowScriptAccess: false, + parameters: {}, + autoplay: "auto" /* AutoPlay.Auto */, + backgroundColor: null, + letterbox: "fullscreen" /* Letterbox.Fullscreen */, + unmuteOverlay: "visible" /* UnmuteOverlay.Visible */, + upgradeToHttps: true, + compatibilityRules: true, + warnOnUnsupportedContent: true, + logLevel: "error" /* LogLevel.Error */, + showSwfDownload: false, + contextMenu: true, + // Backwards-compatibility option + preloader: true, + splashScreen: true, + maxExecutionDuration: 15, + base: null, + menu: true, + salign: "", + quality: "high", + scale: "showAll", + forceScale: false, + frameRate: null, + wmode: "opaque" /* WindowMode.Opaque */, + publicPath: null, + polyfills: true, + playerVersion: null, + preferredRenderer: null, +}; + +;// CONCATENATED MODULE: ../core/dist/flash-identifiers.js +const FLASH_MIMETYPE = "application/x-shockwave-flash"; +const FUTURESPLASH_MIMETYPE = "application/futuresplash"; +const FLASH7_AND_8_MIMETYPE = "application/x-shockwave-flash2-preview"; +const FLASH_MOVIE_MIMETYPE = "application/vnd.adobe.flash.movie"; +const FLASH_ACTIVEX_CLASSID = "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"; + +;// CONCATENATED MODULE: ../core/dist/swf-utils.js + +/** + * Returns whether the given filename ends in a known flash extension. + * + * @param filename The filename to test. + * @returns True if the filename is a flash movie (swf or spl). + */ +function isSwfFilename(filename) { + if (filename) { + let pathname = ""; + try { + // A base URL is required if `filename` is a relative URL, but we don't need to detect the real URL origin. + pathname = new URL(filename, "https://example.com").pathname; + } + catch (err) { + // Some invalid filenames, like `///`, could raise a TypeError. Let's fail silently in this situation. + } + if (pathname && pathname.length >= 4) { + const extension = pathname.slice(-4).toLowerCase(); + if (extension === ".swf" || extension === ".spl") { + return true; + } + } + } + return false; +} +/** + * Returns whether the given MIME type is a known flash type. + * + * @param mimeType The MIME type to test. + * @returns True if the MIME type is a flash MIME type. + */ +function isSwfMimeType(mimeType) { + switch (mimeType.toLowerCase()) { + case FLASH_MIMETYPE.toLowerCase(): + case FUTURESPLASH_MIMETYPE.toLowerCase(): + case FLASH7_AND_8_MIMETYPE.toLowerCase(): + case FLASH_MOVIE_MIMETYPE.toLowerCase(): + return true; + default: + return false; + } +} +/** + * Create a filename to save a downloaded SWF into. + * + * @param swfUrl The URL of the SWF file. + * @returns The filename the SWF file can be saved at. + */ +function swfFileName(swfUrl) { + const pathName = swfUrl.pathname; + const name = pathName.substring(pathName.lastIndexOf("/") + 1); + return name; +} + +;// CONCATENATED MODULE: ../core/dist/build-info.js +/** + * Stores build information. The string literals are replaces at compile time by `set_version.js`. + */ +const buildInfo = { + versionNumber: "0.1.0", + versionName: "nightly 2023-04-29", + versionChannel: "nightly", + buildDate: "2023-04-29T00:20:24.499Z", + commitHash: "1f956ffe55525657dc4748ce39861a995c2fe2a9", +}; + +;// CONCATENATED MODULE: ../core/dist/ruffle-player.js + + + + + + +const RUFFLE_ORIGIN = "https://ruffle.rs"; +const DIMENSION_REGEX = /^\s*(\d+(\.\d+)?(%)?)/; +let isAudioContextUnmuted = false; +/** + * Converts arbitrary input to an easy to use record object. + * + * @param parameters Parameters to sanitize + * @returns A sanitized map of param name to param value + */ +function sanitizeParameters(parameters) { + if (parameters === null || parameters === undefined) { + return {}; + } + if (!(parameters instanceof URLSearchParams)) { + parameters = new URLSearchParams(parameters); + } + const output = {}; + for (const [key, value] of parameters) { + // Every value must be type of string + output[key] = value.toString(); + } + return output; +} +class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } + distanceTo(other) { + const dx = other.x - this.x; + const dy = other.y - this.y; + return Math.sqrt(dx * dx + dy * dy); + } +} +/** + * The ruffle player element that should be inserted onto the page. + * + * This element will represent the rendered and intractable flash movie. + */ +class RufflePlayer extends HTMLElement { + /** + * Indicates the readiness of the playing movie. + * + * @returns The `ReadyState` of the player. + */ + get readyState() { + return this._readyState; + } + /** + * The metadata of the playing movie (such as movie width and height). + * These are inherent properties stored in the SWF file and are not affected by runtime changes. + * For example, `metadata.width` is the width of the SWF file, and not the width of the Ruffle player. + * + * @returns The metadata of the movie, or `null` if the movie metadata has not yet loaded. + */ + get metadata() { + return this._metadata; + } + /** + * Constructs a new Ruffle flash player for insertion onto the page. + */ + constructor() { + super(); + // Allows the user to permanently disable the context menu. + this.contextMenuForceDisabled = false; + // Whether this device is a touch device. + // Set to true when a touch event is encountered. + this.isTouch = false; + // Whether this device sends contextmenu events. + // Set to true when a contextmenu event is seen. + this.contextMenuSupported = false; + this.panicked = false; + this._cachedDebugInfo = null; + this.isExtension = false; + this.longPressTimer = null; + this.pointerDownPosition = null; + this.pointerMoveMaxDistance = 0; + /** + * Any configuration that should apply to this specific player. + * This will be defaulted with any global configuration. + */ + this.config = {}; + this.shadow = this.attachShadow({ mode: "open" }); + this.shadow.appendChild(ruffleShadowTemplate.content.cloneNode(true)); + this.dynamicStyles = (this.shadow.getElementById("dynamic_styles")); + this.container = this.shadow.getElementById("container"); + this.playButton = this.shadow.getElementById("play_button"); + this.playButton.addEventListener("click", () => this.play()); + this.unmuteOverlay = this.shadow.getElementById("unmute_overlay"); + this.splashScreen = this.shadow.getElementById("splash-screen"); + this.virtualKeyboard = (this.shadow.getElementById("virtual-keyboard")); + this.virtualKeyboard.addEventListener("input", this.virtualKeyboardInput.bind(this)); + this.saveManager = (this.shadow.getElementById("save-manager")); + this.saveManager.addEventListener("click", () => this.saveManager.classList.add("hidden")); + const modalArea = this.saveManager.querySelector("#modal-area"); + if (modalArea) { + modalArea.addEventListener("click", (event) => event.stopPropagation()); + } + const closeSaveManager = this.saveManager.querySelector("#close-modal"); + if (closeSaveManager) { + closeSaveManager.addEventListener("click", () => this.saveManager.classList.add("hidden")); + } + const backupSaves = this.saveManager.querySelector("#backup-saves"); + if (backupSaves) { + backupSaves.addEventListener("click", this.backupSaves.bind(this)); + } + this.contextMenuElement = this.shadow.getElementById("context-menu"); + window.addEventListener("pointerdown", this.checkIfTouch.bind(this)); + this.addEventListener("contextmenu", this.showContextMenu.bind(this)); + this.container.addEventListener("pointerdown", this.pointerDown.bind(this)); + this.container.addEventListener("pointermove", this.checkLongPressMovement.bind(this)); + this.container.addEventListener("pointerup", this.checkLongPress.bind(this)); + this.container.addEventListener("pointercancel", this.clearLongPressTimer.bind(this)); + this.addEventListener("fullscreenchange", this.fullScreenChange.bind(this)); + this.addEventListener("webkitfullscreenchange", this.fullScreenChange.bind(this)); + this.instance = null; + this.onFSCommand = null; + this._readyState = 0 /* ReadyState.HaveNothing */; + this._metadata = null; + this.lastActivePlayingState = false; + this.setupPauseOnTabHidden(); + } + /** + * Setup event listener to detect when tab is not active to pause instance playback. + * this.instance.play() is called when the tab becomes visible only if the + * the instance was not paused before tab became hidden. + * + * See: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API + * @ignore + * @internal + */ + setupPauseOnTabHidden() { + document.addEventListener("visibilitychange", () => { + if (!this.instance) { + return; + } + // Tab just changed to be inactive. Record whether instance was playing. + if (document.hidden) { + this.lastActivePlayingState = this.instance.is_playing(); + this.instance.pause(); + } + // Play only if instance was playing originally. + if (!document.hidden && this.lastActivePlayingState === true) { + this.instance.play(); + } + }, false); + } + /** + * @ignore + * @internal + */ + connectedCallback() { + this.updateStyles(); + } + /** + * @ignore + * @internal + */ + static get observedAttributes() { + return ["width", "height"]; + } + /** + * @ignore + * @internal + */ + attributeChangedCallback(name, _oldValue, _newValue) { + if (name === "width" || name === "height") { + this.updateStyles(); + } + } + /** + * @ignore + * @internal + */ + disconnectedCallback() { + this.destroy(); + } + /** + * Updates the internal shadow DOM to reflect any set attributes from + * this element. + */ + updateStyles() { + if (this.dynamicStyles.sheet) { + if (this.dynamicStyles.sheet.rules) { + for (let i = 0; i < this.dynamicStyles.sheet.rules.length; i++) { + this.dynamicStyles.sheet.deleteRule(i); + } + } + const widthAttr = this.attributes.getNamedItem("width"); + if (widthAttr !== undefined && widthAttr !== null) { + const width = RufflePlayer.htmlDimensionToCssDimension(widthAttr.value); + if (width !== null) { + this.dynamicStyles.sheet.insertRule(`:host { width: ${width}; }`); + } + } + const heightAttr = this.attributes.getNamedItem("height"); + if (heightAttr !== undefined && heightAttr !== null) { + const height = RufflePlayer.htmlDimensionToCssDimension(heightAttr.value); + if (height !== null) { + this.dynamicStyles.sheet.insertRule(`:host { height: ${height}; }`); + } + } + } + } + /** + * Determine if this element is the fallback content of another Ruffle + * player. + * + * This heuristic assumes Ruffle objects will never use their fallback + * content. If this changes, then this code also needs to change. + * + * @private + */ + isUnusedFallbackObject() { + const element = lookupElement("ruffle-object"); + if (element !== null) { + let parent = this.parentNode; + while (parent !== document && parent !== null) { + if (parent.nodeName === element.name) { + return true; + } + parent = parent.parentNode; + } + } + return false; + } + /** + * Ensure a fresh Ruffle instance is ready on this player before continuing. + * + * @throws Any exceptions generated by loading Ruffle Core will be logged + * and passed on. + * + * @private + */ + async ensureFreshInstance() { + var _a; + this.destroy(); + if (this.loadedConfig && + this.loadedConfig.splashScreen !== false && + this.loadedConfig.preloader !== false) { + this.showSplashScreen(); + } + if (this.loadedConfig && this.loadedConfig.preloader === false) { + console.warn("The configuration option preloader has been replaced with splashScreen. If you own this website, please update the configuration."); + } + if (this.loadedConfig && + this.loadedConfig.maxExecutionDuration && + typeof this.loadedConfig.maxExecutionDuration !== "number") { + console.warn("Configuration: An obsolete format for duration for 'maxExecutionDuration' was used, " + + "please use a single number indicating seconds instead. For instance '15' instead of " + + "'{secs: 15, nanos: 0}'."); + } + const ruffleConstructor = await loadRuffle(this.loadedConfig || {}, this.onRuffleDownloadProgress.bind(this)).catch((e) => { + console.error(`Serious error loading Ruffle: ${e}`); + // Serious duck typing. In error conditions, let's not make assumptions. + if (window.location.protocol === "file:") { + e.ruffleIndexError = 2 /* PanicError.FileProtocol */; + } + else { + e.ruffleIndexError = 9 /* PanicError.WasmNotFound */; + const message = String(e.message).toLowerCase(); + if (message.includes("mime")) { + e.ruffleIndexError = 8 /* PanicError.WasmMimeType */; + } + else if (message.includes("networkerror") || + message.includes("failed to fetch")) { + e.ruffleIndexError = 6 /* PanicError.WasmCors */; + } + else if (message.includes("disallowed by embedder")) { + e.ruffleIndexError = 1 /* PanicError.CSPConflict */; + } + else if (e.name === "CompileError") { + e.ruffleIndexError = 3 /* PanicError.InvalidWasm */; + } + else if (message.includes("could not download wasm module") && + e.name === "TypeError") { + e.ruffleIndexError = 7 /* PanicError.WasmDownload */; + } + else if (e.name === "TypeError") { + e.ruffleIndexError = 5 /* PanicError.JavascriptConflict */; + } + else if (navigator.userAgent.includes("Edg") && + message.includes("webassembly is not defined")) { + // Microsoft Edge detection. + e.ruffleIndexError = 10 /* PanicError.WasmDisabledMicrosoftEdge */; + } + } + this.panic(e); + throw e; + }); + this.instance = await new ruffleConstructor(this.container, this, this.loadedConfig); + this._cachedDebugInfo = this.instance.renderer_debug_info(); + console.log("New Ruffle instance created (WebAssembly extensions: " + + (ruffleConstructor.is_wasm_simd_used() ? "ON" : "OFF") + + ")"); + // In Firefox, AudioContext.state is always "suspended" when the object has just been created. + // It may change by itself to "running" some milliseconds later. So we need to wait a little + // bit before checking if autoplay is supported and applying the instance config. + if (this.audioState() !== "running") { + this.container.style.visibility = "hidden"; + await new Promise((resolve) => { + window.setTimeout(() => { + resolve(); + }, 200); + }); + this.container.style.visibility = ""; + } + this.unmuteAudioContext(); + // On Android, the virtual keyboard needs to be dismissed as otherwise it re-focuses when clicking elsewhere + if (navigator.userAgent.toLowerCase().includes("android")) { + this.container.addEventListener("click", () => this.virtualKeyboard.blur()); + } + // Treat invalid values as `AutoPlay.Auto`. + if (!this.loadedConfig || + this.loadedConfig.autoplay === "on" /* AutoPlay.On */ || + (this.loadedConfig.autoplay !== "off" /* AutoPlay.Off */ && + this.audioState() === "running")) { + this.play(); + if (this.audioState() !== "running") { + // Treat invalid values as `UnmuteOverlay.Visible`. + if (!this.loadedConfig || + this.loadedConfig.unmuteOverlay !== "hidden" /* UnmuteOverlay.Hidden */) { + this.unmuteOverlay.style.display = "block"; + } + this.container.addEventListener("click", this.unmuteOverlayClicked.bind(this), { + once: true, + }); + const audioContext = (_a = this.instance) === null || _a === void 0 ? void 0 : _a.audio_context(); + if (audioContext) { + audioContext.onstatechange = () => { + if (audioContext.state === "running") { + this.unmuteOverlayClicked(); + } + audioContext.onstatechange = null; + }; + } + } + } + else { + this.playButton.style.display = "block"; + } + } + /** + * Uploads the splash screen progress bar. + * + * @param bytesLoaded The size of the Ruffle WebAssembly file downloaded so far. + * @param bytesTotal The total size of the Ruffle WebAssembly file. + */ + onRuffleDownloadProgress(bytesLoaded, bytesTotal) { + const loadBar = (this.splashScreen.querySelector(".loadbar-inner")); + const outerLoadbar = (this.splashScreen.querySelector(".loadbar")); + if (Number.isNaN(bytesTotal)) { + if (outerLoadbar) { + outerLoadbar.style.display = "none"; + } + } + else { + loadBar.style.width = `${100.0 * (bytesLoaded / bytesTotal)}%`; + } + } + /** + * Destroys the currently running instance of Ruffle. + */ + destroy() { + if (this.instance) { + this.instance.destroy(); + this.instance = null; + this._metadata = null; + this._readyState = 0 /* ReadyState.HaveNothing */; + console.log("Ruffle instance destroyed."); + } + } + checkOptions(options) { + if (typeof options === "string") { + return { url: options }; + } + const check = (condition, message) => { + if (!condition) { + const error = new TypeError(message); + error.ruffleIndexError = 4 /* PanicError.JavascriptConfiguration */; + this.panic(error); + throw error; + } + }; + check(options !== null && typeof options === "object", "Argument 0 must be a string or object"); + check("url" in options || "data" in options, "Argument 0 must contain a `url` or `data` key"); + check(!("url" in options) || typeof options.url === "string", "`url` must be a string"); + return options; + } + /** + * Gets the configuration set by the Ruffle extension + * + * @returns The configuration set by the Ruffle extension + */ + getExtensionConfig() { + var _a; + return window.RufflePlayer && + window.RufflePlayer.conflict && + (window.RufflePlayer.conflict["newestName"] === "extension" || + window.RufflePlayer["newestName"] === "extension") + ? (_a = window.RufflePlayer) === null || _a === void 0 ? void 0 : _a.conflict["config"] + : {}; + } + /** + * Loads a specified movie into this player. + * + * This will replace any existing movie that may be playing. + * + * @param options One of the following: + * - A URL, passed as a string, which will load a URL with default options. + * - A [[URLLoadOptions]] object, to load a URL with options. + * - A [[DataLoadOptions]] object, to load data with options. + * + * The options will be defaulted by the [[config]] field, which itself + * is defaulted by a global `window.RufflePlayer.config`. + */ + async load(options) { + var _a, _b; + options = this.checkOptions(options); + if (!this.isConnected || this.isUnusedFallbackObject()) { + console.warn("Ignoring attempt to play a disconnected or suspended Ruffle element"); + return; + } + if (isFallbackElement(this)) { + // Silently fail on attempt to play a Ruffle element inside a specific node. + return; + } + try { + const extensionConfig = this.getExtensionConfig(); + this.loadedConfig = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, DEFAULT_CONFIG), extensionConfig), ((_b = (_a = window.RufflePlayer) === null || _a === void 0 ? void 0 : _a.config) !== null && _b !== void 0 ? _b : {})), this.config), options); + // `allowScriptAccess` can only be set in `options`. + this.loadedConfig.allowScriptAccess = + options.allowScriptAccess === true; + // Pre-emptively set background color of container while Ruffle/SWF loads. + if (this.loadedConfig.backgroundColor && + this.loadedConfig.wmode !== "transparent" /* WindowMode.Transparent */) { + this.container.style.backgroundColor = + this.loadedConfig.backgroundColor; + } + await this.ensureFreshInstance(); + if ("url" in options) { + console.log(`Loading SWF file ${options.url}`); + this.swfUrl = new URL(options.url, document.baseURI); + const parameters = Object.assign(Object.assign({}, sanitizeParameters(options.url.substring(options.url.indexOf("?")))), sanitizeParameters(options.parameters)); + this.instance.stream_from(this.swfUrl.href, parameters); + } + else if ("data" in options) { + console.log("Loading SWF data"); + this.instance.load_data(new Uint8Array(options.data), sanitizeParameters(options.parameters), options.swfFileName || "movie.swf"); + } + } + catch (e) { + console.error(`Serious error occurred loading SWF file: ${e}`); + const err = new Error(e); + if (err.message.includes("Error parsing config")) { + err.ruffleIndexError = 4 /* PanicError.JavascriptConfiguration */; + } + this.panic(err); + throw err; + } + } + /** + * Plays or resumes the movie. + */ + play() { + if (this.instance) { + this.instance.play(); + this.playButton.style.display = "none"; + } + } + /** + * Whether this player is currently playing. + * + * @returns True if this player is playing, false if it's paused or hasn't started yet. + */ + get isPlaying() { + if (this.instance) { + return this.instance.is_playing(); + } + return false; + } + /** + * Returns the master volume of the player. + * + * @returns The volume. 1.0 is 100% volume. + */ + get volume() { + if (this.instance) { + return this.instance.volume(); + } + return 1.0; + } + /** + * Sets the master volume of the player. + * + * @param value The volume. 1.0 is 100% volume. + */ + set volume(value) { + if (this.instance) { + this.instance.set_volume(value); + } + } + /** + * Checks if this player is allowed to be fullscreen by the browser. + * + * @returns True if you may call [[enterFullscreen]]. + */ + get fullscreenEnabled() { + return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled); + } + /** + * Checks if this player is currently fullscreen inside the browser. + * + * @returns True if it is fullscreen. + */ + get isFullscreen() { + return ((document.fullscreenElement || document.webkitFullscreenElement) === + this); + } + /** + * Exported function that requests the browser to change the fullscreen state if + * it is allowed. + * + * @param isFull Whether to set to fullscreen or return to normal. + */ + setFullscreen(isFull) { + if (this.fullscreenEnabled) { + if (isFull) { + this.enterFullscreen(); + } + else { + this.exitFullscreen(); + } + } + } + /** + * Requests the browser to make this player fullscreen. + * + * This is not guaranteed to succeed, please check [[fullscreenEnabled]] first. + */ + enterFullscreen() { + const options = { + navigationUI: "hide", + }; + if (this.requestFullscreen) { + this.requestFullscreen(options); + } + else if (this.webkitRequestFullscreen) { + this.webkitRequestFullscreen(options); + } + else if (this.webkitRequestFullScreen) { + this.webkitRequestFullScreen(options); + } + } + /** + * Requests the browser to no longer make this player fullscreen. + */ + exitFullscreen() { + if (document.exitFullscreen) { + document.exitFullscreen(); + } + else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } + else if (document.webkitCancelFullScreen) { + document.webkitCancelFullScreen(); + } + } + /** + * Called when entering / leaving fullscreen + */ + fullScreenChange() { + var _a; + (_a = this.instance) === null || _a === void 0 ? void 0 : _a.set_fullscreen(this.isFullscreen); + } + checkIfTouch(event) { + if (event.pointerType === "touch" || event.pointerType === "pen") { + this.isTouch = true; + } + } + base64ToBlob(bytesBase64, mimeString) { + const byteString = atob(bytesBase64); + const ab = new ArrayBuffer(byteString.length); + const ia = new Uint8Array(ab); + for (let i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + const blob = new Blob([ab], { type: mimeString }); + return blob; + } + /** + * Download base-64 string as file + * + * @param bytesBase64 The base-64 encoded SOL string + * @param mimeType The MIME type + * @param fileName The name to give the file + */ + saveFile(bytesBase64, mimeType, fileName) { + const blob = this.base64ToBlob(bytesBase64, mimeType); + const blobURL = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = blobURL; + link.style.display = "none"; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(blobURL); + } + /** + * @returns If the string represent a base-64 encoded SOL file + * Check if string is a base-64 encoded SOL file + * @param solData The base-64 encoded SOL string + */ + isB64SOL(solData) { + try { + const decodedData = atob(solData); + return decodedData.slice(6, 10) === "TCSO"; + } + catch (e) { + return false; + } + } + confirmReloadSave(solKey, b64SolData, replace) { + if (this.isB64SOL(b64SolData)) { + if (localStorage[solKey]) { + if (!replace) { + const confirmDelete = confirm("Are you sure you want to delete this save file?"); + if (!confirmDelete) { + return; + } + } + const swfPath = this.swfUrl ? this.swfUrl.pathname : ""; + const swfHost = this.swfUrl + ? this.swfUrl.hostname + : document.location.hostname; + const savePath = solKey.split("/").slice(1, -1).join("/"); + if (swfPath.includes(savePath) && solKey.startsWith(swfHost)) { + const confirmReload = confirm(`The only way to ${replace ? "replace" : "delete"} this save file without potential conflict is to reload this content. Do you wish to continue anyway?`); + if (confirmReload && this.loadedConfig) { + this.destroy(); + replace + ? localStorage.setItem(solKey, b64SolData) + : localStorage.removeItem(solKey); + this.load(this.loadedConfig); + this.populateSaves(); + this.saveManager.classList.add("hidden"); + } + return; + } + replace + ? localStorage.setItem(solKey, b64SolData) + : localStorage.removeItem(solKey); + this.populateSaves(); + this.saveManager.classList.add("hidden"); + } + } + } + /** + * Replace save from SOL file. + * + * @param event The change event fired + * @param solKey The localStorage save file key + */ + replaceSOL(event, solKey) { + const fileInput = event.target; + const reader = new FileReader(); + reader.addEventListener("load", () => { + if (reader.result && typeof reader.result === "string") { + const b64Regex = new RegExp("data:.*;base64,"); + const b64SolData = reader.result.replace(b64Regex, ""); + this.confirmReloadSave(solKey, b64SolData, true); + } + }); + if (fileInput && + fileInput.files && + fileInput.files.length > 0 && + fileInput.files[0]) { + reader.readAsDataURL(fileInput.files[0]); + } + } + /** + * Delete local save. + * + * @param key The key to remove from local storage + */ + deleteSave(key) { + const b64SolData = localStorage.getItem(key); + if (b64SolData) { + this.confirmReloadSave(key, b64SolData, false); + } + } + /** + * Puts the local save SOL file keys in a table. + */ + populateSaves() { + const saveTable = this.saveManager.querySelector("#local-saves"); + if (!saveTable) { + return; + } + try { + localStorage; + } + catch (e) { + return; + } + saveTable.textContent = ""; + Object.keys(localStorage).forEach((key) => { + const solName = key.split("/").pop(); + const solData = localStorage.getItem(key); + if (solName && solData && this.isB64SOL(solData)) { + const row = document.createElement("TR"); + const keyCol = document.createElement("TD"); + keyCol.textContent = solName; + keyCol.title = key; + const downloadCol = document.createElement("TD"); + const downloadSpan = document.createElement("SPAN"); + downloadSpan.textContent = "Download"; + downloadSpan.className = "save-option"; + downloadSpan.addEventListener("click", () => this.saveFile(solData, "application/octet-stream", solName + ".sol")); + downloadCol.appendChild(downloadSpan); + const replaceCol = document.createElement("TD"); + const replaceInput = (document.createElement("INPUT")); + replaceInput.type = "file"; + replaceInput.accept = ".sol"; + replaceInput.className = "replace-save"; + replaceInput.id = "replace-save-" + key; + const replaceLabel = (document.createElement("LABEL")); + replaceLabel.htmlFor = "replace-save-" + key; + replaceLabel.textContent = "Replace"; + replaceLabel.className = "save-option"; + replaceInput.addEventListener("change", (event) => this.replaceSOL(event, key)); + replaceCol.appendChild(replaceInput); + replaceCol.appendChild(replaceLabel); + const deleteCol = document.createElement("TD"); + const deleteSpan = document.createElement("SPAN"); + deleteSpan.textContent = "Delete"; + deleteSpan.className = "save-option"; + deleteSpan.addEventListener("click", () => this.deleteSave(key)); + deleteCol.appendChild(deleteSpan); + row.appendChild(keyCol); + row.appendChild(downloadCol); + row.appendChild(replaceCol); + row.appendChild(deleteCol); + saveTable.appendChild(row); + } + }); + } + /** + * Gets the local save information as SOL files and downloads them. + */ + backupSaves() { + Object.keys(localStorage).forEach((key) => { + const solName = key.split("/").pop(); + const solData = localStorage.getItem(key); + if (solData && this.isB64SOL(solData)) { + this.saveFile(solData, "application/octet-stream", solName + ".sol"); + } + }); + } + /** + * Opens the save manager. + */ + openSaveManager() { + this.saveManager.classList.remove("hidden"); + } + /** + * Fetches the loaded SWF and downloads it. + */ + async downloadSwf() { + try { + if (this.swfUrl) { + console.log("Downloading SWF: " + this.swfUrl); + const response = await fetch(this.swfUrl.href); + if (!response.ok) { + console.error("SWF download failed"); + return; + } + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + const swfDownloadA = document.createElement("a"); + swfDownloadA.style.display = "none"; + swfDownloadA.href = blobUrl; + swfDownloadA.download = swfFileName(this.swfUrl); + document.body.appendChild(swfDownloadA); + swfDownloadA.click(); + document.body.removeChild(swfDownloadA); + URL.revokeObjectURL(blobUrl); + } + else { + console.error("SWF download failed"); + } + } + catch (err) { + console.error("SWF download failed"); + } + } + virtualKeyboardInput() { + const input = this.virtualKeyboard; + const string = input.value; + for (const char of string) { + for (const eventType of ["keydown", "keyup"]) { + this.dispatchEvent(new KeyboardEvent(eventType, { + key: char, + bubbles: true, + })); + } + } + input.value = ""; + } + openVirtualKeyboard() { + // On Android, the Rust code that opens the virtual keyboard triggers + // before the TypeScript code that closes it, so delay opening it + if (navigator.userAgent.toLowerCase().includes("android")) { + setTimeout(() => { + this.virtualKeyboard.focus({ preventScroll: true }); + }, 100); + } + else { + this.virtualKeyboard.focus({ preventScroll: true }); + } + } + contextMenuItems() { + const CHECKMARK = String.fromCharCode(0x2713); + const items = []; + const addSeparator = () => { + // Don't start with or duplicate separators. + if (items.length > 0 && items.at(-1) !== null) { + items.push(null); + } + }; + if (this.instance) { + const customItems = this.instance.prepare_context_menu(); + customItems.forEach((item, index) => { + if (item.separatorBefore) { + addSeparator(); + } + items.push({ + // TODO: better checkboxes + text: item.caption + (item.checked ? ` (${CHECKMARK})` : ``), + onClick: () => { var _a; return (_a = this.instance) === null || _a === void 0 ? void 0 : _a.run_context_menu_callback(index); }, + enabled: item.enabled, + }); + }); + addSeparator(); + } + if (this.fullscreenEnabled) { + if (this.isFullscreen) { + items.push({ + text: "Exit fullscreen", + onClick: () => { var _a; return (_a = this.instance) === null || _a === void 0 ? void 0 : _a.set_fullscreen(false); }, + }); + } + else { + items.push({ + text: "Enter fullscreen", + onClick: () => { var _a; return (_a = this.instance) === null || _a === void 0 ? void 0 : _a.set_fullscreen(true); }, + }); + } + } + if (this.instance && + this.swfUrl && + this.loadedConfig && + this.loadedConfig.showSwfDownload === true) { + addSeparator(); + items.push({ + text: "Download .swf", + onClick: this.downloadSwf.bind(this), + }); + } + if (window.isSecureContext) { + items.push({ + text: "Copy debug info", + onClick: () => navigator.clipboard.writeText(this.getPanicData()), + }); + } + this.populateSaves(); + const localSaveTable = this.saveManager.querySelector("#local-saves"); + if (localSaveTable && localSaveTable.textContent !== "") { + items.push({ + text: "Open Save Manager", + onClick: this.openSaveManager.bind(this), + }); + } + addSeparator(); + const extensionString = this.isExtension ? "extension" : ""; + items.push({ + text: `About Ruffle ${extensionString} (${buildInfo.versionName})`, + onClick() { + window.open(RUFFLE_ORIGIN, "_blank"); + }, + }); + // Give option to disable context menu when touch support is being used + // to avoid a long press triggering the context menu. (#1972) + if (this.isTouch) { + addSeparator(); + items.push({ + text: "Hide this menu", + onClick: () => (this.contextMenuForceDisabled = true), + }); + } + return items; + } + pointerDown(event) { + this.pointerDownPosition = new Point(event.pageX, event.pageY); + this.pointerMoveMaxDistance = 0; + this.startLongPressTimer(); + } + clearLongPressTimer() { + if (this.longPressTimer) { + clearTimeout(this.longPressTimer); + this.longPressTimer = null; + } + } + startLongPressTimer() { + const longPressTimeout = 800; + this.clearLongPressTimer(); + this.longPressTimer = setTimeout(() => this.clearLongPressTimer(), longPressTimeout); + } + checkLongPressMovement(event) { + if (this.pointerDownPosition !== null) { + const currentPosition = new Point(event.pageX, event.pageY); + const distance = this.pointerDownPosition.distanceTo(currentPosition); + if (distance > this.pointerMoveMaxDistance) { + this.pointerMoveMaxDistance = distance; + } + } + } + checkLongPress(event) { + const maxAllowedDistance = 15; + if (this.longPressTimer) { + this.clearLongPressTimer(); + // The pointerType condition is to ensure right-click does not trigger + // a context menu the wrong way the first time you right-click, + // before contextMenuSupported is set. + } + else if (!this.contextMenuSupported && + event.pointerType !== "mouse" && + this.pointerMoveMaxDistance < maxAllowedDistance) { + this.showContextMenu(event); + } + } + showContextMenu(event) { + event.preventDefault(); + if (event.type === "contextmenu") { + this.contextMenuSupported = true; + window.addEventListener("click", this.hideContextMenu.bind(this), { + once: true, + }); + } + else { + window.addEventListener("pointerup", this.hideContextMenu.bind(this), { once: true }); + event.stopPropagation(); + } + if ((this.loadedConfig && this.loadedConfig.contextMenu === false) || + this.contextMenuForceDisabled) { + return; + } + // Clear all context menu items. + while (this.contextMenuElement.firstChild) { + this.contextMenuElement.removeChild(this.contextMenuElement.firstChild); + } + // Populate context menu items. + for (const item of this.contextMenuItems()) { + if (item === null) { + const menuSeparator = document.createElement("li"); + menuSeparator.className = "menu_separator"; + const hr = document.createElement("hr"); + menuSeparator.appendChild(hr); + this.contextMenuElement.appendChild(menuSeparator); + } + else { + const { text, onClick, enabled } = item; + const menuItem = document.createElement("li"); + menuItem.className = "menu_item"; + menuItem.textContent = text; + this.contextMenuElement.appendChild(menuItem); + if (enabled !== false) { + menuItem.addEventListener(this.contextMenuSupported ? "click" : "pointerup", onClick); + } + else { + menuItem.classList.add("disabled"); + } + } + } + // Place a context menu in the top-left corner, so + // its `clientWidth` and `clientHeight` are not clamped. + this.contextMenuElement.style.left = "0"; + this.contextMenuElement.style.top = "0"; + this.contextMenuElement.style.display = "block"; + const rect = this.getBoundingClientRect(); + const x = event.clientX - rect.x; + const y = event.clientY - rect.y; + const maxX = rect.width - this.contextMenuElement.clientWidth - 1; + const maxY = rect.height - this.contextMenuElement.clientHeight - 1; + this.contextMenuElement.style.left = + Math.floor(Math.min(x, maxX)) + "px"; + this.contextMenuElement.style.top = + Math.floor(Math.min(y, maxY)) + "px"; + } + hideContextMenu() { + var _a; + (_a = this.instance) === null || _a === void 0 ? void 0 : _a.clear_custom_menu_items(); + this.contextMenuElement.style.display = "none"; + } + /** + * Pauses this player. + * + * No more frames, scripts or sounds will be executed. + * This movie will be considered inactive and will not wake up until resumed. + */ + pause() { + if (this.instance) { + this.instance.pause(); + this.playButton.style.display = "block"; + } + } + audioState() { + if (this.instance) { + const audioContext = this.instance.audio_context(); + return (audioContext && audioContext.state) || "running"; + } + return "suspended"; + } + unmuteOverlayClicked() { + if (this.instance) { + if (this.audioState() !== "running") { + const audioContext = this.instance.audio_context(); + if (audioContext) { + audioContext.resume(); + } + } + this.unmuteOverlay.style.display = "none"; + } + } + /** + * Plays a silent sound based on the AudioContext's sample rate. + * + * This is used to unmute audio on iOS and iPadOS when silent mode is enabled on the device (issue 1552). + */ + unmuteAudioContext() { + // No need to play the dummy sound again once audio is unmuted. + if (isAudioContextUnmuted) { + return; + } + // TODO: Use `navigator.userAgentData` to detect the platform when support improves? + if (navigator.maxTouchPoints < 1) { + isAudioContextUnmuted = true; + return; + } + this.container.addEventListener("click", () => { + var _a; + if (isAudioContextUnmuted) { + return; + } + const audioContext = (_a = this.instance) === null || _a === void 0 ? void 0 : _a.audio_context(); + if (!audioContext) { + return; + } + const audio = new Audio(); + audio.src = (() => { + // Returns a seven samples long 8 bit mono WAVE file. + // This is required to prevent the AudioContext from desyncing and crashing. + const arrayBuffer = new ArrayBuffer(10); + const dataView = new DataView(arrayBuffer); + const sampleRate = audioContext.sampleRate; + dataView.setUint32(0, sampleRate, true); + dataView.setUint32(4, sampleRate, true); + dataView.setUint16(8, 1, true); + const missingCharacters = window + .btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))) + .slice(0, 13); + return `data:audio/wav;base64,UklGRisAAABXQVZFZm10IBAAAAABAAEA${missingCharacters}AgAZGF0YQcAAACAgICAgICAAAA=`; + })(); + audio.load(); + audio + .play() + .then(() => { + isAudioContextUnmuted = true; + }) + .catch((err) => { + console.warn(`Failed to play dummy sound: ${err}`); + }); + }, { once: true }); + } + /** + * Copies attributes and children from another element to this player element. + * Used by the polyfill elements, RuffleObject and RuffleEmbed. + * + * @param element The element to copy all attributes from. + */ + copyElement(element) { + if (element) { + for (const attribute of element.attributes) { + if (attribute.specified) { + // Issue 468: Chrome "Click to Active Flash" box stomps on title attribute + if (attribute.name === "title" && + attribute.value === "Adobe Flash Player") { + continue; + } + try { + this.setAttribute(attribute.name, attribute.value); + } + catch (err) { + // The embed may have invalid attributes, so handle these gracefully. + console.warn(`Unable to set attribute ${attribute.name} on Ruffle instance`); + } + } + } + for (const node of Array.from(element.children)) { + this.appendChild(node); + } + } + } + /** + * Converts a dimension attribute on an HTML embed/object element to a valid CSS dimension. + * HTML element dimensions are unitless, but can also be percentages. + * Add a 'px' unit unless the value is a percentage. + * Returns null if this is not a valid dimension. + * + * @param attribute The attribute to convert + * + * @private + */ + static htmlDimensionToCssDimension(attribute) { + if (attribute) { + const match = attribute.match(DIMENSION_REGEX); + if (match) { + let out = match[1]; + if (!match[3]) { + // Unitless -- add px for CSS. + out += "px"; + } + return out; + } + } + return null; + } + /** + * When a movie presents a new callback through `ExternalInterface.addCallback`, + * we are informed so that we can expose the method on any relevant DOM element. + * + * This should only be called by Ruffle itself and not by users. + * + * @param name The name of the callback that is now available. + * + * @internal + * @ignore + */ + onCallbackAvailable(name) { + const instance = this.instance; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this[name] = (...args) => { + return instance === null || instance === void 0 ? void 0 : instance.call_exposed_callback(name, args); + }; + } + /** + * Sets a trace observer on this flash player. + * + * The observer will be called, as a function, for each message that the playing movie will "trace" (output). + * + * @param observer The observer that will be called for each trace. + */ + set traceObserver(observer) { + var _a; + (_a = this.instance) === null || _a === void 0 ? void 0 : _a.set_trace_observer(observer); + } + /** + * Get data included in any panic of this ruffle-player + * + * @returns A string containing all the data included in the panic. + */ + getPanicData() { + const dataArray = []; + dataArray.push("\n# Player Info\n"); + dataArray.push(this.debugPlayerInfo()); + dataArray.push("\n# Page Info\n"); + dataArray.push(`Page URL: ${document.location.href}\n`); + if (this.swfUrl) { + dataArray.push(`SWF URL: ${this.swfUrl}\n`); + } + dataArray.push("\n# Browser Info\n"); + dataArray.push(`User Agent: ${window.navigator.userAgent}\n`); + dataArray.push(`Platform: ${window.navigator.platform}\n`); + dataArray.push(`Has touch support: ${window.navigator.maxTouchPoints > 0}\n`); + dataArray.push("\n# Ruffle Info\n"); + dataArray.push(`Version: ${buildInfo.versionNumber}\n`); + dataArray.push(`Name: ${buildInfo.versionName}\n`); + dataArray.push(`Channel: ${buildInfo.versionChannel}\n`); + dataArray.push(`Built: ${buildInfo.buildDate}\n`); + dataArray.push(`Commit: ${buildInfo.commitHash}\n`); + dataArray.push(`Is extension: ${this.isExtension}\n`); + dataArray.push("\n# Metadata\n"); + if (this.metadata) { + for (const [key, value] of Object.entries(this.metadata)) { + dataArray.push(`${key}: ${value}\n`); + } + } + return dataArray.join(""); + } + /** + * Panics this specific player, forcefully destroying all resources and displays an error message to the user. + * + * This should be called when something went absolutely, incredibly and disastrously wrong and there is no chance + * of recovery. + * + * Ruffle will attempt to isolate all damage to this specific player instance, but no guarantees can be made if there + * was a core issue which triggered the panic. If Ruffle is unable to isolate the cause to a specific player, then + * all players will panic and Ruffle will become "poisoned" - no more players will run on this page until it is + * reloaded fresh. + * + * @param error The error, if any, that triggered this panic. + */ + panic(error) { + var _a; + if (this.panicked) { + // Only show the first major error, not any repeats - they aren't as important + return; + } + this.panicked = true; + this.hideSplashScreen(); + if (error instanceof Error && + (error.name === "AbortError" || + error.message.includes("AbortError"))) { + // Firefox: Don't display the panic screen if the user leaves the page while something is still loading + return; + } + const errorIndex = (_a = error === null || error === void 0 ? void 0 : error.ruffleIndexError) !== null && _a !== void 0 ? _a : 0 /* PanicError.Unknown */; + const errorArray = Object.assign([], { + stackIndex: -1, + avmStackIndex: -1, + }); + errorArray.push("# Error Info\n"); + if (error instanceof Error) { + errorArray.push(`Error name: ${error.name}\n`); + errorArray.push(`Error message: ${error.message}\n`); + if (error.stack) { + const stackIndex = errorArray.push(`Error stack:\n\`\`\`\n${error.stack}\n\`\`\`\n`) - 1; + if (error.avmStack) { + const avmStackIndex = errorArray.push(`AVM2 stack:\n\`\`\`\n ${error.avmStack + .trim() + .replace(/\t/g, " ")}\n\`\`\`\n`) - 1; + errorArray.avmStackIndex = avmStackIndex; + } + errorArray.stackIndex = stackIndex; + } + } + else { + errorArray.push(`Error: ${error}\n`); + } + errorArray.push(this.getPanicData()); + const errorText = errorArray.join(""); + const buildDate = new Date(buildInfo.buildDate); + const monthsPrior = new Date(); + monthsPrior.setMonth(monthsPrior.getMonth() - 6); // 6 months prior + const isBuildOutdated = monthsPrior > buildDate; + // Create a link to GitHub with all of the error data, if the build is not outdated. + // Otherwise, create a link to the downloads section on the Ruffle website. + let actionTag; + if (!isBuildOutdated) { + // Remove query params for the issue title. + const pageUrl = document.location.href.split(/[?#]/)[0]; + const issueTitle = `Error on ${pageUrl}`; + let issueLink = `https://github.com/ruffle-rs/ruffle/issues/new?title=${encodeURIComponent(issueTitle)}&template=error_report.md&labels=error-report&body=`; + let issueBody = encodeURIComponent(errorText); + if (errorArray.stackIndex > -1 && + String(issueLink + issueBody).length > 8195) { + // Strip the stack error from the array when the produced URL is way too long. + // This should prevent "414 Request-URI Too Large" errors on GitHub. + errorArray[errorArray.stackIndex] = null; + if (errorArray.avmStackIndex > -1) { + errorArray[errorArray.avmStackIndex] = null; + } + issueBody = encodeURIComponent(errorArray.join("")); + } + issueLink += issueBody; + actionTag = `Report Bug`; + } + else { + actionTag = `Update Ruffle`; + } + // Clears out any existing content (ie play button or canvas) and replaces it with the error screen + let errorBody, errorFooter; + switch (errorIndex) { + case 2 /* PanicError.FileProtocol */: + // General error: Running on the `file:` protocol + errorBody = ` +

    It appears you are running Ruffle on the "file:" protocol.

    +

    This doesn't work as browsers block many features from working for security reasons.

    +

    Instead, we invite you to setup a local server or either use the web demo or the desktop application.

    + `; + errorFooter = ` +
  • Web Demo
  • +
  • Desktop Application
  • + `; + break; + case 4 /* PanicError.JavascriptConfiguration */: + // General error: Incorrect JavaScript configuration + errorBody = ` +

    Ruffle has encountered a major issue due to an incorrect JavaScript configuration.

    +

    If you are the server administrator, we invite you to check the error details to find out which parameter is at fault.

    +

    You can also consult the Ruffle wiki for help.

    + `; + errorFooter = ` +
  • View Ruffle Wiki
  • +
  • View Error Details
  • + `; + break; + case 9 /* PanicError.WasmNotFound */: + // Self hosted: Cannot load `.wasm` file - file not found + errorBody = ` +

    Ruffle failed to load the required ".wasm" file component.

    +

    If you are the server administrator, please ensure the file has correctly been uploaded.

    +

    If the issue persists, you may need to use the "publicPath" setting: please consult the Ruffle wiki for help.

    + `; + errorFooter = ` +
  • View Ruffle Wiki
  • +
  • View Error Details
  • + `; + break; + case 8 /* PanicError.WasmMimeType */: + // Self hosted: Cannot load `.wasm` file - incorrect MIME type + errorBody = ` +

    Ruffle has encountered a major issue whilst trying to initialize.

    +

    This web server is not serving ".wasm" files with the correct MIME type.

    +

    If you are the server administrator, please consult the Ruffle wiki for help.

    + `; + errorFooter = ` +
  • View Ruffle Wiki
  • +
  • View Error Details
  • + `; + break; + case 11 /* PanicError.SwfFetchError */: + errorBody = ` +

    Ruffle failed to load the Flash SWF file.

    +

    The most likely reason is that the file no longer exists, so there is nothing for Ruffle to load.

    +

    Try contacting the website administrator for help.

    + `; + errorFooter = ` +
  • View Error Details
  • + `; + break; + case 12 /* PanicError.SwfCors */: + // Self hosted: Cannot load SWF file - CORS issues + errorBody = ` +

    Ruffle failed to load the Flash SWF file.

    +

    Access to fetch has likely been blocked by CORS policy.

    +

    If you are the server administrator, please consult the Ruffle wiki for help.

    + `; + errorFooter = ` +
  • View Ruffle Wiki
  • +
  • View Error Details
  • + `; + break; + case 6 /* PanicError.WasmCors */: + // Self hosted: Cannot load `.wasm` file - CORS issues + errorBody = ` +

    Ruffle failed to load the required ".wasm" file component.

    +

    Access to fetch has likely been blocked by CORS policy.

    +

    If you are the server administrator, please consult the Ruffle wiki for help.

    + `; + errorFooter = ` +
  • View Ruffle Wiki
  • +
  • View Error Details
  • + `; + break; + case 3 /* PanicError.InvalidWasm */: + // Self hosted: Cannot load `.wasm` file - incorrect configuration or missing files + errorBody = ` +

    Ruffle has encountered a major issue whilst trying to initialize.

    +

    It seems like this page has missing or invalid files for running Ruffle.

    +

    If you are the server administrator, please consult the Ruffle wiki for help.

    + `; + errorFooter = ` +
  • View Ruffle Wiki
  • +
  • View Error Details
  • + `; + break; + case 7 /* PanicError.WasmDownload */: + // Usually a transient network error or botched deployment + errorBody = ` +

    Ruffle has encountered a major issue whilst trying to initialize.

    +

    This can often resolve itself, so you can try reloading the page.

    +

    Otherwise, please contact the website administrator.

    + `; + errorFooter = ` +
  • View Error Details
  • + `; + break; + case 10 /* PanicError.WasmDisabledMicrosoftEdge */: + // Self hosted: User has disabled WebAssembly in Microsoft Edge through the + // "Enhance your Security on the web" setting. + errorBody = ` +

    Ruffle failed to load the required ".wasm" file component.

    +

    To fix this, try opening your browser's settings, clicking "Privacy, search, and services", scrolling down, and turning off "Enhance your security on the web".

    +

    This will allow your browser to load the required ".wasm" files.

    +

    If the issue persists, you might have to use a different browser.

    + `; + errorFooter = ` +
  • More Information
  • +
  • View Error Details
  • + `; + break; + case 5 /* PanicError.JavascriptConflict */: + // Self hosted: Cannot load `.wasm` file - a native object / function is overriden + errorBody = ` +

    Ruffle has encountered a major issue whilst trying to initialize.

    +

    It seems like this page uses JavaScript code that conflicts with Ruffle.

    +

    If you are the server administrator, we invite you to try loading the file on a blank page.

    + `; + if (isBuildOutdated) { + errorBody += `

    You can also try to upload a more recent version of Ruffle that may circumvent the issue (current build is outdated: ${buildInfo.buildDate}).

    `; + } + errorFooter = ` +
  • ${actionTag}
  • +
  • View Error Details
  • + `; + break; + case 1 /* PanicError.CSPConflict */: + // General error: Cannot load `.wasm` file - a native object / function is overriden + errorBody = ` +

    Ruffle has encountered a major issue whilst trying to initialize.

    +

    This web server's Content Security Policy does not allow the required ".wasm" component to run.

    +

    If you are the server administrator, please consult the Ruffle wiki for help.

    + `; + errorFooter = ` +
  • View Ruffle Wiki
  • +
  • View Error Details
  • + `; + break; + default: + // Unknown error + errorBody = `

    Ruffle has encountered a major issue whilst trying to display this Flash content.

    `; + if (!isBuildOutdated) { + errorBody += `

    This isn't supposed to happen, so we'd really appreciate if you could file a bug!

    `; + } + else { + errorBody += `

    If you are the server administrator, please try to upload a more recent version of Ruffle (current build is outdated: ${buildInfo.buildDate}).

    `; + } + errorFooter = ` +
  • ${actionTag}
  • +
  • View Error Details
  • + `; + break; + } + this.container.innerHTML = ` +
    +
    Something went wrong :(
    +
    ${errorBody}
    + +
    + `; + const viewDetails = (this.container.querySelector("#panic-view-details")); + if (viewDetails) { + viewDetails.onclick = () => { + const panicBody = (this.container.querySelector("#panic-body")); + panicBody.classList.add("details"); + const panicText = document.createElement("textarea"); + panicText.value = errorText; + panicBody.appendChild(panicText); + return false; + }; + } + // Do this last, just in case it causes any cascading issues. + this.destroy(); + } + displayRootMovieDownloadFailedMessage() { + if (this.isExtension && + window.location.origin !== this.swfUrl.origin) { + this.hideSplashScreen(); + const div = document.createElement("div"); + div.id = "message_overlay"; + div.innerHTML = `
    +

    Ruffle wasn't able to run the Flash embedded in this page.

    +

    You can try to open the file in a separate tab, to sidestep this issue.

    + +
    `; + this.container.prepend(div); + } + else { + const error = new Error("Failed to fetch: " + this.swfUrl); + if (!this.swfUrl.protocol.includes("http")) { + error.ruffleIndexError = 2 /* PanicError.FileProtocol */; + } + else if (window.location.origin === this.swfUrl.origin) { + error.ruffleIndexError = 11 /* PanicError.SwfFetchError */; + } + else { + // This is a selfhosted build of Ruffle that tried to make a cross-origin request + error.ruffleIndexError = 12 /* PanicError.SwfCors */; + } + this.panic(error); + } + } + displayUnsupportedMessage() { + const div = document.createElement("div"); + div.id = "message_overlay"; + // TODO: Change link to https://ruffle.rs/faq or similar + // TODO: Pause content until message is dismissed + div.innerHTML = `
    +

    The Ruffle emulator may not yet fully support all of ActionScript 3 used by this content.

    +

    Some parts of the content may not work as expected.

    +
    + More info + +
    +
    `; + this.container.prepend(div); + const button = div.querySelector("#run-anyway-btn"); + button.onclick = () => { + div.parentNode.removeChild(div); + }; + } + /** + * Show a dismissible message in front of the player. + * + * @param message The message shown to the user. + */ + displayMessage(message) { + const div = document.createElement("div"); + div.id = "message_overlay"; + div.innerHTML = `
    +

    ${message}

    +
    + +
    +
    `; + this.container.prepend(div); + (this.container.querySelector("#continue-btn")).onclick = () => { + div.parentNode.removeChild(div); + }; + } + debugPlayerInfo() { + let result = `Allows script access: ${this.loadedConfig ? this.loadedConfig.allowScriptAccess : false}\n`; + let renderInfo = `(Cached) ${this._cachedDebugInfo}`; + if (this.instance) { + try { + renderInfo = this.instance.renderer_debug_info(); + } + catch (_a) { + // ignored + } + } + if (renderInfo) { + result += `${renderInfo}\n`; + } + return result; + } + hideSplashScreen() { + this.splashScreen.classList.add("hidden"); + this.container.classList.remove("hidden"); + } + showSplashScreen() { + this.splashScreen.classList.remove("hidden"); + this.container.classList.add("hidden"); + } + setMetadata(metadata) { + this._metadata = metadata; + // TODO: Switch this to ReadyState.Loading when we have streaming support. + this._readyState = 2 /* ReadyState.Loaded */; + this.hideSplashScreen(); + this.dispatchEvent(new Event(RufflePlayer.LOADED_METADATA)); + // TODO: Move this to whatever function changes the ReadyState to Loaded when we have streaming support. + this.dispatchEvent(new Event(RufflePlayer.LOADED_DATA)); + } + setIsExtension(isExtension) { + this.isExtension = isExtension; + } +} +/** + * Triggered when a movie metadata has been loaded (such as movie width and height). + * + * @event RufflePlayer#loadedmetadata + */ +RufflePlayer.LOADED_METADATA = "loadedmetadata"; +/** + * Triggered when a movie is fully loaded. + * + * @event RufflePlayer#loadeddata + */ +RufflePlayer.LOADED_DATA = "loadeddata"; + +/** + * Returns whether a SWF file can call JavaScript code in the surrounding HTML file. + * + * @param access The value of the `allowScriptAccess` attribute. + * @param url The URL of the SWF file. + * @returns True if script access is allowed. + */ +function isScriptAccessAllowed(access, url) { + if (!access) { + access = "sameDomain"; + } + switch (access.toLowerCase()) { + case "always": + return true; + case "never": + return false; + case "samedomain": + default: + try { + return (new URL(window.location.href).origin === + new URL(url, window.location.href).origin); + } + catch (_a) { + return false; + } + } +} +/** + * Returns whether a SWF file should show the built-in context menu items. + * + * @param menu The value of the `menu` attribute. + * @returns True if the built-in context items should be shown. + */ +function isBuiltInContextMenuVisible(menu) { + if (menu === null || menu.toLowerCase() === "true") { + return true; + } + return false; +} +/** + * Returns whether the given filename is a Youtube Flash source. + * + * @param filename The filename to test. + * @returns True if the filename is a Youtube Flash source. + */ +function isYoutubeFlashSource(filename) { + if (filename) { + let pathname = ""; + let cleaned_hostname = ""; + try { + // A base URL is required if `filename` is a relative URL, but we don't need to detect the real URL origin. + const url = new URL(filename, RUFFLE_ORIGIN); + pathname = url.pathname; + cleaned_hostname = url.hostname.replace("www.", ""); + } + catch (err) { + // Some invalid filenames, like `///`, could raise a TypeError. Let's fail silently in this situation. + } + // See https://wiki.mozilla.org/QA/Youtube_Embedded_Rewrite + if (pathname.startsWith("/v/") && + (cleaned_hostname === "youtube.com" || + cleaned_hostname === "youtube-nocookie.com")) { + return true; + } + } + return false; +} +/** + * Workaround Youtube mixed content if upgradeToHttps is true. + * + * @param elem The element to change. + * @param attr The attribute to adjust. + */ +function workaroundYoutubeMixedContent(elem, attr) { + var _a, _b; + const elem_attr = elem.getAttribute(attr); + const window_config = (_b = (_a = window.RufflePlayer) === null || _a === void 0 ? void 0 : _a.config) !== null && _b !== void 0 ? _b : {}; + if (elem_attr) { + try { + const url = new URL(elem_attr); + if (url.protocol === "http:" && + window.location.protocol === "https:" && + (!("upgradeToHttps" in window_config) || + window_config.upgradeToHttps !== false)) { + url.protocol = "https:"; + elem.setAttribute(attr, url.toString()); + } + } + catch (err) { + // Some invalid filenames, like `///`, could raise a TypeError. Let's fail silently in this situation. + } + } +} +/** + * Determine if an element is a child of a node that was not supported + * in non-HTML5 compliant browsers. If so, the element was meant to be + * used as a fallback content. + * + * @param elem The element to test. + * @returns True if the element is inside an