4252 lines
172 KiB
JavaScript
4252 lines
172 KiB
JavaScript
'use strict';
|
|
{
|
|
window.DOMHandler = class DOMHandler {
|
|
constructor(iRuntime, componentId) {
|
|
this._iRuntime = iRuntime;
|
|
this._componentId = componentId;
|
|
this._hasTickCallback = false;
|
|
this._tickCallback = ()=>this.Tick()
|
|
}
|
|
Attach() {}
|
|
PostToRuntime(handler, data, dispatchOpts, transferables) {
|
|
this._iRuntime.PostToRuntimeComponent(this._componentId, handler, data, dispatchOpts, transferables)
|
|
}
|
|
PostToRuntimeAsync(handler, data, dispatchOpts, transferables) {
|
|
return this._iRuntime.PostToRuntimeComponentAsync(this._componentId, handler, data, dispatchOpts, transferables)
|
|
}
|
|
_PostToRuntimeMaybeSync(name, data, dispatchOpts) {
|
|
if (this._iRuntime.UsesWorker())
|
|
this.PostToRuntime(name, data, dispatchOpts);
|
|
else
|
|
this._iRuntime._GetLocalRuntime()["_OnMessageFromDOM"]({
|
|
"type": "event",
|
|
"component": this._componentId,
|
|
"handler": name,
|
|
"dispatchOpts": dispatchOpts || null,
|
|
"data": data,
|
|
"responseId": null
|
|
})
|
|
}
|
|
AddRuntimeMessageHandler(handler, func) {
|
|
this._iRuntime.AddRuntimeComponentMessageHandler(this._componentId, handler, func)
|
|
}
|
|
AddRuntimeMessageHandlers(list) {
|
|
for (const [handler,func] of list)
|
|
this.AddRuntimeMessageHandler(handler, func)
|
|
}
|
|
GetRuntimeInterface() {
|
|
return this._iRuntime
|
|
}
|
|
GetComponentID() {
|
|
return this._componentId
|
|
}
|
|
_StartTicking() {
|
|
if (this._hasTickCallback)
|
|
return;
|
|
this._iRuntime._AddRAFCallback(this._tickCallback);
|
|
this._hasTickCallback = true
|
|
}
|
|
_StopTicking() {
|
|
if (!this._hasTickCallback)
|
|
return;
|
|
this._iRuntime._RemoveRAFCallback(this._tickCallback);
|
|
this._hasTickCallback = false
|
|
}
|
|
Tick() {}
|
|
}
|
|
;
|
|
window.RateLimiter = class RateLimiter {
|
|
constructor(callback, interval) {
|
|
this._callback = callback;
|
|
this._interval = interval;
|
|
this._timerId = -1;
|
|
this._lastCallTime = -Infinity;
|
|
this._timerCallFunc = ()=>this._OnTimer();
|
|
this._ignoreReset = false;
|
|
this._canRunImmediate = false
|
|
}
|
|
SetCanRunImmediate(c) {
|
|
this._canRunImmediate = !!c
|
|
}
|
|
Call() {
|
|
if (this._timerId !== -1)
|
|
return;
|
|
const nowTime = Date.now();
|
|
const timeSinceLastCall = nowTime - this._lastCallTime;
|
|
const interval = this._interval;
|
|
if (timeSinceLastCall >= interval && this._canRunImmediate) {
|
|
this._lastCallTime = nowTime;
|
|
this._RunCallback()
|
|
} else
|
|
this._timerId = self.setTimeout(this._timerCallFunc, Math.max(interval - timeSinceLastCall, 4))
|
|
}
|
|
_RunCallback() {
|
|
this._ignoreReset = true;
|
|
this._callback();
|
|
this._ignoreReset = false
|
|
}
|
|
Reset() {
|
|
if (this._ignoreReset)
|
|
return;
|
|
this._CancelTimer();
|
|
this._lastCallTime = Date.now()
|
|
}
|
|
_OnTimer() {
|
|
this._timerId = -1;
|
|
this._lastCallTime = Date.now();
|
|
this._RunCallback()
|
|
}
|
|
_CancelTimer() {
|
|
if (this._timerId !== -1) {
|
|
self.clearTimeout(this._timerId);
|
|
this._timerId = -1
|
|
}
|
|
}
|
|
Release() {
|
|
this._CancelTimer();
|
|
this._callback = null;
|
|
this._timerCallFunc = null
|
|
}
|
|
}
|
|
}
|
|
;'use strict';
|
|
{
|
|
window.DOMElementHandler = class DOMElementHandler extends self.DOMHandler {
|
|
constructor(iRuntime, componentId) {
|
|
super(iRuntime, componentId);
|
|
this._elementMap = new Map;
|
|
this._autoAttach = true;
|
|
this.AddRuntimeMessageHandlers([["create", e=>this._OnCreate(e)], ["destroy", e=>this._OnDestroy(e)], ["set-visible", e=>this._OnSetVisible(e)], ["update-position", e=>this._OnUpdatePosition(e)], ["update-state", e=>this._OnUpdateState(e)], ["focus", e=>this._OnSetFocus(e)], ["set-css-style", e=>this._OnSetCssStyle(e)], ["set-attribute", e=>this._OnSetAttribute(e)], ["remove-attribute", e=>this._OnRemoveAttribute(e)]]);
|
|
this.AddDOMElementMessageHandler("get-element", elem=>elem)
|
|
}
|
|
SetAutoAttach(e) {
|
|
this._autoAttach = !!e
|
|
}
|
|
AddDOMElementMessageHandler(handler, func) {
|
|
this.AddRuntimeMessageHandler(handler, e=>{
|
|
const elementId = e["elementId"];
|
|
const elem = this._elementMap.get(elementId);
|
|
return func(elem, e)
|
|
}
|
|
)
|
|
}
|
|
_OnCreate(e) {
|
|
const elementId = e["elementId"];
|
|
const elem = this.CreateElement(elementId, e);
|
|
this._elementMap.set(elementId, elem);
|
|
if (!e["isVisible"])
|
|
elem.style.display = "none";
|
|
const focusElem = this._GetFocusElement(elem);
|
|
focusElem.addEventListener("focus", e=>this._OnFocus(elementId));
|
|
focusElem.addEventListener("blur", e=>this._OnBlur(elementId));
|
|
if (this._autoAttach)
|
|
document.body.appendChild(elem)
|
|
}
|
|
CreateElement(elementId, e) {
|
|
throw new Error("required override");
|
|
}
|
|
DestroyElement(elem) {}
|
|
_OnDestroy(e) {
|
|
const elementId = e["elementId"];
|
|
const elem = this._elementMap.get(elementId);
|
|
this.DestroyElement(elem);
|
|
if (this._autoAttach)
|
|
elem.parentElement.removeChild(elem);
|
|
this._elementMap.delete(elementId)
|
|
}
|
|
PostToRuntimeElement(handler, elementId, data) {
|
|
if (!data)
|
|
data = {};
|
|
data["elementId"] = elementId;
|
|
this.PostToRuntime(handler, data)
|
|
}
|
|
_PostToRuntimeElementMaybeSync(handler, elementId, data) {
|
|
if (!data)
|
|
data = {};
|
|
data["elementId"] = elementId;
|
|
this._PostToRuntimeMaybeSync(handler, data)
|
|
}
|
|
_OnSetVisible(e) {
|
|
if (!this._autoAttach)
|
|
return;
|
|
const elem = this._elementMap.get(e["elementId"]);
|
|
elem.style.display = e["isVisible"] ? "" : "none"
|
|
}
|
|
_OnUpdatePosition(e) {
|
|
if (!this._autoAttach)
|
|
return;
|
|
const elem = this._elementMap.get(e["elementId"]);
|
|
elem.style.left = e["left"] + "px";
|
|
elem.style.top = e["top"] + "px";
|
|
elem.style.width = e["width"] + "px";
|
|
elem.style.height = e["height"] + "px";
|
|
const fontSize = e["fontSize"];
|
|
if (fontSize !== null)
|
|
elem.style.fontSize = fontSize + "em"
|
|
}
|
|
_OnUpdateState(e) {
|
|
const elem = this._elementMap.get(e["elementId"]);
|
|
this.UpdateState(elem, e)
|
|
}
|
|
UpdateState(elem, e) {
|
|
throw new Error("required override");
|
|
}
|
|
_GetFocusElement(elem) {
|
|
return elem
|
|
}
|
|
_OnFocus(elementId) {
|
|
this.PostToRuntimeElement("elem-focused", elementId)
|
|
}
|
|
_OnBlur(elementId) {
|
|
this.PostToRuntimeElement("elem-blurred", elementId)
|
|
}
|
|
_OnSetFocus(e) {
|
|
const elem = this._GetFocusElement(this._elementMap.get(e["elementId"]));
|
|
if (e["focus"])
|
|
elem.focus();
|
|
else
|
|
elem.blur()
|
|
}
|
|
_OnSetCssStyle(e) {
|
|
const elem = this._elementMap.get(e["elementId"]);
|
|
elem.style[e["prop"]] = e["val"]
|
|
}
|
|
_OnSetAttribute(e) {
|
|
const elem = this._elementMap.get(e["elementId"]);
|
|
elem.setAttribute(e["name"], e["val"])
|
|
}
|
|
_OnRemoveAttribute(e) {
|
|
const elem = this._elementMap.get(e["elementId"]);
|
|
elem.removeAttribute(e["name"])
|
|
}
|
|
GetElementById(elementId) {
|
|
return this._elementMap.get(elementId)
|
|
}
|
|
}
|
|
}
|
|
;'use strict';
|
|
{
|
|
const isiOSLike = /(iphone|ipod|ipad|macos|macintosh|mac os x)/i.test(navigator.userAgent);
|
|
function AddScript(url) {
|
|
if (url.isStringSrc) {
|
|
const elem = document.createElement("script");
|
|
elem.async = false;
|
|
elem.textContent = url.str;
|
|
document.head.appendChild(elem)
|
|
} else
|
|
return new Promise((resolve,reject)=>{
|
|
const elem = document.createElement("script");
|
|
elem.onload = resolve;
|
|
elem.onerror = reject;
|
|
elem.async = false;
|
|
elem.src = url;
|
|
document.head.appendChild(elem)
|
|
}
|
|
)
|
|
}
|
|
let tmpAudio = new Audio;
|
|
const supportedAudioFormats = {
|
|
"audio/webm; codecs=opus": !!tmpAudio.canPlayType("audio/webm; codecs=opus"),
|
|
"audio/ogg; codecs=opus": !!tmpAudio.canPlayType("audio/ogg; codecs=opus"),
|
|
"audio/webm; codecs=vorbis": !!tmpAudio.canPlayType("audio/webm; codecs=vorbis"),
|
|
"audio/ogg; codecs=vorbis": !!tmpAudio.canPlayType("audio/ogg; codecs=vorbis"),
|
|
"audio/mp4": !!tmpAudio.canPlayType("audio/mp4"),
|
|
"audio/mpeg": !!tmpAudio.canPlayType("audio/mpeg")
|
|
};
|
|
tmpAudio = null;
|
|
async function BlobToString(blob) {
|
|
const arrayBuffer = await BlobToArrayBuffer(blob);
|
|
const textDecoder = new TextDecoder("utf-8");
|
|
return textDecoder.decode(arrayBuffer)
|
|
}
|
|
function BlobToArrayBuffer(blob) {
|
|
return new Promise((resolve,reject)=>{
|
|
const fileReader = new FileReader;
|
|
fileReader.onload = e=>resolve(e.target.result);
|
|
fileReader.onerror = err=>reject(err);
|
|
fileReader.readAsArrayBuffer(blob)
|
|
}
|
|
)
|
|
}
|
|
const queuedArrayBufferReads = [];
|
|
let activeArrayBufferReads = 0;
|
|
const MAX_ARRAYBUFFER_READS = 8;
|
|
window["RealFile"] = window["File"];
|
|
const domHandlerClasses = [];
|
|
const runtimeEventHandlers = new Map;
|
|
const pendingResponsePromises = new Map;
|
|
let nextResponseId = 0;
|
|
const runOnStartupFunctions = [];
|
|
self.runOnStartup = function runOnStartup(f) {
|
|
if (typeof f !== "function")
|
|
throw new Error("runOnStartup called without a function");
|
|
runOnStartupFunctions.push(f)
|
|
}
|
|
;
|
|
const WEBVIEW_EXPORT_TYPES = new Set(["cordova", "playable-ad", "instant-games"]);
|
|
function IsWebViewExportType(exportType) {
|
|
return WEBVIEW_EXPORT_TYPES.has(exportType)
|
|
}
|
|
window.RuntimeInterface = class RuntimeInterface {
|
|
constructor(opts) {
|
|
this._useWorker = opts.useWorker;
|
|
this._messageChannelPort = null;
|
|
this._baseUrl = "";
|
|
this._scriptFolder = opts.scriptFolder;
|
|
this._workerScriptBlobURLs = {};
|
|
this._worker = null;
|
|
this._localRuntime = null;
|
|
this._loadingElem = null;
|
|
this._domHandlers = [];
|
|
this._runtimeDomHandler = null;
|
|
this._canvas = null;
|
|
this._jobScheduler = null;
|
|
this._rafId = -1;
|
|
this._rafFunc = ()=>this._OnRAFCallback();
|
|
this._rafCallbacks = [];
|
|
this._exportType = opts.exportType;
|
|
if (this._useWorker && (typeof OffscreenCanvas === "undefined" || !navigator["userActivation"]))
|
|
this._useWorker = false;
|
|
if (IsWebViewExportType(this._exportType) && this._useWorker) {
|
|
console.warn("[C3 runtime] Worker mode is enabled and supported, but is disabled in WebViews due to crbug.com/923007. Reverting to DOM mode.");
|
|
this._useWorker = false
|
|
}
|
|
this._localFileBlobs = null;
|
|
this._localFileStrings = null;
|
|
if ((this._exportType === "html5" || this._exportType === "playable-ad") && location.protocol.substr(0, 4) === "file")
|
|
alert("Exported games won't work until you upload them. (When running on the file: protocol, browsers block many features from working for security reasons.)");
|
|
this.AddRuntimeComponentMessageHandler("runtime", "cordova-fetch-local-file", e=>this._OnCordovaFetchLocalFile(e));
|
|
this.AddRuntimeComponentMessageHandler("runtime", "create-job-worker", e=>this._OnCreateJobWorker(e));
|
|
if (this._exportType === "cordova")
|
|
document.addEventListener("deviceready", ()=>this._Init(opts));
|
|
else
|
|
this._Init(opts)
|
|
}
|
|
Release() {
|
|
this._CancelAnimationFrame();
|
|
if (this._messageChannelPort) {
|
|
this._messageChannelPort.onmessage = null;
|
|
this._messageChannelPort = null
|
|
}
|
|
if (this._worker) {
|
|
this._worker.terminate();
|
|
this._worker = null
|
|
}
|
|
if (this._localRuntime) {
|
|
this._localRuntime.Release();
|
|
this._localRuntime = null
|
|
}
|
|
if (this._canvas) {
|
|
this._canvas.parentElement.removeChild(this._canvas);
|
|
this._canvas = null
|
|
}
|
|
}
|
|
GetCanvas() {
|
|
return this._canvas
|
|
}
|
|
GetBaseURL() {
|
|
return this._baseUrl
|
|
}
|
|
UsesWorker() {
|
|
return this._useWorker
|
|
}
|
|
GetExportType() {
|
|
return this._exportType
|
|
}
|
|
GetScriptFolder() {
|
|
return this._scriptFolder
|
|
}
|
|
IsiOSCordova() {
|
|
return isiOSLike && this._exportType === "cordova"
|
|
}
|
|
IsiOSWebView() {
|
|
return isiOSLike && IsWebViewExportType(this._exportType) || navigator["standalone"]
|
|
}
|
|
async _Init(opts) {
|
|
if (this._exportType === "preview") {
|
|
this._loadingElem = document.createElement("div");
|
|
this._loadingElem.className = "previewLoadingMessage";
|
|
this._loadingElem.textContent = opts.previewLoadingMessage;
|
|
document.body.appendChild(this._loadingElem)
|
|
}
|
|
if (this._exportType === "playable-ad") {
|
|
this._localFileBlobs = self["c3_base64files"];
|
|
this._localFileStrings = {};
|
|
await this._ConvertDataUrisToBlobs();
|
|
for (let i = 0, len = opts.engineScripts.length; i < len; ++i) {
|
|
const src = opts.engineScripts[i].toLowerCase();
|
|
if (this._localFileStrings.hasOwnProperty(src))
|
|
opts.engineScripts[i] = {
|
|
isStringSrc: true,
|
|
str: this._localFileStrings[src]
|
|
};
|
|
else if (this._localFileBlobs.hasOwnProperty(src))
|
|
opts.engineScripts[i] = URL.createObjectURL(this._localFileBlobs[src])
|
|
}
|
|
}
|
|
if (opts.baseUrl)
|
|
this._baseUrl = opts.baseUrl;
|
|
else {
|
|
const origin = location.origin;
|
|
this._baseUrl = (origin === "null" ? "file:///" : origin) + location.pathname;
|
|
const i = this._baseUrl.lastIndexOf("/");
|
|
if (i !== -1)
|
|
this._baseUrl = this._baseUrl.substr(0, i + 1)
|
|
}
|
|
if (opts.workerScripts)
|
|
for (const [url,blob] of Object.entries(opts.workerScripts))
|
|
this._workerScriptBlobURLs[url] = URL.createObjectURL(blob);
|
|
const messageChannel = new MessageChannel;
|
|
this._messageChannelPort = messageChannel.port1;
|
|
this._messageChannelPort.onmessage = e=>this["_OnMessageFromRuntime"](e.data);
|
|
if (window["c3_addPortMessageHandler"])
|
|
window["c3_addPortMessageHandler"](e=>this._OnMessageFromDebugger(e));
|
|
this._jobScheduler = new self.JobSchedulerDOM(this);
|
|
await this._jobScheduler.Init();
|
|
this.MaybeForceBodySize();
|
|
if (typeof window["StatusBar"] === "object")
|
|
window["StatusBar"]["hide"]();
|
|
if (typeof window["AndroidFullScreen"] === "object")
|
|
window["AndroidFullScreen"]["immersiveMode"]();
|
|
if (this._useWorker)
|
|
await this._InitWorker(opts, messageChannel.port2);
|
|
else
|
|
await this._InitDOM(opts, messageChannel.port2)
|
|
}
|
|
_GetWorkerURL(url) {
|
|
if (this._workerScriptBlobURLs.hasOwnProperty(url))
|
|
return this._workerScriptBlobURLs[url];
|
|
else if (url.endsWith("/workermain.js") && this._workerScriptBlobURLs.hasOwnProperty("workermain.js"))
|
|
return this._workerScriptBlobURLs["workermain.js"];
|
|
else if (this._exportType === "playable-ad" && this._localFileBlobs.hasOwnProperty(url.toLowerCase()))
|
|
return URL.createObjectURL(this._localFileBlobs[url.toLowerCase()]);
|
|
else
|
|
return url
|
|
}
|
|
async CreateWorker(url, baseUrl, workerOpts) {
|
|
if (url.startsWith("blob:"))
|
|
return new Worker(url,workerOpts);
|
|
if (this.IsiOSCordova() && location.protocol === "file:") {
|
|
const arrayBuffer = await this.CordovaFetchLocalFileAsArrayBuffer(this._scriptFolder + url);
|
|
const blob = new Blob([arrayBuffer],{
|
|
type: "application/javascript"
|
|
});
|
|
return new Worker(URL.createObjectURL(blob),workerOpts)
|
|
}
|
|
const absUrl = new URL(url,baseUrl);
|
|
const isCrossOrigin = location.origin !== absUrl.origin;
|
|
if (isCrossOrigin) {
|
|
const response = await fetch(absUrl);
|
|
if (!response.ok)
|
|
throw new Error("failed to fetch worker script");
|
|
const blob = await response.blob();
|
|
return new Worker(URL.createObjectURL(blob),workerOpts)
|
|
} else
|
|
return new Worker(absUrl,workerOpts)
|
|
}
|
|
_GetWindowInnerWidth() {
|
|
return Math.max(window.innerWidth, 1)
|
|
}
|
|
_GetWindowInnerHeight() {
|
|
return Math.max(window.innerHeight, 1)
|
|
}
|
|
MaybeForceBodySize() {
|
|
if (this.IsiOSWebView()) {
|
|
const docStyle = document["documentElement"].style;
|
|
const bodyStyle = document["body"].style;
|
|
const isPortrait = window.innerWidth < window.innerHeight;
|
|
const width = isPortrait ? window["screen"]["width"] : window["screen"]["height"];
|
|
const height = isPortrait ? window["screen"]["height"] : window["screen"]["width"];
|
|
bodyStyle["height"] = docStyle["height"] = height + "px";
|
|
bodyStyle["width"] = docStyle["width"] = width + "px"
|
|
}
|
|
}
|
|
_GetCommonRuntimeOptions(opts) {
|
|
return {
|
|
"baseUrl": this._baseUrl,
|
|
"windowInnerWidth": this._GetWindowInnerWidth(),
|
|
"windowInnerHeight": this._GetWindowInnerHeight(),
|
|
"devicePixelRatio": window.devicePixelRatio,
|
|
"isFullscreen": RuntimeInterface.IsDocumentFullscreen(),
|
|
"projectData": opts.projectData,
|
|
"previewImageBlobs": window["cr_previewImageBlobs"] || this._localFileBlobs,
|
|
"previewProjectFileBlobs": window["cr_previewProjectFileBlobs"],
|
|
"exportType": opts.exportType,
|
|
"isDebug": self.location.search.indexOf("debug") > -1,
|
|
"ife": !!self.ife,
|
|
"jobScheduler": this._jobScheduler.GetPortData(),
|
|
"supportedAudioFormats": supportedAudioFormats,
|
|
"opusWasmScriptUrl": window["cr_opusWasmScriptUrl"] || this._scriptFolder + "opus.wasm.js",
|
|
"opusWasmBinaryUrl": window["cr_opusWasmBinaryUrl"] || this._scriptFolder + "opus.wasm.wasm",
|
|
"isiOSCordova": this.IsiOSCordova(),
|
|
"isiOSWebView": this.IsiOSWebView(),
|
|
"isFBInstantAvailable": typeof self["FBInstant"] !== "undefined"
|
|
}
|
|
}
|
|
async _InitWorker(opts, port2) {
|
|
const workerMainUrl = this._GetWorkerURL(opts.workerMainUrl);
|
|
this._worker = await this.CreateWorker(workerMainUrl, this._baseUrl, {
|
|
name: "Runtime"
|
|
});
|
|
this._canvas = document.createElement("canvas");
|
|
this._canvas.style.display = "none";
|
|
const offscreenCanvas = this._canvas["transferControlToOffscreen"]();
|
|
document.body.appendChild(this._canvas);
|
|
window["c3canvas"] = this._canvas;
|
|
this._worker.postMessage(Object.assign(this._GetCommonRuntimeOptions(opts), {
|
|
"type": "init-runtime",
|
|
"isInWorker": true,
|
|
"messagePort": port2,
|
|
"canvas": offscreenCanvas,
|
|
"workerDependencyScripts": opts.workerDependencyScripts || [],
|
|
"engineScripts": opts.engineScripts,
|
|
"projectScripts": window.cr_allProjectScripts,
|
|
"projectScriptsStatus": self["C3_ProjectScriptsStatus"]
|
|
}), [port2, offscreenCanvas, ...this._jobScheduler.GetPortTransferables()]);
|
|
this._domHandlers = domHandlerClasses.map(C=>new C(this));
|
|
this._FindRuntimeDOMHandler();
|
|
self["c3_callFunction"] = (name,params)=>this._runtimeDomHandler._InvokeFunctionFromJS(name, params);
|
|
if (this._exportType === "preview")
|
|
self["goToLastErrorScript"] = ()=>this.PostToRuntimeComponent("runtime", "go-to-last-error-script")
|
|
}
|
|
async _InitDOM(opts, port2) {
|
|
this._canvas = document.createElement("canvas");
|
|
this._canvas.style.display = "none";
|
|
document.body.appendChild(this._canvas);
|
|
window["c3canvas"] = this._canvas;
|
|
this._domHandlers = domHandlerClasses.map(C=>new C(this));
|
|
this._FindRuntimeDOMHandler();
|
|
const engineScripts = opts.engineScripts.map(url=>typeof url === "string" ? (new URL(url,this._baseUrl)).toString() : url);
|
|
if (Array.isArray(opts.workerDependencyScripts))
|
|
engineScripts.unshift(...opts.workerDependencyScripts);
|
|
await Promise.all(engineScripts.map(url=>AddScript(url)));
|
|
if (opts.projectScripts && opts.projectScripts.length > 0) {
|
|
const scriptsStatus = self["C3_ProjectScriptsStatus"];
|
|
try {
|
|
await Promise.all(opts.projectScripts.map(e=>AddScript(e[1])));
|
|
if (Object.values(scriptsStatus).some(f=>!f)) {
|
|
self.setTimeout(()=>this._ReportProjectScriptError(scriptsStatus), 100);
|
|
return
|
|
}
|
|
} catch (err) {
|
|
console.error("[Preview] Error loading project scripts: ", err);
|
|
self.setTimeout(()=>this._ReportProjectScriptError(scriptsStatus), 100);
|
|
return
|
|
}
|
|
}
|
|
if (this._exportType === "preview" && typeof self.C3.ScriptsInEvents !== "object") {
|
|
this._RemoveLoadingMessage();
|
|
const msg = "Failed to load JavaScript code used in events. Check all your JavaScript code has valid syntax.";
|
|
console.error("[C3 runtime] " + msg);
|
|
alert(msg);
|
|
return
|
|
}
|
|
const runtimeOpts = Object.assign(this._GetCommonRuntimeOptions(opts), {
|
|
"isInWorker": false,
|
|
"messagePort": port2,
|
|
"canvas": this._canvas,
|
|
"runOnStartupFunctions": runOnStartupFunctions
|
|
});
|
|
this._OnBeforeCreateRuntime();
|
|
this._localRuntime = self["C3_CreateRuntime"](runtimeOpts);
|
|
await self["C3_InitRuntime"](this._localRuntime, runtimeOpts)
|
|
}
|
|
_ReportProjectScriptError(scriptsStatus) {
|
|
this._RemoveLoadingMessage();
|
|
const failedScripts = Object.entries(scriptsStatus).filter(e=>!e[1]).map(e=>e[0]);
|
|
const msg = `Failed to load project script '${failedScripts[0]}'. Check all your JavaScript code has valid syntax.`;
|
|
console.error("[Preview] " + msg);
|
|
alert(msg)
|
|
}
|
|
_OnBeforeCreateRuntime() {
|
|
this._RemoveLoadingMessage()
|
|
}
|
|
_RemoveLoadingMessage() {
|
|
if (this._loadingElem) {
|
|
this._loadingElem.parentElement.removeChild(this._loadingElem);
|
|
this._loadingElem = null
|
|
}
|
|
}
|
|
async _OnCreateJobWorker(e) {
|
|
const outputPort = await this._jobScheduler._CreateJobWorker();
|
|
return {
|
|
"outputPort": outputPort,
|
|
"transferables": [outputPort]
|
|
}
|
|
}
|
|
_GetLocalRuntime() {
|
|
if (this._useWorker)
|
|
throw new Error("not available in worker mode");
|
|
return this._localRuntime
|
|
}
|
|
PostToRuntimeComponent(component, handler, data, dispatchOpts, transferables) {
|
|
this._messageChannelPort.postMessage({
|
|
"type": "event",
|
|
"component": component,
|
|
"handler": handler,
|
|
"dispatchOpts": dispatchOpts || null,
|
|
"data": data,
|
|
"responseId": null
|
|
}, transferables)
|
|
}
|
|
PostToRuntimeComponentAsync(component, handler, data, dispatchOpts, transferables) {
|
|
const responseId = nextResponseId++;
|
|
const ret = new Promise((resolve,reject)=>{
|
|
pendingResponsePromises.set(responseId, {
|
|
resolve,
|
|
reject
|
|
})
|
|
}
|
|
);
|
|
this._messageChannelPort.postMessage({
|
|
"type": "event",
|
|
"component": component,
|
|
"handler": handler,
|
|
"dispatchOpts": dispatchOpts || null,
|
|
"data": data,
|
|
"responseId": responseId
|
|
}, transferables);
|
|
return ret
|
|
}
|
|
["_OnMessageFromRuntime"](data) {
|
|
const type = data["type"];
|
|
if (type === "event")
|
|
return this._OnEventFromRuntime(data);
|
|
else if (type === "result")
|
|
this._OnResultFromRuntime(data);
|
|
else if (type === "runtime-ready")
|
|
this._OnRuntimeReady();
|
|
else if (type === "alert-error") {
|
|
this._RemoveLoadingMessage();
|
|
alert(data["message"])
|
|
} else if (type === "creating-runtime")
|
|
this._OnBeforeCreateRuntime();
|
|
else
|
|
throw new Error(`unknown message '${type}'`);
|
|
}
|
|
_OnEventFromRuntime(e) {
|
|
const component = e["component"];
|
|
const handler = e["handler"];
|
|
const data = e["data"];
|
|
const responseId = e["responseId"];
|
|
const handlerMap = runtimeEventHandlers.get(component);
|
|
if (!handlerMap) {
|
|
console.warn(`[DOM] No event handlers for component '${component}'`);
|
|
return
|
|
}
|
|
const func = handlerMap.get(handler);
|
|
if (!func) {
|
|
console.warn(`[DOM] No handler '${handler}' for component '${component}'`);
|
|
return
|
|
}
|
|
let ret = null;
|
|
try {
|
|
ret = func(data)
|
|
} catch (err) {
|
|
console.error(`Exception in '${component}' handler '${handler}':`, err);
|
|
if (responseId !== null)
|
|
this._PostResultToRuntime(responseId, false, "" + err);
|
|
return
|
|
}
|
|
if (responseId === null)
|
|
return ret;
|
|
else if (ret && ret.then)
|
|
ret.then(result=>this._PostResultToRuntime(responseId, true, result)).catch(err=>{
|
|
console.error(`Rejection from '${component}' handler '${handler}':`, err);
|
|
this._PostResultToRuntime(responseId, false, "" + err)
|
|
}
|
|
);
|
|
else
|
|
this._PostResultToRuntime(responseId, true, ret)
|
|
}
|
|
_PostResultToRuntime(responseId, isOk, result) {
|
|
let transferables;
|
|
if (result && result["transferables"])
|
|
transferables = result["transferables"];
|
|
this._messageChannelPort.postMessage({
|
|
"type": "result",
|
|
"responseId": responseId,
|
|
"isOk": isOk,
|
|
"result": result
|
|
}, transferables)
|
|
}
|
|
_OnResultFromRuntime(data) {
|
|
const responseId = data["responseId"];
|
|
const isOk = data["isOk"];
|
|
const result = data["result"];
|
|
const pendingPromise = pendingResponsePromises.get(responseId);
|
|
if (isOk)
|
|
pendingPromise.resolve(result);
|
|
else
|
|
pendingPromise.reject(result);
|
|
pendingResponsePromises.delete(responseId)
|
|
}
|
|
AddRuntimeComponentMessageHandler(component, handler, func) {
|
|
let handlerMap = runtimeEventHandlers.get(component);
|
|
if (!handlerMap) {
|
|
handlerMap = new Map;
|
|
runtimeEventHandlers.set(component, handlerMap)
|
|
}
|
|
if (handlerMap.has(handler))
|
|
throw new Error(`[DOM] Component '${component}' already has handler '${handler}'`);
|
|
handlerMap.set(handler, func)
|
|
}
|
|
static AddDOMHandlerClass(Class) {
|
|
if (domHandlerClasses.includes(Class))
|
|
throw new Error("DOM handler already added");
|
|
domHandlerClasses.push(Class)
|
|
}
|
|
_FindRuntimeDOMHandler() {
|
|
for (const dh of this._domHandlers)
|
|
if (dh.GetComponentID() === "runtime") {
|
|
this._runtimeDomHandler = dh;
|
|
return
|
|
}
|
|
throw new Error("cannot find runtime DOM handler");
|
|
}
|
|
_OnMessageFromDebugger(e) {
|
|
this.PostToRuntimeComponent("debugger", "message", e)
|
|
}
|
|
_OnRuntimeReady() {
|
|
for (const h of this._domHandlers)
|
|
h.Attach()
|
|
}
|
|
static IsDocumentFullscreen() {
|
|
return !!(document["fullscreenElement"] || document["webkitFullscreenElement"] || document["mozFullScreenElement"])
|
|
}
|
|
async GetRemotePreviewStatusInfo() {
|
|
return await this.PostToRuntimeComponentAsync("runtime", "get-remote-preview-status-info")
|
|
}
|
|
_AddRAFCallback(f) {
|
|
this._rafCallbacks.push(f);
|
|
this._RequestAnimationFrame()
|
|
}
|
|
_RemoveRAFCallback(f) {
|
|
const i = this._rafCallbacks.indexOf(f);
|
|
if (i === -1)
|
|
throw new Error("invalid callback");
|
|
this._rafCallbacks.splice(i, 1);
|
|
if (!this._rafCallbacks.length)
|
|
this._CancelAnimationFrame()
|
|
}
|
|
_RequestAnimationFrame() {
|
|
if (this._rafId === -1 && this._rafCallbacks.length)
|
|
this._rafId = requestAnimationFrame(this._rafFunc)
|
|
}
|
|
_CancelAnimationFrame() {
|
|
if (this._rafId !== -1) {
|
|
cancelAnimationFrame(this._rafId);
|
|
this._rafId = -1
|
|
}
|
|
}
|
|
_OnRAFCallback() {
|
|
this._rafId = -1;
|
|
for (const f of this._rafCallbacks)
|
|
f();
|
|
this._RequestAnimationFrame()
|
|
}
|
|
TryPlayMedia(mediaElem) {
|
|
this._runtimeDomHandler.TryPlayMedia(mediaElem)
|
|
}
|
|
RemovePendingPlay(mediaElem) {
|
|
this._runtimeDomHandler.RemovePendingPlay(mediaElem)
|
|
}
|
|
_PlayPendingMedia() {
|
|
this._runtimeDomHandler._PlayPendingMedia()
|
|
}
|
|
SetSilent(s) {
|
|
this._runtimeDomHandler.SetSilent(s)
|
|
}
|
|
IsAudioFormatSupported(typeStr) {
|
|
return !!supportedAudioFormats[typeStr]
|
|
}
|
|
async _WasmDecodeWebMOpus(arrayBuffer) {
|
|
const result = await this.PostToRuntimeComponentAsync("runtime", "opus-decode", {
|
|
"arrayBuffer": arrayBuffer
|
|
}, null, [arrayBuffer]);
|
|
return new Float32Array(result)
|
|
}
|
|
IsAbsoluteURL(url) {
|
|
return /^(?:[a-z]+:)?\/\//.test(url) || url.substr(0, 5) === "data:" || url.substr(0, 5) === "blob:"
|
|
}
|
|
IsRelativeURL(url) {
|
|
return !this.IsAbsoluteURL(url)
|
|
}
|
|
async _OnCordovaFetchLocalFile(e) {
|
|
const filename = e["filename"];
|
|
switch (e["as"]) {
|
|
case "text":
|
|
return await this.CordovaFetchLocalFileAsText(filename);
|
|
case "buffer":
|
|
return await this.CordovaFetchLocalFileAsArrayBuffer(filename);
|
|
default:
|
|
throw new Error("unsupported type");
|
|
}
|
|
}
|
|
_GetPermissionAPI() {
|
|
const api = window["cordova"] && window["cordova"]["plugins"] && window["cordova"]["plugins"]["permissions"];
|
|
if (typeof api !== "object")
|
|
throw new Error("Permission API is not loaded");
|
|
return api
|
|
}
|
|
_MapPermissionID(api, permission) {
|
|
const permissionID = api[permission];
|
|
if (typeof permissionID !== "string")
|
|
throw new Error("Invalid permission name");
|
|
return permissionID
|
|
}
|
|
_HasPermission(id) {
|
|
const api = this._GetPermissionAPI();
|
|
return new Promise((resolve,reject)=>api["checkPermission"](this._MapPermissionID(api, id), status=>resolve(!!status["hasPermission"]), reject))
|
|
}
|
|
_RequestPermission(id) {
|
|
const api = this._GetPermissionAPI();
|
|
return new Promise((resolve,reject)=>api["requestPermission"](this._MapPermissionID(api, id), status=>resolve(!!status["hasPermission"]), reject))
|
|
}
|
|
async RequestPermissions(permissions) {
|
|
if (this.GetExportType() !== "cordova")
|
|
return true;
|
|
if (this.IsiOSCordova())
|
|
return true;
|
|
for (const id of permissions) {
|
|
const alreadyGranted = await this._HasPermission(id);
|
|
if (alreadyGranted)
|
|
continue;
|
|
const granted = await this._RequestPermission(id);
|
|
if (granted === false)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
async RequirePermissions(...permissions) {
|
|
if (await this.RequestPermissions(permissions) === false)
|
|
throw new Error("Permission not granted");
|
|
}
|
|
CordovaFetchLocalFile(filename) {
|
|
const path = window["cordova"]["file"]["applicationDirectory"] + "www/" + filename.toLowerCase();
|
|
return new Promise((resolve,reject)=>{
|
|
window["resolveLocalFileSystemURL"](path, entry=>{
|
|
entry["file"](resolve, reject)
|
|
}
|
|
, reject)
|
|
}
|
|
)
|
|
}
|
|
async CordovaFetchLocalFileAsText(filename) {
|
|
const file = await this.CordovaFetchLocalFile(filename);
|
|
return await BlobToString(file)
|
|
}
|
|
_CordovaMaybeStartNextArrayBufferRead() {
|
|
if (!queuedArrayBufferReads.length)
|
|
return;
|
|
if (activeArrayBufferReads >= MAX_ARRAYBUFFER_READS)
|
|
return;
|
|
activeArrayBufferReads++;
|
|
const job = queuedArrayBufferReads.shift();
|
|
this._CordovaDoFetchLocalFileAsAsArrayBuffer(job.filename, job.successCallback, job.errorCallback)
|
|
}
|
|
CordovaFetchLocalFileAsArrayBuffer(filename) {
|
|
return new Promise((resolve,reject)=>{
|
|
queuedArrayBufferReads.push({
|
|
filename: filename,
|
|
successCallback: result=>{
|
|
activeArrayBufferReads--;
|
|
this._CordovaMaybeStartNextArrayBufferRead();
|
|
resolve(result)
|
|
}
|
|
,
|
|
errorCallback: err=>{
|
|
activeArrayBufferReads--;
|
|
this._CordovaMaybeStartNextArrayBufferRead();
|
|
reject(err)
|
|
}
|
|
});
|
|
this._CordovaMaybeStartNextArrayBufferRead()
|
|
}
|
|
)
|
|
}
|
|
async _CordovaDoFetchLocalFileAsAsArrayBuffer(filename, successCallback, errorCallback) {
|
|
try {
|
|
const file = await this.CordovaFetchLocalFile(filename);
|
|
const arrayBuffer = await BlobToArrayBuffer(file);
|
|
successCallback(arrayBuffer)
|
|
} catch (err) {
|
|
errorCallback(err)
|
|
}
|
|
}
|
|
async _ConvertDataUrisToBlobs() {
|
|
const promises = [];
|
|
for (const [filename,data] of Object.entries(this._localFileBlobs))
|
|
promises.push(this._ConvertDataUriToBlobs(filename, data));
|
|
await Promise.all(promises)
|
|
}
|
|
async _ConvertDataUriToBlobs(filename, data) {
|
|
if (typeof data === "object") {
|
|
this._localFileBlobs[filename] = new Blob([data["str"]],{
|
|
"type": data["type"]
|
|
});
|
|
this._localFileStrings[filename] = data["str"]
|
|
} else {
|
|
let blob = await this._FetchDataUri(data);
|
|
if (!blob)
|
|
blob = this._DataURIToBinaryBlobSync(data);
|
|
this._localFileBlobs[filename] = blob
|
|
}
|
|
}
|
|
async _FetchDataUri(dataUri) {
|
|
try {
|
|
const response = await fetch(dataUri);
|
|
return await response.blob()
|
|
} catch (err) {
|
|
console.warn("Failed to fetch a data: URI. Falling back to a slower workaround. This is probably because the Content Security Policy unnecessarily blocked it. Allow data: URIs in your CSP to avoid this.", err);
|
|
return null
|
|
}
|
|
}
|
|
_DataURIToBinaryBlobSync(datauri) {
|
|
const o = this._ParseDataURI(datauri);
|
|
return this._BinaryStringToBlob(o.data, o.mime_type)
|
|
}
|
|
_ParseDataURI(datauri) {
|
|
const comma = datauri.indexOf(",");
|
|
if (comma < 0)
|
|
throw new URIError("expected comma in data: uri");
|
|
const typepart = datauri.substring(5, comma);
|
|
const datapart = datauri.substring(comma + 1);
|
|
const typearr = typepart.split(";");
|
|
const mimetype = typearr[0] || "";
|
|
const encoding1 = typearr[1];
|
|
const encoding2 = typearr[2];
|
|
let decodeddata;
|
|
if (encoding1 === "base64" || encoding2 === "base64")
|
|
decodeddata = atob(datapart);
|
|
else
|
|
decodeddata = decodeURIComponent(datapart);
|
|
return {
|
|
mime_type: mimetype,
|
|
data: decodeddata
|
|
}
|
|
}
|
|
_BinaryStringToBlob(binstr, mime_type) {
|
|
let len = binstr.length;
|
|
let len32 = len >> 2;
|
|
let a8 = new Uint8Array(len);
|
|
let a32 = new Uint32Array(a8.buffer,0,len32);
|
|
let i, j;
|
|
for (i = 0,
|
|
j = 0; i < len32; ++i)
|
|
a32[i] = binstr.charCodeAt(j++) | binstr.charCodeAt(j++) << 8 | binstr.charCodeAt(j++) << 16 | binstr.charCodeAt(j++) << 24;
|
|
let tailLength = len & 3;
|
|
while (tailLength--) {
|
|
a8[j] = binstr.charCodeAt(j);
|
|
++j
|
|
}
|
|
return new Blob([a8],{
|
|
"type": mime_type
|
|
})
|
|
}
|
|
}
|
|
}
|
|
;'use strict';
|
|
{
|
|
const RuntimeInterface = self.RuntimeInterface;
|
|
function IsCompatibilityMouseEvent(e) {
|
|
return e["sourceCapabilities"] && e["sourceCapabilities"]["firesTouchEvents"] || e["originalEvent"] && e["originalEvent"]["sourceCapabilities"] && e["originalEvent"]["sourceCapabilities"]["firesTouchEvents"]
|
|
}
|
|
const KEY_CODE_ALIASES = new Map([["OSLeft", "MetaLeft"], ["OSRight", "MetaRight"]]);
|
|
const DISPATCH_RUNTIME_AND_SCRIPT = {
|
|
"dispatchRuntimeEvent": true,
|
|
"dispatchUserScriptEvent": true
|
|
};
|
|
const DISPATCH_SCRIPT_ONLY = {
|
|
"dispatchUserScriptEvent": true
|
|
};
|
|
const DISPATCH_RUNTIME_ONLY = {
|
|
"dispatchRuntimeEvent": true
|
|
};
|
|
function AddStyleSheet(cssUrl) {
|
|
return new Promise((resolve,reject)=>{
|
|
const styleLink = document.createElement("link");
|
|
styleLink.onload = ()=>resolve(styleLink);
|
|
styleLink.onerror = err=>reject(err);
|
|
styleLink.rel = "stylesheet";
|
|
styleLink.href = cssUrl;
|
|
document.head.appendChild(styleLink)
|
|
}
|
|
)
|
|
}
|
|
function FetchImage(url) {
|
|
return new Promise((resolve,reject)=>{
|
|
const img = new Image;
|
|
img.onload = ()=>resolve(img);
|
|
img.onerror = err=>reject(err);
|
|
img.src = url
|
|
}
|
|
)
|
|
}
|
|
async function BlobToImage(blob) {
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
try {
|
|
return await FetchImage(blobUrl)
|
|
} finally {
|
|
URL.revokeObjectURL(blobUrl)
|
|
}
|
|
}
|
|
function BlobToString(blob) {
|
|
return new Promise((resolve,reject)=>{
|
|
let fileReader = new FileReader;
|
|
fileReader.onload = e=>resolve(e.target.result);
|
|
fileReader.onerror = err=>reject(err);
|
|
fileReader.readAsText(blob)
|
|
}
|
|
)
|
|
}
|
|
async function BlobToSvgImage(blob, width, height) {
|
|
if (!/firefox/i.test(navigator.userAgent))
|
|
return await BlobToImage(blob);
|
|
let str = await BlobToString(blob);
|
|
const parser = new DOMParser;
|
|
const doc = parser.parseFromString(str, "image/svg+xml");
|
|
const rootElem = doc.documentElement;
|
|
if (rootElem.hasAttribute("width") && rootElem.hasAttribute("height")) {
|
|
const widthStr = rootElem.getAttribute("width");
|
|
const heightStr = rootElem.getAttribute("height");
|
|
if (!widthStr.includes("%") && !heightStr.includes("%"))
|
|
return await BlobToImage(blob)
|
|
}
|
|
rootElem.setAttribute("width", width + "px");
|
|
rootElem.setAttribute("height", height + "px");
|
|
const serializer = new XMLSerializer;
|
|
str = serializer.serializeToString(doc);
|
|
blob = new Blob([str],{
|
|
type: "image/svg+xml"
|
|
});
|
|
return await BlobToImage(blob)
|
|
}
|
|
function IsInContentEditable(el) {
|
|
do {
|
|
if (el.parentNode && el.hasAttribute("contenteditable"))
|
|
return true;
|
|
el = el.parentNode
|
|
} while (el);
|
|
return false
|
|
}
|
|
const canvasOrDocTags = new Set(["canvas", "body", "html"]);
|
|
function PreventDefaultOnCanvasOrDoc(e) {
|
|
const tagName = e.target.tagName.toLowerCase();
|
|
if (canvasOrDocTags.has(tagName))
|
|
e.preventDefault()
|
|
}
|
|
function BlockWheelZoom(e) {
|
|
if (e.metaKey || e.ctrlKey)
|
|
e.preventDefault()
|
|
}
|
|
self["C3_GetSvgImageSize"] = async function(blob) {
|
|
const img = await BlobToImage(blob);
|
|
if (img.width > 0 && img.height > 0)
|
|
return [img.width, img.height];
|
|
else {
|
|
img.style.position = "absolute";
|
|
img.style.left = "0px";
|
|
img.style.top = "0px";
|
|
img.style.visibility = "hidden";
|
|
document.body.appendChild(img);
|
|
const rc = img.getBoundingClientRect();
|
|
document.body.removeChild(img);
|
|
return [rc.width, rc.height]
|
|
}
|
|
}
|
|
;
|
|
self["C3_RasterSvgImageBlob"] = async function(blob, imageWidth, imageHeight, surfaceWidth, surfaceHeight) {
|
|
const img = await BlobToSvgImage(blob, imageWidth, imageHeight);
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = surfaceWidth;
|
|
canvas.height = surfaceHeight;
|
|
const ctx = canvas.getContext("2d");
|
|
ctx.drawImage(img, 0, 0, imageWidth, imageHeight);
|
|
return canvas
|
|
}
|
|
;
|
|
let isCordovaPaused = false;
|
|
document.addEventListener("pause", ()=>isCordovaPaused = true);
|
|
document.addEventListener("resume", ()=>isCordovaPaused = false);
|
|
function ParentHasFocus() {
|
|
try {
|
|
return window.parent && window.parent.document.hasFocus()
|
|
} catch (err) {
|
|
return false
|
|
}
|
|
}
|
|
function KeyboardIsVisible() {
|
|
const elem = document.activeElement;
|
|
if (!elem)
|
|
return false;
|
|
const tagName = elem.tagName.toLowerCase();
|
|
const inputTypes = new Set(["email", "number", "password", "search", "tel", "text", "url"]);
|
|
if (tagName === "textarea")
|
|
return true;
|
|
if (tagName === "input")
|
|
return inputTypes.has(elem.type.toLowerCase() || "text");
|
|
return IsInContentEditable(elem)
|
|
}
|
|
const DOM_COMPONENT_ID = "runtime";
|
|
const HANDLER_CLASS = class RuntimeDOMHandler extends self.DOMHandler {
|
|
constructor(iRuntime) {
|
|
super(iRuntime, DOM_COMPONENT_ID);
|
|
this._isFirstSizeUpdate = true;
|
|
this._simulatedResizeTimerId = -1;
|
|
this._targetOrientation = "any";
|
|
this._attachedDeviceOrientationEvent = false;
|
|
this._attachedDeviceMotionEvent = false;
|
|
this._debugHighlightElem = null;
|
|
this._pointerRawUpdateRateLimiter = null;
|
|
this._lastPointerRawUpdateEvent = null;
|
|
iRuntime.AddRuntimeComponentMessageHandler("canvas", "update-size", e=>this._OnUpdateCanvasSize(e));
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "invoke-download", e=>this._OnInvokeDownload(e));
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "raster-svg-image", e=>this._OnRasterSvgImage(e));
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "get-svg-image-size", e=>this._OnGetSvgImageSize(e));
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "set-target-orientation", e=>this._OnSetTargetOrientation(e));
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "register-sw", ()=>this._OnRegisterSW());
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "post-to-debugger", e=>this._OnPostToDebugger(e));
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "go-to-script", e=>this._OnPostToDebugger(e));
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "before-start-ticking", ()=>this._OnBeforeStartTicking());
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "debug-highlight", e=>this._OnDebugHighlight(e));
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "enable-device-orientation", ()=>this._AttachDeviceOrientationEvent());
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "enable-device-motion", ()=>this._AttachDeviceMotionEvent());
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "add-stylesheet", e=>this._OnAddStylesheet(e));
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "alert", e=>this._OnAlert(e));
|
|
iRuntime.AddRuntimeComponentMessageHandler("runtime", "hide-cordova-splash", ()=>this._OnHideCordovaSplash());
|
|
const allowDefaultContextMenuTagNames = new Set(["input", "textarea", "datalist"]);
|
|
window.addEventListener("contextmenu", e=>{
|
|
const t = e.target;
|
|
const name = t.tagName.toLowerCase();
|
|
if (!allowDefaultContextMenuTagNames.has(name) && !IsInContentEditable(t))
|
|
e.preventDefault()
|
|
}
|
|
);
|
|
const canvas = iRuntime.GetCanvas();
|
|
window.addEventListener("selectstart", PreventDefaultOnCanvasOrDoc);
|
|
window.addEventListener("gesturehold", PreventDefaultOnCanvasOrDoc);
|
|
canvas.addEventListener("selectstart", PreventDefaultOnCanvasOrDoc);
|
|
canvas.addEventListener("gesturehold", PreventDefaultOnCanvasOrDoc);
|
|
window.addEventListener("touchstart", PreventDefaultOnCanvasOrDoc, {
|
|
"passive": false
|
|
});
|
|
if (typeof PointerEvent !== "undefined") {
|
|
window.addEventListener("pointerdown", PreventDefaultOnCanvasOrDoc, {
|
|
"passive": false
|
|
});
|
|
canvas.addEventListener("pointerdown", PreventDefaultOnCanvasOrDoc)
|
|
} else
|
|
canvas.addEventListener("touchstart", PreventDefaultOnCanvasOrDoc);
|
|
this._mousePointerLastButtons = 0;
|
|
window.addEventListener("mousedown", e=>{
|
|
if (e.button === 1)
|
|
e.preventDefault()
|
|
}
|
|
);
|
|
window.addEventListener("mousewheel", BlockWheelZoom, {
|
|
"passive": false
|
|
});
|
|
window.addEventListener("wheel", BlockWheelZoom, {
|
|
"passive": false
|
|
});
|
|
window.addEventListener("resize", ()=>this._OnWindowResize());
|
|
if (iRuntime.IsiOSWebView())
|
|
window.addEventListener("focusout", ()=>{
|
|
if (!KeyboardIsVisible())
|
|
document.scrollingElement.scrollTop = 0
|
|
}
|
|
);
|
|
this._mediaPendingPlay = new Set;
|
|
this._mediaRemovedPendingPlay = new WeakSet;
|
|
this._isSilent = false
|
|
}
|
|
_OnBeforeStartTicking() {
|
|
if (this._iRuntime.GetExportType() === "cordova") {
|
|
document.addEventListener("pause", ()=>this._OnVisibilityChange(true));
|
|
document.addEventListener("resume", ()=>this._OnVisibilityChange(false))
|
|
} else
|
|
document.addEventListener("visibilitychange", ()=>this._OnVisibilityChange(document.hidden));
|
|
return {
|
|
"isSuspended": !!(document.hidden || isCordovaPaused)
|
|
}
|
|
}
|
|
Attach() {
|
|
window.addEventListener("focus", ()=>this._PostRuntimeEvent("window-focus"));
|
|
window.addEventListener("blur", ()=>{
|
|
this._PostRuntimeEvent("window-blur", {
|
|
"parentHasFocus": ParentHasFocus()
|
|
});
|
|
this._mousePointerLastButtons = 0
|
|
}
|
|
);
|
|
window.addEventListener("fullscreenchange", ()=>this._OnFullscreenChange());
|
|
window.addEventListener("webkitfullscreenchange", ()=>this._OnFullscreenChange());
|
|
window.addEventListener("mozfullscreenchange", ()=>this._OnFullscreenChange());
|
|
window.addEventListener("fullscreenerror", e=>this._OnFullscreenError(e));
|
|
window.addEventListener("webkitfullscreenerror", e=>this._OnFullscreenError(e));
|
|
window.addEventListener("mozfullscreenerror", e=>this._OnFullscreenError(e));
|
|
window.addEventListener("keydown", e=>this._OnKeyEvent("keydown", e));
|
|
window.addEventListener("keyup", e=>this._OnKeyEvent("keyup", e));
|
|
window.addEventListener("dblclick", e=>this._OnMouseEvent("dblclick", e, DISPATCH_RUNTIME_AND_SCRIPT));
|
|
window.addEventListener("wheel", e=>this._OnMouseWheelEvent("wheel", e));
|
|
if (typeof PointerEvent !== "undefined") {
|
|
window.addEventListener("pointerdown", e=>{
|
|
this._HandlePointerDownFocus(e);
|
|
this._OnPointerEvent("pointerdown", e)
|
|
}
|
|
);
|
|
if (this._iRuntime.UsesWorker() && typeof window["onpointerrawupdate"] !== "undefined" && self === self.top) {
|
|
this._pointerRawUpdateRateLimiter = new self.RateLimiter(()=>this._DoSendPointerRawUpdate(),5);
|
|
this._pointerRawUpdateRateLimiter.SetCanRunImmediate(true);
|
|
window.addEventListener("pointerrawupdate", e=>this._OnPointerRawUpdate(e))
|
|
} else
|
|
window.addEventListener("pointermove", e=>this._OnPointerEvent("pointermove", e));
|
|
window.addEventListener("pointerup", e=>this._OnPointerEvent("pointerup", e));
|
|
window.addEventListener("pointercancel", e=>this._OnPointerEvent("pointercancel", e))
|
|
} else {
|
|
window.addEventListener("mousedown", e=>{
|
|
this._HandlePointerDownFocus(e);
|
|
this._OnMouseEventAsPointer("pointerdown", e)
|
|
}
|
|
);
|
|
window.addEventListener("mousemove", e=>this._OnMouseEventAsPointer("pointermove", e));
|
|
window.addEventListener("mouseup", e=>this._OnMouseEventAsPointer("pointerup", e));
|
|
window.addEventListener("touchstart", e=>{
|
|
this._HandlePointerDownFocus(e);
|
|
this._OnTouchEvent("pointerdown", e)
|
|
}
|
|
);
|
|
window.addEventListener("touchmove", e=>this._OnTouchEvent("pointermove", e));
|
|
window.addEventListener("touchend", e=>this._OnTouchEvent("pointerup", e));
|
|
window.addEventListener("touchcancel", e=>this._OnTouchEvent("pointercancel", e))
|
|
}
|
|
const playFunc = ()=>this._PlayPendingMedia();
|
|
window.addEventListener("pointerup", playFunc, true);
|
|
window.addEventListener("touchend", playFunc, true);
|
|
window.addEventListener("click", playFunc, true);
|
|
window.addEventListener("keydown", playFunc, true);
|
|
window.addEventListener("gamepadconnected", playFunc, true)
|
|
}
|
|
_PostRuntimeEvent(name, data) {
|
|
this.PostToRuntime(name, data || null, DISPATCH_RUNTIME_ONLY)
|
|
}
|
|
_GetWindowInnerWidth() {
|
|
return this._iRuntime._GetWindowInnerWidth()
|
|
}
|
|
_GetWindowInnerHeight() {
|
|
return this._iRuntime._GetWindowInnerHeight()
|
|
}
|
|
_OnWindowResize() {
|
|
const width = this._GetWindowInnerWidth();
|
|
const height = this._GetWindowInnerHeight();
|
|
this._PostRuntimeEvent("window-resize", {
|
|
"innerWidth": width,
|
|
"innerHeight": height,
|
|
"devicePixelRatio": window.devicePixelRatio
|
|
});
|
|
if (this._iRuntime.IsiOSWebView()) {
|
|
if (this._simulatedResizeTimerId !== -1)
|
|
clearTimeout(this._simulatedResizeTimerId);
|
|
this._OnSimulatedResize(width, height, 0)
|
|
}
|
|
}
|
|
_ScheduleSimulatedResize(width, height, count) {
|
|
if (this._simulatedResizeTimerId !== -1)
|
|
clearTimeout(this._simulatedResizeTimerId);
|
|
this._simulatedResizeTimerId = setTimeout(()=>this._OnSimulatedResize(width, height, count), 48)
|
|
}
|
|
_OnSimulatedResize(originalWidth, originalHeight, count) {
|
|
const width = this._GetWindowInnerWidth();
|
|
const height = this._GetWindowInnerHeight();
|
|
this._simulatedResizeTimerId = -1;
|
|
if (width != originalWidth || height != originalHeight)
|
|
this._PostRuntimeEvent("window-resize", {
|
|
"innerWidth": width,
|
|
"innerHeight": height,
|
|
"devicePixelRatio": window.devicePixelRatio
|
|
});
|
|
else if (count < 10)
|
|
this._ScheduleSimulatedResize(width, height, count + 1)
|
|
}
|
|
_OnSetTargetOrientation(e) {
|
|
this._targetOrientation = e["targetOrientation"]
|
|
}
|
|
_TrySetTargetOrientation() {
|
|
const orientation = this._targetOrientation;
|
|
if (screen["orientation"] && screen["orientation"]["lock"])
|
|
screen["orientation"]["lock"](orientation).catch(err=>console.warn("[Construct 3] Failed to lock orientation: ", err));
|
|
else
|
|
try {
|
|
let result = false;
|
|
if (screen["lockOrientation"])
|
|
result = screen["lockOrientation"](orientation);
|
|
else if (screen["webkitLockOrientation"])
|
|
result = screen["webkitLockOrientation"](orientation);
|
|
else if (screen["mozLockOrientation"])
|
|
result = screen["mozLockOrientation"](orientation);
|
|
else if (screen["msLockOrientation"])
|
|
result = screen["msLockOrientation"](orientation);
|
|
if (!result)
|
|
console.warn("[Construct 3] Failed to lock orientation")
|
|
} catch (err) {
|
|
console.warn("[Construct 3] Failed to lock orientation: ", err)
|
|
}
|
|
}
|
|
_OnFullscreenChange() {
|
|
const isDocFullscreen = RuntimeInterface.IsDocumentFullscreen();
|
|
if (isDocFullscreen && this._targetOrientation !== "any")
|
|
this._TrySetTargetOrientation();
|
|
this.PostToRuntime("fullscreenchange", {
|
|
"isFullscreen": isDocFullscreen,
|
|
"innerWidth": this._GetWindowInnerWidth(),
|
|
"innerHeight": this._GetWindowInnerHeight()
|
|
})
|
|
}
|
|
_OnFullscreenError(e) {
|
|
console.warn("[Construct 3] Fullscreen request failed: ", e);
|
|
this.PostToRuntime("fullscreenerror", {
|
|
"isFullscreen": RuntimeInterface.IsDocumentFullscreen(),
|
|
"innerWidth": this._GetWindowInnerWidth(),
|
|
"innerHeight": this._GetWindowInnerHeight()
|
|
})
|
|
}
|
|
_OnVisibilityChange(isHidden) {
|
|
if (isHidden)
|
|
this._iRuntime._CancelAnimationFrame();
|
|
else
|
|
this._iRuntime._RequestAnimationFrame();
|
|
this.PostToRuntime("visibilitychange", {
|
|
"hidden": isHidden
|
|
})
|
|
}
|
|
_OnKeyEvent(name, e) {
|
|
if (e.key === "Backspace")
|
|
PreventDefaultOnCanvasOrDoc(e);
|
|
const code = KEY_CODE_ALIASES.get(e.code) || e.code;
|
|
this._PostToRuntimeMaybeSync(name, {
|
|
"code": code,
|
|
"key": e.key,
|
|
"which": e.which,
|
|
"repeat": e.repeat,
|
|
"altKey": e.altKey,
|
|
"ctrlKey": e.ctrlKey,
|
|
"metaKey": e.metaKey,
|
|
"shiftKey": e.shiftKey,
|
|
"timeStamp": e.timeStamp
|
|
}, DISPATCH_RUNTIME_AND_SCRIPT)
|
|
}
|
|
_OnMouseWheelEvent(name, e) {
|
|
this.PostToRuntime(name, {
|
|
"clientX": e.clientX,
|
|
"clientY": e.clientY,
|
|
"pageX": e.pageX,
|
|
"pageY": e.pageY,
|
|
"deltaX": e.deltaX,
|
|
"deltaY": e.deltaY,
|
|
"deltaZ": e.deltaZ,
|
|
"deltaMode": e.deltaMode,
|
|
"timeStamp": e.timeStamp
|
|
}, DISPATCH_RUNTIME_AND_SCRIPT)
|
|
}
|
|
_OnMouseEvent(name, e, opts) {
|
|
if (IsCompatibilityMouseEvent(e))
|
|
return;
|
|
this._PostToRuntimeMaybeSync(name, {
|
|
"button": e.button,
|
|
"buttons": e.buttons,
|
|
"clientX": e.clientX,
|
|
"clientY": e.clientY,
|
|
"pageX": e.pageX,
|
|
"pageY": e.pageY,
|
|
"timeStamp": e.timeStamp
|
|
}, opts)
|
|
}
|
|
_OnMouseEventAsPointer(name, e) {
|
|
if (IsCompatibilityMouseEvent(e))
|
|
return;
|
|
const pointerId = 1;
|
|
const lastButtons = this._mousePointerLastButtons;
|
|
if (name === "pointerdown" && lastButtons !== 0)
|
|
name = "pointermove";
|
|
else if (name === "pointerup" && e.buttons !== 0)
|
|
name = "pointermove";
|
|
this._PostToRuntimeMaybeSync(name, {
|
|
"pointerId": pointerId,
|
|
"pointerType": "mouse",
|
|
"button": e.button,
|
|
"buttons": e.buttons,
|
|
"lastButtons": lastButtons,
|
|
"clientX": e.clientX,
|
|
"clientY": e.clientY,
|
|
"pageX": e.pageX,
|
|
"pageY": e.pageY,
|
|
"width": 0,
|
|
"height": 0,
|
|
"pressure": 0,
|
|
"tangentialPressure": 0,
|
|
"tiltX": 0,
|
|
"tiltY": 0,
|
|
"twist": 0,
|
|
"timeStamp": e.timeStamp
|
|
}, DISPATCH_RUNTIME_AND_SCRIPT);
|
|
this._mousePointerLastButtons = e.buttons;
|
|
this._OnMouseEvent(e.type, e, DISPATCH_SCRIPT_ONLY)
|
|
}
|
|
_OnPointerEvent(name, e) {
|
|
if (this._pointerRawUpdateRateLimiter && name !== "pointermove")
|
|
this._pointerRawUpdateRateLimiter.Reset();
|
|
let lastButtons = 0;
|
|
if (e.pointerType === "mouse")
|
|
lastButtons = this._mousePointerLastButtons;
|
|
this._PostToRuntimeMaybeSync(name, {
|
|
"pointerId": e.pointerId,
|
|
"pointerType": e.pointerType,
|
|
"button": e.button,
|
|
"buttons": e.buttons,
|
|
"lastButtons": lastButtons,
|
|
"clientX": e.clientX,
|
|
"clientY": e.clientY,
|
|
"pageX": e.pageX,
|
|
"pageY": e.pageY,
|
|
"width": e.width || 0,
|
|
"height": e.height || 0,
|
|
"pressure": e.pressure || 0,
|
|
"tangentialPressure": e["tangentialPressure"] || 0,
|
|
"tiltX": e.tiltX || 0,
|
|
"tiltY": e.tiltY || 0,
|
|
"twist": e["twist"] || 0,
|
|
"timeStamp": e.timeStamp
|
|
}, DISPATCH_RUNTIME_AND_SCRIPT);
|
|
if (e.pointerType === "mouse") {
|
|
let mouseEventName = "mousemove";
|
|
if (name === "pointerdown")
|
|
mouseEventName = "mousedown";
|
|
else if (name === "pointerup")
|
|
mouseEventName = "pointerup";
|
|
this._OnMouseEvent(mouseEventName, e, DISPATCH_SCRIPT_ONLY);
|
|
this._mousePointerLastButtons = e.buttons
|
|
}
|
|
}
|
|
_OnPointerRawUpdate(e) {
|
|
this._lastPointerRawUpdateEvent = e;
|
|
this._pointerRawUpdateRateLimiter.Call()
|
|
}
|
|
_DoSendPointerRawUpdate() {
|
|
this._OnPointerEvent("pointermove", this._lastPointerRawUpdateEvent);
|
|
this._lastPointerRawUpdateEvent = null
|
|
}
|
|
_OnTouchEvent(fireName, e) {
|
|
for (let i = 0, len = e.changedTouches.length; i < len; ++i) {
|
|
const t = e.changedTouches[i];
|
|
this._PostToRuntimeMaybeSync(fireName, {
|
|
"pointerId": t.identifier,
|
|
"pointerType": "touch",
|
|
"button": 0,
|
|
"buttons": 0,
|
|
"lastButtons": 0,
|
|
"clientX": t.clientX,
|
|
"clientY": t.clientY,
|
|
"pageX": t.pageX,
|
|
"pageY": t.pageY,
|
|
"width": (t["radiusX"] || t["webkitRadiusX"] || 0) * 2,
|
|
"height": (t["radiusY"] || t["webkitRadiusY"] || 0) * 2,
|
|
"pressure": t["force"] || t["webkitForce"] || 0,
|
|
"tangentialPressure": 0,
|
|
"tiltX": 0,
|
|
"tiltY": 0,
|
|
"twist": t["rotationAngle"] || 0,
|
|
"timeStamp": e.timeStamp
|
|
}, DISPATCH_RUNTIME_AND_SCRIPT)
|
|
}
|
|
}
|
|
_HandlePointerDownFocus(e) {
|
|
if (window !== window.top)
|
|
window.focus();
|
|
if (this._IsElementCanvasOrDocument(e.target) && document.activeElement && !this._IsElementCanvasOrDocument(document.activeElement))
|
|
document.activeElement.blur()
|
|
}
|
|
_IsElementCanvasOrDocument(elem) {
|
|
return !elem || elem === document || elem === window || elem === document.body || elem.tagName.toLowerCase() === "canvas"
|
|
}
|
|
_AttachDeviceOrientationEvent() {
|
|
if (this._attachedDeviceOrientationEvent)
|
|
return;
|
|
this._attachedDeviceOrientationEvent = true;
|
|
window.addEventListener("deviceorientation", e=>this._OnDeviceOrientation(e));
|
|
window.addEventListener("deviceorientationabsolute", e=>this._OnDeviceOrientationAbsolute(e))
|
|
}
|
|
_AttachDeviceMotionEvent() {
|
|
if (this._attachedDeviceMotionEvent)
|
|
return;
|
|
this._attachedDeviceMotionEvent = true;
|
|
window.addEventListener("devicemotion", e=>this._OnDeviceMotion(e))
|
|
}
|
|
_OnDeviceOrientation(e) {
|
|
this.PostToRuntime("deviceorientation", {
|
|
"absolute": !!e["absolute"],
|
|
"alpha": e["alpha"] || 0,
|
|
"beta": e["beta"] || 0,
|
|
"gamma": e["gamma"] || 0,
|
|
"timeStamp": e.timeStamp,
|
|
"webkitCompassHeading": e["webkitCompassHeading"],
|
|
"webkitCompassAccuracy": e["webkitCompassAccuracy"]
|
|
}, DISPATCH_RUNTIME_AND_SCRIPT)
|
|
}
|
|
_OnDeviceOrientationAbsolute(e) {
|
|
this.PostToRuntime("deviceorientationabsolute", {
|
|
"absolute": !!e["absolute"],
|
|
"alpha": e["alpha"] || 0,
|
|
"beta": e["beta"] || 0,
|
|
"gamma": e["gamma"] || 0,
|
|
"timeStamp": e.timeStamp
|
|
}, DISPATCH_RUNTIME_AND_SCRIPT)
|
|
}
|
|
_OnDeviceMotion(e) {
|
|
let accProp = null;
|
|
const acc = e["acceleration"];
|
|
if (acc)
|
|
accProp = {
|
|
"x": acc["x"] || 0,
|
|
"y": acc["y"] || 0,
|
|
"z": acc["z"] || 0
|
|
};
|
|
let withGProp = null;
|
|
const withG = e["accelerationIncludingGravity"];
|
|
if (withG)
|
|
withGProp = {
|
|
"x": withG["x"] || 0,
|
|
"y": withG["y"] || 0,
|
|
"z": withG["z"] || 0
|
|
};
|
|
let rotationRateProp = null;
|
|
const rotationRate = e["rotationRate"];
|
|
if (rotationRate)
|
|
rotationRateProp = {
|
|
"alpha": rotationRate["alpha"] || 0,
|
|
"beta": rotationRate["beta"] || 0,
|
|
"gamma": rotationRate["gamma"] || 0
|
|
};
|
|
this.PostToRuntime("devicemotion", {
|
|
"acceleration": accProp,
|
|
"accelerationIncludingGravity": withGProp,
|
|
"rotationRate": rotationRateProp,
|
|
"interval": e["interval"],
|
|
"timeStamp": e.timeStamp
|
|
}, DISPATCH_RUNTIME_AND_SCRIPT)
|
|
}
|
|
_OnUpdateCanvasSize(e) {
|
|
const runtimeInterface = this.GetRuntimeInterface();
|
|
const canvas = runtimeInterface.GetCanvas();
|
|
canvas.style.width = e["styleWidth"] + "px";
|
|
canvas.style.height = e["styleHeight"] + "px";
|
|
canvas.style.marginLeft = e["marginLeft"] + "px";
|
|
canvas.style.marginTop = e["marginTop"] + "px";
|
|
runtimeInterface.MaybeForceBodySize();
|
|
if (this._isFirstSizeUpdate) {
|
|
canvas.style.display = "";
|
|
this._isFirstSizeUpdate = false
|
|
}
|
|
}
|
|
_OnInvokeDownload(e) {
|
|
const url = e["url"];
|
|
const filename = e["filename"];
|
|
const a = document.createElement("a");
|
|
const body = document.body;
|
|
a.textContent = filename;
|
|
a.href = url;
|
|
a.download = filename;
|
|
body.appendChild(a);
|
|
a.click();
|
|
body.removeChild(a)
|
|
}
|
|
async _OnRasterSvgImage(e) {
|
|
const blob = e["blob"];
|
|
const imageWidth = e["imageWidth"];
|
|
const imageHeight = e["imageHeight"];
|
|
const surfaceWidth = e["surfaceWidth"];
|
|
const surfaceHeight = e["surfaceHeight"];
|
|
const imageBitmapOpts = e["imageBitmapOpts"];
|
|
const canvas = await self["C3_RasterSvgImageBlob"](blob, imageWidth, imageHeight, surfaceWidth, surfaceHeight);
|
|
let ret;
|
|
if (imageBitmapOpts)
|
|
ret = await createImageBitmap(canvas, imageBitmapOpts);
|
|
else
|
|
ret = await createImageBitmap(canvas);
|
|
return {
|
|
"imageBitmap": ret,
|
|
"transferables": [ret]
|
|
}
|
|
}
|
|
async _OnGetSvgImageSize(e) {
|
|
return await self["C3_GetSvgImageSize"](e["blob"])
|
|
}
|
|
async _OnAddStylesheet(e) {
|
|
await AddStyleSheet(e["url"])
|
|
}
|
|
_PlayPendingMedia() {
|
|
const mediaToTryPlay = [...this._mediaPendingPlay];
|
|
this._mediaPendingPlay.clear();
|
|
if (!this._isSilent)
|
|
for (const mediaElem of mediaToTryPlay) {
|
|
const playRet = mediaElem.play();
|
|
if (playRet)
|
|
playRet.catch(err=>{
|
|
if (!this._mediaRemovedPendingPlay.has(mediaElem))
|
|
this._mediaPendingPlay.add(mediaElem)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
TryPlayMedia(mediaElem) {
|
|
if (typeof mediaElem.play !== "function")
|
|
throw new Error("missing play function");
|
|
this._mediaRemovedPendingPlay.delete(mediaElem);
|
|
let playRet;
|
|
try {
|
|
playRet = mediaElem.play()
|
|
} catch (err) {
|
|
this._mediaPendingPlay.add(mediaElem);
|
|
return
|
|
}
|
|
if (playRet)
|
|
playRet.catch(err=>{
|
|
if (!this._mediaRemovedPendingPlay.has(mediaElem))
|
|
this._mediaPendingPlay.add(mediaElem)
|
|
}
|
|
)
|
|
}
|
|
RemovePendingPlay(mediaElem) {
|
|
this._mediaPendingPlay.delete(mediaElem);
|
|
this._mediaRemovedPendingPlay.add(mediaElem)
|
|
}
|
|
SetSilent(s) {
|
|
this._isSilent = !!s
|
|
}
|
|
_OnHideCordovaSplash() {
|
|
if (navigator["splashscreen"] && navigator["splashscreen"]["hide"])
|
|
navigator["splashscreen"]["hide"]()
|
|
}
|
|
_OnDebugHighlight(e) {
|
|
const show = e["show"];
|
|
if (!show) {
|
|
if (this._debugHighlightElem)
|
|
this._debugHighlightElem.style.display = "none";
|
|
return
|
|
}
|
|
if (!this._debugHighlightElem) {
|
|
this._debugHighlightElem = document.createElement("div");
|
|
this._debugHighlightElem.id = "inspectOutline";
|
|
document.body.appendChild(this._debugHighlightElem)
|
|
}
|
|
const elem = this._debugHighlightElem;
|
|
elem.style.display = "";
|
|
elem.style.left = e["left"] - 1 + "px";
|
|
elem.style.top = e["top"] - 1 + "px";
|
|
elem.style.width = e["width"] + 2 + "px";
|
|
elem.style.height = e["height"] + 2 + "px";
|
|
elem.textContent = e["name"]
|
|
}
|
|
_OnRegisterSW() {
|
|
if (window["C3_RegisterSW"])
|
|
window["C3_RegisterSW"]()
|
|
}
|
|
_OnPostToDebugger(data) {
|
|
if (!window["c3_postToMessagePort"])
|
|
return;
|
|
data["from"] = "runtime";
|
|
window["c3_postToMessagePort"](data)
|
|
}
|
|
_InvokeFunctionFromJS(name, params) {
|
|
return this.PostToRuntimeAsync("js-invoke-function", {
|
|
"name": name,
|
|
"params": params
|
|
})
|
|
}
|
|
_OnAlert(e) {
|
|
alert(e["message"])
|
|
}
|
|
}
|
|
;
|
|
RuntimeInterface.AddDOMHandlerClass(HANDLER_CLASS)
|
|
}
|
|
;'use strict';
|
|
{
|
|
const DISPATCH_WORKER_SCRIPT_NAME = "dispatchworker.js";
|
|
const JOB_WORKER_SCRIPT_NAME = "jobworker.js";
|
|
self.JobSchedulerDOM = class JobSchedulerDOM {
|
|
constructor(runtimeInterface) {
|
|
this._runtimeInterface = runtimeInterface;
|
|
this._baseUrl = runtimeInterface.GetBaseURL();
|
|
if (runtimeInterface.GetExportType() === "preview")
|
|
this._baseUrl += "c3/workers/";
|
|
else
|
|
this._baseUrl += runtimeInterface.GetScriptFolder();
|
|
this._maxNumWorkers = Math.min(navigator.hardwareConcurrency || 2, 16);
|
|
this._dispatchWorker = null;
|
|
this._jobWorkers = [];
|
|
this._inputPort = null;
|
|
this._outputPort = null
|
|
}
|
|
async Init() {
|
|
if (this._hasInitialised)
|
|
throw new Error("already initialised");
|
|
this._hasInitialised = true;
|
|
const dispatchWorkerScriptUrl = this._runtimeInterface._GetWorkerURL(DISPATCH_WORKER_SCRIPT_NAME);
|
|
this._dispatchWorker = await this._runtimeInterface.CreateWorker(dispatchWorkerScriptUrl, this._baseUrl, {
|
|
name: "DispatchWorker"
|
|
});
|
|
const messageChannel = new MessageChannel;
|
|
this._inputPort = messageChannel.port1;
|
|
this._dispatchWorker.postMessage({
|
|
"type": "_init",
|
|
"in-port": messageChannel.port2
|
|
}, [messageChannel.port2]);
|
|
this._outputPort = await this._CreateJobWorker()
|
|
}
|
|
async _CreateJobWorker() {
|
|
const number = this._jobWorkers.length;
|
|
const jobWorkerScriptUrl = this._runtimeInterface._GetWorkerURL(JOB_WORKER_SCRIPT_NAME);
|
|
const jobWorker = await this._runtimeInterface.CreateWorker(jobWorkerScriptUrl, this._baseUrl, {
|
|
name: "JobWorker" + number
|
|
});
|
|
const dispatchChannel = new MessageChannel;
|
|
const outputChannel = new MessageChannel;
|
|
this._dispatchWorker.postMessage({
|
|
"type": "_addJobWorker",
|
|
"port": dispatchChannel.port1
|
|
}, [dispatchChannel.port1]);
|
|
jobWorker.postMessage({
|
|
"type": "init",
|
|
"number": number,
|
|
"dispatch-port": dispatchChannel.port2,
|
|
"output-port": outputChannel.port2
|
|
}, [dispatchChannel.port2, outputChannel.port2]);
|
|
this._jobWorkers.push(jobWorker);
|
|
return outputChannel.port1
|
|
}
|
|
GetPortData() {
|
|
return {
|
|
"inputPort": this._inputPort,
|
|
"outputPort": this._outputPort,
|
|
"maxNumWorkers": this._maxNumWorkers
|
|
}
|
|
}
|
|
GetPortTransferables() {
|
|
return [this._inputPort, this._outputPort]
|
|
}
|
|
}
|
|
}
|
|
;'use strict';
|
|
{
|
|
if (window["C3_IsSupported"]) {
|
|
const enableWorker = false;
|
|
window["c3_runtimeInterface"] = new self.RuntimeInterface({
|
|
useWorker: enableWorker,
|
|
workerMainUrl: "workermain.js",
|
|
engineScripts: ["scripts/c3runtime-f.js"],
|
|
scriptFolder: "scripts/",
|
|
workerDependencyScripts: [],
|
|
exportType: "html5"
|
|
})
|
|
}
|
|
}
|
|
;'use strict';
|
|
{
|
|
const DOM_COMPONENT_ID = "touch";
|
|
const HANDLER_CLASS = class TouchDOMHandler extends self.DOMHandler {
|
|
constructor(iRuntime) {
|
|
super(iRuntime, DOM_COMPONENT_ID);
|
|
this.AddRuntimeMessageHandler("request-permission", e=>this._OnRequestPermission(e))
|
|
}
|
|
async _OnRequestPermission(e) {
|
|
const type = e["type"];
|
|
let result = true;
|
|
if (type === 0)
|
|
result = await this._RequestOrientationPermission();
|
|
else if (type === 1)
|
|
result = await this._RequestMotionPermission();
|
|
this.PostToRuntime("permission-result", {
|
|
"type": type,
|
|
"result": result
|
|
})
|
|
}
|
|
async _RequestOrientationPermission() {
|
|
if (!self["DeviceOrientationEvent"] || !self["DeviceOrientationEvent"]["requestPermission"])
|
|
return true;
|
|
try {
|
|
const state = await self["DeviceOrientationEvent"]["requestPermission"]();
|
|
return state === "granted"
|
|
} catch (err) {
|
|
console.warn("[Touch] Failed to request orientation permission: ", err);
|
|
return false
|
|
}
|
|
}
|
|
async _RequestMotionPermission() {
|
|
if (!self["DeviceMotionEvent"] || !self["DeviceMotionEvent"]["requestPermission"])
|
|
return true;
|
|
try {
|
|
const state = await self["DeviceMotionEvent"]["requestPermission"]();
|
|
return state === "granted"
|
|
} catch (err) {
|
|
console.warn("[Touch] Failed to request motion permission: ", err);
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
;
|
|
self.RuntimeInterface.AddDOMHandlerClass(HANDLER_CLASS)
|
|
}
|
|
;'use strict';
|
|
{
|
|
const DOM_COMPONENT_ID = "mouse";
|
|
const HANDLER_CLASS = class MouseDOMHandler extends self.DOMHandler {
|
|
constructor(iRuntime) {
|
|
super(iRuntime, DOM_COMPONENT_ID);
|
|
this.AddRuntimeMessageHandler("cursor", e=>this._OnChangeCursorStyle(e))
|
|
}
|
|
_OnChangeCursorStyle(e) {
|
|
document.documentElement.style.cursor = e
|
|
}
|
|
}
|
|
;
|
|
self.RuntimeInterface.AddDOMHandlerClass(HANDLER_CLASS)
|
|
}
|
|
;'use strict';
|
|
{
|
|
const DOM_COMPONENT_ID = "browser";
|
|
const HANDLER_CLASS = class BrowserDOMHandler extends self.DOMHandler {
|
|
constructor(iRuntime) {
|
|
super(iRuntime, DOM_COMPONENT_ID);
|
|
this._exportType = "";
|
|
this.AddRuntimeMessageHandlers([["get-initial-state", e=>this._OnGetInitialState(e)], ["ready-for-sw-messages", ()=>this._OnReadyForSWMessages()], ["alert", e=>this._OnAlert(e)], ["close", ()=>this._OnClose()], ["set-focus", e=>this._OnSetFocus(e)], ["vibrate", e=>this._OnVibrate(e)], ["lock-orientation", e=>this._OnLockOrientation(e)], ["unlock-orientation", ()=>this._OnUnlockOrientation()], ["navigate", e=>this._OnNavigate(e)], ["request-fullscreen", e=>this._OnRequestFullscreen(e)], ["exit-fullscreen", ()=>this._OnExitFullscreen()], ["set-hash", e=>this._OnSetHash(e)]]);
|
|
window.addEventListener("online", ()=>this._OnOnlineStateChanged(true));
|
|
window.addEventListener("offline", ()=>this._OnOnlineStateChanged(false));
|
|
window.addEventListener("hashchange", ()=>this._OnHashChange());
|
|
document.addEventListener("backbutton", ()=>this._OnCordovaBackButton());
|
|
if (typeof Windows !== "undefined")
|
|
Windows["UI"]["Core"]["SystemNavigationManager"]["getForCurrentView"]().addEventListener("backrequested", e=>this._OnWin10BackRequested(e))
|
|
}
|
|
_OnGetInitialState(e) {
|
|
this._exportType = e["exportType"];
|
|
return {
|
|
"location": location.toString(),
|
|
"isOnline": !!navigator.onLine,
|
|
"referrer": document.referrer,
|
|
"title": document.title,
|
|
"isCookieEnabled": !!navigator.cookieEnabled,
|
|
"screenWidth": screen.width,
|
|
"screenHeight": screen.height,
|
|
"windowOuterWidth": window.outerWidth,
|
|
"windowOuterHeight": window.outerHeight,
|
|
"isScirraArcade": typeof window["is_scirra_arcade"] !== "undefined"
|
|
}
|
|
}
|
|
_OnReadyForSWMessages() {
|
|
if (!window["C3_RegisterSW"] || !window["OfflineClientInfo"])
|
|
return;
|
|
window["OfflineClientInfo"]["SetMessageCallback"](e=>this.PostToRuntime("sw-message", e["data"]))
|
|
}
|
|
_OnOnlineStateChanged(isOnline) {
|
|
this.PostToRuntime("online-state", {
|
|
"isOnline": isOnline
|
|
})
|
|
}
|
|
_OnCordovaBackButton() {
|
|
this.PostToRuntime("backbutton")
|
|
}
|
|
_OnWin10BackRequested(e) {
|
|
e["handled"] = true;
|
|
this.PostToRuntime("backbutton")
|
|
}
|
|
GetNWjsWindow() {
|
|
if (this._exportType === "nwjs")
|
|
return nw["Window"]["get"]();
|
|
else
|
|
return null
|
|
}
|
|
_OnAlert(e) {
|
|
alert(e["message"])
|
|
}
|
|
_OnClose() {
|
|
if (navigator["app"] && navigator["app"]["exitApp"])
|
|
navigator["app"]["exitApp"]();
|
|
else if (navigator["device"] && navigator["device"]["exitApp"])
|
|
navigator["device"]["exitApp"]();
|
|
else
|
|
window.close()
|
|
}
|
|
_OnSetFocus(e) {
|
|
const isFocus = e["isFocus"];
|
|
if (this._exportType === "nwjs") {
|
|
const win = this.GetNWjsWindow();
|
|
if (isFocus)
|
|
win["focus"]();
|
|
else
|
|
win["blur"]()
|
|
} else if (isFocus)
|
|
window.focus();
|
|
else
|
|
window.blur()
|
|
}
|
|
_OnVibrate(e) {
|
|
if (navigator["vibrate"])
|
|
navigator["vibrate"](e["pattern"])
|
|
}
|
|
_OnLockOrientation(e) {
|
|
const orientation = e["orientation"];
|
|
if (screen["orientation"] && screen["orientation"]["lock"])
|
|
screen["orientation"]["lock"](orientation).catch(err=>console.warn("[Construct 3] Failed to lock orientation: ", err));
|
|
else
|
|
try {
|
|
let result = false;
|
|
if (screen["lockOrientation"])
|
|
result = screen["lockOrientation"](orientation);
|
|
else if (screen["webkitLockOrientation"])
|
|
result = screen["webkitLockOrientation"](orientation);
|
|
else if (screen["mozLockOrientation"])
|
|
result = screen["mozLockOrientation"](orientation);
|
|
else if (screen["msLockOrientation"])
|
|
result = screen["msLockOrientation"](orientation);
|
|
if (!result)
|
|
console.warn("[Construct 3] Failed to lock orientation")
|
|
} catch (err) {
|
|
console.warn("[Construct 3] Failed to lock orientation: ", err)
|
|
}
|
|
}
|
|
_OnUnlockOrientation() {
|
|
try {
|
|
if (screen["orientation"] && screen["orientation"]["unlock"])
|
|
screen["orientation"]["unlock"]();
|
|
else if (screen["unlockOrientation"])
|
|
screen["unlockOrientation"]();
|
|
else if (screen["webkitUnlockOrientation"])
|
|
screen["webkitUnlockOrientation"]();
|
|
else if (screen["mozUnlockOrientation"])
|
|
screen["mozUnlockOrientation"]();
|
|
else if (screen["msUnlockOrientation"])
|
|
screen["msUnlockOrientation"]()
|
|
} catch (err) {}
|
|
}
|
|
_OnNavigate(e) {
|
|
const type = e["type"];
|
|
if (type === "back")
|
|
if (navigator["app"] && navigator["app"]["backHistory"])
|
|
navigator["app"]["backHistory"]();
|
|
else
|
|
window.back();
|
|
else if (type === "forward")
|
|
window.forward();
|
|
else if (type === "home")
|
|
window.home();
|
|
else if (type === "reload")
|
|
location.reload();
|
|
else if (type === "url") {
|
|
const url = e["url"];
|
|
const target = e["target"];
|
|
const exportType = e["exportType"];
|
|
if (exportType === "windows-uwp" && typeof Windows !== "undefined")
|
|
Windows["System"]["Launcher"]["launchUriAsync"](new Windows["Foundation"]["Uri"](url));
|
|
else if (self["cordova"] && self["cordova"]["InAppBrowser"])
|
|
self["cordova"]["InAppBrowser"]["open"](url, "_system");
|
|
else if (exportType === "preview")
|
|
window.open(url, "_blank");
|
|
else if (!this._isScirraArcade)
|
|
if (target === 2)
|
|
window.top.location = url;
|
|
else if (target === 1)
|
|
window.parent.location = url;
|
|
else
|
|
window.location = url
|
|
} else if (type === "new-window") {
|
|
const url = e["url"];
|
|
const tag = e["tag"];
|
|
const exportType = e["exportType"];
|
|
if (exportType === "windows-uwp" && typeof Windows !== "undefined")
|
|
Windows["System"]["Launcher"]["launchUriAsync"](new Windows["Foundation"]["Uri"](url));
|
|
else if (self["cordova"] && self["cordova"]["InAppBrowser"])
|
|
self["cordova"]["InAppBrowser"]["open"](url, "_system");
|
|
else
|
|
window.open(url, tag)
|
|
}
|
|
}
|
|
_OnRequestFullscreen(e) {
|
|
const opts = {
|
|
"navigationUI": "auto"
|
|
};
|
|
const navUI = e["navUI"];
|
|
if (navUI === 1)
|
|
opts["navigationUI"] = "hide";
|
|
else if (navUI === 2)
|
|
opts["navigationUI"] = "show";
|
|
const elem = document.documentElement;
|
|
if (elem["requestFullscreen"])
|
|
elem["requestFullscreen"](opts);
|
|
else if (elem["mozRequestFullScreen"])
|
|
elem["mozRequestFullScreen"](opts);
|
|
else if (elem["msRequestFullscreen"])
|
|
elem["msRequestFullscreen"](opts);
|
|
else if (elem["webkitRequestFullScreen"])
|
|
if (typeof Element["ALLOW_KEYBOARD_INPUT"] !== "undefined")
|
|
elem["webkitRequestFullScreen"](Element["ALLOW_KEYBOARD_INPUT"]);
|
|
else
|
|
elem["webkitRequestFullScreen"]()
|
|
}
|
|
_OnExitFullscreen() {
|
|
if (document["exitFullscreen"])
|
|
document["exitFullscreen"]();
|
|
else if (document["mozCancelFullScreen"])
|
|
document["mozCancelFullScreen"]();
|
|
else if (document["msExitFullscreen"])
|
|
document["msExitFullscreen"]();
|
|
else if (document["webkitCancelFullScreen"])
|
|
document["webkitCancelFullScreen"]()
|
|
}
|
|
_OnSetHash(e) {
|
|
location.hash = e["hash"]
|
|
}
|
|
_OnHashChange() {
|
|
this.PostToRuntime("hashchange", {
|
|
"location": location.toString()
|
|
})
|
|
}
|
|
}
|
|
;
|
|
self.RuntimeInterface.AddDOMHandlerClass(HANDLER_CLASS)
|
|
}
|
|
;'use strict';
|
|
{
|
|
const R_TO_D = 180 / Math.PI;
|
|
const DOM_COMPONENT_ID = "audio";
|
|
self.AudioDOMHandler = class AudioDOMHandler extends self.DOMHandler {
|
|
constructor(iRuntime) {
|
|
super(iRuntime, DOM_COMPONENT_ID);
|
|
this._audioContext = null;
|
|
this._destinationNode = null;
|
|
this._hasUnblocked = false;
|
|
this._hasAttachedUnblockEvents = false;
|
|
this._unblockFunc = ()=>this._UnblockAudioContext();
|
|
this._audioBuffers = [];
|
|
this._audioInstances = [];
|
|
this._lastAudioInstance = null;
|
|
this._lastPlayedTag = "";
|
|
this._lastTickCount = -1;
|
|
this._pendingTags = new Map;
|
|
this._masterVolume = 1;
|
|
this._isSilent = false;
|
|
this._timeScaleMode = 0;
|
|
this._timeScale = 1;
|
|
this._gameTime = 0;
|
|
this._panningModel = "HRTF";
|
|
this._distanceModel = "inverse";
|
|
this._refDistance = 600;
|
|
this._maxDistance = 1E4;
|
|
this._rolloffFactor = 1;
|
|
this._playMusicAsSound = false;
|
|
this._hasAnySoftwareDecodedMusic = false;
|
|
this._supportsWebMOpus = this._iRuntime.IsAudioFormatSupported("audio/webm; codecs=opus");
|
|
this._effects = new Map;
|
|
this._analysers = new Set;
|
|
this._isPendingPostFxState = false;
|
|
this._microphoneTag = "";
|
|
this._microphoneSource = null;
|
|
self["C3Audio_OnMicrophoneStream"] = (localMediaStream,tag)=>this._OnMicrophoneStream(localMediaStream, tag);
|
|
this._destMediaStreamNode = null;
|
|
self["C3Audio_GetOutputStream"] = ()=>this._OnGetOutputStream();
|
|
self["C3Audio_DOMInterface"] = this;
|
|
this.AddRuntimeMessageHandlers([["create-audio-context", e=>this._CreateAudioContext(e)], ["play", e=>this._Play(e)], ["stop", e=>this._Stop(e)], ["stop-all", ()=>this._StopAll()], ["set-paused", e=>this._SetPaused(e)], ["set-volume", e=>this._SetVolume(e)], ["fade-volume", e=>this._FadeVolume(e)], ["set-master-volume", e=>this._SetMasterVolume(e)], ["set-muted", e=>this._SetMuted(e)], ["set-silent", e=>this._SetSilent(e)], ["set-looping", e=>this._SetLooping(e)], ["set-playback-rate", e=>this._SetPlaybackRate(e)], ["seek", e=>this._Seek(e)], ["preload", e=>this._Preload(e)], ["unload", e=>this._Unload(e)], ["unload-all", ()=>this._UnloadAll()], ["set-suspended", e=>this._SetSuspended(e)], ["add-effect", e=>this._AddEffect(e)], ["set-effect-param", e=>this._SetEffectParam(e)], ["remove-effects", e=>this._RemoveEffects(e)], ["tick", e=>this._OnTick(e)], ["load-state", e=>this._OnLoadState(e)]])
|
|
}
|
|
async _CreateAudioContext(e) {
|
|
if (e["isiOSCordova"])
|
|
this._playMusicAsSound = true;
|
|
this._timeScaleMode = e["timeScaleMode"];
|
|
this._panningModel = ["equalpower", "HRTF", "soundfield"][e["panningModel"]];
|
|
this._distanceModel = ["linear", "inverse", "exponential"][e["distanceModel"]];
|
|
this._refDistance = e["refDistance"];
|
|
this._maxDistance = e["maxDistance"];
|
|
this._rolloffFactor = e["rolloffFactor"];
|
|
const opts = {
|
|
"latencyHint": e["latencyHint"]
|
|
};
|
|
if (typeof AudioContext !== "undefined")
|
|
this._audioContext = new AudioContext(opts);
|
|
else if (typeof webkitAudioContext !== "undefined")
|
|
this._audioContext = new webkitAudioContext(opts);
|
|
else
|
|
throw new Error("Web Audio API not supported");
|
|
this._AttachUnblockEvents();
|
|
this._audioContext.onstatechange = ()=>{
|
|
if (this._audioContext.state !== "running")
|
|
this._AttachUnblockEvents()
|
|
}
|
|
;
|
|
this._destinationNode = this._audioContext["createGain"]();
|
|
this._destinationNode["connect"](this._audioContext["destination"]);
|
|
const listenerPos = e["listenerPos"];
|
|
this._audioContext["listener"]["setPosition"](listenerPos[0], listenerPos[1], listenerPos[2]);
|
|
this._audioContext["listener"]["setOrientation"](0, 0, 1, 0, -1, 0);
|
|
self["C3_GetAudioContextCurrentTime"] = ()=>this.GetAudioCurrentTime();
|
|
try {
|
|
await Promise.all(e["preloadList"].map(o=>this._GetAudioBuffer(o["originalUrl"], o["url"], o["type"], false)))
|
|
} catch (err) {
|
|
console.error("[Construct 3] Preloading sounds failed: ", err)
|
|
}
|
|
return {
|
|
"sampleRate": this._audioContext["sampleRate"]
|
|
}
|
|
}
|
|
_AttachUnblockEvents() {
|
|
if (this._hasAttachedUnblockEvents)
|
|
return;
|
|
this._hasUnblocked = false;
|
|
window.addEventListener("pointerup", this._unblockFunc, true);
|
|
window.addEventListener("touchend", this._unblockFunc, true);
|
|
window.addEventListener("click", this._unblockFunc, true);
|
|
window.addEventListener("keydown", this._unblockFunc, true);
|
|
this._hasAttachedUnblockEvents = true
|
|
}
|
|
_DetachUnblockEvents() {
|
|
if (!this._hasAttachedUnblockEvents)
|
|
return;
|
|
this._hasUnblocked = true;
|
|
window.removeEventListener("pointerup", this._unblockFunc, true);
|
|
window.removeEventListener("touchend", this._unblockFunc, true);
|
|
window.removeEventListener("click", this._unblockFunc, true);
|
|
window.removeEventListener("keydown", this._unblockFunc, true);
|
|
this._hasAttachedUnblockEvents = false
|
|
}
|
|
_UnblockAudioContext() {
|
|
if (this._hasUnblocked)
|
|
return;
|
|
const audioContext = this._audioContext;
|
|
if (audioContext["state"] === "suspended" && audioContext["resume"])
|
|
audioContext["resume"]();
|
|
const buffer = audioContext["createBuffer"](1, 220, 22050);
|
|
const source = audioContext["createBufferSource"]();
|
|
source["buffer"] = buffer;
|
|
source["connect"](audioContext["destination"]);
|
|
source["start"](0);
|
|
if (audioContext["state"] === "running")
|
|
this._DetachUnblockEvents()
|
|
}
|
|
GetAudioContext() {
|
|
return this._audioContext
|
|
}
|
|
GetAudioCurrentTime() {
|
|
return this._audioContext["currentTime"]
|
|
}
|
|
GetDestinationNode() {
|
|
return this._destinationNode
|
|
}
|
|
GetDestinationForTag(tag) {
|
|
const fxChain = this._effects.get(tag.toLowerCase());
|
|
if (fxChain)
|
|
return fxChain[0].GetInputNode();
|
|
else
|
|
return this.GetDestinationNode()
|
|
}
|
|
AddEffectForTag(tag, effect) {
|
|
tag = tag.toLowerCase();
|
|
let fxChain = this._effects.get(tag);
|
|
if (!fxChain) {
|
|
fxChain = [];
|
|
this._effects.set(tag, fxChain)
|
|
}
|
|
effect._SetIndex(fxChain.length);
|
|
effect._SetTag(tag);
|
|
fxChain.push(effect);
|
|
this._ReconnectEffects(tag)
|
|
}
|
|
_ReconnectEffects(tag) {
|
|
let destNode = this.GetDestinationNode();
|
|
const fxChain = this._effects.get(tag);
|
|
if (fxChain && fxChain.length) {
|
|
destNode = fxChain[0].GetInputNode();
|
|
for (let i = 0, len = fxChain.length; i < len; ++i) {
|
|
const n = fxChain[i];
|
|
if (i + 1 === len)
|
|
n.ConnectTo(this.GetDestinationNode());
|
|
else
|
|
n.ConnectTo(fxChain[i + 1].GetInputNode())
|
|
}
|
|
}
|
|
for (const ai of this.audioInstancesByTag(tag))
|
|
ai.Reconnect(destNode);
|
|
if (this._microphoneSource && this._microphoneTag === tag) {
|
|
this._microphoneSource["disconnect"]();
|
|
this._microphoneSource["connect"](destNode)
|
|
}
|
|
}
|
|
GetMasterVolume() {
|
|
return this._masterVolume
|
|
}
|
|
IsSilent() {
|
|
return this._isSilent
|
|
}
|
|
GetTimeScaleMode() {
|
|
return this._timeScaleMode
|
|
}
|
|
GetTimeScale() {
|
|
return this._timeScale
|
|
}
|
|
GetGameTime() {
|
|
return this._gameTime
|
|
}
|
|
IsPlayMusicAsSound() {
|
|
return this._playMusicAsSound
|
|
}
|
|
SupportsWebMOpus() {
|
|
return this._supportsWebMOpus
|
|
}
|
|
_SetHasAnySoftwareDecodedMusic() {
|
|
this._hasAnySoftwareDecodedMusic = true
|
|
}
|
|
GetPanningModel() {
|
|
return this._panningModel
|
|
}
|
|
GetDistanceModel() {
|
|
return this._distanceModel
|
|
}
|
|
GetReferenceDistance() {
|
|
return this._refDistance
|
|
}
|
|
GetMaxDistance() {
|
|
return this._maxDistance
|
|
}
|
|
GetRolloffFactor() {
|
|
return this._rolloffFactor
|
|
}
|
|
DecodeAudioData(audioData, needsSoftwareDecode) {
|
|
if (needsSoftwareDecode)
|
|
return this._iRuntime._WasmDecodeWebMOpus(audioData).then(rawAudio=>{
|
|
const audioBuffer = this._audioContext["createBuffer"](1, rawAudio.length, 48E3);
|
|
const channelBuffer = audioBuffer["getChannelData"](0);
|
|
channelBuffer.set(rawAudio);
|
|
return audioBuffer
|
|
}
|
|
);
|
|
else
|
|
return new Promise((resolve,reject)=>{
|
|
this._audioContext["decodeAudioData"](audioData, resolve, reject)
|
|
}
|
|
)
|
|
}
|
|
TryPlayMedia(mediaElem) {
|
|
this._iRuntime.TryPlayMedia(mediaElem)
|
|
}
|
|
RemovePendingPlay(mediaElem) {
|
|
this._iRuntime.RemovePendingPlay(mediaElem)
|
|
}
|
|
ReleaseInstancesForBuffer(buffer) {
|
|
let j = 0;
|
|
for (let i = 0, len = this._audioInstances.length; i < len; ++i) {
|
|
const a = this._audioInstances[i];
|
|
this._audioInstances[j] = a;
|
|
if (a.GetBuffer() === buffer)
|
|
a.Release();
|
|
else
|
|
++j
|
|
}
|
|
this._audioInstances.length = j
|
|
}
|
|
ReleaseAllMusicBuffers() {
|
|
let j = 0;
|
|
for (let i = 0, len = this._audioBuffers.length; i < len; ++i) {
|
|
const b = this._audioBuffers[i];
|
|
this._audioBuffers[j] = b;
|
|
if (b.IsMusic())
|
|
b.Release();
|
|
else
|
|
++j
|
|
}
|
|
this._audioBuffers.length = j
|
|
}
|
|
*audioInstancesByTag(tag) {
|
|
if (tag)
|
|
for (const ai of this._audioInstances) {
|
|
if (self.AudioDOMHandler.EqualsNoCase(ai.GetTag(), tag))
|
|
yield ai
|
|
}
|
|
else if (this._lastAudioInstance && !this._lastAudioInstance.HasEnded())
|
|
yield this._lastAudioInstance
|
|
}
|
|
async _GetAudioBuffer(originalUrl, url, type, isMusic, dontCreate) {
|
|
for (const ab of this._audioBuffers)
|
|
if (ab.GetUrl() === url) {
|
|
await ab.Load();
|
|
return ab
|
|
}
|
|
if (dontCreate)
|
|
return null;
|
|
if (isMusic && (this._playMusicAsSound || this._hasAnySoftwareDecodedMusic))
|
|
this.ReleaseAllMusicBuffers();
|
|
const ret = self.C3AudioBuffer.Create(this, originalUrl, url, type, isMusic);
|
|
this._audioBuffers.push(ret);
|
|
await ret.Load();
|
|
return ret
|
|
}
|
|
async _GetAudioInstance(originalUrl, url, type, tag, isMusic) {
|
|
for (const ai of this._audioInstances)
|
|
if (ai.GetUrl() === url && (ai.CanBeRecycled() || isMusic)) {
|
|
ai.SetTag(tag);
|
|
return ai
|
|
}
|
|
const buffer = await this._GetAudioBuffer(originalUrl, url, type, isMusic);
|
|
const ret = buffer.CreateInstance(tag);
|
|
this._audioInstances.push(ret);
|
|
return ret
|
|
}
|
|
_AddPendingTag(tag) {
|
|
let info = this._pendingTags.get(tag);
|
|
if (!info) {
|
|
let resolve = null;
|
|
const promise = new Promise(r=>resolve = r);
|
|
info = {
|
|
pendingCount: 0,
|
|
promise,
|
|
resolve
|
|
};
|
|
this._pendingTags.set(tag, info)
|
|
}
|
|
info.pendingCount++
|
|
}
|
|
_RemovePendingTag(tag) {
|
|
const info = this._pendingTags.get(tag);
|
|
if (!info)
|
|
throw new Error("expected pending tag");
|
|
info.pendingCount--;
|
|
if (info.pendingCount === 0) {
|
|
info.resolve();
|
|
this._pendingTags.delete(tag)
|
|
}
|
|
}
|
|
TagReady(tag) {
|
|
if (!tag)
|
|
tag = this._lastPlayedTag;
|
|
const info = this._pendingTags.get(tag);
|
|
if (info)
|
|
return info.promise;
|
|
else
|
|
return Promise.resolve()
|
|
}
|
|
_MaybeStartTicking() {
|
|
if (this._analysers.size > 0) {
|
|
this._StartTicking();
|
|
return
|
|
}
|
|
for (const ai of this._audioInstances)
|
|
if (ai.IsActive()) {
|
|
this._StartTicking();
|
|
return
|
|
}
|
|
}
|
|
Tick() {
|
|
for (const a of this._analysers)
|
|
a.Tick();
|
|
const currentTime = this.GetAudioCurrentTime();
|
|
for (const ai of this._audioInstances)
|
|
ai.Tick(currentTime);
|
|
const instStates = this._audioInstances.filter(a=>a.IsActive()).map(a=>a.GetState());
|
|
this.PostToRuntime("state", {
|
|
"tickCount": this._lastTickCount,
|
|
"audioInstances": instStates,
|
|
"analysers": [...this._analysers].map(a=>a.GetData())
|
|
});
|
|
if (instStates.length === 0 && this._analysers.size === 0)
|
|
this._StopTicking()
|
|
}
|
|
PostTrigger(type, tag, aiid) {
|
|
this.PostToRuntime("trigger", {
|
|
"type": type,
|
|
"tag": tag,
|
|
"aiid": aiid
|
|
})
|
|
}
|
|
async _Play(e) {
|
|
const originalUrl = e["originalUrl"];
|
|
const url = e["url"];
|
|
const type = e["type"];
|
|
const isMusic = e["isMusic"];
|
|
const tag = e["tag"];
|
|
const isLooping = e["isLooping"];
|
|
const volume = e["vol"];
|
|
const position = e["pos"];
|
|
const panning = e["panning"];
|
|
let startTime = e["off"];
|
|
if (startTime > 0 && !e["trueClock"])
|
|
if (this._audioContext["getOutputTimestamp"]) {
|
|
const outputTimestamp = this._audioContext["getOutputTimestamp"]();
|
|
startTime = startTime - outputTimestamp["performanceTime"] / 1E3 + outputTimestamp["contextTime"]
|
|
} else
|
|
startTime = startTime - performance.now() / 1E3 + this._audioContext["currentTime"];
|
|
this._lastPlayedTag = tag;
|
|
this._AddPendingTag(tag);
|
|
try {
|
|
this._lastAudioInstance = await this._GetAudioInstance(originalUrl, url, type, tag, isMusic);
|
|
if (panning) {
|
|
this._lastAudioInstance.SetPannerEnabled(true);
|
|
this._lastAudioInstance.SetPan(panning["x"], panning["y"], panning["angle"], panning["innerAngle"], panning["outerAngle"], panning["outerGain"]);
|
|
if (panning.hasOwnProperty("uid"))
|
|
this._lastAudioInstance.SetUID(panning["uid"])
|
|
} else
|
|
this._lastAudioInstance.SetPannerEnabled(false);
|
|
this._lastAudioInstance.Play(isLooping, volume, position, startTime)
|
|
} catch (err) {
|
|
console.error("[Construct 3] Audio: error starting playback: ", err);
|
|
return
|
|
} finally {
|
|
this._RemovePendingTag(tag)
|
|
}
|
|
this._StartTicking()
|
|
}
|
|
_Stop(e) {
|
|
const tag = e["tag"];
|
|
for (const ai of this.audioInstancesByTag(tag))
|
|
ai.Stop()
|
|
}
|
|
_StopAll() {
|
|
for (const ai of this._audioInstances)
|
|
ai.Stop()
|
|
}
|
|
_SetPaused(e) {
|
|
const tag = e["tag"];
|
|
const paused = e["paused"];
|
|
for (const ai of this.audioInstancesByTag(tag))
|
|
if (paused)
|
|
ai.Pause();
|
|
else
|
|
ai.Resume();
|
|
this._MaybeStartTicking()
|
|
}
|
|
_SetVolume(e) {
|
|
const tag = e["tag"];
|
|
const vol = e["vol"];
|
|
for (const ai of this.audioInstancesByTag(tag))
|
|
ai.SetVolume(vol)
|
|
}
|
|
async _FadeVolume(e) {
|
|
const tag = e["tag"];
|
|
const vol = e["vol"];
|
|
const duration = e["duration"];
|
|
const stopOnEnd = e["stopOnEnd"];
|
|
await this.TagReady(tag);
|
|
for (const ai of this.audioInstancesByTag(tag))
|
|
ai.FadeVolume(vol, duration, stopOnEnd);
|
|
this._MaybeStartTicking()
|
|
}
|
|
_SetMasterVolume(e) {
|
|
this._masterVolume = e["vol"];
|
|
for (const ai of this._audioInstances)
|
|
ai._UpdateVolume()
|
|
}
|
|
_SetMuted(e) {
|
|
const tag = e["tag"];
|
|
const isMuted = e["isMuted"];
|
|
for (const ai of this.audioInstancesByTag(tag))
|
|
ai.SetMuted(isMuted)
|
|
}
|
|
_SetSilent(e) {
|
|
this._isSilent = e["isSilent"];
|
|
this._iRuntime.SetSilent(this._isSilent);
|
|
for (const ai of this._audioInstances)
|
|
ai._UpdateMuted()
|
|
}
|
|
_SetLooping(e) {
|
|
const tag = e["tag"];
|
|
const isLooping = e["isLooping"];
|
|
for (const ai of this.audioInstancesByTag(tag))
|
|
ai.SetLooping(isLooping)
|
|
}
|
|
async _SetPlaybackRate(e) {
|
|
const tag = e["tag"];
|
|
const rate = e["rate"];
|
|
await this.TagReady(tag);
|
|
for (const ai of this.audioInstancesByTag(tag))
|
|
ai.SetPlaybackRate(rate)
|
|
}
|
|
async _Seek(e) {
|
|
const tag = e["tag"];
|
|
const pos = e["pos"];
|
|
await this.TagReady(tag);
|
|
for (const ai of this.audioInstancesByTag(tag))
|
|
ai.Seek(pos)
|
|
}
|
|
async _Preload(e) {
|
|
const originalUrl = e["originalUrl"];
|
|
const url = e["url"];
|
|
const type = e["type"];
|
|
const isMusic = e["isMusic"];
|
|
try {
|
|
await this._GetAudioInstance(originalUrl, url, type, "", isMusic)
|
|
} catch (err) {
|
|
console.error("[Construct 3] Audio: error preloading: ", err)
|
|
}
|
|
}
|
|
async _Unload(e) {
|
|
const url = e["url"];
|
|
const type = e["type"];
|
|
const isMusic = e["isMusic"];
|
|
const buffer = await this._GetAudioBuffer("", url, type, isMusic, true);
|
|
if (!buffer)
|
|
return;
|
|
buffer.Release();
|
|
const i = this._audioBuffers.indexOf(buffer);
|
|
if (i !== -1)
|
|
this._audioBuffers.splice(i, 1)
|
|
}
|
|
_UnloadAll() {
|
|
for (const buffer of this._audioBuffers)
|
|
buffer.Release();
|
|
this._audioBuffers.length = 0
|
|
}
|
|
_SetSuspended(e) {
|
|
const isSuspended = e["isSuspended"];
|
|
if (!isSuspended && this._audioContext["resume"])
|
|
this._audioContext["resume"]();
|
|
for (const ai of this._audioInstances)
|
|
ai.SetSuspended(isSuspended);
|
|
if (isSuspended && this._audioContext["suspend"])
|
|
this._audioContext["suspend"]()
|
|
}
|
|
_OnTick(e) {
|
|
this._timeScale = e["timeScale"];
|
|
this._gameTime = e["gameTime"];
|
|
this._lastTickCount = e["tickCount"];
|
|
if (this._timeScaleMode !== 0)
|
|
for (const ai of this._audioInstances)
|
|
ai._UpdatePlaybackRate();
|
|
const listenerPos = e["listenerPos"];
|
|
if (listenerPos)
|
|
this._audioContext["listener"]["setPosition"](listenerPos[0], listenerPos[1], listenerPos[2]);
|
|
for (const instPan of e["instPans"]) {
|
|
const uid = instPan["uid"];
|
|
for (const ai of this._audioInstances)
|
|
if (ai.GetUID() === uid)
|
|
ai.SetPanXYA(instPan["x"], instPan["y"], instPan["angle"])
|
|
}
|
|
}
|
|
async _AddEffect(e) {
|
|
const type = e["type"];
|
|
const tag = e["tag"];
|
|
const params = e["params"];
|
|
let effect;
|
|
if (type === "filter")
|
|
effect = new self.C3AudioFilterFX(this,...params);
|
|
else if (type === "delay")
|
|
effect = new self.C3AudioDelayFX(this,...params);
|
|
else if (type === "convolution") {
|
|
let buffer = null;
|
|
try {
|
|
buffer = await this._GetAudioBuffer(e["bufferOriginalUrl"], e["bufferUrl"], e["bufferType"], false)
|
|
} catch (err) {
|
|
console.log("[Construct 3] Audio: error loading convolution: ", err);
|
|
return
|
|
}
|
|
effect = new self.C3AudioConvolveFX(this,buffer.GetAudioBuffer(),...params);
|
|
effect._SetBufferInfo(e["bufferOriginalUrl"], e["bufferUrl"], e["bufferType"])
|
|
} else if (type === "flanger")
|
|
effect = new self.C3AudioFlangerFX(this,...params);
|
|
else if (type === "phaser")
|
|
effect = new self.C3AudioPhaserFX(this,...params);
|
|
else if (type === "gain")
|
|
effect = new self.C3AudioGainFX(this,...params);
|
|
else if (type === "tremolo")
|
|
effect = new self.C3AudioTremoloFX(this,...params);
|
|
else if (type === "ringmod")
|
|
effect = new self.C3AudioRingModFX(this,...params);
|
|
else if (type === "distortion")
|
|
effect = new self.C3AudioDistortionFX(this,...params);
|
|
else if (type === "compressor")
|
|
effect = new self.C3AudioCompressorFX(this,...params);
|
|
else if (type === "analyser")
|
|
effect = new self.C3AudioAnalyserFX(this,...params);
|
|
else
|
|
throw new Error("invalid effect type");
|
|
this.AddEffectForTag(tag, effect);
|
|
this._PostUpdatedFxState()
|
|
}
|
|
_SetEffectParam(e) {
|
|
const tag = e["tag"];
|
|
const index = e["index"];
|
|
const param = e["param"];
|
|
const value = e["value"];
|
|
const ramp = e["ramp"];
|
|
const time = e["time"];
|
|
const fxChain = this._effects.get(tag);
|
|
if (!fxChain || index < 0 || index >= fxChain.length)
|
|
return;
|
|
fxChain[index].SetParam(param, value, ramp, time);
|
|
this._PostUpdatedFxState()
|
|
}
|
|
_RemoveEffects(e) {
|
|
const tag = e["tag"].toLowerCase();
|
|
const fxChain = this._effects.get(tag);
|
|
if (!fxChain || !fxChain.length)
|
|
return;
|
|
for (const effect of fxChain)
|
|
effect.Release();
|
|
this._effects.delete(tag);
|
|
this._ReconnectEffects(tag)
|
|
}
|
|
_AddAnalyser(analyser) {
|
|
this._analysers.add(analyser);
|
|
this._MaybeStartTicking()
|
|
}
|
|
_RemoveAnalyser(analyser) {
|
|
this._analysers.delete(analyser)
|
|
}
|
|
_PostUpdatedFxState() {
|
|
if (this._isPendingPostFxState)
|
|
return;
|
|
this._isPendingPostFxState = true;
|
|
Promise.resolve().then(()=>this._DoPostUpdatedFxState())
|
|
}
|
|
_DoPostUpdatedFxState() {
|
|
const fxstate = {};
|
|
for (const [tag,fxChain] of this._effects)
|
|
fxstate[tag] = fxChain.map(e=>e.GetState());
|
|
this.PostToRuntime("fxstate", {
|
|
"fxstate": fxstate
|
|
});
|
|
this._isPendingPostFxState = false
|
|
}
|
|
async _OnLoadState(e) {
|
|
const saveLoadMode = e["saveLoadMode"];
|
|
if (saveLoadMode !== 3)
|
|
for (const ai of this._audioInstances) {
|
|
if (ai.IsMusic() && saveLoadMode === 1)
|
|
continue;
|
|
if (!ai.IsMusic() && saveLoadMode === 2)
|
|
continue;
|
|
ai.Stop()
|
|
}
|
|
for (const fxChain of this._effects.values())
|
|
for (const effect of fxChain)
|
|
effect.Release();
|
|
this._effects.clear();
|
|
this._timeScale = e["timeScale"];
|
|
this._gameTime = e["gameTime"];
|
|
const listenerPos = e["listenerPos"];
|
|
this._audioContext["listener"]["setPosition"](listenerPos[0], listenerPos[1], listenerPos[2]);
|
|
this._isSilent = e["isSilent"];
|
|
this._iRuntime.SetSilent(this._isSilent);
|
|
this._masterVolume = e["masterVolume"];
|
|
const promises = [];
|
|
for (const fxChainData of Object.values(e["effects"]))
|
|
promises.push(Promise.all(fxChainData.map(d=>this._AddEffect(d))));
|
|
await Promise.all(promises);
|
|
await Promise.all(e["playing"].map(d=>this._LoadAudioInstance(d, saveLoadMode)));
|
|
this._MaybeStartTicking()
|
|
}
|
|
async _LoadAudioInstance(d, saveLoadMode) {
|
|
if (saveLoadMode === 3)
|
|
return;
|
|
const originalUrl = d["bufferOriginalUrl"];
|
|
const url = d["bufferUrl"];
|
|
const type = d["bufferType"];
|
|
const isMusic = d["isMusic"];
|
|
const tag = d["tag"];
|
|
const isLooping = d["isLooping"];
|
|
const volume = d["volume"];
|
|
const position = d["playbackTime"];
|
|
if (isMusic && saveLoadMode === 1)
|
|
return;
|
|
if (!isMusic && saveLoadMode === 2)
|
|
return;
|
|
let ai = null;
|
|
try {
|
|
ai = await this._GetAudioInstance(originalUrl, url, type, tag, isMusic)
|
|
} catch (err) {
|
|
console.error("[Construct 3] Audio: error loading audio state: ", err);
|
|
return
|
|
}
|
|
ai.LoadPanState(d["pan"]);
|
|
ai.Play(isLooping, volume, position, 0);
|
|
if (!d["isPlaying"])
|
|
ai.Pause();
|
|
ai._LoadAdditionalState(d)
|
|
}
|
|
_OnMicrophoneStream(localMediaStream, tag) {
|
|
if (this._microphoneSource)
|
|
this._microphoneSource["disconnect"]();
|
|
this._microphoneTag = tag.toLowerCase();
|
|
this._microphoneSource = this._audioContext["createMediaStreamSource"](localMediaStream);
|
|
this._microphoneSource["connect"](this.GetDestinationForTag(this._microphoneTag))
|
|
}
|
|
_OnGetOutputStream() {
|
|
if (!this._destMediaStreamNode) {
|
|
this._destMediaStreamNode = this._audioContext["createMediaStreamDestination"]();
|
|
this._destinationNode["connect"](this._destMediaStreamNode)
|
|
}
|
|
return this._destMediaStreamNode["stream"]
|
|
}
|
|
static EqualsNoCase(a, b) {
|
|
if (a.length !== b.length)
|
|
return false;
|
|
if (a === b)
|
|
return true;
|
|
return a.toLowerCase() === b.toLowerCase()
|
|
}
|
|
static ToDegrees(x) {
|
|
return x * R_TO_D
|
|
}
|
|
static DbToLinearNoCap(x) {
|
|
return Math.pow(10, x / 20)
|
|
}
|
|
static DbToLinear(x) {
|
|
return Math.max(Math.min(self.AudioDOMHandler.DbToLinearNoCap(x), 1), 0)
|
|
}
|
|
static LinearToDbNoCap(x) {
|
|
return Math.log(x) / Math.log(10) * 20
|
|
}
|
|
static LinearToDb(x) {
|
|
return self.AudioDOMHandler.LinearToDbNoCap(Math.max(Math.min(x, 1), 0))
|
|
}
|
|
static e4(x, k) {
|
|
return 1 - Math.exp(-k * x)
|
|
}
|
|
}
|
|
;
|
|
self.RuntimeInterface.AddDOMHandlerClass(self.AudioDOMHandler)
|
|
}
|
|
;'use strict';
|
|
{
|
|
self.C3AudioBuffer = class C3AudioBuffer {
|
|
constructor(audioDomHandler, originalUrl, url, type, isMusic) {
|
|
this._audioDomHandler = audioDomHandler;
|
|
this._originalUrl = originalUrl;
|
|
this._url = url;
|
|
this._type = type;
|
|
this._isMusic = isMusic;
|
|
this._api = "";
|
|
this._loadState = "not-loaded";
|
|
this._loadPromise = null
|
|
}
|
|
Release() {
|
|
this._loadState = "not-loaded";
|
|
this._audioDomHandler = null;
|
|
this._loadPromise = null
|
|
}
|
|
static Create(audioDomHandler, originalUrl, url, type, isMusic) {
|
|
const needsSoftwareDecode = type === "audio/webm; codecs=opus" && !audioDomHandler.SupportsWebMOpus();
|
|
if (isMusic && needsSoftwareDecode)
|
|
audioDomHandler._SetHasAnySoftwareDecodedMusic();
|
|
if (!isMusic || audioDomHandler.IsPlayMusicAsSound() || needsSoftwareDecode)
|
|
return new self.C3WebAudioBuffer(audioDomHandler,originalUrl,url,type,isMusic,needsSoftwareDecode);
|
|
else
|
|
return new self.C3Html5AudioBuffer(audioDomHandler,originalUrl,url,type,isMusic)
|
|
}
|
|
CreateInstance(tag) {
|
|
if (this._api === "html5")
|
|
return new self.C3Html5AudioInstance(this._audioDomHandler,this,tag);
|
|
else
|
|
return new self.C3WebAudioInstance(this._audioDomHandler,this,tag)
|
|
}
|
|
_Load() {}
|
|
Load() {
|
|
if (!this._loadPromise)
|
|
this._loadPromise = this._Load();
|
|
return this._loadPromise
|
|
}
|
|
IsLoaded() {}
|
|
IsLoadedAndDecoded() {}
|
|
HasFailedToLoad() {
|
|
return this._loadState === "failed"
|
|
}
|
|
GetAudioContext() {
|
|
return this._audioDomHandler.GetAudioContext()
|
|
}
|
|
GetApi() {
|
|
return this._api
|
|
}
|
|
GetOriginalUrl() {
|
|
return this._originalUrl
|
|
}
|
|
GetUrl() {
|
|
return this._url
|
|
}
|
|
GetContentType() {
|
|
return this._type
|
|
}
|
|
IsMusic() {
|
|
return this._isMusic
|
|
}
|
|
GetDuration() {}
|
|
}
|
|
}
|
|
;'use strict';
|
|
{
|
|
self.C3Html5AudioBuffer = class C3Html5AudioBuffer extends self.C3AudioBuffer {
|
|
constructor(audioDomHandler, originalUrl, url, type, isMusic) {
|
|
super(audioDomHandler, originalUrl, url, type, isMusic);
|
|
this._api = "html5";
|
|
this._audioElem = new Audio;
|
|
this._audioElem.crossOrigin = "anonymous";
|
|
this._audioElem.autoplay = false;
|
|
this._audioElem.preload = "auto";
|
|
this._loadResolve = null;
|
|
this._loadReject = null;
|
|
this._reachedCanPlayThrough = false;
|
|
this._audioElem.addEventListener("canplaythrough", ()=>this._reachedCanPlayThrough = true);
|
|
this._outNode = this.GetAudioContext()["createGain"]();
|
|
this._mediaSourceNode = null;
|
|
this._audioElem.addEventListener("canplay", ()=>{
|
|
if (this._loadResolve) {
|
|
this._loadState = "loaded";
|
|
this._loadResolve();
|
|
this._loadResolve = null;
|
|
this._loadReject = null
|
|
}
|
|
if (this._mediaSourceNode || !this._audioElem)
|
|
return;
|
|
this._mediaSourceNode = this.GetAudioContext()["createMediaElementSource"](this._audioElem);
|
|
this._mediaSourceNode["connect"](this._outNode)
|
|
}
|
|
);
|
|
this.onended = null;
|
|
this._audioElem.addEventListener("ended", ()=>{
|
|
if (this.onended)
|
|
this.onended()
|
|
}
|
|
);
|
|
this._audioElem.addEventListener("error", e=>this._OnError(e))
|
|
}
|
|
Release() {
|
|
this._audioDomHandler.ReleaseInstancesForBuffer(this);
|
|
this._outNode["disconnect"]();
|
|
this._outNode = null;
|
|
this._mediaSourceNode["disconnect"]();
|
|
this._mediaSourceNode = null;
|
|
if (this._audioElem && !this._audioElem.paused)
|
|
this._audioElem.pause();
|
|
this.onended = null;
|
|
this._audioElem = null;
|
|
super.Release()
|
|
}
|
|
_Load() {
|
|
this._loadState = "loading";
|
|
return new Promise((resolve,reject)=>{
|
|
this._loadResolve = resolve;
|
|
this._loadReject = reject;
|
|
this._audioElem.src = this._url
|
|
}
|
|
)
|
|
}
|
|
_OnError(e) {
|
|
console.error(`[Construct 3] Audio '${this._url}' error: `, e);
|
|
if (this._loadReject) {
|
|
this._loadState = "failed";
|
|
this._loadReject(e);
|
|
this._loadResolve = null;
|
|
this._loadReject = null
|
|
}
|
|
}
|
|
IsLoaded() {
|
|
const ret = this._audioElem["readyState"] >= 4;
|
|
if (ret)
|
|
this._reachedCanPlayThrough = true;
|
|
return ret || this._reachedCanPlayThrough
|
|
}
|
|
IsLoadedAndDecoded() {
|
|
return this.IsLoaded()
|
|
}
|
|
GetAudioElement() {
|
|
return this._audioElem
|
|
}
|
|
GetOutputNode() {
|
|
return this._outNode
|
|
}
|
|
GetDuration() {
|
|
return this._audioElem["duration"]
|
|
}
|
|
}
|
|
}
|
|
;'use strict';
|
|
{
|
|
self.C3WebAudioBuffer = class C3WebAudioBuffer extends self.C3AudioBuffer {
|
|
constructor(audioDomHandler, originalUrl, url, type, isMusic, needsSoftwareDecode) {
|
|
super(audioDomHandler, originalUrl, url, type, isMusic);
|
|
this._api = "webaudio";
|
|
this._audioData = null;
|
|
this._audioBuffer = null;
|
|
this._needsSoftwareDecode = !!needsSoftwareDecode
|
|
}
|
|
Release() {
|
|
this._audioDomHandler.ReleaseInstancesForBuffer(this);
|
|
this._audioData = null;
|
|
this._audioBuffer = null;
|
|
super.Release()
|
|
}
|
|
async _Fetch() {
|
|
if (this._audioData)
|
|
return this._audioData;
|
|
const iRuntime = this._audioDomHandler.GetRuntimeInterface();
|
|
if (iRuntime.GetExportType() === "cordova" && iRuntime.IsRelativeURL(this._url) && location.protocol === "file:")
|
|
this._audioData = await iRuntime.CordovaFetchLocalFileAsArrayBuffer(this._url);
|
|
else {
|
|
const response = await fetch(this._url);
|
|
if (!response.ok)
|
|
throw new Error(`error fetching audio data: ${response.status} ${response.statusText}`);
|
|
this._audioData = await response.arrayBuffer()
|
|
}
|
|
}
|
|
async _Decode() {
|
|
if (this._audioBuffer)
|
|
return this._audioBuffer;
|
|
this._audioBuffer = await this._audioDomHandler.DecodeAudioData(this._audioData, this._needsSoftwareDecode);
|
|
this._audioData = null
|
|
}
|
|
async _Load() {
|
|
try {
|
|
this._loadState = "loading";
|
|
await this._Fetch();
|
|
await this._Decode();
|
|
this._loadState = "loaded"
|
|
} catch (err) {
|
|
this._loadState = "failed";
|
|
console.error(`[Construct 3] Failed to load audio '${this._url}': `, err)
|
|
}
|
|
}
|
|
IsLoaded() {
|
|
return !!(this._audioData || this._audioBuffer)
|
|
}
|
|
IsLoadedAndDecoded() {
|
|
return !!this._audioBuffer
|
|
}
|
|
GetAudioBuffer() {
|
|
return this._audioBuffer
|
|
}
|
|
GetDuration() {
|
|
return this._audioBuffer ? this._audioBuffer["duration"] : 0
|
|
}
|
|
}
|
|
}
|
|
;'use strict';
|
|
{
|
|
let nextAiId = 0;
|
|
self.C3AudioInstance = class C3AudioInstance {
|
|
constructor(audioDomHandler, buffer, tag) {
|
|
this._audioDomHandler = audioDomHandler;
|
|
this._buffer = buffer;
|
|
this._tag = tag;
|
|
this._aiId = nextAiId++;
|
|
this._gainNode = this.GetAudioContext()["createGain"]();
|
|
this._gainNode["connect"](this.GetDestinationNode());
|
|
this._pannerNode = null;
|
|
this._isPannerEnabled = false;
|
|
this._isStopped = true;
|
|
this._isPaused = false;
|
|
this._resumeMe = false;
|
|
this._isLooping = false;
|
|
this._volume = 1;
|
|
this._isMuted = false;
|
|
this._playbackRate = 1;
|
|
const timeScaleMode = this._audioDomHandler.GetTimeScaleMode();
|
|
this._isTimescaled = timeScaleMode === 1 && !this.IsMusic() || timeScaleMode === 2;
|
|
this._instUid = -1;
|
|
this._fadeEndTime = -1;
|
|
this._stopOnFadeEnd = false
|
|
}
|
|
Release() {
|
|
this._audioDomHandler = null;
|
|
this._buffer = null;
|
|
if (this._pannerNode) {
|
|
this._pannerNode["disconnect"]();
|
|
this._pannerNode = null
|
|
}
|
|
this._gainNode["disconnect"]();
|
|
this._gainNode = null
|
|
}
|
|
GetAudioContext() {
|
|
return this._audioDomHandler.GetAudioContext()
|
|
}
|
|
GetDestinationNode() {
|
|
return this._audioDomHandler.GetDestinationForTag(this._tag)
|
|
}
|
|
GetMasterVolume() {
|
|
return this._audioDomHandler.GetMasterVolume()
|
|
}
|
|
GetCurrentTime() {
|
|
if (this._isTimescaled)
|
|
return this._audioDomHandler.GetGameTime();
|
|
else
|
|
return performance.now() / 1E3
|
|
}
|
|
GetOriginalUrl() {
|
|
return this._buffer.GetOriginalUrl()
|
|
}
|
|
GetUrl() {
|
|
return this._buffer.GetUrl()
|
|
}
|
|
GetContentType() {
|
|
return this._buffer.GetContentType()
|
|
}
|
|
GetBuffer() {
|
|
return this._buffer
|
|
}
|
|
IsMusic() {
|
|
return this._buffer.IsMusic()
|
|
}
|
|
SetTag(tag) {
|
|
this._tag = tag
|
|
}
|
|
GetTag() {
|
|
return this._tag
|
|
}
|
|
GetAiId() {
|
|
return this._aiId
|
|
}
|
|
HasEnded() {}
|
|
CanBeRecycled() {}
|
|
IsPlaying() {
|
|
return !this._isStopped && !this._isPaused && !this.HasEnded()
|
|
}
|
|
IsActive() {
|
|
return !this._isStopped && !this.HasEnded()
|
|
}
|
|
GetPlaybackTime(applyPlaybackRate) {}
|
|
GetDuration(applyPlaybackRate) {
|
|
let ret = this._buffer.GetDuration();
|
|
if (applyPlaybackRate)
|
|
ret /= this._playbackRate || .001;
|
|
return ret
|
|
}
|
|
Play(isLooping, vol, seekPos, scheduledTime) {}
|
|
Stop() {}
|
|
Pause() {}
|
|
IsPaused() {
|
|
return this._isPaused
|
|
}
|
|
Resume() {}
|
|
SetVolume(v) {
|
|
this._volume = v;
|
|
this._gainNode["gain"]["cancelScheduledValues"](0);
|
|
this._fadeEndTime = -1;
|
|
this._gainNode["gain"]["value"] = this.GetOverallVolume()
|
|
}
|
|
FadeVolume(vol, duration, stopOnEnd) {
|
|
if (this.IsMuted())
|
|
return;
|
|
vol *= this.GetMasterVolume();
|
|
const gainParam = this._gainNode["gain"];
|
|
gainParam["cancelScheduledValues"](0);
|
|
const currentTime = this._audioDomHandler.GetAudioCurrentTime();
|
|
const endTime = currentTime + duration;
|
|
gainParam["setValueAtTime"](gainParam["value"], currentTime);
|
|
gainParam["linearRampToValueAtTime"](vol, endTime);
|
|
this._volume = vol;
|
|
this._fadeEndTime = endTime;
|
|
this._stopOnFadeEnd = stopOnEnd
|
|
}
|
|
_UpdateVolume() {
|
|
this.SetVolume(this._volume)
|
|
}
|
|
Tick(currentTime) {
|
|
if (this._fadeEndTime !== -1 && currentTime >= this._fadeEndTime) {
|
|
this._fadeEndTime = -1;
|
|
if (this._stopOnFadeEnd)
|
|
this.Stop();
|
|
this._audioDomHandler.PostTrigger("fade-ended", this._tag, this._aiId)
|
|
}
|
|
}
|
|
GetOverallVolume() {
|
|
const ret = this._volume * this.GetMasterVolume();
|
|
return isFinite(ret) ? ret : 0
|
|
}
|
|
SetMuted(m) {
|
|
m = !!m;
|
|
if (this._isMuted === m)
|
|
return;
|
|
this._isMuted = m;
|
|
this._UpdateMuted()
|
|
}
|
|
IsMuted() {
|
|
return this._isMuted
|
|
}
|
|
IsSilent() {
|
|
return this._audioDomHandler.IsSilent()
|
|
}
|
|
_UpdateMuted() {}
|
|
SetLooping(l) {}
|
|
IsLooping() {
|
|
return this._isLooping
|
|
}
|
|
SetPlaybackRate(r) {
|
|
if (this._playbackRate === r)
|
|
return;
|
|
this._playbackRate = r;
|
|
this._UpdatePlaybackRate()
|
|
}
|
|
_UpdatePlaybackRate() {}
|
|
GetPlaybackRate() {
|
|
return this._playbackRate
|
|
}
|
|
Seek(pos) {}
|
|
SetSuspended(s) {}
|
|
SetPannerEnabled(e) {
|
|
e = !!e;
|
|
if (this._isPannerEnabled === e)
|
|
return;
|
|
this._isPannerEnabled = e;
|
|
if (this._isPannerEnabled) {
|
|
if (!this._pannerNode) {
|
|
this._pannerNode = this.GetAudioContext()["createPanner"]();
|
|
this._pannerNode["panningModel"] = this._audioDomHandler.GetPanningModel();
|
|
this._pannerNode["distanceModel"] = this._audioDomHandler.GetDistanceModel();
|
|
this._pannerNode["refDistance"] = this._audioDomHandler.GetReferenceDistance();
|
|
this._pannerNode["maxDistance"] = this._audioDomHandler.GetMaxDistance();
|
|
this._pannerNode["rolloffFactor"] = this._audioDomHandler.GetRolloffFactor()
|
|
}
|
|
this._gainNode["disconnect"]();
|
|
this._gainNode["connect"](this._pannerNode);
|
|
this._pannerNode["connect"](this.GetDestinationNode())
|
|
} else {
|
|
this._pannerNode["disconnect"]();
|
|
this._gainNode["disconnect"]();
|
|
this._gainNode["connect"](this.GetDestinationNode())
|
|
}
|
|
}
|
|
SetPan(x, y, angle, innerAngle, outerAngle, outerGain) {
|
|
if (!this._isPannerEnabled)
|
|
return;
|
|
this.SetPanXYA(x, y, angle);
|
|
const toDegrees = self.AudioDOMHandler.ToDegrees;
|
|
this._pannerNode["coneInnerAngle"] = toDegrees(innerAngle);
|
|
this._pannerNode["coneOuterAngle"] = toDegrees(outerAngle);
|
|
this._pannerNode["coneOuterGain"] = outerGain
|
|
}
|
|
SetPanXYA(x, y, angle) {
|
|
if (!this._isPannerEnabled)
|
|
return;
|
|
this._pannerNode["setPosition"](x, y, 0);
|
|
this._pannerNode["setOrientation"](Math.cos(angle), Math.sin(angle), 0)
|
|
}
|
|
SetUID(uid) {
|
|
this._instUid = uid
|
|
}
|
|
GetUID() {
|
|
return this._instUid
|
|
}
|
|
GetResumePosition() {}
|
|
Reconnect(toNode) {
|
|
const outNode = this._pannerNode || this._gainNode;
|
|
outNode["disconnect"]();
|
|
outNode["connect"](toNode)
|
|
}
|
|
GetState() {
|
|
return {
|
|
"aiid": this.GetAiId(),
|
|
"tag": this._tag,
|
|
"duration": this.GetDuration(),
|
|
"volume": this._volume,
|
|
"isPlaying": this.IsPlaying(),
|
|
"playbackTime": this.GetPlaybackTime(),
|
|
"playbackRate": this.GetPlaybackRate(),
|
|
"uid": this._instUid,
|
|
"bufferOriginalUrl": this.GetOriginalUrl(),
|
|
"bufferUrl": "",
|
|
"bufferType": this.GetContentType(),
|
|
"isMusic": this.IsMusic(),
|
|
"isLooping": this.IsLooping(),
|
|
"isMuted": this.IsMuted(),
|
|
"resumePosition": this.GetResumePosition(),
|
|
"pan": this.GetPanState()
|
|
}
|
|
}
|
|
_LoadAdditionalState(d) {
|
|
this.SetPlaybackRate(d["playbackRate"]);
|
|
this.SetMuted(d["isMuted"])
|
|
}
|
|
GetPanState() {
|
|
if (!this._pannerNode)
|
|
return null;
|
|
const pn = this._pannerNode;
|
|
return {
|
|
"pos": [pn["positionX"]["value"], pn["positionY"]["value"], pn["positionZ"]["value"]],
|
|
"orient": [pn["orientationX"]["value"], pn["orientationY"]["value"], pn["orientationZ"]["value"]],
|
|
"cia": pn["coneInnerAngle"],
|
|
"coa": pn["coneOuterAngle"],
|
|
"cog": pn["coneOuterGain"],
|
|
"uid": this._instUid
|
|
}
|
|
}
|
|
LoadPanState(d) {
|
|
if (!d) {
|
|
this.SetPannerEnabled(false);
|
|
return
|
|
}
|
|
this.SetPannerEnabled(true);
|
|
const pn = this._pannerNode;
|
|
pn["setPosition"](...pn["pos"]);
|
|
pn["setOrientation"](...pn["orient"]);
|
|
pn["coneInnerAngle"] = pn["cia"];
|
|
pn["coneOuterAngle"] = pn["coa"];
|
|
pn["coneOuterGain"] = pn["cog"];
|
|
this._instUid = pn["uid"]
|
|
}
|
|
}
|
|
}
|
|
;'use strict';
|
|
{
|
|
self.C3Html5AudioInstance = class C3Html5AudioInstance extends self.C3AudioInstance {
|
|
constructor(audioDomHandler, buffer, tag) {
|
|
super(audioDomHandler, buffer, tag);
|
|
this._buffer.GetOutputNode()["connect"](this._gainNode);
|
|
this._buffer.onended = ()=>this._OnEnded()
|
|
}
|
|
Release() {
|
|
this.Stop();
|
|
this._buffer.GetOutputNode()["disconnect"]();
|
|
super.Release()
|
|
}
|
|
GetAudioElement() {
|
|
return this._buffer.GetAudioElement()
|
|
}
|
|
_OnEnded() {
|
|
this._isStopped = true;
|
|
this._instUid = -1;
|
|
this._audioDomHandler.PostTrigger("ended", this._tag, this._aiId)
|
|
}
|
|
HasEnded() {
|
|
return this.GetAudioElement()["ended"]
|
|
}
|
|
CanBeRecycled() {
|
|
if (this._isStopped)
|
|
return true;
|
|
return this.HasEnded()
|
|
}
|
|
GetPlaybackTime(applyPlaybackRate) {
|
|
let ret = this.GetAudioElement()["currentTime"];
|
|
if (applyPlaybackRate)
|
|
ret *= this._playbackRate;
|
|
if (!this._isLooping)
|
|
ret = Math.min(ret, this.GetDuration());
|
|
return ret
|
|
}
|
|
Play(isLooping, vol, seekPos, scheduledTime) {
|
|
const audioElem = this.GetAudioElement();
|
|
if (audioElem.playbackRate !== 1)
|
|
audioElem.playbackRate = 1;
|
|
if (audioElem.loop !== isLooping)
|
|
audioElem.loop = isLooping;
|
|
this.SetVolume(vol);
|
|
if (audioElem.muted)
|
|
audioElem.muted = false;
|
|
if (audioElem.currentTime !== seekPos)
|
|
try {
|
|
audioElem.currentTime = seekPos
|
|
} catch (err) {
|
|
console.warn(`[Construct 3] Exception seeking audio '${this._buffer.GetUrl()}' to position '${seekPos}': `, err)
|
|
}
|
|
this._audioDomHandler.TryPlayMedia(audioElem);
|
|
this._isStopped = false;
|
|
this._isPaused = false;
|
|
this._isLooping = isLooping;
|
|
this._playbackRate = 1
|
|
}
|
|
Stop() {
|
|
const audioElem = this.GetAudioElement();
|
|
if (!audioElem.paused)
|
|
audioElem.pause();
|
|
this._audioDomHandler.RemovePendingPlay(audioElem);
|
|
this._isStopped = true;
|
|
this._isPaused = false;
|
|
this._instUid = -1
|
|
}
|
|
Pause() {
|
|
if (this._isPaused || this._isStopped || this.HasEnded())
|
|
return;
|
|
const audioElem = this.GetAudioElement();
|
|
if (!audioElem.paused)
|
|
audioElem.pause();
|
|
this._audioDomHandler.RemovePendingPlay(audioElem);
|
|
this._isPaused = true
|
|
}
|
|
Resume() {
|
|
if (!this._isPaused || this._isStopped || this.HasEnded())
|
|
return;
|
|
this._audioDomHandler.TryPlayMedia(this.GetAudioElement());
|
|
this._isPaused = false
|
|
}
|
|
_UpdateMuted() {
|
|
this.GetAudioElement().muted = this._isMuted || this.IsSilent()
|
|
}
|
|
SetLooping(l) {
|
|
l = !!l;
|
|
if (this._isLooping === l)
|
|
return;
|
|
this._isLooping = l;
|
|
this.GetAudioElement().loop = l
|
|
}
|
|
_UpdatePlaybackRate() {
|
|
let r = this._playbackRate;
|
|
if (this._isTimescaled)
|
|
r *= this._audioDomHandler.GetTimeScale();
|
|
try {
|
|
this.GetAudioElement()["playbackRate"] = r
|
|
} catch (err) {
|
|
console.warn(`[Construct 3] Unable to set playback rate '${r}':`, err)
|
|
}
|
|
}
|
|
Seek(pos) {
|
|
if (this._isStopped || this.HasEnded())
|
|
return;
|
|
try {
|
|
this.GetAudioElement()["currentTime"] = pos
|
|
} catch (err) {
|
|
console.warn(`[Construct 3] Error seeking audio to '${pos}': `, err)
|
|
}
|
|
}
|
|
GetResumePosition() {
|
|
return this.GetPlaybackTime()
|
|
}
|
|
SetSuspended(s) {
|
|
if (s)
|
|
if (this.IsPlaying()) {
|
|
this.GetAudioElement()["pause"]();
|
|
this._resumeMe = true
|
|
} else
|
|
this._resumeMe = false;
|
|
else if (this._resumeMe) {
|
|
this._audioDomHandler.TryPlayMedia(this.GetAudioElement());
|
|
this._resumeMe = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
;'use strict';
|
|
{
|
|
self.C3WebAudioInstance = class C3WebAudioInstance extends self.C3AudioInstance {
|
|
constructor(audioDomHandler, buffer, tag) {
|
|
super(audioDomHandler, buffer, tag);
|
|
this._bufferSource = null;
|
|
this._onended_handler = e=>this._OnEnded(e);
|
|
this._hasPlaybackEnded = true;
|
|
this._activeSource = null;
|
|
this._startTime = 0;
|
|
this._resumePosition = 0;
|
|
this._muteVol = 1
|
|
}
|
|
Release() {
|
|
this.Stop();
|
|
this._ReleaseBufferSource();
|
|
this._onended_handler = null;
|
|
super.Release()
|
|
}
|
|
_ReleaseBufferSource() {
|
|
if (this._bufferSource)
|
|
this._bufferSource["disconnect"]();
|
|
this._bufferSource = null;
|
|
this._activeSource = null
|
|
}
|
|
_OnEnded(e) {
|
|
if (this._isPaused || this._resumeMe)
|
|
return;
|
|
if (e.target !== this._activeSource)
|
|
return;
|
|
this._hasPlaybackEnded = true;
|
|
this._isStopped = true;
|
|
this._instUid = -1;
|
|
this._ReleaseBufferSource();
|
|
this._audioDomHandler.PostTrigger("ended", this._tag, this._aiId)
|
|
}
|
|
HasEnded() {
|
|
if (!this._isStopped && this._bufferSource && this._bufferSource["loop"])
|
|
return false;
|
|
if (this._isPaused)
|
|
return false;
|
|
return this._hasPlaybackEnded
|
|
}
|
|
CanBeRecycled() {
|
|
if (!this._bufferSource || this._isStopped)
|
|
return true;
|
|
return this.HasEnded()
|
|
}
|
|
GetPlaybackTime(applyPlaybackRate) {
|
|
let ret = 0;
|
|
if (this._isPaused)
|
|
ret = this._resumePosition;
|
|
else
|
|
ret = this.GetCurrentTime() - this._startTime;
|
|
if (applyPlaybackRate)
|
|
ret *= this._playbackRate;
|
|
if (!this._isLooping)
|
|
ret = Math.min(ret, this.GetDuration());
|
|
return ret
|
|
}
|
|
Play(isLooping, vol, seekPos, scheduledTime) {
|
|
this._muteVol = 1;
|
|
this.SetVolume(vol);
|
|
this._ReleaseBufferSource();
|
|
this._bufferSource = this.GetAudioContext()["createBufferSource"]();
|
|
this._bufferSource["buffer"] = this._buffer.GetAudioBuffer();
|
|
this._bufferSource["connect"](this._gainNode);
|
|
this._activeSource = this._bufferSource;
|
|
this._bufferSource["onended"] = this._onended_handler;
|
|
this._bufferSource["loop"] = isLooping;
|
|
this._bufferSource["start"](scheduledTime, seekPos);
|
|
this._hasPlaybackEnded = false;
|
|
this._isStopped = false;
|
|
this._isPaused = false;
|
|
this._isLooping = isLooping;
|
|
this._playbackRate = 1;
|
|
this._startTime = this.GetCurrentTime() - seekPos
|
|
}
|
|
Stop() {
|
|
if (this._bufferSource)
|
|
try {
|
|
this._bufferSource["stop"](0)
|
|
} catch (err) {}
|
|
this._isStopped = true;
|
|
this._isPaused = false;
|
|
this._instUid = -1
|
|
}
|
|
Pause() {
|
|
if (this._isPaused || this._isStopped || this.HasEnded())
|
|
return;
|
|
this._resumePosition = this.GetPlaybackTime(true);
|
|
if (this._isLooping)
|
|
this._resumePosition %= this.GetDuration();
|
|
this._isPaused = true;
|
|
this._bufferSource["stop"](0)
|
|
}
|
|
Resume() {
|
|
if (!this._isPaused || this._isStopped || this.HasEnded())
|
|
return;
|
|
this._ReleaseBufferSource();
|
|
this._bufferSource = this.GetAudioContext()["createBufferSource"]();
|
|
this._bufferSource["buffer"] = this._buffer.GetAudioBuffer();
|
|
this._bufferSource["connect"](this._gainNode);
|
|
this._activeSource = this._bufferSource;
|
|
this._bufferSource["onended"] = this._onended_handler;
|
|
this._bufferSource["loop"] = this._isLooping;
|
|
this._UpdateVolume();
|
|
this._UpdatePlaybackRate();
|
|
this._startTime = this.GetCurrentTime() - this._resumePosition / (this._playbackRate || .001);
|
|
this._bufferSource["start"](0, this._resumePosition);
|
|
this._isPaused = false
|
|
}
|
|
GetOverallVolume() {
|
|
return super.GetOverallVolume() * this._muteVol
|
|
}
|
|
_UpdateMuted() {
|
|
this._muteVol = this._isMuted || this.IsSilent() ? 0 : 1;
|
|
this._UpdateVolume()
|
|
}
|
|
SetLooping(l) {
|
|
l = !!l;
|
|
if (this._isLooping === l)
|
|
return;
|
|
this._isLooping = l;
|
|
if (this._bufferSource)
|
|
this._bufferSource["loop"] = l
|
|
}
|
|
_UpdatePlaybackRate() {
|
|
let r = this._playbackRate;
|
|
if (this._isTimescaled)
|
|
r *= this._audioDomHandler.GetTimeScale();
|
|
if (this._bufferSource)
|
|
this._bufferSource["playbackRate"]["value"] = r
|
|
}
|
|
Seek(pos) {
|
|
if (this._isStopped || this.HasEnded())
|
|
return;
|
|
if (this._isPaused)
|
|
this._resumePosition = pos;
|
|
else {
|
|
this.Pause();
|
|
this._resumePosition = pos;
|
|
this.Resume()
|
|
}
|
|
}
|
|
GetResumePosition() {
|
|
return this._resumePosition
|
|
}
|
|
SetSuspended(s) {
|
|
if (s)
|
|
if (this.IsPlaying()) {
|
|
this._resumeMe = true;
|
|
this._resumePosition = this.GetPlaybackTime(true);
|
|
if (this._isLooping)
|
|
this._resumePosition %= this.GetDuration();
|
|
this._bufferSource["stop"](0)
|
|
} else
|
|
this._resumeMe = false;
|
|
else if (this._resumeMe) {
|
|
this._ReleaseBufferSource();
|
|
this._bufferSource = this.GetAudioContext()["createBufferSource"]();
|
|
this._bufferSource["buffer"] = this._buffer.GetAudioBuffer();
|
|
this._bufferSource["connect"](this._gainNode);
|
|
this._activeSource = this._bufferSource;
|
|
this._bufferSource["onended"] = this._onended_handler;
|
|
this._bufferSource["loop"] = this._isLooping;
|
|
this._UpdateVolume();
|
|
this._UpdatePlaybackRate();
|
|
this._startTime = this.GetCurrentTime() - this._resumePosition / (this._playbackRate || .001);
|
|
this._bufferSource["start"](0, this._resumePosition);
|
|
this._resumeMe = false
|
|
}
|
|
}
|
|
_LoadAdditionalState(d) {
|
|
super._LoadAdditionalState(d);
|
|
this._resumePosition = d["resumePosition"]
|
|
}
|
|
}
|
|
}
|
|
;'use strict';
|
|
{
|
|
class AudioFXBase {
|
|
constructor(audioDomHandler) {
|
|
this._audioDomHandler = audioDomHandler;
|
|
this._audioContext = audioDomHandler.GetAudioContext();
|
|
this._index = -1;
|
|
this._tag = "";
|
|
this._type = "";
|
|
this._params = null
|
|
}
|
|
Release() {
|
|
this._audioContext = null
|
|
}
|
|
_SetIndex(i) {
|
|
this._index = i
|
|
}
|
|
GetIndex() {
|
|
return this._index
|
|
}
|
|
_SetTag(t) {
|
|
this._tag = t
|
|
}
|
|
GetTag() {
|
|
return this._tag
|
|
}
|
|
CreateGain() {
|
|
return this._audioContext["createGain"]()
|
|
}
|
|
GetInputNode() {}
|
|
ConnectTo(node) {}
|
|
SetAudioParam(ap, value, ramp, time) {
|
|
ap["cancelScheduledValues"](0);
|
|
if (time === 0) {
|
|
ap["value"] = value;
|
|
return
|
|
}
|
|
const curTime = this._audioContext["currentTime"];
|
|
time += curTime;
|
|
switch (ramp) {
|
|
case 0:
|
|
ap["setValueAtTime"](value, time);
|
|
break;
|
|
case 1:
|
|
ap["setValueAtTime"](ap["value"], curTime);
|
|
ap["linearRampToValueAtTime"](value, time);
|
|
break;
|
|
case 2:
|
|
ap["setValueAtTime"](ap["value"], curTime);
|
|
ap["exponentialRampToValueAtTime"](value, time);
|
|
break
|
|
}
|
|
}
|
|
GetState() {
|
|
return {
|
|
"type": this._type,
|
|
"tag": this._tag,
|
|
"params": this._params
|
|
}
|
|
}
|
|
}
|
|
self.C3AudioFilterFX = class C3AudioFilterFX extends AudioFXBase {
|
|
constructor(audioDomHandler, type, freq, detune, q, gain, mix) {
|
|
super(audioDomHandler);
|
|
this._type = "filter";
|
|
this._params = [type, freq, detune, q, gain, mix];
|
|
this._inputNode = this.CreateGain();
|
|
this._wetNode = this.CreateGain();
|
|
this._wetNode["gain"]["value"] = mix;
|
|
this._dryNode = this.CreateGain();
|
|
this._dryNode["gain"]["value"] = 1 - mix;
|
|
this._filterNode = this._audioContext["createBiquadFilter"]();
|
|
this._filterNode["type"] = type;
|
|
this._filterNode["frequency"]["value"] = freq;
|
|
this._filterNode["detune"]["value"] = detune;
|
|
this._filterNode["Q"]["value"] = q;
|
|
this._filterNode["gain"]["vlaue"] = gain;
|
|
this._inputNode["connect"](this._filterNode);
|
|
this._inputNode["connect"](this._dryNode);
|
|
this._filterNode["connect"](this._wetNode)
|
|
}
|
|
Release() {
|
|
this._inputNode["disconnect"]();
|
|
this._filterNode["disconnect"]();
|
|
this._wetNode["disconnect"]();
|
|
this._dryNode["disconnect"]();
|
|
super.Release()
|
|
}
|
|
ConnectTo(node) {
|
|
this._wetNode["disconnect"]();
|
|
this._wetNode["connect"](node);
|
|
this._dryNode["disconnect"]();
|
|
this._dryNode["connect"](node)
|
|
}
|
|
GetInputNode() {
|
|
return this._inputNode
|
|
}
|
|
SetParam(param, value, ramp, time) {
|
|
switch (param) {
|
|
case 0:
|
|
value = Math.max(Math.min(value / 100, 1), 0);
|
|
this._params[5] = value;
|
|
this.SetAudioParam(this._wetNode["gain"], value, ramp, time);
|
|
this.SetAudioParam(this._dryNode["gain"], 1 - value, ramp, time);
|
|
break;
|
|
case 1:
|
|
this._params[1] = value;
|
|
this.SetAudioParam(this._filterNode["frequency"], value, ramp, time);
|
|
break;
|
|
case 2:
|
|
this._params[2] = value;
|
|
this.SetAudioParam(this._filterNode["detune"], value, ramp, time);
|
|
break;
|
|
case 3:
|
|
this._params[3] = value;
|
|
this.SetAudioParam(this._filterNode["Q"], value, ramp, time);
|
|
break;
|
|
case 4:
|
|
this._params[4] = value;
|
|
this.SetAudioParam(this._filterNode["gain"], value, ramp, time);
|
|
break
|
|
}
|
|
}
|
|
}
|
|
;
|
|
self.C3AudioDelayFX = class C3AudioDelayFX extends AudioFXBase {
|
|
constructor(audioDomHandler, delayTime, delayGain, mix) {
|
|
super(audioDomHandler);
|
|
this._type = "delay";
|
|
this._params = [delayTime, delayGain, mix];
|
|
this._inputNode = this.CreateGain();
|
|
this._wetNode = this.CreateGain();
|
|
this._wetNode["gain"]["value"] = mix;
|
|
this._dryNode = this.CreateGain();
|
|
this._dryNode["gain"]["value"] = 1 - mix;
|
|
this._mainNode = this.CreateGain();
|
|
this._delayNode = this._audioContext["createDelay"](delayTime);
|
|
this._delayNode["delayTime"]["value"] = delayTime;
|
|
this._delayGainNode = this.CreateGain();
|
|
this._delayGainNode["gain"]["value"] = delayGain;
|
|
this._inputNode["connect"](this._mainNode);
|
|
this._inputNode["connect"](this._dryNode);
|
|
this._mainNode["connect"](this._wetNode);
|
|
this._mainNode["connect"](this._delayNode);
|
|
this._delayNode["connect"](this._delayGainNode);
|
|
this._delayGainNode["connect"](this._mainNode)
|
|
}
|
|
Release() {
|
|
this._inputNode["disconnect"]();
|
|
this._wetNode["disconnect"]();
|
|
this._dryNode["disconnect"]();
|
|
this._mainNode["disconnect"]();
|
|
this._delayNode["disconnect"]();
|
|
this._delayGainNode["disconnect"]();
|
|
super.Release()
|
|
}
|
|
ConnectTo(node) {
|
|
this._wetNode["disconnect"]();
|
|
this._wetNode["connect"](node);
|
|
this._dryNode["disconnect"]();
|
|
this._dryNode["connect"](node)
|
|
}
|
|
GetInputNode() {
|
|
return this._inputNode
|
|
}
|
|
SetParam(param, value, ramp, time) {
|
|
const DbToLinear = self.AudioDOMHandler.DbToLinear;
|
|
switch (param) {
|
|
case 0:
|
|
value = Math.max(Math.min(value / 100, 1), 0);
|
|
this._params[2] = value;
|
|
this.SetAudioParam(this._wetNode["gain"], value, ramp, time);
|
|
this.SetAudioParam(this._dryNode["gain"], 1 - value, ramp, time);
|
|
break;
|
|
case 4:
|
|
this._params[1] = DbToLinear(value);
|
|
this.SetAudioParam(this._delayGainNode["gain"], DbToLinear(value), ramp, time);
|
|
break;
|
|
case 5:
|
|
this._params[0] = value;
|
|
this.SetAudioParam(this._delayNode["delayTime"], value, ramp, time);
|
|
break
|
|
}
|
|
}
|
|
}
|
|
;
|
|
self.C3AudioConvolveFX = class C3AudioConvolveFX extends AudioFXBase {
|
|
constructor(audioDomHandler, buffer, normalize, mix) {
|
|
super(audioDomHandler);
|
|
this._type = "convolution";
|
|
this._params = [normalize, mix];
|
|
this._bufferOriginalUrl = "";
|
|
this._bufferUrl = "";
|
|
this._bufferType = "";
|
|
this._inputNode = this.CreateGain();
|
|
this._wetNode = this.CreateGain();
|
|
this._wetNode["gain"]["value"] = mix;
|
|
this._dryNode = this.CreateGain();
|
|
this._dryNode["gain"]["value"] = 1 - mix;
|
|
this._convolveNode = this._audioContext["createConvolver"]();
|
|
this._convolveNode["normalize"] = normalize;
|
|
this._convolveNode["buffer"] = buffer;
|
|
this._inputNode["connect"](this._convolveNode);
|
|
this._inputNode["connect"](this._dryNode);
|
|
this._convolveNode["connect"](this._wetNode)
|
|
}
|
|
Release() {
|
|
this._inputNode["disconnect"]();
|
|
this._convolveNode["disconnect"]();
|
|
this._wetNode["disconnect"]();
|
|
this._dryNode["disconnect"]();
|
|
super.Release()
|
|
}
|
|
ConnectTo(node) {
|
|
this._wetNode["disconnect"]();
|
|
this._wetNode["connect"](node);
|
|
this._dryNode["disconnect"]();
|
|
this._dryNode["connect"](node)
|
|
}
|
|
GetInputNode() {
|
|
return this._inputNode
|
|
}
|
|
SetParam(param, value, ramp, time) {
|
|
switch (param) {
|
|
case 0:
|
|
value = Math.max(Math.min(value / 100, 1), 0);
|
|
this._params[1] = value;
|
|
this.SetAudioParam(this._wetNode["gain"], value, ramp, time);
|
|
this.SetAudioParam(this._dryNode["gain"], 1 - value, ramp, time);
|
|
break
|
|
}
|
|
}
|
|
_SetBufferInfo(bufferOriginalUrl, bufferUrl, bufferType) {
|
|
this._bufferOriginalUrl = bufferOriginalUrl;
|
|
this._bufferUrl = bufferUrl;
|
|
this._bufferType = bufferType
|
|
}
|
|
GetState() {
|
|
const ret = super.GetState();
|
|
ret["bufferOriginalUrl"] = this._bufferOriginalUrl;
|
|
ret["bufferUrl"] = "";
|
|
ret["bufferType"] = this._bufferType;
|
|
return ret
|
|
}
|
|
}
|
|
;
|
|
self.C3AudioFlangerFX = class C3AudioFlangerFX extends AudioFXBase {
|
|
constructor(audioDomHandler, delay, modulation, freq, feedback, mix) {
|
|
super(audioDomHandler);
|
|
this._type = "flanger";
|
|
this._params = [delay, modulation, freq, feedback, mix];
|
|
this._inputNode = this.CreateGain();
|
|
this._dryNode = this.CreateGain();
|
|
this._dryNode["gain"]["value"] = 1 - mix / 2;
|
|
this._wetNode = this.CreateGain();
|
|
this._wetNode["gain"]["value"] = mix / 2;
|
|
this._feedbackNode = this.CreateGain();
|
|
this._feedbackNode["gain"]["value"] = feedback;
|
|
this._delayNode = this._audioContext["createDelay"](delay + modulation);
|
|
this._delayNode["delayTime"]["value"] = delay;
|
|
this._oscNode = this._audioContext["createOscillator"]();
|
|
this._oscNode["frequency"]["value"] = freq;
|
|
this._oscGainNode = this.CreateGain();
|
|
this._oscGainNode["gain"]["value"] = modulation;
|
|
this._inputNode["connect"](this._delayNode);
|
|
this._inputNode["connect"](this._dryNode);
|
|
this._delayNode["connect"](this._wetNode);
|
|
this._delayNode["connect"](this._feedbackNode);
|
|
this._feedbackNode["connect"](this._delayNode);
|
|
this._oscNode["connect"](this._oscGainNode);
|
|
this._oscGainNode["connect"](this._delayNode["delayTime"]);
|
|
this._oscNode["start"](0)
|
|
}
|
|
Release() {
|
|
this._oscNode["stop"](0);
|
|
this._inputNode["disconnect"]();
|
|
this._delayNode["disconnect"]();
|
|
this._oscNode["disconnect"]();
|
|
this._oscGainNode["disconnect"]();
|
|
this._dryNode["disconnect"]();
|
|
this._wetNode["disconnect"]();
|
|
this._feedbackNode["disconnect"]();
|
|
super.Release()
|
|
}
|
|
ConnectTo(node) {
|
|
this._wetNode["disconnect"]();
|
|
this._wetNode["connect"](node);
|
|
this._dryNode["disconnect"]();
|
|
this._dryNode["connect"](node)
|
|
}
|
|
GetInputNode() {
|
|
return this._inputNode
|
|
}
|
|
SetParam(param, value, ramp, time) {
|
|
switch (param) {
|
|
case 0:
|
|
value = Math.max(Math.min(value / 100, 1), 0);
|
|
this._params[4] = value;
|
|
this.SetAudioParam(this._wetNode["gain"], value / 2, ramp, time);
|
|
this.SetAudioParam(this._dryNode["gain"], 1 - value / 2, ramp, time);
|
|
break;
|
|
case 6:
|
|
this._params[1] = value / 1E3;
|
|
this.SetAudioParam(this._oscGainNode["gain"], value / 1E3, ramp, time);
|
|
break;
|
|
case 7:
|
|
this._params[2] = value;
|
|
this.SetAudioParam(this._oscNode["frequency"], value, ramp, time);
|
|
break;
|
|
case 8:
|
|
this._params[3] = value / 100;
|
|
this.SetAudioParam(this._feedbackNode["gain"], value / 100, ramp, time);
|
|
break
|
|
}
|
|
}
|
|
}
|
|
;
|
|
self.C3AudioPhaserFX = class C3AudioPhaserFX extends AudioFXBase {
|
|
constructor(audioDomHandler, freq, detune, q, modulation, modfreq, mix) {
|
|
super(audioDomHandler);
|
|
this._type = "phaser";
|
|
this._params = [freq, detune, q, modulation, modfreq, mix];
|
|
this._inputNode = this.CreateGain();
|
|
this._dryNode = this.CreateGain();
|
|
this._dryNode["gain"]["value"] = 1 - mix / 2;
|
|
this._wetNode = this.CreateGain();
|
|
this._wetNode["gain"]["value"] = mix / 2;
|
|
this._filterNode = this._audioContext["createBiquadFilter"]();
|
|
this._filterNode["type"] = "allpass";
|
|
this._filterNode["frequency"]["value"] = freq;
|
|
this._filterNode["detune"]["value"] = detune;
|
|
this._filterNode["Q"]["value"] = q;
|
|
this._oscNode = this._audioContext["createOscillator"]();
|
|
this._oscNode["frequency"]["value"] = modfreq;
|
|
this._oscGainNode = this.CreateGain();
|
|
this._oscGainNode["gain"]["value"] = modulation;
|
|
this._inputNode["connect"](this._filterNode);
|
|
this._inputNode["connect"](this._dryNode);
|
|
this._filterNode["connect"](this._wetNode);
|
|
this._oscNode["connect"](this._oscGainNode);
|
|
this._oscGainNode["connect"](this._filterNode["frequency"]);
|
|
this._oscNode["start"](0)
|
|
}
|
|
Release() {
|
|
this._oscNode["stop"](0);
|
|
this._inputNode["disconnect"]();
|
|
this._filterNode["disconnect"]();
|
|
this._oscNode["disconnect"]();
|
|
this._oscGainNode["disconnect"]();
|
|
this._dryNode["disconnect"]();
|
|
this._wetNode["disconnect"]();
|
|
super.Release()
|
|
}
|
|
ConnectTo(node) {
|
|
this._wetNode["disconnect"]();
|
|
this._wetNode["connect"](node);
|
|
this._dryNode["disconnect"]();
|
|
this._dryNode["connect"](node)
|
|
}
|
|
GetInputNode() {
|
|
return this._inputNode
|
|
}
|
|
SetParam(param, value, ramp, time) {
|
|
switch (param) {
|
|
case 0:
|
|
value = Math.max(Math.min(value / 100, 1), 0);
|
|
this._params[5] = value;
|
|
this.SetAudioParam(this._wetNode["gain"], value / 2, ramp, time);
|
|
this.SetAudioParam(this._dryNode["gain"], 1 - value / 2, ramp, time);
|
|
break;
|
|
case 1:
|
|
this._params[0] = value;
|
|
this.SetAudioParam(this._filterNode["frequency"], value, ramp, time);
|
|
break;
|
|
case 2:
|
|
this._params[1] = value;
|
|
this.SetAudioParam(this._filterNode["detune"], value, ramp, time);
|
|
break;
|
|
case 3:
|
|
this._params[2] = value;
|
|
this.SetAudioParam(this._filterNode["Q"], value, ramp, time);
|
|
break;
|
|
case 6:
|
|
this._params[3] = value;
|
|
this.SetAudioParam(this._oscGainNode["gain"], value, ramp, time);
|
|
break;
|
|
case 7:
|
|
this._params[4] = value;
|
|
this.SetAudioParam(this._oscNode["frequency"], value, ramp, time);
|
|
break
|
|
}
|
|
}
|
|
}
|
|
;
|
|
self.C3AudioGainFX = class C3AudioGainFX extends AudioFXBase {
|
|
constructor(audioDomHandler, g) {
|
|
super(audioDomHandler);
|
|
this._type = "gain";
|
|
this._params = [g];
|
|
this._node = this.CreateGain();
|
|
this._node["gain"]["value"] = g
|
|
}
|
|
Release() {
|
|
this._node["disconnect"]();
|
|
super.Release()
|
|
}
|
|
ConnectTo(node) {
|
|
this._node["disconnect"]();
|
|
this._node["connect"](node)
|
|
}
|
|
GetInputNode() {
|
|
return this._node
|
|
}
|
|
SetParam(param, value, ramp, time) {
|
|
const DbToLinear = self.AudioDOMHandler.DbToLinear;
|
|
switch (param) {
|
|
case 4:
|
|
this._params[0] = DbToLinear(value);
|
|
this.SetAudioParam(this._node["gain"], DbToLinear(value), ramp, time);
|
|
break
|
|
}
|
|
}
|
|
}
|
|
;
|
|
self.C3AudioTremoloFX = class C3AudioTremoloFX extends AudioFXBase {
|
|
constructor(audioDomHandler, freq, mix) {
|
|
super(audioDomHandler);
|
|
this._type = "tremolo";
|
|
this._params = [freq, mix];
|
|
this._node = this.CreateGain();
|
|
this._node["gain"]["value"] = 1 - mix / 2;
|
|
this._oscNode = this._audioContext["createOscillator"]();
|
|
this._oscNode["frequency"]["value"] = freq;
|
|
this._oscGainNode = this.CreateGain();
|
|
this._oscGainNode["gain"]["value"] = mix / 2;
|
|
this._oscNode["connect"](this._oscGainNode);
|
|
this._oscGainNode["connect"](this._node["gain"]);
|
|
this._oscNode["start"](0)
|
|
}
|
|
Release() {
|
|
this._oscNode["stop"](0);
|
|
this._oscNode["disconnect"]();
|
|
this._oscGainNode["disconnect"]();
|
|
this._node["disconnect"]();
|
|
super.Release()
|
|
}
|
|
ConnectTo(node) {
|
|
this._node["disconnect"]();
|
|
this._node["connect"](node)
|
|
}
|
|
GetInputNode() {
|
|
return this._node
|
|
}
|
|
SetParam(param, value, ramp, time) {
|
|
switch (param) {
|
|
case 0:
|
|
value = Math.max(Math.min(value / 100, 1), 0);
|
|
this._params[1] = value;
|
|
this.SetAudioParam(this._node["gain"]["value"], 1 - value / 2, ramp, time);
|
|
this.SetAudioParam(this._oscGainNode["gain"]["value"], value / 2, ramp, time);
|
|
break;
|
|
case 7:
|
|
this._params[0] = value;
|
|
this.SetAudioParam(this._oscNode["frequency"], value, ramp, time);
|
|
break
|
|
}
|
|
}
|
|
}
|
|
;
|
|
self.C3AudioRingModFX = class C3AudioRingModFX extends AudioFXBase {
|
|
constructor(audioDomHandler, freq, mix) {
|
|
super(audioDomHandler);
|
|
this._type = "ringmod";
|
|
this._params = [freq, mix];
|
|
this._inputNode = this.CreateGain();
|
|
this._wetNode = this.CreateGain();
|
|
this._wetNode["gain"]["value"] = mix;
|
|
this._dryNode = this.CreateGain();
|
|
this._dryNode["gain"]["value"] = 1 - mix;
|
|
this._ringNode = this.CreateGain();
|
|
this._ringNode["gain"]["value"] = 0;
|
|
this._oscNode = this._audioContext["createOscillator"]();
|
|
this._oscNode["frequency"]["value"] = freq;
|
|
this._oscNode["connect"](this._ringNode["gain"]);
|
|
this._oscNode["start"](0);
|
|
this._inputNode["connect"](this._ringNode);
|
|
this._inputNode["connect"](this._dryNode);
|
|
this._ringNode["connect"](this._wetNode)
|
|
}
|
|
Release() {
|
|
this._oscNode["stop"](0);
|
|
this._oscNode["disconnect"]();
|
|
this._ringNode["disconnect"]();
|
|
this._inputNode["disconnect"]();
|
|
this._wetNode["disconnect"]();
|
|
this._dryNode["disconnect"]();
|
|
super.Release()
|
|
}
|
|
ConnectTo(node) {
|
|
this._wetNode["disconnect"]();
|
|
this._wetNode["connect"](node);
|
|
this._dryNode["disconnect"]();
|
|
this._dryNode["connect"](node)
|
|
}
|
|
GetInputNode() {
|
|
return this._inputNode
|
|
}
|
|
SetParam(param, value, ramp, time) {
|
|
switch (param) {
|
|
case 0:
|
|
value = Math.max(Math.min(value / 100, 1), 0);
|
|
this._params[1] = value;
|
|
this.SetAudioParam(this._wetNode["gain"], value, ramp, time);
|
|
this.SetAudioParam(this._dryNode["gain"], 1 - value, ramp, time);
|
|
break;
|
|
case 7:
|
|
this._params[0] = value;
|
|
this.SetAudioParam(this._oscNode["frequency"], value, ramp, time);
|
|
break
|
|
}
|
|
}
|
|
}
|
|
;
|
|
self.C3AudioDistortionFX = class C3AudioDistortionFX extends AudioFXBase {
|
|
constructor(audioDomHandler, threshold, headroom, drive, makeupgain, mix) {
|
|
super(audioDomHandler);
|
|
this._type = "distortion";
|
|
this._params = [threshold, headroom, drive, makeupgain, mix];
|
|
this._inputNode = this.CreateGain();
|
|
this._preGain = this.CreateGain();
|
|
this._postGain = this.CreateGain();
|
|
this._SetDrive(drive, makeupgain);
|
|
this._wetNode = this.CreateGain();
|
|
this._wetNode["gain"]["value"] = mix;
|
|
this._dryNode = this.CreateGain();
|
|
this._dryNode["gain"]["value"] = 1 - mix;
|
|
this._waveShaper = this._audioContext["createWaveShaper"]();
|
|
this._curve = new Float32Array(65536);
|
|
this._GenerateColortouchCurve(threshold, headroom);
|
|
this._waveShaper.curve = this._curve;
|
|
this._inputNode["connect"](this._preGain);
|
|
this._inputNode["connect"](this._dryNode);
|
|
this._preGain["connect"](this._waveShaper);
|
|
this._waveShaper["connect"](this._postGain);
|
|
this._postGain["connect"](this._wetNode)
|
|
}
|
|
Release() {
|
|
this._inputNode["disconnect"]();
|
|
this._preGain["disconnect"]();
|
|
this._waveShaper["disconnect"]();
|
|
this._postGain["disconnect"]();
|
|
this._wetNode["disconnect"]();
|
|
this._dryNode["disconnect"]();
|
|
super.Release()
|
|
}
|
|
_SetDrive(drive, makeupgain) {
|
|
if (drive < .01)
|
|
drive = .01;
|
|
this._preGain["gain"]["value"] = drive;
|
|
this._postGain["gain"]["value"] = Math.pow(1 / drive, .6) * makeupgain
|
|
}
|
|
_GenerateColortouchCurve(threshold, headroom) {
|
|
const n = 65536;
|
|
const n2 = n / 2;
|
|
for (let i = 0; i < n2; ++i) {
|
|
let x = i / n2;
|
|
x = this._Shape(x, threshold, headroom);
|
|
this._curve[n2 + i] = x;
|
|
this._curve[n2 - i - 1] = -x
|
|
}
|
|
}
|
|
_Shape(x, threshold, headroom) {
|
|
const maximum = 1.05 * headroom * threshold;
|
|
const kk = maximum - threshold;
|
|
const sign = x < 0 ? -1 : +1;
|
|
const absx = x < 0 ? -x : x;
|
|
let shapedInput = absx < threshold ? absx : threshold + kk * self.AudioDOMHandler.e4(absx - threshold, 1 / kk);
|
|
shapedInput *= sign;
|
|
return shapedInput
|
|
}
|
|
ConnectTo(node) {
|
|
this._wetNode["disconnect"]();
|
|
this._wetNode["connect"](node);
|
|
this._dryNode["disconnect"]();
|
|
this._dryNode["connect"](node)
|
|
}
|
|
GetInputNode() {
|
|
return this._inputNode
|
|
}
|
|
SetParam(param, value, ramp, time) {
|
|
switch (param) {
|
|
case 0:
|
|
value = Math.max(Math.min(value / 100, 1), 0);
|
|
this._params[4] = value;
|
|
this.SetAudioParam(this._wetNode["gain"], value, ramp, time);
|
|
this.SetAudioParam(this._dryNode["gain"], 1 - value, ramp, time);
|
|
break
|
|
}
|
|
}
|
|
}
|
|
;
|
|
self.C3AudioCompressorFX = class C3AudioCompressorFX extends AudioFXBase {
|
|
constructor(audioDomHandler, threshold, knee, ratio, attack, release) {
|
|
super(audioDomHandler);
|
|
this._type = "compressor";
|
|
this._params = [threshold, knee, ratio, attack, release];
|
|
this._node = this._audioContext["createDynamicsCompressor"]();
|
|
this._node["threshold"]["value"] = threshold;
|
|
this._node["knee"]["value"] = knee;
|
|
this._node["ratio"]["value"] = ratio;
|
|
this._node["attack"]["value"] = attack;
|
|
this._node["release"]["value"] = release
|
|
}
|
|
Release() {
|
|
this._node["disconnect"]();
|
|
super.Release()
|
|
}
|
|
ConnectTo(node) {
|
|
this._node["disconnect"]();
|
|
this._node["connect"](node)
|
|
}
|
|
GetInputNode() {
|
|
return this._node
|
|
}
|
|
SetParam(param, value, ramp, time) {}
|
|
}
|
|
;
|
|
self.C3AudioAnalyserFX = class C3AudioAnalyserFX extends AudioFXBase {
|
|
constructor(audioDomHandler, fftSize, smoothing) {
|
|
super(audioDomHandler);
|
|
this._type = "analyser";
|
|
this._params = [fftSize, smoothing];
|
|
this._node = this._audioContext["createAnalyser"]();
|
|
this._node["fftSize"] = fftSize;
|
|
this._node["smoothingTimeConstant"] = smoothing;
|
|
this._freqBins = new Float32Array(this._node["frequencyBinCount"]);
|
|
this._signal = new Uint8Array(fftSize);
|
|
this._peak = 0;
|
|
this._rms = 0;
|
|
this._audioDomHandler._AddAnalyser(this)
|
|
}
|
|
Release() {
|
|
this._audioDomHandler._RemoveAnalyser(this);
|
|
this._node["disconnect"]();
|
|
super.Release()
|
|
}
|
|
Tick() {
|
|
this._node["getFloatFrequencyData"](this._freqBins);
|
|
this._node["getByteTimeDomainData"](this._signal);
|
|
const fftSize = this._node["fftSize"];
|
|
this._peak = 0;
|
|
let rmsSquaredSum = 0;
|
|
for (let i = 0; i < fftSize; ++i) {
|
|
let s = (this._signal[i] - 128) / 128;
|
|
if (s < 0)
|
|
s = -s;
|
|
if (this._peak < s)
|
|
this._peak = s;
|
|
rmsSquaredSum += s * s
|
|
}
|
|
const LinearToDb = self.AudioDOMHandler.LinearToDb;
|
|
this._peak = LinearToDb(this._peak);
|
|
this._rms = LinearToDb(Math.sqrt(rmsSquaredSum / fftSize))
|
|
}
|
|
ConnectTo(node) {
|
|
this._node["disconnect"]();
|
|
this._node["connect"](node)
|
|
}
|
|
GetInputNode() {
|
|
return this._node
|
|
}
|
|
SetParam(param, value, ramp, time) {}
|
|
GetData() {
|
|
return {
|
|
"tag": this.GetTag(),
|
|
"index": this.GetIndex(),
|
|
"peak": this._peak,
|
|
"rms": this._rms,
|
|
"binCount": this._node["frequencyBinCount"],
|
|
"freqBins": this._freqBins
|
|
}
|
|
}
|
|
}
|
|
}
|
|
;'use strict';
|
|
{
|
|
const DOM_COMPONENT_ID = "text-input";
|
|
function StopPropagation(e) {
|
|
e.stopPropagation()
|
|
}
|
|
function StopKeyPropagation(e) {
|
|
if (e.which !== 13 && e.which !== 27)
|
|
e.stopPropagation()
|
|
}
|
|
const HANDLER_CLASS = class TextInputDOMHandler extends self.DOMElementHandler {
|
|
constructor(iRuntime) {
|
|
super(iRuntime, DOM_COMPONENT_ID);
|
|
this.AddDOMElementMessageHandler("scroll-to-bottom", elem=>this._OnScrollToBottom(elem))
|
|
}
|
|
CreateElement(elementId, e) {
|
|
let elem;
|
|
const type = e["type"];
|
|
if (type === "textarea") {
|
|
elem = document.createElement("textarea");
|
|
elem.style.resize = "none"
|
|
} else {
|
|
elem = document.createElement("input");
|
|
elem.type = type
|
|
}
|
|
elem.style.position = "absolute";
|
|
elem.autocomplete = "off";
|
|
elem.addEventListener("touchstart", StopPropagation);
|
|
elem.addEventListener("touchmove", StopPropagation);
|
|
elem.addEventListener("touchend", StopPropagation);
|
|
elem.addEventListener("mousedown", StopPropagation);
|
|
elem.addEventListener("mouseup", StopPropagation);
|
|
elem.addEventListener("keydown", StopKeyPropagation);
|
|
elem.addEventListener("keyup", StopKeyPropagation);
|
|
elem.addEventListener("click", e=>{
|
|
e.stopPropagation();
|
|
this._PostToRuntimeElementMaybeSync("click", elementId)
|
|
}
|
|
);
|
|
elem.addEventListener("dblclick", e=>{
|
|
e.stopPropagation();
|
|
this._PostToRuntimeElementMaybeSync("dblclick", elementId)
|
|
}
|
|
);
|
|
elem.addEventListener("input", ()=>this.PostToRuntimeElement("change", elementId, {
|
|
"text": elem.value
|
|
}));
|
|
if (e["id"])
|
|
elem.id = e["id"];
|
|
this.UpdateState(elem, e);
|
|
return elem
|
|
}
|
|
UpdateState(elem, e) {
|
|
elem.value = e["text"];
|
|
elem.placeholder = e["placeholder"];
|
|
elem.title = e["title"];
|
|
elem.disabled = !e["isEnabled"];
|
|
elem.readOnly = e["isReadOnly"];
|
|
elem.spellcheck = e["spellCheck"];
|
|
const maxLength = e["maxLength"];
|
|
if (maxLength < 0)
|
|
elem.removeAttribute("maxlength");
|
|
else
|
|
elem.setAttribute("maxlength", maxLength)
|
|
}
|
|
_OnScrollToBottom(elem) {
|
|
elem.scrollTop = elem.scrollHeight
|
|
}
|
|
}
|
|
;
|
|
self.RuntimeInterface.AddDOMHandlerClass(HANDLER_CLASS)
|
|
}
|
|
; |