Funscript.io - a website for playing, modifying and generating funscripts!

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 :wink: )

@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 :smiley:

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:
Capture

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;
}
1 Like

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;
}
2 Likes

For some reason the new version of Funscript Player does not work with my handy, any clue on how to fix this?

1 Like

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?

Try beta.funscript.io

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}));
}
5 Likes

is there a playlist function

1 Like

Hey everyone! Regarding Auto Mode! I love it and it is unique as a randomizer, but: Would it be possible to get a ā€œStroke rangeā€ slider with min and max points into the mix?

That is right now the only thing that this misses, otherwise seems like an absolutely amazing (the best imo because of the randomized ramping) random stroker/cycler/automode whatever! :smiley:

Thanks yall and have a great day. ^^ <3

Here’s a custom modification I’ve found fun. It adds waviness to the script over a period and amplitude you specify. You could use a remapper modifier first to restrict positions to a narrower range in the middle (for instance 30 to 80) and then add this waviness custom modifier to make it move up and down over time.

WAVY

        // applies wavy pattern up and down over specified period and amplitude
        return actions.map(action => ({...action, pos: Math.sin(((action.at / 1000) % 60)* Math.PI * 2 / 60) * 20 + action.pos}));
    }

60 is the repeat period in seconds
20 is the amplitude change in either direction (total 40)

3 Likes

Feature request. For the Offset modifier, be able to enter a negative number in order to offset in the reverse direction.