From 9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22 Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 13 Apr 2025 18:48:02 +0100 Subject: initial --- site/app/twine/clip.js | 769 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 769 insertions(+) create mode 100644 site/app/twine/clip.js (limited to 'site/app/twine/clip.js') diff --git a/site/app/twine/clip.js b/site/app/twine/clip.js new file mode 100644 index 0000000..4705d95 --- /dev/null +++ b/site/app/twine/clip.js @@ -0,0 +1,769 @@ +var Clip = function(twine, data, parent) { + var clip = this; + var loaded = false; + var waveformClip; + var waveformEdit; + var datatable; + this.channel = null; + var minWidth = 10; + this.types = {AUDIO: 0, SCRIPT: 1}; + + + if (!data) { + var id = twine.getNewID(); + var data = { + name: "Clip " + id, + type: (parent) ? parent.type : null, + id: id, + clipindex: null, + playLength: 1, + pitch: 1, + colour: "#" + (Math.random() * 0xFFFFFF << 0).toString(16), + position: 0, + // debugs: + duration: 1, + warp: 0, + loop: 0, + script: "" + }; + } else { + data.id = twine.getNewID(); + loaded = true; + } + + this.data = data; + Object.defineProperty(this, "colour", { + get: function() { return data.colour; }, + set: function(x) { + data.colour = x; + clip.element.css("background-color", data.colour); + } + }); + + Object.defineProperty(this, "isAudio", { + get: function() { return (clip.data.type == clip.types.AUDIO); }, + set: function(x) {} + }); + + twine.undo.add("add clip", function(){ + clip.destroy(); + twine.timeline.redraw(); + }); + + this.exportData = async function() { + // tablecopyout messes first few values. so loop... + var len = await app.getCsound().tableLength(datatable); + var items = []; + for (var i = 0; i < len; i ++) { + items.push(await app.getCsound().tableGet(datatable, i)); + } + var local = {}; + for (var d in dataMode) { + if (dataMode[d] < 0) { + local[d] = data[d]; + } + } + return {table: items, local: local}; + }; + + this.importData = async function(loadData) { + + if (datatable && clip.data.clipindex) { + await app.insertScoreAsync("twine_removeclip", [clip.data.clipindex]); + } + datatable = await twine.timeline.copyNewTableIn(loadData.table); + var ndata = await app.insertScoreAsync("twine_importclip", [datatable]); + for (var d in loadData.local) { + data[d] = loadData.local[d]; + } + data.clipindex = ndata.data.clipindex; + await getDataFromTable(); + console.log("import", loadData, data); + loaded = true; + clip.redraw(); // race here? + }; + + this.destroy = function(onComplete) { + function done() { + clip.element.remove(); + clip.channel.removeClip(clip); + if (onComplete) { + onComplete(); + } + } + if (clip.isAudio) { + app.insertScore("twine_removeclip", [0, 1, app.createCallback(done), clip.data.clipindex]); + } else { + done(); + } + }; + + var dataMode = { + fnL: 0, fnR: 1, divisionsPerBeat: 2, duration: 3, beatsLength: 4, utilisedLength: 5, + warpMode: 6, pitch: 7, amp: 8, fftSize: 9, txtWinSize: 10, txtRandom: 11, txtOverlap: 12, + loop: 13, warp: 14, txtWinType: 15, utilisedStart: 16, phaseLock: 17, sr: 18, + // warp points are after this+ in table + position: -1, name: -2, clipindex: -3, playLength: -4, type: -5 + }; + + + this.element = $("
").addClass("twine_clip").css({ + "background-color": data.colour + }).on("contextmenu", function(e){ + return twirl.contextMenu.show(e, [ + {name: "Delete", click: function(){ + clip.destroy(); + }}, + {name: "Audition", click: function(){ + clip.play(); + }} + ]); + }).click(function(e){ + if (e.ctrlKey) { + twine.timeline.selectedClips.push(clip); + } else { + $(".twine_clip").css("outline", "none"); + twine.timeline.selectedClips = [clip]; + } + var uiType; + var cui = twine.ui.clip; + clip.markSelected(); + if (clip.isAudio) { + uiType = twine.ui.pane.CLIPAUDIO; + var items = [ + "name", "colour", "amp", "warp", "warpMode", "pitch", + "fftSize", "txtWinSize", "txtRandom", "txtOverlap", + "txtWinType", "phaseLock" + // , "loop" + ]; + for (let i of items) { + cui[i].setValue(data[i]); + } + showEditWaveform($("#twine_clipdetailsrightaudio")); + } else { + uiType = twine.ui.pane.CLIPSCRIPT; + cui.scriptEdit.setValue(data.script); + } + twine.ui.showPane(uiType); + }); + + var elWaveClip = $("
").css({position: "absolute", width: "100%", height: "100%", top: "0px", left: "0px"}).appendTo(clip.element); + var elWaveText = $("
").css({position: "absolute", width: "100%", height: "100%", top: "0px", left: "0px", "font-size": "var(--fontSizeSmall)", color: "var(--fgColor1)"}).text(data.name).appendTo(clip.element); + + var elResizeLeft = $("
").addClass("twine_clip_edge_left").appendTo(clip.element); + var elResizeRight = $("
").addClass("twine_clip_edge_right").appendTo(clip.element); + var elMove = $("
").addClass("twine_clip_centre").appendTo(clip.element); + var elWaveEdit = $("
").css({width: "100%", height: "100%", top: "0px", left: "0px"}); + + async function getDataFromTable() { + if (!clip.isAudio) return; + async function setFromKey(key) { + if (dataMode[key] < 0) return; + var value = await app.getCsound().tableGet(datatable, dataMode[key]) + data[key] = value; + } + + for (var k in dataMode) { + await setFromKey(k); + } + } + + function setClipAudioUnique(onComplete) { + if (!clip.isAudio) return onComplete(); + twirl.loading.show(); + var cbid = app.createCallback(async function(ndata){ + await getDataFromTable(); + twirl.loading.hide(); + if (onComplete) onComplete(); + }); + app.insertScore("twine_setclipaudiounique", [0, 1, cbid]); + } + + function getScriptInstrName() { + return "twinescript" + id; + } + + this.setScript = function(script, onready) { + var originalScript = clip.data.script; + if (script) { + clip.data.script = script; + } + var instr = "instr " + getScriptInstrName() + "\n" + + "iduration = p5\nichannel = p6\n" + + clip.data.script + "\nendin"; + app.compileOrc(instr).then(function(status){ + if (status >= 0 && onready) { // errors will be caught by app and shown + twine.undo.add("set script", function(){ + clip.data.script = originalScript; + if (twine.timeline.selectedClip == clip) { + cui.scriptEdit.setValue(data.script); + } + }); + onready(); + } + }); + }; + + this.initScript = function() { + data.warp = 1; + data.duration = 1; + clip.data.playLength = 4; + clip.data.type = clip.types.SCRIPT; + clip.data.script = "; your Csound instrument here"; + loaded = true; + }; + + function reloadAfterEdit(tables) { + twirl.loading.show("Loading"); + var cbid = app.createCallback(async function(ndata){ + datatable = ndata.datatable; + await getDataFromTable(); + clip.redraw(); + twine.setVisible(true); + twigs.setVisible(false); + twist.setVisible(false); + twirl.loading.hide(); + }); + var call = [0, 1, cbid, data.clipindex]; + for (let t of tables) { + call.push(t); + } + app.insertScore("twine_clipreplacetables", call); + } + + this.editInTwist = function(asUnique) { + if (!window.twist) return twirl.prompt.show("twist is unavailable in this session"); + function edit() { + twist.boot(twine); + twist.bootAudio(twine); + var tables = [data.fnL]; + if (data.fnR) tables.push(data.fnR); + twist.loadFileFromFtable(data.name, tables, function(ndata){ + if (ndata.status > 0) { + twine.setVisible(false); + twist.setVisible(true); + } + }, reloadAfterEdit); + } + + function checksr() { // twist uses tfi transforms that may require sample sr to be running sr.. + if (twine.sr != data.sr) { + twirl.loading.show(); + var cbid = app.createCallback(async function(ndata){ + await getDataFromTable(); + twirl.loading.hide(); + edit(); + }); + app.insertScore("twine_convertsr", [0, 1, cbid, data.clipindex, twine.sr]); + } else { + edit(); + } + } + if (asUnique) { + setClipAudioUnique(checksr); + } else { + checksr(); + } + + }; + + this.editInTwigs = function(asUnique) { + if (!window.twigs) return twirl.prompt.show("twigs is unavailable in this session"); + function edit() { + twigs.boot(twine); + var tables = [data.fnL]; + if (data.fnR) tables.push(data.fnR); + twigs.loadFileFromFtable(data.name, tables, function(ndata){ + if (ndata.status > 0) { + twine.setVisible(false); + twigs.setVisible(true); + } + }, reloadAfterEdit); + } + if (asUnique) { + setClipAudioUnique(edit); + } else { + edit(); + } + }; + + this.setData = function(modeString, v, onComplete) { + data[modeString] = v; + if (dataMode[modeString] < 0) { + if (modeString == "name") { + elWaveText.text(data.name); + setClipWaveform(); + } + return; + } + + if (!twine.offline || !clip.isAudio) app.getCsound().tableSet(datatable, dataMode[modeString], v); + + if (onComplete) onComplete(); + }; + + var playbackcbid; + this.play = function(onCallback) { + var instr; + var args; + var channel = clip.channel.getCsChannelName(); + var cbid = app.createCallback(function(ndata) { + if (ndata.status == 0) { + app.removeCallback(ndata.cbid); + playbackcbid = null; + } else { + playbackcbid = ndata.cbid; + } + if (onCallback) { + onCallback(ndata); + } + }, true); + if (clip.isAudio) { + instr = "ecp_playaudition"; + args = [0, data.playLength, cbid, data.clipindex, data.playLength, channel]; + } else { + instr = getScriptInstrName(); + args = [0, data.playLength, cbid, data.playLength, channel]; + } + app.insertScore(instr, args); + }; + + this.stop = function(onCallback) { + if (!playbackcbid) return; + app.insertScore("ecp_stopaudition", [0, 1, playbackcbid]); + }; + + async function getSourceTableData() { + if (!clip.isAudio) return; + var wavedata = []; + var tbL = await app.getTable(data.fnL); + wavedata.push(tbL); + if (data.hasOwnProperty("fnR") && data.fnR > 0) { + var tbR = await app.getTable(data.fnR); + wavedata.push(tbR); + } + return wavedata; + } + + async function setClipWaveform(noRedraw) { + if (twine.offline || !clip.isAudio || !twine.storage.showClipWaveforms) return; + if (!waveformClip) { + waveformClip = new Waveform({ + target: elWaveClip, + allowSelect: false, + showGrid: false, + bgColor: "rgb(255, 255, 255, 0)", + fgColor: "#000000" + }); + setTimeout(async function(){ + var sourceTables = await getSourceTableData(); + waveformClip.setData(sourceTables, data.duration); + }, 100); + } else if (!noRedraw) { + console.log("redraw wave"); + waveformClip.redraw(); + } + } + + async function showEditWaveform(target) { + if (twine.offline || !clip.isAudio) return; + target.empty().append(elWaveEdit); + if (!waveformEdit) { + waveformEdit = new Waveform({ + target: elWaveEdit, + allowSelect: true, + showGrid: true, + latencyCorrection: twirl.latencyCorrection // , markers: + }); + setTimeout(async function(){ + var sourceTables = await getSourceTableData(); + waveformEdit.setData(sourceTables, data.duration); + }, 100); + } else { + waveformEdit.redraw(); + } + } + + this.setWarp = function(v) { + clip.setData("warp", v); + if (!data.warp && !data.loop && data.playLength > data.duration) { + data.playLength = data.duration; + clip.setSize(); + } + }; + + this.setLoop = function(v) { + clip.setData("loop", v); + if (!data.warp && !data.loop && data.playLength > data.duration) { + data.playLength = data.duration; + clip.setSize(); + } + }; + + this.setPitch = function(semitones) { + var pitchRatio = Math.pow(2, (semitones / 12)); + clip.setData("pitch", semitones); + if (data.warpMode == 0 && data.loop == 0 && data.warp == 0) { + data.playLength = data.duration / pitchRatio; + clip.setSize(); + } + }; + + this.setWarpMode = function(v) { + var prevMode = data.warpMode; + clip.setData("warpMode", v); + if (prevMode == 0 && data.warpMode != 0 && !data.loop && !data.warp) { + data.playLength = data.duration; + clip.setSize(); + } + }; + + this.setSize = function(noWaveRedraw) { + var width = data.playLength * twine.timeline.pixelsPerBeat; + clip.element.css("width", width + "px"); + setClipWaveform(noWaveRedraw); + } + + this.redraw = function(noWaveRedraw) { + if (!loaded) return; + var b = twine.timeline.beatRegion; + clip.setSize(noWaveRedraw); + var endPos = data.position + data.playLength; + if (endPos < b[0] || data.position > b[1]) { + return clip.element.hide(); + } + + var css = { + height: clip.channel.height + "px", + left: (data.position - b[0]) * twine.timeline.pixelsPerBeat + "px" + }; + elWaveText.text(data.name); + clip.element.show().css(css); + if (endPos > twine.timeline.beatRegion[1] - 8) { + var extension = endPos + 8; + twine.timeline.extend(twine.timeline.beatRegion[1] + extension); + } + }; + + this.clone = async function() { + var newData = Object.assign({}, data); + newData.id = twine.getNewID(); + var c = new Clip(twine, newData, clip); + clip.channel.addClip(c); + if (!twine.offline && clip.isAudio) { + var ndata = await app.insertScoreAsync("twine_cloneclip", [clip.data.clipindex]); + await c.loadFromDataTable(ndata.datatable, ndata.clipindex); + + } else { + loaded = true; + c.setScript(); + c.redraw(); + } + return c; + }; + + + async function loadData(ndata, name, colour, defaultLength) { + twirl.loading.show("Loading"); + if (ndata.status == -1) { + return twirl.errorHandler("File not valid"); + } else if (ndata.status == -2) { + return twirl.errorHandler("File too large"); + } + datatable = ndata.data.datatable; + await getDataFromTable(); + data.clipindex = ndata.data.clipindex; + if (name) { + data.name = name; + } + setTimeout(function(){ + if (defaultLength) { + data.playLength = data.duration / (60 / twine.timeline.data.bpm); + console.log("deflength", data.playLength); + } + if (!colour) colour = twine.randomColour(); + data.colour = colour; + loaded = true; + clip.redraw(); + }, 50); // csound race + }; + + this.loadFromDataTable = async function(newDatatable, clipindex) { + datatable = newDatatable; + await getDataFromTable(); + data.clipindex = clipindex; + clip.data.type = clip.types.AUDIO; + loaded = true; + setTimeout(clip.redraw, 20); + //clip.redraw(); + }; + + this.loadFromFtables = function(name, tables, colour) { + clip.data.type = clip.types.AUDIO; + twirl.loading.show("Loading"); + var cbid = app.createCallback(async function(ndata){ + await loadData(ndata, name, colour); + twirl.loading.hide(); + }); + var call = [0, 1, cbid]; + for (let t of tables) { + call.push(t); + } + app.insertScore("twine_loadftables", call); + }; + + this.loadFromPath = function(path, colour) { + clip.data.type = clip.types.AUDIO; + if (twine.offline) { + loaded = true; + clip.redraw(); + return; + } + twirl.loading.show("Loading"); + var cbid = app.createCallback(async function(ndata){ + await loadData(ndata, path, colour, true); + twirl.loading.hide(); + }); + app.insertScore("twine_loadpath", [0, 1, cbid, path]); + }; + + this.createSilence = function(stereo, duration, name, colour) { + clip.data.type = clip.types.AUDIO; + twirl.loading.show("Creating"); + var cbid = app.createCallback(async function(ndata){ + await loadData(ndata, name, colour); + twirl.loading.hide(); + }); + app.insertScore("twine_createblankclip", [0, 1, cbid, stereo, duration]); + }; + + function getMaxClipWidth() { + var maxWidth = 9999; + if (!data.warp && !data.loop) { + maxWidth = data.duration * twine.timeline.pixelsPerBeat; + } + return maxWidth; + } + + this.markSelected = function (unselected) { + if (unselected) { + clip.element.css("outline", "none"); + } else { + clip.element.css("outline", "1px dashed white"); + } + }; + + clip.movement = { + startX: 0, startY: 0, startXabs: 0, startYabs: 0, clipWidth: 0, + clipLeft: 0, clipTop: 0, lastLeft: 0, isCopying: false, lastYabs: 0, extendHold: false, doClipRedraw: false, originalClipData: null, mouseMoveInnerFunc: null, clonedClips: null, masterClip: null, moved: false, channel: null, + mouseDown: function(e, dragType) { + e.preventDefault(); + e.stopPropagation(); + var cm = clip.movement; + cm.moved = false; + cm.originalClipData = []; + cm.clonedClips = []; + cm.mouseMoveInnerFunc = function(e) { + clip.movement.doDragInner(e, dragType); + }; + for (let c of twine.timeline.selectedClips) { + cm.originalClipData.push({ + clip: c, + position: c.data.position, + playLength: c.data.playLength + }); + c.movement.mouseDownInner(e, dragType); + } + $("html").on("mouseup", cm.endDragMaster); + }, + mouseDownInner: function(e, dragType) { + var cm = clip.movement; + $("html").on("mousemove", cm.mouseMoveInnerFunc).on("mouseup", cm.endDrag); + cm.masterClip = clip; // for cloning + cm.channel = clip.channel; + cm.originalPlaylength = data.playlength; + cm.originalPosition = data.position; + cm.isCopying = false; + cm.clipWidth = parseFloat(clip.element.css("width")); + cm.clipTop = parseFloat(clip.element.css("top")); + cm.clipLeft = parseFloat(clip.element.css("left")); + cm.startXabs = e.clientX; + cm.startYabs = e.clientY; + cm.startX = cm.startXabs - e.target.getBoundingClientRect().left; + cm.startY = cm.startYabs - e.target.getBoundingClientRect().top; + cm.lastLeft = (e.clientX - cm.startX - clip.channel.offset.left); + cm.lastYabs = cm.startYabs; + //$("#container").css("cursor", "e-resize"); + }, + endDragMaster: function(e) { + var cm = clip.movement; + $("html").off("mouseup", cm.endDragMaster); + if (!cm.moved) return; + var clipData = [...cm.originalClipData]; + var clonedClips = [...cm.clonedClips]; + console.log("add move undo"); + twine.undo.add("move/resize clip", function(){ + clipData.forEach(function(d){ + d.clip.data.position = d.position; + d.clip.data.playLength = d.playLength; + d.clip.redraw(); + }); + clonedClips.forEach(function(c){ + c.destroy(); + }); + }); + }, + endDrag: function(e) { + e.preventDefault(); + var cm = clip.movement; + cm.isCopying = false; + $("html").off("mouseup", cm.endDrag).off("mousemove", cm.mouseMoveInnerFunc); + $("#container").css("cursor", "pointer"); + if (cm.doClipRedraw) { + setClipWaveform(); + } + }, + doDrag: function(e, dragType) { + e.preventDefault(); + //$("html").off("mouseup", this.initialMouseUp); + twine.timeline.selectedClips.forEach(function(c){ + c.movement.doDragInner(e, dragType); + }); + }, + setExtendHold: function() { + var cm = clip.movement; + cm.extendHold = true; + setTimeout(function() { + cm.extendHold = false; + }, 250); + }, + doDragInner: async function (e, dragType) { + var cm = clip.movement; + if (dragType == "right") { + var maxWidth = getMaxClipWidth(); + var xMovement = e.clientX - cm.startXabs; + var newWidth = xMovement + cm.clipWidth; + newWidth = twine.timeline.roundToGrid(newWidth); + if (newWidth > maxWidth) newWidth = maxWidth; + if (newWidth < minWidth) newWidth = minWidth; + if (newWidth != parseFloat(clip.element.css("width"))) { + cm.moved = true; + cm.doClipRedraw = true; + } + var playLength = newWidth / twine.timeline.pixelsPerBeat; + if (!cm.masterClip.channel.hasOverlap(cm.masterClip, null, playLength)) { + data.playLength = playLength; + clip.element.css("width", newWidth + "px"); + } + } else if (dragType == "left") { + var maxWidth = getMaxClipWidth(); + var xMovement = e.clientX - cm.startXabs; + var left = cm.clipLeft + xMovement; + //var left = (e.clientX - cm.startX - clip.channel.offset.left); + left = twine.timeline.roundToGrid(left); + if (left < 0) left = 0; + var newWidth = (cm.clipWidth - left) + cm.clipLeft; + var cWidth, cLeft; + if (newWidth < minWidth) { + cWidth = minWidth, cm.clipLeft + minWidth; //(minWidth - left) + clipLeft; + } else if (newWidth > maxWidth) { + cWidth = maxWidth, cLeft = cm.lastLeft; + } else { + lastLeft = left; + cWidth = newWidth, cLeft = left; + } + if (cWidth != parseFloat(clip.element.css("width"))) { + cm.moved = true; + cm.doClipRedraw = true; + } + var position = Math.min(0, (left / twine.timeline.pixelsPerBeat) + twine.timeline.beatRegion[0]); + var playLength = newWidth / twine.timeline.pixelsPerBeat; + if (!cm.masterClip.channel.hasOverlap(cm.masterClip, position, playLength)) { + data.position = position; + data.playLength = playLength + clip.element.css({width: cWidth + "px", left: cLeft + "px"}); + } + } else { + if (cm.extendHold) return; + if (e.ctrlKey && !cm.isCopying) { + cm.isCopying = true; + cm.masterClip = await clip.clone(); + } + var xMovement = e.clientX - cm.startXabs; + var left = xMovement + cm.clipLeft; + left = twine.timeline.roundToGrid(left); + + var yMovement = e.clientY - cm.lastYabs; + var top = (e.clientY - cm.lastYabs) + cm.clipTop; + //console.log(top); + var ttop = (e.clientY - cm.startY) + cm.clipTop; + var tshift = ttop / cm.channel.height; //cm.masterClip.channel.height; + tshift = (tshift > 0) ? Math.floor(tshift) : Math.ceil(tshift); + console.log(tshift); + + var channelShift = top / cm.masterClip.channel.height; + channelShift = (channelShift > 0) ? Math.floor(channelShift) : Math.ceil(channelShift); + + if (channelShift != 0 || left != parseFloat(clip.element.css("left"))) { + cm.moved = true; + } + if (channelShift != 0) { + cm.lastYabs = e.clientY; + var newChannel = cm.masterClip.channel.index + channelShift; + if (newChannel < twine.timeline.channels.length && newChannel >= 0) { + if (!twine.timeline.channels[newChannel].hasOverlap(cm.masterClip)) { + cm.masterClip.channel.removeClip(cm.masterClip); + twine.timeline.channels[newChannel].addClip(cm.masterClip); + } + } + } + + var doRedraw = false; + var extension = cm.masterClip.data.playLength + 8; + var newBeats; + var d = twine.timeline.data; + if (left < 0) { + cm.setExtendHold(); + extension = -extension; + left = 0; + newBeats = twine.timeline.beatRegion[1]; + doRedraw = true; + } + + var position = (left / twine.timeline.pixelsPerBeat) + twine.timeline.beatRegion[0]; + + if (!cm.masterClip.channel.hasOverlap(cm.masterClip, position)) { + data.position = position; + cm.masterClip.element.css("left", left + "px"); + } + + if (left + cm.clipWidth > cm.masterClip.channel.width) { + cm.setExtendHold(); + doRedraw = true; + newBeats = twine.timeline.beatRegion[1] + extension; + twine.timeline.extend(newBeats, true); + } + + if (doRedraw) { + var extensionRatio = (extension / newBeats); + var newStart = d.regionStart + ((d.regionEnd - d.regionStart) * (extensionRatio)); + var newEnd = d.regionEnd + extensionRatio; + newStart = Math.max(0, newStart); + newEnd = Math.max(0, newEnd); + twine.timeline.setRegion(newStart, newEnd, true); + } + } // move type + } // end doDragInner + }; + + elMove.mousedown(function(e){ + clip.movement.mouseDown(e, "mid"); + }); + elResizeRight.mousedown(function(e){ + clip.movement.mouseDown(e, "right"); + }); + elResizeLeft.mousedown(function(e){ + clip.movement.mouseDown(e, "left"); + }); + +}; \ No newline at end of file -- cgit v1.2.3