Skip to content
This repository was archived by the owner on Nov 30, 2020. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 48 additions & 6 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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[];
Expand All @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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}';
Expand Down
59 changes: 59 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
@pause="onPause"
@timeUpdate="onTimeUpdate"
@inputTimeUpdate="onInputTimeUpdate"
@export="onExport"
@dragStart="onDragStart"
@dragStop="onDragStop"
/>
Expand Down Expand Up @@ -157,6 +158,31 @@
</v-navigation-drawer>
</v-container>
</v-touch>
<section v-if="showHapticsAlert" class="modal" @click="showHapticsAlert = false">
<div class="modal-card">
<h2>Haptics File Error</h2>
<br>
<p>An HTML file was uploaded for haptics. Please make sure that you use a supported haptics file format.</p>
<p>Commonly used file extension for haptics are <b>.json</b>, <b>.funscript</b>, <b>.csv</b>, etc.</p>
<br>
<p>For a more complete list of accepted file formats see this page of <a href="https://stpihkal.docs.buttplug.io/video-encoding-formats/feelme.html" target="blank">Video Encoding Formats</a></p>
<br>
<button class="alert-button">
OK
</button>
</div>
</section>
<section v-if="showFileDownload" class="modal" @click="showFileDownload = false">
<div class="modal-card">
<h2>Haptics File Download</h2>
<br>
<p>Click the download button to save the file and enjoy!</p>
<br>
<a class="download-link">
Download
</a>
</div>
</section>
</v-app>
</template>

Expand Down Expand Up @@ -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;
}

</style>
<style src="vuetify/dist/vuetify.min.css"></style>
21 changes: 20 additions & 1 deletion src/components/VideoEncoder/VideoEncoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,37 @@ 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);
}

public beforeDestroy() {
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?!
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 11 additions & 6 deletions src/components/VideoEncoder/VideoEncoder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,25 @@
<ul>
<li @click="ToggleHapticsPlayback">
<v-icon
color="white" v-if="!this.hapticsPlaying">play_circle_outline</v-icon>
color="white" v-if="!this.hapticsPlaying" title="play haptics script" alt="">play_circle_outline</v-icon>
<v-icon
color="white" v-if="this.hapticsPlaying">pause_circle_outline</v-icon>
color="white" v-if="this.hapticsPlaying" title="pause haptics script" alt="">pause_circle_outline</v-icon>
</li>
<li @click="ToggleHapticsLooped">
<v-icon
color="white" v-if="this.hapticsLooped">sync</v-icon>
color="white" v-if="this.hapticsLooped" title="haptics synced" alt="">sync</v-icon>
<v-icon
color="white" v-if="!this.hapticsLooped">sync_disabled</v-icon>
color="white" v-if="!this.hapticsLooped" title="haptics syncing disabled" alt="">sync_disabled</v-icon>
</li>
<li @click="ToggleHapticsLocked">
<v-icon
color="white" v-if="this.hapticsLocked">lock_outline</v-icon>
color="white" v-if="this.hapticsLocked" title="haptics editing enabled" alt="">lock_outline</v-icon>
<v-icon
color="white" v-if="!this.hapticsLocked">lock_open</v-icon>
color="white" v-if="!this.hapticsLocked" title="haptics editing disabled" alt="">lock_open</v-icon>
</li>
<li @click="ExportHaptics">
<v-icon
color="white" title="export haptics script" alt="">import_export</v-icon>
</li>
</ul>
</div>
Expand Down Expand Up @@ -101,6 +105,7 @@
clear: both;
padding: 0px;
margin: 0px;
z-index: 1;
}

.encoder-button-bar ul > li {
Expand Down
6 changes: 4 additions & 2 deletions src/components/VideoPlayer/VideoPlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down