Is it me or it doesn’t work for handy? I connect using the code but it doesn’t play back.
Same thing, this is on mobile so I’m not sure if it would be different on a desktop.
Can upload to list scripts, but can’t add them to any of the options to modify or play.
[edit] Can confirm it is functional for desktop from what I can tell.
@defucilis the work that goes into this
Are there plans to add a bar or an on-screen script positions like in the original non beta version? I like to test and see how the script looks before connecting my Handy to see if I want to use it.
I sometimes use the orignal non beta version for that, but there is no audio control for some reason and some videos are loud AF.
Anyone know if i can add like two actions before the the first action in a script, (at a time before and relative to first action).
The purpose would be to set my own starting position and a slow first movement, to always have a known safe way to enter and begin scripts.
I guess i could do it manually with notepad and a calculator, but would be nice to do it more elegantly with a program.
Yeah that’s been on my list for ages. I actually do have some concrete plans to revisit this project, fix some bugs and add some missing features (for secret reasons that I can’t talk about yet )
@Seraphine yep, you could create a new custom modifier and use this code (replace the array of extraActions with whatever you like)
actions => {
const extraActions = [
{ pos: 0, at: 0 },
{ pos: 100, at: 1000 },
];
const offsetTime = extraActions.slice(-1)[0].at;
const newActions = [];
for(let i = 0; i < actions.length + extraActions.length; i++) {
if(i < extraActions.length) {
newActions.push(extraActions[i]);
continue;
}
const offsetAction = { ...actions[i - extraActions.length] };
offsetAction.at += offsetTime;
newActions.push(offsetAction);
}
return newActions;
}
wow thank you for making that! unfortunately it don’t fully do what i ment.
i now managed to change the code you made into something that creates what i wanted
actions => {
const extraActions = [
{ pos: 100, at: 2000},
{ pos: 100, at: -4000 },
{ pos: 50, at: -1000},
];
const newActions = [];
for(let i = 0; i < actions.length + extraActions.length; i++) {
if(i < extraActions.length) {
if(extraActions[i].at >= 0){
newActions.push(extraActions[i]);
}
else{
extraActions[i].at = actions[0].at + extraActions[i].at;
newActions.push(extraActions[i]);
}
continue;
}
newActions.push(actions[i - extraActions.length]);
}
return newActions;
}
Ah yeah that makes sense, glad you were able to use it as a starting point
One wish I have for the List Scripts page is to display the minimum length field right away. Currently it only appears after at least 1 heatmap was generated.
Sometimes I want to compare different scripts for a PMV shorter than 5mins, but after dragging and dropping them, nothing is displayed (and the minimum length input field stays absent, too). To be able to change the minimum length from the defaut 5mins to e.g. 1min, I need to drag&drop a longer script first, which then makes the minimum length field display.
Can javascript be used to remove actions which are too close together (interval is too fast)? I want an automatic way to remove actions too fast for Handy. I’d like the result to contain actions which are all at least 0.06 seconds apart.
Start with this:
Result:
Oops, sorry wasn’t checking the forums much (new job, crazily busy @_@)
You can definitely do this with a custom script modifier. I just wrote this one up that seems to work - modify the minInterval
value (it’s in milliseconds) to fine-tune
actions => {
//actions under this threshold will use the previous action's position
const minInterval = 300;
const outputActions = [];
let lastPos = actions[0].pos;
for(let i = 1; i < actions.length; i++) {
const offset = actions[i].at - actions[i - 1].at;
if(offset < minInterval) outputActions.push({ at: actions[i].at, pos: lastPos });
else {
lastPos = actions[i].pos;
outputActions.push(actions[i]);
}
}
return outputActions;
}
That’s pretty close. I actually want to delete the matching action points altogether. I monkeyed with your example and came up with this.
actions => {
// Drop actions when interval is lower than 60 ms or, too fast for Handy
const minInterval = 60;
const outputActions = [];
let lastAction = actions[0].at;
outputActions.push(actions[0]);
for(let i = 1; i < actions.length; i++) {
const offset = actions[i].at - lastAction;
if (offset > minInterval) {
lastAction = actions[i].at;
outputActions.push(actions[i]);
}
}
return outputActions;
}
For some reason the new version of Funscript Player does not work with my handy, any clue on how to fix this?
Same Problem, got the handy new and until now it works onlx one time with funscript.io and was not good to sync. The website handyverse works fine, but not so much control there, any Ideas?
does this work with the new funsr1 2.0? or is there a similar site for the funsr1?
I love the website especially the cycler and random modes! Especially like them combined with audios from the GWA subreddit. Is it possible to create random funscripts of a certain length for download with the app? I would love that for a few vr scenes I have that dont involve any actions by itself like this one: https://vrporn.com/body-to-body-milana-may-vina-sky.
If that is not possible with funscript.io, does anyone know how to do that with another software?
Is it possible to play a video/script on repeat? I see status LOOP on top right but unsure how to loop the video/script.
I had tinkered around with node scripts to modify funscripts to more closely meet my preferences. I’d always wished there was a way to compare the original script to the modified script and your editor lets me do just that… so I ported my script to be used as a custom script and it worked out great. I thought I’d share it here…
What the script does is turn short strokes into longer strokes based on a configuration, and then it also adds a speed limit based on a configuration.
/**
* @typedef {Object} Action
* @property {number} pos
* @property {number} at
* @property {'up'|'down'|'flat'|undefined} dir
*/
/**
* @param {Action[]} actions
*/
actions => {
actions = actions.map(action => ({...action}));
// minimum movement length
const minDiff = 70;
// speed limit
const maxSpeed = 400;
/**
* @param {Action[]} actions
* @param {number} maxOffset
*/
function speedLimit(actions, maxOffset) {
for (let i = 1; i < actions.length; i++) {
let prevAction = actions[i-1];
let posDiff = actions[i].pos - prevAction.pos;
let timeDiff = actions[i].at - prevAction.at;
let speed = (Math.abs(posDiff) / Math.abs(timeDiff)) * 1000;
let correctionRatio = maxSpeed / speed;
let offset = (1 - correctionRatio) * Math.abs(posDiff);
offset = Math.floor(Math.min(maxOffset, offset) / 2);
if (speed > maxSpeed) {
if (posDiff > 0) { // up
actions[i].pos -= offset;
actions[i-1].pos += offset;
} else { // down
actions[i].pos += offset;
actions[i-1].pos -= offset;
}
actions[i].pos = Math.min(100, actions[i].pos);
actions[i].pos = Math.max(0, actions[i].pos);
actions[i-1].pos = Math.min(100, actions[i-1].pos);
actions[i-1].pos = Math.max(0, actions[i-1].pos);
}
}
return actions;
}
/**
* position [1, 2, 3, 2]
* directions [u, u, u, d]
* grouped [[1, 2, 3], [2]]
*
* @param {Action[]} actions
* @returns {Action[][]}
*/
function groupByDirection(actions) {
/** @type {Action[][]} */
let alternatingActions = [];
let i = 0;
while (i < actions.length) {
let direction = actions[i].dir;
let directionGroup = [];
do {
directionGroup.push(actions[i]);
i++;
} while (i < actions.length && actions[i].dir === direction);
alternatingActions.push(directionGroup);
}
return alternatingActions;
}
/**
* @param {Action[]} actions
* @returns {Action[]}
*/
function setDirections(actions) {
actions[0].dir = 'up';
// set directions
for (let i = 1; i < actions.length; i++) {
let posDiff = actions[i].pos - actions[i-1].pos;
if (posDiff === 0) {
actions[i].dir = 'flat';
} else {
actions[i].dir = posDiff > 0 ? 'up' : 'down';
}
}
return actions;
}
/**
* @param {Action[][]} alternatingActions
* @param {number}
* @returns {Action[][]}
*/
function minimumMovement(alternatingActions, maxOffset) {
for (let i = 1; i < alternatingActions.length; i++) {
let prevAction = alternatingActions[i-1][alternatingActions[i-1].length - 1];
let currAction = alternatingActions[i][alternatingActions[i].length - 1];
if (currAction.dir === 'flat') {
continue;
}
// distance / time
let posDiff = currAction.pos - prevAction.pos;
let offset = Math.min(minDiff - posDiff, maxOffset);
offset = Math.floor(offset / 2);
// movement length below threshold
if (Math.abs(posDiff) < minDiff) {
if (currAction.dir === 'up') {
currAction.pos += offset;
currAction.dir = 'up';
prevAction.pos -= offset;
} else { // down
currAction.pos -= offset;
currAction.dir = 'down';
prevAction.pos += offset;
}
currAction.pos = Math.min(100, currAction.pos);
currAction.pos = Math.max(0, currAction.pos);
prevAction.pos = Math.min(100, prevAction.pos);
prevAction.pos = Math.max(0, prevAction.pos);
alternatingActions[i] = [currAction];
}
}
return alternatingActions;
}
/**
* @param {Action[][]} alternatingActions
* @returns {Action[]}
*/
function flattenGroups(alternatingActions) {
return alternatingActions.reduce((result, group) => {
return [...result, ...group];
}, []);
}
for (let i = 0; i < 10; i++) {
actions = setDirections(actions);
actions = flattenGroups(minimumMovement(groupByDirection(actions), 5));
}
for (let i = 0; i < 10; i++) {
actions = speedLimit(actions, 5);
}
return actions.map(action => ({pos: action.pos, at: action.at}));
}