diff --git a/src/App.ts b/src/App.ts index c5db425..3b47b62 100644 --- a/src/App.ts +++ b/src/App.ts @@ -54,6 +54,8 @@ export default class App extends Vue { private lastTimeChecked: number = 0; private desiredPlayTime: number = 0; private currentMessages: ButtplugMessage[] = []; + private showHapticsAlert: boolean = false; + private showFileDownload: boolean = false; // Buttplug properties private devices: ButtplugClientDevice[] = []; @@ -129,6 +131,10 @@ export default class App extends Vue { ///////////////////////////////////// private SetHapticsFile(aFile: File) { + if (/html$/.test(aFile.name)) { + this.showBadHapticsAlert(); + return; + } this.hapticsFile = aFile; LoadFile(this.hapticsFile).then((h: HapticFileHandler) => { this.hapticsCommands = h.Commands as FunscriptCommand[]; @@ -137,11 +143,13 @@ export default class App extends Vue { }); } - private onVideoLoaded(duration: number) { + private onVideoLoaded(duration: number, currentTime: number) { if (this.hapticsCommands.length === 0) { this.hapticsCommands.push(new FunscriptCommand(0, 0)); this.hapticsCommands.push(new FunscriptCommand(duration, 0)); } + // seeking the time with hotkeys sends a video loaded event + this.advanceFrame(currentTime); } private onHapticsFileChange(hapticsFile: FileList) { @@ -179,15 +187,45 @@ export default class App extends Vue { this.currentPlayTime = this.desiredPlayTime; } - private advanceFrame(direction: number) { - this.desiredPlayTime = (this.currentPlayTime) + (((1.0 / 60.0) * direction) * 1000); - this.currentPlayTime = this.desiredPlayTime; + private advanceFrame(currentTime: number) { + this.currentPlayTime = currentTime; + this.runHapticsLoop(); + } + + private onExport() { + if (!this.hapticsCommands.length) { + alert("Error: No haptics events set. \n\nPlease enter haptics into timeline before exporting."); + } + + const actions = this.hapticsCommands.map((command) => ({ + at: command.Time, + pos: command.Position, + })); + const script = { + version: "1.0", + inverted: false, + range: 90, + actions, + }; + + const file = new Blob( + [JSON.stringify(script)], + { type: "application/json" }, + ); + const fileURL = URL.createObjectURL(file); + + this.showFileDownload = true; + + setTimeout(() => { + const $downloadBtn = document.querySelector(".download-link"); + $downloadBtn.setAttribute("href", fileURL); + $downloadBtn.setAttribute("download", "new-script.funscript"); + }, 500); } private runHapticsLoop() { window.requestAnimationFrame(() => { - // If we paused before this fired, just return - if (this.paused || this.commands.size === 0) { + if (this.commands.size === 0) { return; } // Backwards seek. Reset index retreived. @@ -225,6 +263,10 @@ export default class App extends Vue { }); } + private showBadHapticsAlert() { + this.showHapticsAlert = true; + } + private loadHapticsTestData() { // tslint:disable-next-line:max-line-length const testFile = '{"version": "1.0", "inverted": false, "actions": [{"at": 367, "pos": 20}, {"at": 667, "pos": 80}, {"at": 1101, "pos": 20}, {"at": 1535, "pos": 80}, {"at": 1902, "pos": 20}, {"at": 2269, "pos": 80}, {"at": 2569, "pos": 20}, {"at": 3036, "pos": 80}, {"at": 3403, "pos": 20}, {"at": 3670, "pos": 80}, {"at": 4137, "pos": 20}, {"at": 4505, "pos": 80}, {"at": 4838, "pos": 20}, {"at": 5472, "pos": 30}, {"at": 5706, "pos": 50}, {"at": 5873, "pos": 30}, {"at": 6206, "pos": 30}, {"at": 6840, "pos": 40}, {"at": 7307, "pos": 30}, {"at": 7674, "pos": 20}, {"at": 8175, "pos": 80}, {"at": 8575, "pos": 20}, {"at": 8876, "pos": 80}, {"at": 9276, "pos": 20}, {"at": 9576, "pos": 80}, {"at": 9943, "pos": 20}, {"at": 10277, "pos": 80}, {"at": 10644, "pos": 20}, {"at": 11144, "pos": 80}, {"at": 11512, "pos": 20}, {"at": 11945, "pos": 80}, {"at": 12279, "pos": 20}, {"at": 12713, "pos": 80}, {"at": 13113, "pos": 20}, {"at": 13447, "pos": 80}, {"at": 13947, "pos": 20}, {"at": 14281, "pos": 80}, {"at": 14581, "pos": 20}, {"at": 15048, "pos": 80}, {"at": 15382, "pos": 20}, {"at": 15749, "pos": 80}, {"at": 16116, "pos": 20}, {"at": 16517, "pos": 80}, {"at": 16917, "pos": 20}, {"at": 17217, "pos": 80}, {"at": 17551, "pos": 20}, {"at": 17918, "pos": 80}, {"at": 18318, "pos": 20}, {"at": 18685, "pos": 80}, {"at": 19019, "pos": 20}, {"at": 19386, "pos": 80}, {"at": 19686, "pos": 20}, {"at": 20020, "pos": 70}, {"at": 20354, "pos": 20}, {"at": 20687, "pos": 70}, {"at": 21021, "pos": 20}, {"at": 21455, "pos": 50}, {"at": 21889, "pos": 20}, {"at": 22189, "pos": 50}, {"at": 22723, "pos": 20}, {"at": 23223, "pos": 60}, {"at": 23557, "pos": 10}, {"at": 23957, "pos": 60}, {"at": 24358, "pos": 60}, {"at": 24925, "pos": 60}, {"at": 25225, "pos": 10}, {"at": 25626, "pos": 50}, {"at": 26026, "pos": 10}, {"at": 26493, "pos": 50}, {"at": 26660, "pos": 10}, {"at": 26927, "pos": 50}, {"at": 27261, "pos": 10}, {"at": 27528, "pos": 50}, {"at": 27861, "pos": 10}, {"at": 28195, "pos": 50}, {"at": 28629, "pos": 10}, {"at": 28996, "pos": 50}, {"at": 29296, "pos": 10}, {"at": 29596, "pos": 50}, {"at": 30163, "pos": 10}, {"at": 30230, "pos": 10}, {"at": 30664, "pos": 50}, {"at": 31064, "pos": 10}, {"at": 31532, "pos": 50}, {"at": 31798, "pos": 40}, {"at": 31999, "pos": 70}, {"at": 32299, "pos": 20}, {"at": 32733, "pos": 70}, {"at": 33100, "pos": 20}, {"at": 33367, "pos": 60}, {"at": 33901, "pos": 20}, {"at": 34301, "pos": 70}, {"at": 34601, "pos": 20}, {"at": 34968, "pos": 60}, {"at": 35369, "pos": 20}, {"at": 35736, "pos": 60}, {"at": 36103, "pos": 20}, {"at": 36470, "pos": 60}, {"at": 36803, "pos": 20}, {"at": 37137, "pos": 60}, {"at": 37504, "pos": 20}, {"at": 37938, "pos": 60}, {"at": 38338, "pos": 20}, {"at": 38672, "pos": 60}, {"at": 39072, "pos": 20}, {"at": 39506, "pos": 60}, {"at": 39840, "pos": 20}, {"at": 40207, "pos": 60}, {"at": 40507, "pos": 20}, {"at": 41208, "pos": 50}, {"at": 41508, "pos": 10}, {"at": 41842, "pos": 50}, {"at": 42175, "pos": 10}, {"at": 42476, "pos": 50}, {"at": 42910, "pos": 10}, {"at": 43176, "pos": 50}, {"at": 43443, "pos": 10}, {"at": 43677, "pos": 50}, {"at": 44011, "pos": 10}, {"at": 44244, "pos": 50}, {"at": 44344, "pos": 10}, {"at": 44511, "pos": 50}, {"at": 44745, "pos": 10}, {"at": 45112, "pos": 40}, {"at": 45779, "pos": 10}, {"at": 46847, "pos": 0}], "range": 100}'; diff --git a/src/App.vue b/src/App.vue index 5635d53..6675e34 100644 --- a/src/App.vue +++ b/src/App.vue @@ -76,6 +76,7 @@ @pause="onPause" @timeUpdate="onTimeUpdate" @inputTimeUpdate="onInputTimeUpdate" + @export="onExport" @dragStart="onDragStart" @dragStop="onDragStop" /> @@ -157,6 +158,31 @@ + + @@ -348,5 +374,38 @@ justify-content: center; } + .modal { + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + outline: 1000vw; + background-color: rgba(0, 0, 0, 50%); + } + + .download-link { + text-decoration: none; + color: #2c3e50; + } + + .download-link, + .alert-button { + background-color: lightgray; + padding: 10px 20px; + margin-right: 5%; + float: right; + } + + .download-link:hover, + .alert-button:hover { + background-color: darkgray; + } + + .modal-card { + background-color: white; + padding: 30px; + } + diff --git a/src/components/VideoEncoder/VideoEncoder.ts b/src/components/VideoEncoder/VideoEncoder.ts index 7ea91e1..5b792f9 100644 --- a/src/components/VideoEncoder/VideoEncoder.ts +++ b/src/components/VideoEncoder/VideoEncoder.ts @@ -42,6 +42,7 @@ export default class VideoEncoder extends Vue { for (const i of [...Array(10).keys()]) { Mousetrap.bind(i.toString(), () => this.addNodeAtPoint(i * 10)); } + Mousetrap.bind("backspace", () => this.removeNodeAtPoint()); window.addEventListener("resize", this.onResize); } @@ -49,15 +50,29 @@ export default class VideoEncoder extends Vue { for (const i of [...Array(10).keys()]) { Mousetrap.unbind(i.toString()); } + Mousetrap.unbind("backspace"); window.removeEventListener("resize", this.onResize); } private addNodeAtPoint(value: number) { + // Clear any previous values at the current play time before adding the new one + this.removeNodeAtPoint(); this.hapticsValues.push([this.currentPlayTime, value]); this.hapticsValues.sort((a, b) => a[0] > b[0] ? 1 : -1); this.updateGraph(); } + private removeNodeAtPoint() { + this.hapticsValues = this.hapticsValues.filter(([time, val]) => { + // Remove for nodes within 999ms of the current position + return !( + this.currentPlayTime - 999 <= time && + this.currentPlayTime + 999 >= time + ); + }); + this.updateGraph(); + } + private updateGraph() { this.path.attr("d", this.line(this.hapticsValues)); // Additions aren't working without full removal?! @@ -204,7 +219,7 @@ export default class VideoEncoder extends Vue { .range([0, graphdiv.clientHeight]); this.xAxis = d3.axisTop(this.xScale) - .ticks(Math.min(this.dataExtent[1] / 1000, 50)) + .ticks(Math.min(this.dataExtent[1] / 1000, 25)) .tickFormat((d) => `${(d as number) / 1000}s`); this.yAxis = d3.axisRight(this.yScale) .tickSize(graphdiv.clientWidth); @@ -319,6 +334,10 @@ export default class VideoEncoder extends Vue { this.hapticsLocked = !this.hapticsLocked; } + private ExportHaptics() { + this.$emit("export"); + } + private runTimeUpdateLoop() { window.requestAnimationFrame(() => { if (!this.hapticsPlaying) { diff --git a/src/components/VideoEncoder/VideoEncoder.vue b/src/components/VideoEncoder/VideoEncoder.vue index 22f185c..bceb693 100644 --- a/src/components/VideoEncoder/VideoEncoder.vue +++ b/src/components/VideoEncoder/VideoEncoder.vue @@ -9,21 +9,25 @@ @@ -101,6 +105,7 @@ clear: both; padding: 0px; margin: 0px; + z-index: 1; } .encoder-button-bar ul > li { diff --git a/src/components/VideoPlayer/VideoPlayer.ts b/src/components/VideoPlayer/VideoPlayer.ts index 0573b6e..52813af 100644 --- a/src/components/VideoPlayer/VideoPlayer.ts +++ b/src/components/VideoPlayer/VideoPlayer.ts @@ -123,8 +123,10 @@ export default class VideoPlayer extends Vue { const playerElement = (this.$refs.videoPlayer as Vue).$el; this.currentPlayer = (this.$refs.videoPlayer as any).player; this.currentPlayer!.height(document.getElementById("video-container")!.offsetHeight); - this.$emit("videoLoaded", this.currentPlayer!.duration() * 1000); - console.log(`duration: ${this.currentPlayer!.duration() * 1000}`); + const duration = this.currentPlayer!.duration() * 1000; + const currentTime = this.currentPlayer!.currentTime() * 1000; + this.$emit("videoLoaded", duration, currentTime); + console.log(`duration: ${duration}`); this.onHeightUpdate(); }