Encore - Strokes To Vibration For Vibration-Toys

I, occasionally I want to create some vibration scripts. These can also be used to complement a stroke script for a butplug or as a secondary vibrator. To make these scripts, I developed some converters that transform stroke scripts into vibration scripts. I based this partly on the options that Script Player provides for this task during playback, but I adapted it so I could generate scripts for later manual manipulation.

Script 1: Speed to Constant Vibration

I try to avoid fades, since fades can sometimes behave poorly with script interpolation, which is how strokes are usually played (as a fade from low to high). This script keeps the same reference points but converts the stroke speed into a constant vibration from start to finish, at a level corresponding to that speed.

What does this mean? If I have a stroke going from top to bottom at 100 milliseconds per second, it becomes a straight line at speed 10, for example. This straight line runs from the beginning to the end of the original stroke, maintaining perfect synchronization.

The function takes two parameters: minimum sensitivity and maximum sensitivity. It only generates a vibration when the speed is above the minimum, and the strongest vibration corresponds to the maximum speed. You can adjust these parameters depending on the script’s speeds to achieve a more interesting or accurate result.

Additionally, I snap all speeds in increments of 5, since very subtle changes are not very noticeable in vibration. This can also be adjusted to increments of 10 or 20, though that may be too much. Finally, there is another parameter that weights the height: a stroke at 100 milliseconds near the top (the head) will translate to a stronger vibration than one at the base, even if both are at the same speed.

(actions) => {
    var minSpeed = 45
    var maxSpeed = 500
    var roundBy = 5
    var withPos = false

    // Sort actions by 'at'
    actions = actions.sort((x, i) => x.at - i.at);

    // Add 'prev' property to each action
    actions.forEach((x, i) => {
        if (i != 0) x.prev = actions[i - 1];
        else x.prev = { at: 0, pos: 0 };
    });

    // Calculate speed and add intermediate points
    let result = [];
    actions.forEach((x, i) => {
        let speed = ((Math.abs(x.pos - (x.prev.pos || 0)) / ((x.at - x.prev.at) * 1.0)) * 1000) || 0;
        let pos = Math.max(0, Math.min(100, ((speed - minSpeed) / (maxSpeed - minSpeed)) * 100))
                    * (withPos ? (x.pos / 100) : 1);

        // Add original point rounded
        if (i > 0) {
            result.push({
                at: x.prev.at,
                pos: Math.round(pos / roundBy) * roundBy
            });
        }

        // Determine 'at' for the second point
        let nextAt = x.at - 1;

        // Add additional point with the same position
        result.push({
            at: nextAt,
            pos: Math.round(pos / roundBy) * roundBy
        });
    });
    result.forEach((x, i) => {
        if (i != 0) x.prev = result[i - 1];
        else x.prev = { at: 0, pos: 0 };
    });

    // Filter actions to remove consecutive points with the same position,
    // but keep the last one if the next one is different
    result = result.filter((x, i) => {
        if (i === 0) return true;
        if (x.pos !== x.prev.pos) return true;
        if (i + 1 < result.length && result[i + 1].pos !== x.pos) return true;
        return false;
    });

    return result.map(x => ({ at: x.at, pos: x.pos }));

};

I’ve noticed that not all scripts convert well with these functions. It depends on the creator’s style. With some styles, they work better than with others. In general, I got good results converting @Ahe scripts with this technique, since their particular style responds very well to these algorithms. It also works well with scripts generated based on beats from 0 to 100

Script 2: Find of Middlepoint

I originally wrote this script to try to improve a recording with many points that had been oversimplified and played back very poorly. The goal was to smooth the movement by merging two points that were almost at the same height and time, but by finding the midpoint in both time and position. with two parameter to Define thresholds for Pos Diff and Time Diff when the 2 point are merged

When you increces the Time parameter, Incredibly, and for some reason, this script does the opposite of the previous one. I was surprised the first time I ran this script on the result of the previous one and practically got back the original script I had started from.
it also fix the bug of double point in the Doubler function of this Page.

This script merges straight lines and turns them back into fades, but by finding the midpoint between them, while still respecting the synchronization of the original script. It serves as a way to simplify the output of our previous script and use it as another source for manual manipulation and make a mix beetween

//Find Middle Point
(actions) => {

    // Define thresholds
    const maxPosDiff = 10;
    const maxTimeDiff = 100;

    // Sort actions by time
    actions = actions.sort((a, b) => a.at - b.at);

    // Create a list to store the processed actions
    let processedActions = [];

    for (let i = 0; i < actions.length; i++) {
        let current = actions[i];
        let next = actions[i + 1];

        // Check if there is a next action and if it meets the removal conditions
        if (next && Math.abs(current.pos - next.pos) <= maxPosDiff && (next.at - current.at) < maxTimeDiff) {
            // Calculate the intermediate position and time
            let midPos = (current.pos + next.pos) / 2;
            let midTime = (current.at + next.at) / 2;

            // Create the new intermediate action
            let newAction = { at: midTime, pos: midPos };
            processedActions.push(newAction);

            // Skip the next action
            i++;
        } else {
            // If not removed, add the current action
            processedActions.push(current);
        }
    }

    return processedActions.map(x => ({ at: x.at, pos: x.pos }));
}

Script 3: Stroke to Square

I made this one yesterday to convert a user’s scripts. Basically, it transforms a stroke into a square while respecting the original timing and position of each stroke — For some vibrators, a square stroke feels better and is more noticeable than a fade up and down.


// Stroke To Square
(actions) => {
    // Define the variable for the offset in milliseconds
    const offsetMs = 1;

    // Sort actions by time
    actions = actions.sort((a, b) => a.at - b.at);

    // Create a list to store the processed actions
    let processedActions = [];

    for (let i = 0; i < actions.length - 1; i++) {
        let current = actions[i];
        let next = actions[i + 1];

        // Calculate stroke height (maximum; use (current.pos + next.pos) / 2 for the middle)
        let strokeHeight = Math.max(current.pos, next.pos);

        // Calculate the total duration of the stroke
        let duration = next.at - current.at;

        // Add first intermediate point (offsetMs after the start)
        let firstMidAt = current.at + offsetMs;
        if (firstMidAt < next.at && firstMidAt !== current.at && firstMidAt !== next.at) {
            processedActions.push({ at: firstMidAt, pos: strokeHeight });
        }

        // Add second intermediate point (offsetMs before the end)
        let secondMidAt = next.at - offsetMs;
        if (secondMidAt > current.at && secondMidAt !== current.at && secondMidAt !== next.at) {
            processedActions.push({ at: secondMidAt, pos: strokeHeight });
        }
    }

    // Sort and remove duplicates if any
    processedActions = processedActions.sort((a, b) => a.at - b.at)
        .filter((x, i, self) => i === 0 || x.at !== self[i - 1].at);
    processedActions = processedActions.filter((x, i) => {
        if (i === 0) return true;
        if (x.pos !== processedActions[i - 1].pos) return true;
        if (i + 1 < processedActions.length && processedActions[i + 1].pos !== x.pos) return true;
        return false;
    });
    return processedActions.map(x => ({ at: x.at, pos: x.pos }));
}

How to use them? To use them, go to beta.funscript.io, then select Modify Script and follow the steps in the image to paste the custom script into the code box.



you can adapt them with artificial intelligence to work with other tools , such as a PowerShell script . And if someone wants to make a plugin for OFS, that would be more than welcome.

9 Likes

Tried it with
HMV - ACID - SC by tyreoa - ACTION & MUSIC.funscript (45.6 KB)

AFTER SPEED TO CONSTANT VIBRATION

AFTER SPEED TO CONSTANT VIBRATION - HMV - ACID - SC by tyreoa - ACTION & MUSIC_MODIFIED.funscript (45.0 KB)

AFTER LIMIT 500

AFTER LIMIT 500 - HMV - ACID - SC by tyreoa - ACTION & MUSIC_MODIFIED_MODIFIED.funscript (44.9 KB)

AFTER LIMIT HANDY

AFTER LIMIT HANDY - HMV - ACID - SC by tyreoa - ACTION & MUSIC_MODIFIED_MODIFIED.funscript (44.9 KB)

After i limited it to 500 or Handy Limiter it becomes a completely messed up different Script.

1 Like

The script is too fast. I increased the maximum limit from 500 to 750, and the result is a bit more varied. Using the speed limiter would be a “mistake” since this isn’t a movement script but a vibration one. With 100 meaning fully On and 0 meaning Off, there’s really no actual movement to limit.
DUMM - ACID1 [4K60]_MODIFIED.funscript (56.8 KB)

By setting the weighting by position to true, I think it gives a script that’s maybe closer to what you’re looking for.

DUMM - ACID1 [4K60]_MODIFIED.withPos.funscript (83.4 KB)


DUMM - ACID1 [4K60]_MODIFIED.noPostRound20.funscript (50.1 KB)

Ok will play around with it thx mate!

1 Like

first one works the best for very long scripts like Fhs, cant get the other 2 scripts to work on those. Cool thing tho!

1 Like

I tested it with this funscript:
[D-ART] Konoha Nights
and honestly, great job! It’s so good that it seems truly native, and it’s hard to believe that it’s an adaptation with such a low line of code. I tested it with some presets, and it seems more interesting to slightly increase the minimum speed limit and mainly unlock the 1000 limit. This made things much more intense.

My presets were these:

(actions) => {
    var minSpeed = 380
    var maxSpeed = 1000
    var roundBy = 5
    var withPos = false

    // Sort actions by 'at'
    actions = actions.sort((x, i) => x.at - i.at);

    // Add 'prev' property to each action
    actions.forEach((x, i) => {
        if (i != 0) x.prev = actions[i - 1];
        else x.prev = { at: 0, pos: 0 };
    });

    // Calculate speed and add intermediate points
    let result = [];
    actions.forEach((x, i) => {
        let speed = ((Math.abs(x.pos - (x.prev.pos || 0)) / ((x.at - x.prev.at) * 1.0)) * 1000) || 0;
        let pos = Math.max(0, Math.min(100, ((speed - minSpeed) / (maxSpeed - minSpeed)) * 100))
                    * (withPos ? (x.pos / 100) : 1);

        // Add original point rounded
        if (i > 0) {
            result.push({
                at: x.prev.at,
                pos: Math.round(pos / roundBy) * roundBy
1 Like