diff options
author | Richard <q@1bpm.net> | 2025-04-13 18:48:02 +0100 |
---|---|---|
committer | Richard <q@1bpm.net> | 2025-04-13 18:48:02 +0100 |
commit | 9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22 (patch) | |
tree | 291bd79ce340e67affa755a8a6b4f6a83cce93ea /site/app/twist/twist_ui.js | |
download | apps.csound.1bpm.net-9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22.tar.gz apps.csound.1bpm.net-9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22.tar.bz2 apps.csound.1bpm.net-9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22.zip |
initial
Diffstat (limited to 'site/app/twist/twist_ui.js')
-rw-r--r-- | site/app/twist/twist_ui.js | 674 |
1 files changed, 674 insertions, 0 deletions
diff --git a/site/app/twist/twist_ui.js b/site/app/twist/twist_ui.js new file mode 100644 index 0000000..08e5fe1 --- /dev/null +++ b/site/app/twist/twist_ui.js @@ -0,0 +1,674 @@ +var twistTopMenuData = [
+ {name: "File", contents: [
+ {name: "New", disableOnPlay: true, shortcut: {name: "Ctrl N", ctrlKey: true, key: "n"}, click: function(twist) {
+ twist.createNewInstance();
+ }, condition: function(twist) {
+ return (!twist.twine);
+ }},
+ {name: "Save", disableOnPlay: true, shortcut: {name: "Ctrl S", ctrlKey: true, key: "s"}, click: function(twist) {
+ twist.saveFile();
+ }},
+ {name: "Close", disableOnPlay: true, shortcut: {name: "Ctrl W", ctrlKey: true, key: "w"}, click: function(twist) {
+ twist.closeInstance();
+ }, condition: function(twist) {
+ return (!twist.twine && twist.waveforms.length != 1);
+ }},
+ {name: "Edit in twigs", click: function(twist) {
+ twist.editInTwigs();
+ }, condition: function(twist) {
+ return window.hasOwnProperty("Twigs");
+ }}
+ ]},
+ {name: "Edit", contents: [
+ {name: "Undo", disableOnPlay: true, shortcut: {name: "Ctrl Z", ctrlKey: true, key: "z"}, click: function(twist) {
+ twist.undo();
+ }, condition: function(twist) {
+ return (twist.storage.maxundo > 0 && twist.undoLevel > 0);
+ }},
+ {preset: "divider"},
+ {name: "Copy", disableOnPlay: true, shortcut: {name: "Ctrl C", ctrlKey: true, key: "c"}, click: function(twist) {
+ twist.copy();
+ }},
+ {name: "Cut", disableOnPlay: true, shortcut: {name: "Ctrl X", ctrlKey: true, key: "x"}, click: function(twist) {
+ twist.cut();
+ }},
+ {name: "Paste", disableOnPlay: true, shortcut: {name: "Ctrl V", ctrlKey: true, key: "v"}, click: function(twist) {
+ twist.paste();
+ }, condition: function(twist) {
+ return twist.hasClipboard;
+ }},
+ {name: "Paste special", disableOnPlay: true, shortcut: {name: "Ctrl shift V", ctrlKey: true, shiftKey: true, key: "v"}, click: function() {
+ twist.pasteSpecial();
+ }, condition: function(twist) {
+ return twist.hasClipboard;
+ }},
+ {name: "Trim", disableOnPlay: true, shortcut: {name: "T", key: "t"}, click: function() {
+ twist.trim();
+ }},
+ {name: "Delete", disableOnPlay: true, shortcut: {name: "Del", key: "delete"}, keyCondition: function(twist) {
+ return !twist.ui.deleteSupressed;
+ }, click: function(twist) {
+ twist.delete();
+ }},
+ {preset: "divider"},
+ {name: "Select all", shortcut: {name: "Ctrl A", ctrlKey: true, key: "a"}, click: function(twist) {
+ twist.selectAll();
+ }},
+ {name: "Select to end", shortcut: {name: "W", key: "w"}, click: function(twist) {
+ twist.selectToEnd();
+ }},
+ {name: "Select from start", shortcut: {name: "Q", key: "q"}, click: function(twist) {
+ twist.selectFromStart();
+ }},
+ {name: "Select none", shortcut: {name: "Ctrl M", ctrlKey: true, key: "m"}, click: function(twist) {
+ twist.selectNone();
+ }},
+ {name: "Move to next transient", shortcut: {name: "[",key: "["}, click: function(twist) {
+ twist.moveToNextTransient();
+ }},
+ {name: "Select to next transient", shortcut: {name: "]",key: "]"}, click: function(twist) {
+ twist.selectToNextTransient();
+ }}
+ ]},
+ {name: "View", contents: [
+ {name: "Zoom selection", shortcut: {name: "Z", key: "z"}, click: function(twist) {
+ twist.waveform.zoomSelection();
+ }},
+ {name: "Zoom in", shortcut: {name: "+", key: "+"}, click: function(twist) {
+ twist.waveform.zoomIn();
+ }},
+ {name: "Zoom out", shortcut: {name: "-", key: "-"}, click: function(twist) {
+ twist.waveform.zoomOut();
+ }},
+ {name: "Show all", shortcut: {name: "0", key: "0"}, click: function(twist) {
+ twist.waveform.setRegion(0, 1);
+ }},
+ {preset: "divider"},
+ {name: "Toggle analysis", click: function(twist){
+ twist.ui.toggleScope();
+ }},
+ {name: "Toggle layout", shortcut: {name: "L", key: "l"}, click: function(twist){
+ twist.ui.toggleLayout();
+ }},
+ ]},
+ {name: "Action", contents: [
+ {name: "Play/stop", shortcut: {name: "Space", key: " "}, click: function(twist) {
+ if (twist.isPlaying()) {
+ twist.stop();
+ } else {
+ twist.play();
+ }
+ }},
+ {name: "Audition", disableOnPlay: true, shortcut: {name: "Enter", key: "enter"}, click: function(twist) {
+ twist.audition();
+ }},
+ {name: "Commit", disableOnPlay: true, shortcut: {name: "Alt enter", altKey: true, key: "enter"}, click: function(twist) {
+ twist.commit();
+ }},
+ {name: "Record", disableOnPlay: true, shortcut: {name: "R", key: "r"}, click: function(twist) {
+ twist.record();
+ }},
+ {preset: "divider"},
+ {name: "Scripting", shortcut: {name: "Ctrl K", ctrlKey: true, key: "k"}, click: function(twist) {
+ twist.ui.scriptEdit();
+ }},
+ {name: "Developer", shortcut: {name: "Ctrl L", ctrlKey: true, key: "l"}, click: function(twist) {
+ twist.ui.developerConsole();
+ }},
+
+ ]},
+ {name: "Transform", contents: [
+ {name: "Randomise", shortcut: {name: "Z", key: "z"}, click: function(twist) {
+ twist.currentTransform.randomise();
+ }, condition: function(twist) {
+ return (twist.currentTransform) ? true : false;
+ }},
+ {name: "Reset", shortcut: {name: "R", key: "r"}, click: function(twist) {
+ twist.currentTransform.reset();
+ }, condition: function(twist) {
+ return (twist.currentTransform) ? true : false;
+ }},
+ {name: "Hide automation", shortcut: {name: "H", key: "h"}, click: function(twist) {
+ twist.currentTransform.hideAllAutomation();
+ }, condition: function(twist) {
+ return (twist.currentTransform) ? true : false;
+ }}
+ ]},
+ {name: "Options", contents: [
+ {name: "Settings", click: function(twist) {
+ twist.ui.showSettings();
+ }}
+ ]},
+ {name: "Help", contents: [
+ {name: "Help", click: function(twist){
+ $("#twist_documentation")[0].click();
+ }},
+ {name: "Developer reference", click: function(twist){
+ $("#twist_developer_documentation")[0].click();
+ }},
+ {name: "Report bug", click: function(twist){
+ $("#twist_reportbug")[0].click();
+ }},
+ {name: "Contact owner", click: function(twist){
+ $("#twist_contact")[0].click();
+ }},
+ {name: "Submit transform code", click: function(twist){
+ $("#twist_developer_submit")[0].click();
+ }},
+ {name: "About", click: function(twist) {
+ twist.ui.showAbout();
+ }},
+ ]},
+];
+
+
+var TwistUI = function(twist) {
+ var self = this;
+ var scope;
+ var elCrossfades = [];
+ var topMenu = new twirl.TopMenu(twist, twistTopMenuData, $("#twist_menubar"));
+ this.deleteSupressed = false;
+
+ this.setPlaying = function(state) {
+ if (scope) {
+ scope.setPlaying(state);
+ }
+ if (state) {
+ $(".twist_scriptbutton").hide();
+ $("#twist_scriptstop").show();
+ } else {
+ $(".twist_scriptbutton").show();
+ $("#twist_scriptstop").hide();
+ }
+ };
+
+ this.getCrossFadeValues = function() {
+ return [elCrossfades[0].val(), elCrossfades[1].val()];
+ };
+
+
+ var contractedWaveform = false;
+ function setLayout() {
+ var elViews = $("#twist_views");
+ var elWave = $("#twist_waveforms");
+ var elSpline = $("#twist_splines");
+ var elScope = $("#twist_analyser");
+ var elControls = $("#twist_controls");
+
+ if (contractedWaveform) {
+ elViews.css({height: "20%"});
+ elControls.css({top: "20%"});
+ } else {
+ elViews.css({height: "50%"});
+ elControls.css({top: "50%"});
+ }
+
+ if (scope) {
+ elScope.css({height: "40%", top: "0px"});
+ elWave.css({top: "40%"});
+ elSpline.css({top: elWave.css("top")});
+ } else {
+ elWave.css({top: "0px"});
+ elSpline.css({top: "0px"});
+ }
+
+ twist.redraw();
+ }
+
+ this.toggleLayout = function() {
+ contractedWaveform = !contractedWaveform;
+ setLayout();
+ };
+
+ this.toggleScope = function(noSaveState) {
+ var state;
+ if (!scope) {
+ state = true;
+ var elScope = $("<div />").addClass("twist_scope").appendTo($("#twist_analyser"));
+ var type = (twist.storage.scopeType) ? twist.storage.scopeType : 0;
+ scope = new Analyser(
+ type, twist, elScope, app
+ );
+ $("#twist_analyser").show();
+ } else {
+ $("#twist_analyser").hide();
+ state = false;
+ scope.remove();
+ delete scope;
+ scope = null;
+ }
+
+ if (!noSaveState) {
+ twist.storage.showScope = state;
+ twist.saveStorage();
+ }
+ setLayout();
+ };
+
+
+ this.tooltip = twirl.tooltip;
+
+ this.boot = function() {
+ if (twist.storage.hasOwnProperty("showShortcuts")) {
+ if (twist.storage.showShortcuts) {
+ $("#twist_wavecontrols_inner").show();
+ } else {
+ $("#twist_wavecontrols_inner").hide();
+ }
+ }
+
+ if (twist.storage.develop) {
+ if (twist.storage.develop.csound) {
+ $("#twist_devcsound").val(twist.storage.develop.csound);
+ }
+ if (twist.storage.develop.json) {
+ $("#twist_devjson").val(twist.storage.develop.json);
+ }
+ }
+ $("#loading_background").css("opacity", 1).animate({opacity: 0.2}, 1000);
+ };
+
+ this.postBoot = function() {
+ self.setLoadingStatus(false);
+
+ if (!twist.storage.hasOwnProperty("firstLoadDone")) {
+ twist.storage.firstLoadDone = true;
+ twist.saveStorage();
+ self.showPrompt($("#twist_welcome").detach().show(), twist.createNewInstance);
+ } else {
+ twist.createNewInstance();
+ }
+
+ if (twist.storage.showScope) {
+ self.toggleScope(true);
+ }
+ };
+
+ this.hidePrompt = function() {
+ twirl.prompt.hide();
+ };
+
+ this.showPrompt = function(text, oncomplete, noButton) {
+ twirl.prompt.show(text, oncomplete, noButton);
+ if (twist.playheadInterval) {
+ twist.waveform.movePlayhead(0);
+ clearInterval(twist.playheadInterval);
+ }
+ if (self.waveform) {
+ self.waveform.cover(false);
+ }
+ };
+
+
+ this.showLoadNewPrompt = function() {
+ var elNewFile = $("<div />").css({"font-size": "var(--fontSizeDefault)"});
+ if (twist.hasClipboard) {
+ $("<button />").text("Create from clipboard").appendTo(elNewFile).click(function(){
+ twist.loadFileFromClipboard();
+ twirl.prompt.hide();
+ });
+ }
+
+ elNewFile.append($("<h3 />").text("Drag an audio file here to load")).append($("<p />").text("or"));
+
+ var elEmpty = $("<div />").appendTo(elNewFile);
+ $("<h4 />").text("Create an empty file").css("cursor", "pointer").appendTo(elEmpty);
+
+ var tpDuration = new twirl.transform.Parameter({
+ definition: {name: "Duration", min: 0.1, max: 60, dfault: 10, automatable: false, fireChanges: false},
+ host: twist
+ });
+
+ var tpChannels = new twirl.transform.Parameter({
+ definition: {name: "Channels", min: 1, max: 2, dfault: 2, step: 1, automatable: false, fireChanges: false},
+ host: twist
+ });
+
+ var tpName = new twirl.transform.Parameter({
+ definition: {name: "Name", type: "string", dfault: "New file", fireChanges: false},
+ host: twist
+ });
+
+ var tb = $("<tbody />");
+ $("<table />").append(tb).css("margin", "0 auto").appendTo(elEmpty);
+ tb.append(tpDuration.getElementRow(true)).append(tpChannels.getElementRow(true)).append(tpName.getElementRow(true));
+
+ $("<button />").text("Create").appendTo(elEmpty).click(function() {
+ twist.createEmpty(tpName.getValue(), tpDuration.getValue(), tpChannels.getValue());
+ });
+
+
+ self.showPrompt(elNewFile, null, true);
+ }
+
+
+ this.setTheme = function(name, nosave) {
+ twirl.setTheme(name, nosave);
+ };
+
+ this.showSettings = function() {
+ var settings = [
+ {
+ name: "Commit history limit",
+ description: "Number of transform states to store (can be accessed via the script editor). 0 = infinite",
+ min: 0, max: 32, step: 1, dfault: 16, storageKey: "commitHistoryLevel"
+ },
+ {
+ name: "Undo levels",
+ description: "Number of undo levels stored. Large numbers may affect memory usage",
+ min: 0, max: 32, step: 1, dfault: 2, storageKey: "maxundo",
+ onChange: function(val) {
+ app.setControlChannel("twst_maxundo", val);
+ }
+ },
+ {
+ name: "Analysis type",
+ description: "Type of analysis to be shown",
+ options: ["Frequency", "Oscilloscope"],
+ dfault: 0,
+ storageKey: "scopeType",
+ onChange: function(val) {
+ if (scope) scope.setType(val);
+ }
+ },
+ {
+ name: "Show shortcuts",
+ description: "Show shortcuts toolbar below waveform",
+ bool: true,
+ storageKey: "showShortcuts",
+ dfault: 1,
+ onChange: function(val) {
+ if (val) {
+ $("#twist_wavecontrols_inner").show();
+ } else {
+ $("#twist_wavecontrols_inner").hide();
+ }
+ }
+ },
+ {
+ name: "DC block processing",
+ description: "Apply DC blocking to all processing",
+ bool: true,
+ storageKey: "dcblockoutputs",
+ dfault: 0,
+ onChange: function(val) {
+ app.setControlChannel("twst_dcblockoutputs", val);
+ }
+ },
+ {
+ name: "Tanh limit all processing",
+ description: "Apply tanh to all processing",
+ bool: true,
+ storageKey: "tanhoutputs",
+ dfault: 0,
+ onChange: function(val) {
+ app.setControlChannel("twst_tanhtanhoutputs", val);
+ }
+ },
+ {
+ name: "Autosave before each commit",
+ description: "Automatically save file locally before each new commit",
+ bool: true,
+ storageKey: "autosave",
+ dfault: 0
+ }
+ ];
+ twirl.showSettings(twist, settings);
+ };
+
+
+ this.setPercent = function(percent) {
+ twist.watchdog.setActive(percent);
+ twirl.loading.setPercent(percent);
+ };
+
+ this.setLoadingStatus = function(state, showpercent, text) {
+ if (state) {
+ twirl.loading.show(text, showpercent);
+ } else {
+ twirl.loading.hide();
+ }
+ };
+
+ this.scriptEdit = function() {
+ var el = $("#twist_script").show();
+ var te = $("#twist_scriptsource");
+
+
+ function runScript(audition) {
+ try {
+ var script = JSON.parse(te.val());
+ } catch (e) {
+ twist.errorHandler("Cannot parse script: " + e);
+ return false;
+ }
+ twist.applyScript(script, audition);
+ return true;
+ };
+
+
+ $("#twist_scriptaudition").unbind().click(function(){
+ runScript(true);
+ });
+
+ $("#twist_scriptstop").unbind().click(function(){
+ twist.stop();
+ });
+
+ $("#twist_scriptcommit").unbind().click(function(){
+ if (runScript(false)) {
+ el.hide();
+ }
+ });
+
+ $("#twist_scriptloadlast").unbind().click(function(){
+ te.val(JSON.stringify(twist.lastOperation(), null, 2));
+ });
+
+ $("#twist_scriptloadall").unbind().click(function(){
+ te.val(JSON.stringify(twist.operationLog, null, 2));
+ });
+
+ $("#twist_scriptcancel").unbind().click(function(){
+ el.hide();
+ });
+ };
+
+ this.developerConsole = function() {
+ $("#twist_developer").show();
+ $("#twist_inject_devcsound").click(async function() {
+ var code = $("#twist_devcsound").val();
+ var result = await app.compileOrc(code);
+ if (result == 0) {
+ if (!twist.storage.develop) {
+ twist.storage.develop = {};
+ }
+ twist.storage.develop.csound = code;
+ twist.saveStorage();
+ self.showPrompt("Successfully injected Csound code");
+ }
+ });
+ $("#twist_inject_devjson").click(async function() {
+ var code = $("#twist_devjson").val();
+ try {
+ var json = JSON.parse(code);
+ } catch (e) {
+ return twist.errorHandler("Cannot parse JSON: " + e);
+ }
+ try {
+ twist.loadTransforms(json);
+ } catch (e) {
+ return twist.errorHandler("Cannot load transform: " + e);
+ }
+ if (!twist.storage.develop) {
+ twist.storage.develop = {};
+ }
+ twist.storage.develop.json = code;
+ twist.saveStorage();
+ self.showPrompt("Successfully injected transform definition");
+ });
+ $("#twist_exit_devcode").click(async function() {
+ $("#twist_developer").hide();
+ });
+ };
+
+ function buildWavecontrols() {
+ var el = $("#twist_wavecontrols_inner");
+ var onPlayDisables = [];
+
+ var play = twirl.createIcon({label: "Play", icon: "play", label2: "Stop", icon2: "stop", click: function(obj){
+ if (twist.isPlaying()) {
+ twist.stop();
+ } else {
+ twist.play();
+ }
+ }});
+ var audition = twirl.createIcon({label: "Audition", icon: "audition", label2: "Stop", icon2: "stop", click: function(obj){
+ if (twist.isPlaying()) {
+ twist.stop();
+ } else {
+ twist.audition();
+ }
+ }});
+
+
+ var record = twirl.createIcon({label: "Record", icon: "record", label2: "Stop", icon2: "stop", click: function() {
+ if (twist.isPlaying()) {
+ twist.stop();
+ } else {
+ twist.record();
+ }
+ }});
+
+ var items = [
+ {label: "Rewind", icon: "rewind", disableOnPlay: true, click: function() { twist.moveToStart() }},
+ play,
+ audition,
+ {label: "Commit", icon: "commit", disableOnPlay: true, click: function() { twist.commit() }},
+ record,
+ {preset: "spacer"},
+ {label: "Cut", icon: "cut", disableOnPlay: true, click: function() { twist.cut() }},
+ {label: "Copy", icon: "copy", disableOnPlay: true, click: function() { twist.copy() }},
+ {label: "Paste", icon: "paste", disableOnPlay: true, click: function() { twist.paste() }},
+ {label: "Paste special", icon: "pasteSpecial", disableOnPlay: true, click: function() { twist.pasteSpecial() }},
+ {label: "Trim", icon: "trim", disableOnPlay: true, click: function() { twist.trim() }},
+ {preset: "spacer"}
+ ];
+
+ for (let i of items) {
+ var icon;
+ var td = $("<td />");
+ if (i.preset && i.preset == "spacer") {
+ td.css("width", "20px");
+ } else {
+ if (i.icon) {
+ icon = twirl.createIcon(i);
+ if (i.disableOnPlay) {
+ onPlayDisables.push(icon);
+ }
+ } else {
+ icon = i;
+ }
+ td.append(icon.el);
+ }
+ td.appendTo(el);
+ }
+
+ twist.onPlays.push(async function(playing, auditioning, recording) {
+ if (playing) {
+ if (auditioning) {
+ play.setActive(false);
+ audition.setState(false);
+ record.setActive(false);
+ } else if (recording) {
+ audition.setActive(false);
+ play.setActive(false);
+ record.setState(false);
+ } else {
+ audition.setActive(false);
+ play.setState(false);
+ record.setActive(false);
+ }
+ } else {
+ audition.setActive(true);
+ play.setActive(true);
+ play.setState(true);
+ audition.setState(true);
+ record.setActive(true);
+ record.setState(true);
+ }
+ for (let o of onPlayDisables) {
+ o.setActive(!playing);
+ }
+ });
+
+ for (let e of ["In", "Out"]) {
+ let elRange = $("<input />").addClass("twirl_slider").attr("type", "range").attr("min", 0).attr("max", 0.45).attr("step", 0.00001).val(0).on("input", function() {
+ if (e == "In") {
+ twist.waveform.crossFadeInRatio = $(this).val();
+ } else {
+ twist.waveform.crossFadeOutRatio = $(this).val();
+ }
+ });
+ elCrossfades.push(elRange);
+ $("<td />").addClass("crossfade").append($("<div />").css("font-size", "var(--fontSizeSmall)").text("Crossfade " + e)).append(elRange).appendTo(el);
+ }
+
+ $("<td />").css("font-size", "var(--fontSizeSmall").append("Loop playback<br />").append(
+ $("<input />").addClass("tp_checkbox").attr("type", "checkbox").change(function(){
+ twist.playbackLoop = $(this).is(":checked");
+ })
+ ).appendTo(el);
+
+ };
+
+ function formatVersion(ver) {
+ ver = ver.toString();
+ var major = ver.substr(0, 1);
+ var remainder = ver.substr(1);
+ if (remainder.length == 2) {
+ return major + "." + remainder;
+ } else {
+ var mid = remainder.substr(1, 2);
+ var minor = remainder.substr(2);
+ return major + "." + mid + "." + minor;
+ }
+ }
+
+ this.showAbout = async function() {
+ var csVer = await app.getCsound().getVersion();
+ var apiVer = await app.getCsound().getAPIVersion();
+ var el = $("<div />");
+ var x = $("<h3 />").text("twist").appendTo(el);
+ $("<p />").css("font-size", "12px").text("By Richard Knight 2024").appendTo(el);
+ $("<p />").text("Version " + twist.version.toFixed(1)).appendTo(el);
+ $("<p />").text("Csound " + formatVersion(csVer) + "; API " + formatVersion(apiVer)).appendTo(el);
+
+ var skewMax = 30;
+ var skew = 0;
+ var skewDirection = true;
+ var twistInterval = setInterval(function(){
+ if (skewDirection) {
+ if (skew < skewMax) {
+ skew ++;
+ } else {
+ skewDirection = false;
+ }
+ } else {
+ if (skew > -skewMax) {
+ skew --;
+ } else {
+ skewDirection = true;
+ }
+ }
+ x.css("transform", "skewX(" + skew + "deg)");
+ }, 10);
+
+ self.showPrompt(el, function(){
+ clearInterval(twistInterval);
+ });
+ };
+
+
+ buildWavecontrols();
+};
\ No newline at end of file |