var twineTopMenuData = [
{name: "File", contents: [
{name: "Save", disableOnPlay: true, shortcut: {name: "Ctrl S", ctrlKey: true, key: "s"}, click: function(twine) {
twine.downloadExportData();
}},
]},
{name: "Edit", contents: [
{shortcut: {name: "Ctrl Z", ctrlKey: true, key: "z"}, click: function(twine) {
twine.undo.apply();
}, condition: function(twine) {
return twine.undo.has();
}, name: function(twine) {
return "Undo " + twine.undo.lastName();
}},
{name: "Copy", shortcut: {name: "Ctrl C", ctrlKey: true, key: "c"}, click: function(twine) {
twine.copySelected();
}, condition: function(twine) {
return twine.timeline.selectedClips.length > 0;
}}
]},
{name: "View", contents: [
{name: "Zoom in", shortcut: {name: ",", key: ","}, click: function(twine){
twine.timeline.zoomIn();
}},
{name: "Zoom out", shortcut: {name: ".", key: "."}, click: function(twine){
twine.timeline.zoomOut();
}},
{name: "Show all", shortcut: {name: "/", key: "/"}, click: function(twine){
twine.timeline.showAll();
}},
{preset: "divider"},
{name: "Mixer", shortcut: {name: "M", key: "m"}, click: function(twine){
twine.showMixer();
}},
{name: "Contract channels", shortcut: {name: "C", key: "c"}, click: function(twine) {
twine.timeline.contractChannels();
}}
]},
{name: "Action", contents: [
{name: "Play/stop", shortcut: {name: "Space", key: "space"}, click: function(twine) {
twine.ui.head.play.element.click();
}},
{preset: "divider"},
{name: "Add channel", shortcut: {name: "A", key: "a"}, click: function(twine) {
twine.timeline.addChannel();
}},
{name: "Add script clip", click: function(twine) {
twine.timeline.addScriptClip();
}},
{name: "Add blank clip", click: function(twine) {
twine.timeline.addBlankClip();
}},
{preset: "divider"},
{name: "Delete clip(s)", shortcut: {name: "Del", key: "delete"}, click: function(twine) {
twine.timeline.selectedClips.forEach(function(clip){
clip.destroy();
});
twine.ui.showPane(twine.ui.pane.NONE);
}, condition: function(twine){
return twine.timeline.selectedClips.length > 0;
}},
{preset: "divider"},
{name: "Render to file", click: function(twine) {
twine.renderToFile();
}},
{name: "Bounce", shortcut: {name: "A", key: "a"}, click: function(twine) {
twine.renderToClip();
}}
]},
{name: "Options", contents: [
{name: "Settings", click: function(twine) {
twine.ui.showSettings();
}}
]},
{name: "Help", contents: [
{name: "Help", click: function(twine){
$("#twist_documentation")[0].click();
}},
{name: "About", click: function(twine) {
twine.ui.showAbout();
}},
]}
];
var TwineUI = function(twine) {
var ui = this;
ui.topMenu = new twirl.TopMenu(twine, twineTopMenuData, $("#twine_menubar"));
ui.showSettings = function() {
var settings = [
{
name: "Show master VU meter",
description: "Show the master VU mixer in the mixer view",
bool: true,
storageKey: "showMasterVu"
},
{
name: "Show clip waveforms",
description: "Show waveforms in clips",
bool: true,
storageKey: "showClipWaveforms"
}
];
twirl.showSettings(twine, settings);
};
this.pane = {NONE: -1, MIXER: 0, CHANNEL: 1, CLIPAUDIO: 2, CLIPSCRIPT: 3};
this.showPane = function(pane) {
var chd = $("#twine_channeldetails");
var cld = $("#twine_clipdetails");
if (pane == ui.pane.MIXER) {
twine.mixer.show();
chd.hide();
cld.hide();
} else if (pane == ui.pane.CHANNEL) {
twine.mixer.hide();
chd.show();
cld.hide();
} else if (pane >= 2) {
twine.mixer.hide();
chd.hide();
cld.show();
var cda = $("#twine_clipdetailsaudio");
var cds = $("#twine_clipdetailsscript");
if (pane == ui.pane.CLIPAUDIO) {
cda.show();
cds.hide();
} else {
cda.hide();
cds.show();
}
} else if (pane == ui.pane.NONE) {
twine.mixer.hide();
chd.hide();
cld.hide();
};
};
ui.showAbout = function() {
var el = $("
");
var x = $("").appendTo(el);
var string = "twine";
var intervals = [];
function addChar(c, left) {
left = Math.min(Math.max(left, 30), 70);
var elC = $("").text(c).css({position: "absolute", left: left + "%"}).appendTo(x);
var rate = (Math.random() * 0.1) + 0.15;
var leftDirection = Boolean(Math.round(Math.random()));
setTimeout(function(){
intervals.push(setInterval(function(){
if (leftDirection) {
if (left < 70) {
left += rate;
} else {
leftDirection = false;
}
} else {
if (left > 30) {
left -= rate;
} else {
leftDirection = true;
}
}
//console.log(left, rate, leftDirection);
elC.css("left", left + "%");
}, (Math.random() * 20) + 20));
}, (Math.random() * 1000) + 500);
}
var widthPercent = (40 / (string.length));
for (let c in string) {
intervals.push(addChar(string[c], (widthPercent * c) + 30));
}
$("
").appendTo(el);
$("
").appendTo(el);
$("").text("Version " + twine.version.toFixed(1)).appendTo(el);
$("").css("font-size", "12px").text("By Richard Knight 2024").appendTo(el);
twirl.prompt.show(el, function(){
for (let i of intervals) clearInterval(i);
});
};
function refreshWarpParams() {
var warp = ui.clip.warp.element.val();
var warpMode = ui.clip.warpMode.element.val();
ui.clip.warpMode.hide();
ui.clip.fftSize.hide();
ui.clip.phaseLock.hide();
ui.clip.txtWinSize.hide();
ui.clip.txtRandom.hide();
ui.clip.txtOverlap.hide();
ui.clip.txtWinType.hide();
if (!warp) return;
ui.clip.warpMode.show();
if (warpMode == 1) {
ui.clip.txtWinSize.show();
ui.clip.txtRandom.show();
ui.clip.txtOverlap.show();
ui.clip.txtWinType.show();
} else if (warpMode > 1) {
ui.clip.fftSize.show();
ui.clip.phaseLock.show();
}
}
function applyValToSelectedFunc(dataKey, applyFunc, undoName, noApplyUndo) {
if (!undoName) undoName = dataKey;
var func = function(val) {
if (!noApplyUndo) {
var selected = [...twine.timeline.selectedClips];
var values = [];
}
twine.timeline.selectedClips.forEach(async function(clip){
if (!noApplyUndo) {
values.push(clip.data[dataKey]);
}
applyFunc(clip, val);
});
if (!noApplyUndo) {
twine.undo.add("clip " + undoName, function(){
for (var i in selected) {
applyFunc(selected[i], values[i]);
if (twine.timeline.selectedClip == selected[i]) {
ui.clip[dataKey].setValue(values[i]);
}
}
});
}
};
return func;
}
ui.clip = {
scriptEdit: new twirl.stdui.TextArea({
target: "twine_clip_scriptedit",
height: "100%",
width: "100%"
}),
scriptAudition: new twirl.stdui.PlayButton({
target: "twine_clip_scriptaudition",
change: function(v, obj) {
if (obj.state == true) {
} else {
app.insertScore("twine_scriptstop");
}
}
}),
scriptApply: new twirl.stdui.StandardButton({
target: "twine_clip_scriptapply",
label: "Apply script",
change: function() {
twine.timeline.selectedClip.setScript(
ui.clip.scriptEdit.element.val(),
function() {
twirl.prompt.show("Script successfully compiled");
}
);
}
}),
audition: new twirl.stdui.PlayButton({
target: "twine_clip_audition",
tooltip: "Audition clip",
change: function(v, obj) {
if (obj.state == true) {
twine.timeline.selectedClip.play(function(ndata) {
if (ndata.status == 0) {
obj.setValue(false);
}
});
} else {
twine.timeline.selectedClip.stop();
}
}
}),
name: new twirl.stdui.TextInput({
target: "twine_clip_name",
change: applyValToSelectedFunc("name", function(clip, val){
clip.setData("name", val);
}),
css: {
border: "none"
}
}),
colour: new twirl.stdui.ColourInput({
target: "twine_clip_colour",
change: applyValToSelectedFunc("colour", function(clip, val){
clip.colour = val;
}),
css: {
border: "none"
}
}),
editTwist: new twirl.stdui.StandardButton({
label: "Twist",
target: "twine_clip_edittwist",
change: function(e) {
twirl.contextMenu.show(e, [
{name: "Edit all references", click: function(){
twine.timeline.selectedClip.editInTwist();
}},
{name: "Edit as unique", click: function(){
twine.timeline.selectedClip.editInTwist(true);
}}
]);
}
}),
editTwigs: new twirl.stdui.StandardButton({
label: "Twigs",
target: "twine_clip_edittwigs",
change: function(e) {
twirl.contextMenu.show(e, [
{name: "Edit all references", click: function(){
twine.timeline.selectedClip.editInTwigs();
}},
{name: "Edit as unique", click: function(){
twine.timeline.selectedClip.editInTwigs(true);
}}
]);
}
}),
warp: new twirl.stdui.StandardToggle({
label: "Warp",
target: "twine_clip_warp",
change: applyValToSelectedFunc("warp", function(clip, val){
clip.setWarp(val);
}),
stateAlter: function(val) {
refreshWarpParams();
}
}),/*
loop: new twirl.stdui.StandardToggle({
label: "Loop",
target: "twine_clip_loop",
change: function(val) {
twine.timeline.selectedClip.setLoop(val);
}
}),*/
warpMode: new twirl.stdui.ComboBox({
target: "twine_clip_warpmode",
options: [
"Repitch", "Grain", "Mince" //, "FFTab"
],
change: applyValToSelectedFunc("warpMode", function(clip, val){
clip.setWarpMode(val);
}, "warp mode"),
stateAlter: function(val) {
refreshWarpParams();
}
}),
amp: new twirl.stdui.Slider({
label: "Gain",
valueLabel: true,
value: 1,
min: 0,
max: 2,
size: 32,
target: "twine_clipparamsbottom",
input: applyValToSelectedFunc("amp", function(clip, val){
clip.setData("amp", val);
}, "gain", true),
change: applyValToSelectedFunc("amp", function(clip, val){
clip.setData("amp", val);
}, "gain")
}),
pitch: new twirl.stdui.Slider({
label: "Pitch",
valueLabel: true,
min: -12,
max: 12,
step: 1,
value: 0,
size: 32,
target: "twine_clipparamsbottom",
input: applyValToSelectedFunc("pitch", function(clip, val){
clip.setPitch(val);
}, null, true),
change: applyValToSelectedFunc("pitch", function(clip, val){
clip.setPitch(val);
})
}),
fftSize: new twirl.stdui.ComboBox({
label: "FFT Size",
asRow: true,
target: "twine_clipparamsbottom",
options: [
"256", "512", "1024", "2048"
],
asValue: true,
change: applyValToSelectedFunc("fftSize", function(clip, val){
clip.setData("fftSize", val);
}, "FFT size")
}),
phaseLock: new twirl.stdui.StandardToggle({
label: "Phase lock",
target: "twine_clipparamsbottom",
change: applyValToSelectedFunc("phaseLock", function(clip, val){
clip.setData("phaseLock", val);
}, "phase lock"),
stateAlter: function(val) {
refreshWarpParams();
}
}),
txtWinSize: new twirl.stdui.Slider({
label: "Window size",
valueLabel: true,
min: 44,
max: 4410,
step: 1,
value: 4410,
size: 32,
target: "twine_clipparamsbottom",
change: applyValToSelectedFunc("txtWinSize", function(clip, val){
clip.setData("txtWinSize", val);
}, "window size")
}),
txtRandom: new twirl.stdui.Slider({
label: "Window random",
valueLabel: true,
min: 0,
max: 441,
step: 1,
value: 441,
size: 32,
target: "twine_clipparamsbottom",
change: applyValToSelectedFunc("txtRandom", function(clip, val){
clip.setData("txtRandom", val);
}, "window random")
}),
txtOverlap: new twirl.stdui.Slider({
label: "Window overlap",
valueLabel: true,
min: 0,
max: 16,
step: 1,
value: 4,
size: 32,
target: "twine_clipparamsbottom",
change: applyValToSelectedFunc("txtOverlap", function(clip, val){
clip.setData("txtOverlap", val);
}, "window overlap")
}),
txtWinType: new twirl.stdui.ComboBox({
label: "Window type",
asRow: true,
target: "twine_clipparamsbottom",
options: [
"Hanning", "Hamming", "Half sine"
],
change: applyValToSelectedFunc("txtWinType", function(clip, val){
clip.setData("txtWinType", val);
}, "window type")
})
};
ui.head = {
play: new twirl.stdui.PlayButton({
target: "twine_head_play",
fontsize: "14pt",
change: function(v, obj) {
if (obj.state == true) {
twine.play();
} else {
twine.stop();
obj.setValue(false);
}
}
}),
snap: new twirl.stdui.StandardToggle({
label: "Snap",
target: "twine_head_snap",
change: function(val) {
val = (val) ? 4 : 0;
twine.timeline.data.snapToGrid = val;
}
}),
grid: new twirl.stdui.StandardToggle({
label: "Grid",
target: "twine_head_showgrid",
change: function(val) {
twine.timeline.data.gridVisible = val;
twine.timeline.redraw();
}
}),
name: new twirl.stdui.TextInput({
target: "twine_head_name",
css: {
border: "none",
"font-family": "var(--fontFace)",
"font-size": "var(--fontSizeLarge)",
},
change: function(val) {
twine.timeline.arrangementName = val;
}
})
};
};