From b8715f6bfbba38af74458e2d22f10df806986cb0 Mon Sep 17 00:00:00 2001 From: greenyouse Date: Tue, 17 Nov 2020 20:30:08 -0600 Subject: [PATCH 1/7] feat: Add warning for HTML haptics files --- src/App.ts | 9 +++++++++ src/App.vue | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/App.ts b/src/App.ts index c5db425..02659f5 100644 --- a/src/App.ts +++ b/src/App.ts @@ -54,6 +54,7 @@ export default class App extends Vue { private lastTimeChecked: number = 0; private desiredPlayTime: number = 0; private currentMessages: ButtplugMessage[] = []; + private showHapticsAlert: boolean = false; // Buttplug properties private devices: ButtplugClientDevice[] = []; @@ -129,6 +130,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[]; @@ -225,6 +230,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..b7f98aa 100644 --- a/src/App.vue +++ b/src/App.vue @@ -157,6 +157,20 @@ +
+
+

Haptics File Error

+
+

An HTML file was uploaded for haptics. Please make sure that you use a supported haptics file format.

+

Commonly used file extension for haptics are .json, .funscript, .csv, etc.

+
+

For a more complete list of accepted file formats see this page of Video Encoding Formats

+
+ +
+
@@ -348,5 +362,31 @@ justify-content: center; } + .alert-modal { + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + outline: 1000vw; + background-color: rgba(0, 0, 0, 50%); + } + + .alert-modal button { + background-color: lightgray; + padding: 10px 20px; + margin-right: 5%; + float: right; + } + + .alert-modal button:hover { + background-color: darkgray; + } + + .alert-card { + background-color: white; + padding: 30px; + } + From 10df5d446b73ee843c7f357050b287bcbf50db96 Mon Sep 17 00:00:00 2001 From: greenyouse Date: Fri, 20 Nov 2020 20:43:24 -0600 Subject: [PATCH 2/7] fix: Add titles to haptics buttons --- src/components/VideoEncoder/VideoEncoder.vue | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/VideoEncoder/VideoEncoder.vue b/src/components/VideoEncoder/VideoEncoder.vue index 22f185c..2f0d128 100644 --- a/src/components/VideoEncoder/VideoEncoder.vue +++ b/src/components/VideoEncoder/VideoEncoder.vue @@ -9,21 +9,21 @@ @@ -101,6 +101,7 @@ clear: both; padding: 0px; margin: 0px; + z-index: 1; } .encoder-button-bar ul > li { From 6eefbff71ab223fea9ffc81e4c23a6591995f758 Mon Sep 17 00:00:00 2001 From: greenyouse Date: Fri, 20 Nov 2020 22:31:24 -0600 Subject: [PATCH 3/7] feat: Sync video seeks with haptics --- src/App.ts | 13 +++++++------ src/components/VideoPlayer/VideoPlayer.ts | 6 ++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/App.ts b/src/App.ts index 02659f5..a9585d4 100644 --- a/src/App.ts +++ b/src/App.ts @@ -142,11 +142,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) { @@ -184,15 +186,14 @@ 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 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. 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(); } From 2e78384a688496dbbe83b148c2168c1cea619222 Mon Sep 17 00:00:00 2001 From: greenyouse Date: Sat, 21 Nov 2020 13:47:55 -0600 Subject: [PATCH 4/7] fix: Limit haptic nodes to one per position There was a bug allowing multiple 0-9 nodes on the haptics graph at any given position. That didn't work for device playback and was confusing on the UI. --- src/components/VideoEncoder/VideoEncoder.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/VideoEncoder/VideoEncoder.ts b/src/components/VideoEncoder/VideoEncoder.ts index 7ea91e1..a1ff117 100644 --- a/src/components/VideoEncoder/VideoEncoder.ts +++ b/src/components/VideoEncoder/VideoEncoder.ts @@ -53,6 +53,14 @@ export default class VideoEncoder extends Vue { } private addNodeAtPoint(value: number) { + // Clear any previous values at the current play time before adding the new one + 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.hapticsValues.push([this.currentPlayTime, value]); this.hapticsValues.sort((a, b) => a[0] > b[0] ? 1 : -1); this.updateGraph(); From ebe209566935a3e995eb1586b8fc35f9f26071a9 Mon Sep 17 00:00:00 2001 From: greenyouse Date: Sat, 21 Nov 2020 14:41:14 -0600 Subject: [PATCH 5/7] feat: Allow deletion of haptics editor nodes --- src/components/VideoEncoder/VideoEncoder.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/VideoEncoder/VideoEncoder.ts b/src/components/VideoEncoder/VideoEncoder.ts index a1ff117..b334269 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,11 +50,19 @@ 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 !( @@ -61,8 +70,6 @@ export default class VideoEncoder extends Vue { this.currentPlayTime + 999 >= time ); }); - this.hapticsValues.push([this.currentPlayTime, value]); - this.hapticsValues.sort((a, b) => a[0] > b[0] ? 1 : -1); this.updateGraph(); } From f69bd7c31f06c8be3a230417212fa82d923e65a1 Mon Sep 17 00:00:00 2001 From: greenyouse Date: Sat, 21 Nov 2020 15:02:31 -0600 Subject: [PATCH 6/7] fix: Decrease number of ticks on haptics editor There were 50 ticks being used which would cause issues when zooming the timeline. Setting to 25 works for all zoom levels. --- src/components/VideoEncoder/VideoEncoder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/VideoEncoder/VideoEncoder.ts b/src/components/VideoEncoder/VideoEncoder.ts index b334269..4b49c7c 100644 --- a/src/components/VideoEncoder/VideoEncoder.ts +++ b/src/components/VideoEncoder/VideoEncoder.ts @@ -219,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); From 28c79f6c515885f0cb390e17bc0cb7421d5cffd2 Mon Sep 17 00:00:00 2001 From: greenyouse Date: Sat, 21 Nov 2020 16:32:58 -0600 Subject: [PATCH 7/7] feat: add haptics file export --- src/App.ts | 32 +++++++++++++++++++ src/App.vue | 33 +++++++++++++++----- src/components/VideoEncoder/VideoEncoder.ts | 4 +++ src/components/VideoEncoder/VideoEncoder.vue | 4 +++ 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/App.ts b/src/App.ts index a9585d4..3b47b62 100644 --- a/src/App.ts +++ b/src/App.ts @@ -55,6 +55,7 @@ export default class App extends Vue { private desiredPlayTime: number = 0; private currentMessages: ButtplugMessage[] = []; private showHapticsAlert: boolean = false; + private showFileDownload: boolean = false; // Buttplug properties private devices: ButtplugClientDevice[] = []; @@ -191,6 +192,37 @@ export default class App extends Vue { 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 (this.commands.size === 0) { diff --git a/src/App.vue b/src/App.vue index b7f98aa..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,8 +158,8 @@ -
-
+ + @@ -362,7 +374,7 @@ justify-content: center; } - .alert-modal { + .modal { height: 100vh; display: flex; align-items: center; @@ -372,18 +384,25 @@ background-color: rgba(0, 0, 0, 50%); } - .alert-modal button { + .download-link { + text-decoration: none; + color: #2c3e50; + } + + .download-link, + .alert-button { background-color: lightgray; padding: 10px 20px; margin-right: 5%; float: right; } - .alert-modal button:hover { + .download-link:hover, + .alert-button:hover { background-color: darkgray; } - .alert-card { + .modal-card { background-color: white; padding: 30px; } diff --git a/src/components/VideoEncoder/VideoEncoder.ts b/src/components/VideoEncoder/VideoEncoder.ts index 4b49c7c..5b792f9 100644 --- a/src/components/VideoEncoder/VideoEncoder.ts +++ b/src/components/VideoEncoder/VideoEncoder.ts @@ -334,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 2f0d128..bceb693 100644 --- a/src/components/VideoEncoder/VideoEncoder.vue +++ b/src/components/VideoEncoder/VideoEncoder.vue @@ -25,6 +25,10 @@ lock_open +
  • + import_export +