kopia lustrzana https://github.com/jameshball/osci-render
493 wiersze
14 KiB
JavaScript
493 wiersze
14 KiB
JavaScript
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE framework.
|
|
Copyright (c) Raw Material Software Limited
|
|
|
|
JUCE is an open source framework subject to commercial or open source
|
|
licensing.
|
|
|
|
By downloading, installing, or using the JUCE framework, or combining the
|
|
JUCE framework with any other source code, object code, content or any other
|
|
copyrightable work, you agree to the terms of the JUCE End User Licence
|
|
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
|
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
|
do not agree to the terms of these agreements, we will not license the JUCE
|
|
framework to you, and you must discontinue the installation or download
|
|
process and cease use of the JUCE framework.
|
|
|
|
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
|
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
|
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
|
|
|
Or:
|
|
|
|
You may also use this code under the terms of the AGPLv3:
|
|
https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
|
|
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
|
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
|
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
import "./check_native_interop.js";
|
|
|
|
class PromiseHandler {
|
|
constructor() {
|
|
this.lastPromiseId = 0;
|
|
this.promises = new Map();
|
|
|
|
window.__JUCE__.backend.addEventListener(
|
|
"__juce__complete",
|
|
({ promiseId, result }) => {
|
|
if (this.promises.has(promiseId)) {
|
|
this.promises.get(promiseId).resolve(result);
|
|
this.promises.delete(promiseId);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
createPromise() {
|
|
const promiseId = this.lastPromiseId++;
|
|
const result = new Promise((resolve, reject) => {
|
|
this.promises.set(promiseId, { resolve: resolve, reject: reject });
|
|
});
|
|
return [promiseId, result];
|
|
}
|
|
}
|
|
|
|
const promiseHandler = new PromiseHandler();
|
|
|
|
/**
|
|
* Returns a function object that calls a function registered on the JUCE backend and forwards all
|
|
* parameters to it.
|
|
*
|
|
* The provided name should be the same as the name argument passed to
|
|
* WebBrowserComponent::Options.withNativeFunction() on the backend.
|
|
*
|
|
* @param {String} name
|
|
*/
|
|
function getNativeFunction(name) {
|
|
if (!window.__JUCE__.initialisationData.__juce__functions.includes(name))
|
|
console.warn(
|
|
`Creating native function binding for '${name}', which is unknown to the backend`
|
|
);
|
|
|
|
const f = function () {
|
|
const [promiseId, result] = promiseHandler.createPromise();
|
|
|
|
window.__JUCE__.backend.emitEvent("__juce__invoke", {
|
|
name: name,
|
|
params: Array.prototype.slice.call(arguments),
|
|
resultId: promiseId,
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
return f;
|
|
}
|
|
|
|
//==============================================================================
|
|
|
|
class ListenerList {
|
|
constructor() {
|
|
this.listeners = new Map();
|
|
this.listenerId = 0;
|
|
}
|
|
|
|
addListener(fn) {
|
|
const newListenerId = this.listenerId++;
|
|
this.listeners.set(newListenerId, fn);
|
|
return newListenerId;
|
|
}
|
|
|
|
removeListener(id) {
|
|
if (this.listeners.has(id)) {
|
|
this.listeners.delete(id);
|
|
}
|
|
}
|
|
|
|
callListeners(payload) {
|
|
for (const [, value] of this.listeners) {
|
|
value(payload);
|
|
}
|
|
}
|
|
}
|
|
|
|
const BasicControl_valueChangedEventId = "valueChanged";
|
|
const BasicControl_propertiesChangedId = "propertiesChanged";
|
|
|
|
class SliderState {
|
|
constructor(name) {
|
|
if (!window.__JUCE__.initialisationData.__juce__sliders.includes(name))
|
|
console.warn(
|
|
"Creating SliderState for '" +
|
|
name +
|
|
"', which is unknown to the backend"
|
|
);
|
|
|
|
this.name = name;
|
|
this.identifier = "__juce__slider" + this.name;
|
|
this.scaledValue = 0;
|
|
this.properties = {
|
|
start: 0,
|
|
end: 1,
|
|
skew: 1,
|
|
name: "",
|
|
label: "",
|
|
numSteps: 100,
|
|
interval: 0,
|
|
parameterIndex: -1,
|
|
};
|
|
this.valueChangedEvent = new ListenerList();
|
|
this.propertiesChangedEvent = new ListenerList();
|
|
|
|
window.__JUCE__.backend.addEventListener(this.identifier, (event) =>
|
|
this.handleEvent(event)
|
|
);
|
|
|
|
window.__JUCE__.backend.emitEvent(this.identifier, {
|
|
eventType: "requestInitialUpdate",
|
|
});
|
|
}
|
|
|
|
setNormalisedValue(newValue) {
|
|
this.scaledValue = this.snapToLegalValue(
|
|
this.normalisedToScaledValue(newValue)
|
|
);
|
|
|
|
window.__JUCE__.backend.emitEvent(this.identifier, {
|
|
eventType: BasicControl_valueChangedEventId,
|
|
value: this.scaledValue,
|
|
});
|
|
}
|
|
|
|
sliderDragStarted() {}
|
|
|
|
sliderDragEnded() {}
|
|
|
|
handleEvent(event) {
|
|
if (event.eventType == BasicControl_valueChangedEventId) {
|
|
this.scaledValue = event.value;
|
|
this.valueChangedEvent.callListeners();
|
|
}
|
|
if (event.eventType == BasicControl_propertiesChangedId) {
|
|
// eslint-disable-next-line no-unused-vars
|
|
let { eventType: _, ...rest } = event;
|
|
this.properties = rest;
|
|
this.propertiesChangedEvent.callListeners();
|
|
}
|
|
}
|
|
|
|
getScaledValue() {
|
|
return this.scaledValue;
|
|
}
|
|
|
|
getNormalisedValue() {
|
|
return Math.pow(
|
|
(this.scaledValue - this.properties.start) /
|
|
(this.properties.end - this.properties.start),
|
|
this.properties.skew
|
|
);
|
|
}
|
|
|
|
normalisedToScaledValue(normalisedValue) {
|
|
return (
|
|
Math.pow(normalisedValue, 1 / this.properties.skew) *
|
|
(this.properties.end - this.properties.start) +
|
|
this.properties.start
|
|
);
|
|
}
|
|
|
|
snapToLegalValue(value) {
|
|
const interval = this.properties.interval;
|
|
|
|
if (interval == 0) return value;
|
|
|
|
const start = this.properties.start;
|
|
const clamp = (val, min = 0, max = 1) => Math.max(min, Math.min(max, val));
|
|
|
|
return clamp(
|
|
start + interval * Math.floor((value - start) / interval + 0.5),
|
|
this.properties.start,
|
|
this.properties.end
|
|
);
|
|
}
|
|
}
|
|
|
|
const sliderStates = new Map();
|
|
|
|
for (const sliderName of window.__JUCE__.initialisationData.__juce__sliders)
|
|
sliderStates.set(sliderName, new SliderState(sliderName));
|
|
|
|
/**
|
|
* Returns a SliderState object that is connected to the backend WebSliderRelay object that was
|
|
* created with the same name argument.
|
|
*
|
|
* To register a WebSliderRelay object create one with the right name and add it to the
|
|
* WebBrowserComponent::Options struct using withOptionsFrom.
|
|
*
|
|
* @param {String} name
|
|
*/
|
|
function getSliderState(name) {
|
|
if (!sliderStates.has(name)) sliderStates.set(name, new SliderState(name));
|
|
|
|
return sliderStates.get(name);
|
|
}
|
|
|
|
class ToggleState {
|
|
constructor(name) {
|
|
if (!window.__JUCE__.initialisationData.__juce__toggles.includes(name))
|
|
console.warn(
|
|
"Creating ToggleState for '" +
|
|
name +
|
|
"', which is unknown to the backend"
|
|
);
|
|
|
|
this.name = name;
|
|
this.identifier = "__juce__toggle" + this.name;
|
|
this.value = false;
|
|
this.properties = {
|
|
name: "",
|
|
parameterIndex: -1,
|
|
};
|
|
this.valueChangedEvent = new ListenerList();
|
|
this.propertiesChangedEvent = new ListenerList();
|
|
|
|
window.__JUCE__.backend.addEventListener(this.identifier, (event) =>
|
|
this.handleEvent(event)
|
|
);
|
|
|
|
window.__JUCE__.backend.emitEvent(this.identifier, {
|
|
eventType: "requestInitialUpdate",
|
|
});
|
|
}
|
|
|
|
getValue() {
|
|
return this.value;
|
|
}
|
|
|
|
setValue(newValue) {
|
|
this.value = newValue;
|
|
|
|
window.__JUCE__.backend.emitEvent(this.identifier, {
|
|
eventType: BasicControl_valueChangedEventId,
|
|
value: this.value,
|
|
});
|
|
}
|
|
|
|
handleEvent(event) {
|
|
if (event.eventType == BasicControl_valueChangedEventId) {
|
|
this.value = event.value;
|
|
this.valueChangedEvent.callListeners();
|
|
}
|
|
if (event.eventType == BasicControl_propertiesChangedId) {
|
|
// eslint-disable-next-line no-unused-vars
|
|
let { eventType: _, ...rest } = event;
|
|
this.properties = rest;
|
|
this.propertiesChangedEvent.callListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
const toggleStates = new Map();
|
|
|
|
for (const name of window.__JUCE__.initialisationData.__juce__toggles)
|
|
toggleStates.set(name, new ToggleState(name));
|
|
|
|
/**
|
|
* Returns a ToggleState object that is connected to the backend WebToggleButtonRelay object that was
|
|
* created with the same name argument.
|
|
*
|
|
* To register a WebToggleButtonRelay object create one with the right name and add it to the
|
|
* WebBrowserComponent::Options struct using withOptionsFrom.
|
|
*
|
|
* @param {String} name
|
|
*/
|
|
function getToggleState(name) {
|
|
if (!toggleStates.has(name)) toggleStates.set(name, new ToggleState(name));
|
|
|
|
return toggleStates.get(name);
|
|
}
|
|
|
|
class ComboBoxState {
|
|
constructor(name) {
|
|
if (!window.__JUCE__.initialisationData.__juce__comboBoxes.includes(name))
|
|
console.warn(
|
|
"Creating ComboBoxState for '" +
|
|
name +
|
|
"', which is unknown to the backend"
|
|
);
|
|
|
|
this.name = name;
|
|
this.identifier = "__juce__comboBox" + this.name;
|
|
this.value = 0.0;
|
|
this.properties = {
|
|
name: "",
|
|
parameterIndex: -1,
|
|
choices: [],
|
|
};
|
|
this.valueChangedEvent = new ListenerList();
|
|
this.propertiesChangedEvent = new ListenerList();
|
|
|
|
window.__JUCE__.backend.addEventListener(this.identifier, (event) =>
|
|
this.handleEvent(event)
|
|
);
|
|
|
|
window.__JUCE__.backend.emitEvent(this.identifier, {
|
|
eventType: "requestInitialUpdate",
|
|
});
|
|
}
|
|
|
|
getChoiceIndex() {
|
|
return Math.round(this.value * (this.properties.choices.length - 1));
|
|
}
|
|
|
|
setChoiceIndex(index) {
|
|
const numItems = this.properties.choices.length;
|
|
this.value = numItems > 1 ? index / (numItems - 1) : 0.0;
|
|
|
|
window.__JUCE__.backend.emitEvent(this.identifier, {
|
|
eventType: BasicControl_valueChangedEventId,
|
|
value: this.value,
|
|
});
|
|
}
|
|
|
|
handleEvent(event) {
|
|
if (event.eventType == BasicControl_valueChangedEventId) {
|
|
this.value = event.value;
|
|
this.valueChangedEvent.callListeners();
|
|
}
|
|
if (event.eventType == BasicControl_propertiesChangedId) {
|
|
// eslint-disable-next-line no-unused-vars
|
|
let { eventType: _, ...rest } = event;
|
|
this.properties = rest;
|
|
this.propertiesChangedEvent.callListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
const comboBoxStates = new Map();
|
|
|
|
for (const name of window.__JUCE__.initialisationData.__juce__comboBoxes)
|
|
comboBoxStates.set(name, new ComboBoxState(name));
|
|
|
|
/**
|
|
* Returns a ComboBoxState object that is connected to the backend WebComboBoxRelay object that was
|
|
* created with the same name argument.
|
|
*
|
|
* To register a WebComboBoxRelay object create one with the right name and add it to the
|
|
* WebBrowserComponent::Options struct using withOptionsFrom.
|
|
*
|
|
* @param {String} name
|
|
*/
|
|
function getComboBoxState(name) {
|
|
if (!comboBoxStates.has(name))
|
|
comboBoxStates.set(name, new ComboBoxState(name));
|
|
|
|
return comboBoxStates.get(name);
|
|
}
|
|
|
|
/**
|
|
* Appends a platform-specific prefix to the path to ensure that a request sent to this address will
|
|
* be received by the backend's ResourceProvider.
|
|
* @param {String} path
|
|
*/
|
|
function getBackendResourceAddress(path) {
|
|
const platform =
|
|
window.__JUCE__.initialisationData.__juce__platform.length > 0
|
|
? window.__JUCE__.initialisationData.__juce__platform[0]
|
|
: "";
|
|
|
|
if (platform == "windows" || platform == "android")
|
|
return "https://juce.backend/" + path;
|
|
|
|
if (platform == "macos" || platform == "ios" || platform == "linux")
|
|
return "juce://juce.backend/" + path;
|
|
|
|
console.warn(
|
|
"getBackendResourceAddress() called, but no JUCE native backend is detected."
|
|
);
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* This helper class is intended to aid the implementation of
|
|
* AudioProcessorEditor::getControlParameterIndex() for editors using a WebView interface.
|
|
*
|
|
* Create an instance of this class and call its handleMouseMove() method in each mousemove event.
|
|
*
|
|
* This class can be used to continuously report the controlParameterIndexAnnotation attribute's
|
|
* value related to the DOM element that is currently under the mouse pointer.
|
|
*
|
|
* This value is defined at all times as follows
|
|
* * the annotation attribute's value for the DOM element directly under the mouse, if it has it,
|
|
* * the annotation attribute's value for the first parent element, that has it,
|
|
* * -1 otherwise.
|
|
*
|
|
* Whenever there is a change in this value, an event is emitted to the frontend with the new value.
|
|
* You can use a ControlParameterIndexReceiver object on the backend to listen to these events.
|
|
*
|
|
* @param {String} controlParameterIndexAnnotation
|
|
*/
|
|
class ControlParameterIndexUpdater {
|
|
constructor(controlParameterIndexAnnotation) {
|
|
this.controlParameterIndexAnnotation = controlParameterIndexAnnotation;
|
|
this.lastElement = null;
|
|
this.lastControlParameterIndex = null;
|
|
}
|
|
|
|
handleMouseMove(event) {
|
|
const currentElement = document.elementFromPoint(
|
|
event.clientX,
|
|
event.clientY
|
|
);
|
|
|
|
if (currentElement === this.lastElement) return;
|
|
this.lastElement = currentElement;
|
|
|
|
let controlParameterIndex = -1;
|
|
|
|
if (currentElement !== null)
|
|
controlParameterIndex = this.#getControlParameterIndex(currentElement);
|
|
|
|
if (controlParameterIndex === this.lastControlParameterIndex) return;
|
|
this.lastControlParameterIndex = controlParameterIndex;
|
|
|
|
window.__JUCE__.backend.emitEvent(
|
|
"__juce__controlParameterIndexChanged",
|
|
controlParameterIndex
|
|
);
|
|
}
|
|
|
|
//==============================================================================
|
|
#getControlParameterIndex(element) {
|
|
const isValidNonRootElement = (e) => {
|
|
return e !== null && e !== document.documentElement;
|
|
};
|
|
|
|
while (isValidNonRootElement(element)) {
|
|
if (element.hasAttribute(this.controlParameterIndexAnnotation)) {
|
|
return element.getAttribute(this.controlParameterIndexAnnotation);
|
|
}
|
|
|
|
element = element.parentElement;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
export {
|
|
getNativeFunction,
|
|
getSliderState,
|
|
getToggleState,
|
|
getComboBoxState,
|
|
getBackendResourceAddress,
|
|
ControlParameterIndexUpdater,
|
|
};
|