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/timeline.js | 736 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 736 insertions(+) create mode 100644 site/app/twine/timeline.js (limited to 'site/app/twine/timeline.js') diff --git a/site/app/twine/timeline.js b/site/app/twine/timeline.js new file mode 100644 index 0000000..9e33a4c --- /dev/null +++ b/site/app/twine/timeline.js @@ -0,0 +1,736 @@ +var Locators = function(timeline, elTimebar, elChannelOverlay) { + var locators = this; + var items = { + start: { + elMain: $("
").addClass("twine_timeline_timebar_locatorhead").appendTo(elTimebar), + elLine: $("
").addClass("twine_timeline_timebar_locatorline").appendTo(elChannelOverlay), + setLeft: function(px){ + items.start.elMain.show().css("left", px - (items.start.elMain.width() / 2)); + items.start.elLine.show().css("left", px); + }, + hide: function() { + items.start.elMain.hide(); + items.start.elLine.hide(); + } + }, + regionStart: { + elMain: $("
").addClass("twine_timeline_timebar_regionstart").appendTo(elTimebar), + elLine: $("
").addClass("twine_timeline_timebar_regionstart").appendTo(elChannelOverlay), + setLeft: function(px){ + items.regionStart.elMain.show().css("left", px - (items.regionStart.elMain.width() / 2)); + items.regionStart.elLine.show().css("left", px); + }, + hide: function() { + items.regionStart.elMain.hide(); + items.regionStart.elLine.hide(); + } + }, + regionEnd: { + elMain: $("
").addClass("twine_timeline_timebar_regionstart").appendTo(elTimebar), + elLine: $("
").addClass("twine_timeline_timebar_regionstart").appendTo(elChannelOverlay), + setLeft: function(px){ + items.regionEnd.elMain.show().css("left", px - (items.regionEnd.elMain.width() / 2)); + items.regionEnd.elLine.show().css("left", px); + }, + hide: function() { + items.regionEnd.elMain.hide(); + items.regionEnd.elLine.hide(); + } + } + }; + + + items.regionStart.elMain.on("mousedown", function(es) { + es.preventDefault(); + es.stopPropagation(); + var offset = elTimebar.offset().left; + var max = elTimebar.width(); + var startLeft = parseFloat(items.regionStart.elLine.css("left")); + function mouseup() { + $("body").off("mousemove", mousemove).off("mouseup", mouseup); + } + + function mousemove(e) { + var newPos = (e.clientX - es.clientX) + startLeft; + newPos = timeline.roundToGrid(newPos); + if (newPos < 0) { + newPos = 0; + } else if (newPos > max) { + newPos = max; + } + + var offset = elTimebar.offset().left; + var beats = timeline.beatRegion[1] - timeline.beatRegion[0]; + var beat = timeline.beatRegion[0] + (Math.round((newPos / (max - offset)) * beats * 100) / 100); + if (beat > timeline.data.beatEnd - 1) { + return; + } + items.regionStart.setLeft(newPos); + timeline.data.beatStart = beat; + } + $("body").on("mousemove", mousemove).on("mouseup", mouseup); + }); + + items.regionEnd.elMain.on("mousedown", function(es) { + es.preventDefault(); + es.stopPropagation(); + var offset = elTimebar.offset().left; + var max = elTimebar.width(); + var startLeft = parseFloat(items.regionEnd.elLine.css("left")); + function mouseup() { + $("body").off("mousemove", mousemove).off("mouseup", mouseup); + } + + function mousemove(e) { + var newPos = (e.clientX - es.clientX) + startLeft; + newPos = timeline.roundToGrid(newPos); + if (newPos < 0) { + newPos = 0; + } else if (newPos > max) { + newPos = max; + } + + var offset = elTimebar.offset().left; + var beats = timeline.beatRegion[1] - timeline.beatRegion[0]; + var beat = timeline.beatRegion[0] + (Math.round((newPos / (max - offset)) * beats * 100) / 100); + if (beat < timeline.data.beatStart + 1) { + return; + } + items.regionEnd.setLeft(newPos); + timeline.data.beatEnd = beat; + } + $("body").on("mousemove", mousemove).on("mouseup", mouseup); + }); + + + items.start.elMain.on("mousedown", function(es) { + es.preventDefault(); + es.stopPropagation(); + var offset = elTimebar.offset().left; + var max = elTimebar.width(); + var startLeft = parseFloat(items.start.elLine.css("left")); + function mouseup() { + $("body").off("mousemove", mousemove).off("mouseup", mouseup); + } + + function mousemove(e) { + var newPos = (e.clientX - es.clientX) + startLeft; + newPos = timeline.roundToGrid(newPos); + if (newPos < 0) { + newPos = 0; + } else if (newPos > max) { + newPos = max; + } + items.start.setLeft(newPos); + var offset = elTimebar.offset().left; + var beats = timeline.beatRegion[1] - timeline.beatRegion[0]; + var beat = timeline.beatRegion[0] + (Math.round((newPos / (max - offset)) * beats * 100) / 100); + timeline.startLocation = beat; + } + $("body").on("mousemove", mousemove).on("mouseup", mouseup); + }); + + this.redrawStart = function() { + if (timeline.startLocation >= timeline.beatRegion[1] || timeline.startLocation < timeline.beatRegion[0]) { + items.start.hide(); + return; + } + var ratio = (timeline.startLocation - timeline.beatRegion[0]) / (timeline.beatRegion[1] - timeline.beatRegion[0]); + var eltWidth = elTimebar.width() - elTimebar.offset().left; + var px = ratio * eltWidth; + items.start.setLeft(px); + } +}; // end locators + + + +var Timeline = function(twine, data) { + var timeline = this; + var elDragSelection; + timeline.selectedClips = []; + timeline.selectedChannel = null; + timeline.startLocation = 0; + + Object.defineProperty(timeline, "selectedClip", { + get: function() { + return timeline.selectedClips[timeline.selectedClips.length - 1]; + }, + set: function(c) { + timeline.selectedClips = [c]; + } + }); + + var container = $("#twine_timeline"); + var elTimebarContainer = $("
").attr("id", "twine_timeline_timebar").appendTo(container); + var elTimebar = $("
").attr("id", "twine_timeline_timebar_inner").appendTo(elTimebarContainer); + this.element = $("
").attr("id", "twine_timeline_inner").appendTo(container); + $("
").attr("id", "twine_timelineoverlay").appendTo(timeline.element); + var elChannelOverlay = $("
").attr("id", "twine_timeline_channeloverlay").appendTo(container); + var elScrollOuter = $("
").attr("id", "twine_timeline_scroll_outer").appendTo(container); + var elScrollInner = $("
").attr("id", "twine_timeline_scroll_inner").appendTo(elScrollOuter); + var elIcons = $("
").attr("id", "twine_timeline_scroll_filler").appendTo(container); + var elPlayhead = $("
").attr("id", "twine_timeline_playposition").appendTo(elChannelOverlay); + + var locators = new Locators(timeline, elTimebar, elChannelOverlay); + + elIcons.append(twirl.createIcon({ + label: "Zoom in", + size: 20, + icon: "zoomIn", + click: function() { + timeline.zoomIn(); + } + }).el); + + elIcons.append(twirl.createIcon({ + label: "Zoom out", + size: 20, + icon: "zoomOut", + click: function() { + timeline.zoomOut(); + } + }).el); + + elIcons.append(twirl.createIcon({ + label: "Show all", + size: 20, + icon: "showAll", + click: function() { + timeline.showAll(); + } + }).el); + + var channelIndex = 0; + this.channels = []; + this.gridPixels = 40; + this.pixelsPerBeat = 10; + this.beatRegion = []; + this.playbackBeatStart = 0; + + if (data) { + this.data = data; + } else { + this.data = { + name: "New arrangement", + snapToGrid: 4, + gridVisible: true, + beatStart: 0, + beatEnd: 16, + bpm: 120, + timeSigMarker: 4, + regionStart: 0, + regionEnd: 1 + }; + } + + elTimebar.click(function(e) { + var offset = elTimebar.offset().left; + var beats = timeline.beatRegion[1] - timeline.beatRegion[0]; + var px = timeline.roundToGrid(e.clientX - offset); + var width = elTimebar.width() - offset; // ???? + var beat = timeline.beatRegion[0] + (Math.round((px / width) * beats * 100) / 100); + timeline.startLocation = beat; + locators.redrawStart(); + twine.stopAndPlay(beat); + }); + + + this.getTotalBeatDuration = function() { + return timeline.data.beatEnd * (60 / timeline.data.bpm); + }; + + this.changeBeatEnd = function(newBeatEnd, noredraw) { + var original = timeline.data.beatEnd; + timeline.data.beatEnd = newBeatEnd; + for (let c of timeline.channels) { + c.changeBeatEnd(original, newBeatEnd, noredraw); + } + }; + + this.extend = function(newBeatEnd, noredraw) { + if (newBeatEnd > timeline.data.beatEnd) { + timeline.changeBeatEnd(newBeatEnd, noredraw); + } + }; + + this.reduce = function() { + var end = 0; + for (let ch of timeline.channels) { + for (let i in ch.clips) { + if (ch.clips[i]) { + var e = ch.clips[i].data.position + ch.clips[i].data.playLength; + if (e > end) { + end = e; + } + } + } + } + end += 8; + timeline.data.beatEnd = end; + }; + + this.exportData = async function() { + var saveData = { + channels: [], + data: timeline.data, + masterAmp: (await app.getControlChannel(twine.mixer.masterAmpChannel)), + ftables: {} + }; + for (let c of timeline.channels) { + saveData.channels.push(await c.exportData()); + } + for (let ch of saveData.channels) { + for (let cl of ch.clips) { + var fnL = cl.table[0]; + var fnR = cl.table[1]; + if (!saveData.ftables[fnL] && fnL > 0) { + saveData.ftables[fnL] = { + length: await app.getCsound().tableLength(fnL), + data: (await app.getCsound().tableCopyOut(fnL)).values().toArray() + }; + } + if (!saveData.ftables[fnR] && fnR > 0) { + saveData.ftables[fnR] = { + length: await app.getCsound().tableLength(fnR), + data: (await app.getCsound().tableCopyOut(fnR)).values().toArray() + }; + } + } + } + return saveData; + }; + + this.createFtable = async function(length) { + return new Promise(function(resolve, reject) { + var cbid = app.createCallback(function(ndata){ + resolve(ndata.table); + }); + app.insertScore("twine_createtable", [0, 1, cbid, length]); + }); + }; + + this.copyNewTableIn = async function(data) { + var ftable = await timeline.createFtable(data.length); + await app.getCsound().tableCopyIn(ftable, data); + return ftable; + }; + + this.importData = async function(loadData) { + timeline.data = loadData.data; + await app.setControlChannel(twine.mixer.masterAmpChannel, loadData.masterAmp); + var ftMap = {}; + console.log("load ftables", loadData.ftables); + for (let i in loadData.ftables) { + var fn = await timeline.copyNewTableIn(loadData.ftables[i].data); + console.log("copy into", fn, loadData.ftables[i]); + window.fn = loadData.ftables[i]; + ftMap[i] = fn; + } + + while (timeline.channels.length > 0) { + timeline.channels[0].remove(); + } + + timeline.channels = []; + var channelIndex = 0; + for (let c of loadData.channels) { + var channel = new Channel(timeline, channelIndex ++); + timeline.channels.push(channel); + await channel.importData(c, ftMap); + } + timeline.redraw(); + }; + + var playheadInterval; + this.setPlaying = function(state) { + twine.ui.head.play.setValue(state); + if (playheadInterval) { + clearInterval(playheadInterval); + } + if (state) { + playheadInterval = setInterval(async function(){ + var val = await app.getControlChannel("twine_playpos"); + var beat = (val * (timeline.data.bpm / 60)) + timeline.playbackBeatStart; + if (beat > timeline.beatRegion[1]) { + clearInterval(playheadInterval); + elPlayhead.hide(); + } + var pos = beat * timeline.pixelsPerBeat; + elPlayhead.css("left", pos + "px"); + }, 50); + elPlayhead.show(); + } else { + elPlayhead.hide(); + } + }; + + this.zoomIn = function() { + timeline.setRegion( + timeline.data.regionStart * 1.1, + timeline.data.regionEnd * 0.9 + ); + }; + + this.zoomOut = function() { + timeline.setRegion( + timeline.data.regionStart * 0.9, + timeline.data.regionEnd * 1.1 + ); + timeline.redraw(); + }; + + this.showAll = function() { + timeline.setRegion(0, 1); + }; + + this.compileAutomationData = function(onready) { + var changed = false; + for (let c of timeline.channels) { + if (c.automationChanged()) { + changed = true; + break; + } + } + if (!changed) { + return onready(1); + } + + var start = timeline.data.beatStart / (timeline.data.beatEnd - timeline.data.beatStart); + var instr = "instr twine_automaterun\n"; + for (let c of timeline.channels) { + for (let a of c.getAutomationData(start, 1)) { + instr += a + "\n" + } + } + instr += "a_ init 0\nout a_\nendin\n"; + console.log(instr); + app.compileOrc(instr).then(function(status){ + if (status < 0) { + self.errorHandler("Cannot parse automation data"); + } else { + onready(1); + } + }); + }; + + function determineChannelForAdd() { + var channel; + if (!timeline.selectedChannel) { + if (timeline.channels.length == 0) { + channel = timeline.addChannel(); + } else { + channel = timeline.channels[0]; + } + } else { + channel = timeline.selectedChannel; + } + return channel; + } + + this.addScriptClip = function() { + var channel = determineChannelForAdd(); + var clip = new Clip(twine); + clip.data.position = timeline.playbackBeatStart; //timeline.beatRegion[0]; + channel.addClip(clip); + clip.initScript(); + clip.redraw(); + }; + + this.addBlankClip = function() { + var el = $("
"); + el.append($("

").text("New blank clip")); + let pStereo = new twirl.transform.Parameter({ + definition: { + name: "Stereo", + description: "Whether the clip should be stereo or mono", + fireChanges: false, automatable: false, + min: 0, max: 1, step: 1, dfault: 1 + }, + host: twine + }); + let pDuration = new twirl.transform.Parameter({ + definition: { + name: "Duration", + description: "Duration of the clip in seconds", + fireChanges: false, automatable: false, + min: 0.1, max: 30, step: 0.01, dfault: 10 + }, + host: twine + }); + var tb = $("").appendTo($("").appendTo(el)); + tb.append(pStereo.getElementRow(true)); + tb.append(pDuration.getElementRow(true)); + twirl.prompt.show(el, function(){ + var channel = determineChannelForAdd(); + var clip = new Clip(twine); + clip.data.position = timeline.playbackBeatStart; //timeline.beatRegion[0]; + channel.addClip(clip); + clip.createSilence(pStereo.getValue(), pDuration.getValue(), name); + }); + }; + + this.addChannel = function() { + var channel = new Channel(timeline, channelIndex ++); + timeline.channels.push(channel); + timeline.drawGrid(); + return channel; + }; + + + this.contractChannels = function() { + for (let c of timeline.channels) { + c.contract(); + } + }; + + this.roundToGrid = function(px) { + if (timeline.data.snapToGrid) { + return twine.roundToNearest(px, twine.timeline.gridPixels, twine.timeline.gridOffset); + } else { + return px; + } + }; + + + + this.roundToChannel = function(px) { + var total = 0; + var rounded = px; + for (let i in timeline.channels) { + var c = timeline.channels[i]; + if (i == 0) rounded = c.height; + total += c.height + 1; + if (px > total) { + rounded = total; + } + } + return rounded; + }; + + this.determineChannelFromTop = function(posTop) { + var top = 0; + for (var c of timeline.channels) { + top += c.height; + if (posTop < top) { + return c; + } + } + return c; + }; + + function calcViewport() { + var cc = $(".twine_channelclips"); + var width = $("#twine_timeline_channeloverlay").width(); //cc.width(); + var d = timeline.data; + timeline.beatRegion = [ + d.regionStart * d.beatEnd, + d.regionEnd * d.beatEnd + ]; + var gridScale = 1; + var beats = timeline.beatRegion[1] - timeline.beatRegion[0]; + timeline.pixelsPerBeat = width / beats; + timeline.gridPixels = timeline.pixelsPerBeat / parseInt(d.snapToGrid); + var beat = timeline.beatRegion[0]; + timeline.gridOffset = (parseInt(beat) - beat) * timeline.pixelsPerBeat; + return { + minLeft: 0, //cc.offset().left, + width: width, + beat: beat + } + } + + + this.drawGrid = function() { + if (timeline.channels.length == 0) return; + $(".twine_timelinemarker").remove(); + $(".twine_timelinetext").remove(); + var vp = calcViewport(); + if (!timeline.data.gridVisible) return; + + var width; + var fontWeight; + beat = Math.floor(vp.beat); + for (var x = vp.minLeft + timeline.gridOffset; x < vp.width; x += timeline.pixelsPerBeat) { + if ((beat - 1) % timeline.data.timeSigMarker == 0) { + width = 2; + fontWeight = "bold"; + } else { + width = 1; + fontWeight = "normal"; + } + if (x >= 0) { + $("
").attr("class", "twine_vline twine_timelinemarker").appendTo(elChannelOverlay).css("width", width).css("left", x).css("top", "0px"); + $("
").attr("class", "twine_timelinetext").appendTo(elChannelOverlay).css("font-weight", fontWeight).css("left", x + 2).text(beat); + } + beat ++; + } + locators.redrawStart(); + } + + var drawing; + this.redraw = function(noClipWaveRedraw) { + if (drawing) return; + drawing = true; + timeline.drawGrid(); + for (let c of timeline.channels) { + c.redraw(noClipWaveRedraw); + } + drawing = false; + }; + + this.setRegion = function(start, end, noClipWaveRedraw) { + timeline.reduce(); + if (end <= start) return; + if (end > 1) end = 1; + if (start < 0) start = 0; + timeline.data.regionStart = start; + timeline.data.regionEnd = end; + timeline.redraw(noClipWaveRedraw); + var elTbcw = elScrollOuter.width(); + elScrollInner.css({left: (timeline.data.regionStart * elTbcw) + "px", right: ((1 - timeline.data.regionEnd) * elTbcw) + "px"}); + if (self.onRegionChange) { + self.onRegionChange([timeline.data.regionStart, timeline.data.regionEnd]); + } + }; + + this.onRegionChange = function(region) { + + }; + + + function setScrollBarPosition(displayLeft, displayRight, setRegion) { + if (displayLeft >= 0 && displayRight >= 0) { + elScrollInner.css({left: displayLeft, right: displayRight}); + var w = elScrollOuter.width(); + if (setRegion) { + timeline.data.regionStart = displayLeft / w; + timeline.data.regionEnd = 1 - (displayRight / w); + if (self.onRegionChange) { + self.onRegionChange([timeline.data.regionStart, timeline.data.regionEnd]); + } + } + } + } + + elScrollOuter.mousedown(function() { + var increment = 20; + var apos = event.pageX - elScrollOuter.offset().left; + var left = parseInt(elScrollInner.css("left")); + var right = parseInt(elScrollInner.css("right")); + var tbWidth = parseInt(elScrollInner.css("width")); + if (apos < left) { + left -= increment; + right += increment; + } else if (apos > left + tbWidth) { + left += increment; + right -= increment; + } else { + return; + } + setScrollBarPosition(left, right, true); + timeline.redraw(); + }); + + elScrollInner.mousedown(function(e){ + var pageX = e.pageX; + var offset = elScrollOuter.offset(); + var cWidth = elScrollOuter.width(); + var tbWidth = elScrollInner.width(); + var sLeft = pageX - offset.left - parseInt(elScrollInner.css("left")); + + function handleDrag(e) { + var left = ((e.pageX - pageX) + (pageX - offset.left)); + left = left - sLeft; + var end = left + tbWidth; + var right = cWidth - end; + setScrollBarPosition(left, cWidth - end, true); + timeline.redraw(true); + + } + function handleMouseUp(e) { + $("body").off("mousemove", handleDrag).off("mouseup", handleMouseUp); + function ensureDraw() { + if (drawing) return setTimeout(ensureDraw, 20); + timeline.redraw(); + } + ensureDraw(); + } + $("body").on("mouseup", handleMouseUp).on("mousemove", handleDrag); + }); + + this.deselectClips = function() { + timeline.selectedClips = []; + $(".twine_clip").css("outline", "none"); + }; + + this.dragSelection = function(e){ + var elChannelOverlay = $("#twine_timeline_channeloverlay"); + var pageX = e.pageX; + var pageY = e.pageY; + var offset = elChannelOverlay.offset(); + var width = elChannelOverlay.width(); + var height = elChannelOverlay.height(); + var left = (pageX - offset.left); + var top = (pageY - offset.top); + if (!elDragSelection) { + elDragSelection = $("
").addClass("drag_selection").appendTo(elChannelOverlay); + } + elDragSelection.hide().css({left: left + "px", top: top + "px"}); + + function handleDrag(e) { + elDragSelection.show(); + var xMovement = e.pageX - pageX; + var yMovement = e.pageY - pageY; + if (xMovement < 0) { + elDragSelection.css({ + left: (left + xMovement) + "px" + }); + } + elDragSelection.css({ + width: Math.abs(xMovement) + "px" + }); + + if (yMovement < 0) { + elDragSelection.css({ + top: (top + yMovement) + "px" + }); + } + elDragSelection.css({ + height: Math.abs(yMovement) + "px" + }); + + } + + function handleMouseUp(e) { + elDragSelection.hide(); + var left = parseFloat(elDragSelection.css("left")); + var width = parseFloat(elDragSelection.css("width")); + var top = parseFloat(elDragSelection.css("top")); + var height = parseFloat(elDragSelection.css("height")) - 10; + var channelStart = timeline.determineChannelFromTop(top).index; + var channelEnd = timeline.determineChannelFromTop(top + height).index; + + var beatStart = ((left / timeline.pixelsPerBeat) + timeline.beatRegion[0]) - 0.01; + var beatEnd = (((left + width) / timeline.pixelsPerBeat) + timeline.beatRegion[0]) + 0.01; + timeline.deselectClips(); + for (let ch of timeline.channels) { + if (ch.index >= channelStart && ch.index <= channelEnd) { + for (let ci in ch.clips) { + var cl = ch.clips[ci]; + if (cl) { + if (cl.data.position >= beatStart && cl.data.position + cl.data.playLength <= beatEnd) { + cl.markSelected(); + timeline.selectedClips.push(cl); + + } + } + } + } + } + $("body").off("mousemove", handleDrag).off("mouseup", handleMouseUp); + } + $("body").on("mouseup", handleMouseUp).on("mousemove", handleDrag); + }; + + timeline.drawGrid(); +}; -- cgit v1.2.3