RxLaboratory / Duik

Duik Ángela | Rigging and animation tools for After Effects
https://rxlaboratory.org/tools/duik/
GNU General Public License v3.0
343 stars 66 forks source link

Add drag/overlap to simulated kleaner #562

Closed Nico-Duduf closed 4 years ago

Nico-Duduf commented 4 years ago

New Expression:

//Duik.kleaner
var fx = effect("Kleaner");
//parameters
var doAnticipation = fx(1).value;
var doInterpolation = fx(2).value;
var doFollowThrough = fx(3).value;
var damping = fx(15).value;
var elasticity = fx(13).value;
var bounce = fx(17).value;
var anticipationDuration = fx(5).value;
var anticipationQuantity = fx(6).value;
var slowIn = fx(9).value;
var slowOut = fx(10).value;
var spatialMode = fx(20).value;
var spatialDoInterpolation = fx(21).value;
var moBlurPrecision = fx(22).value;
var elasticityRandom = fx(14).value;
var dampingRandom = fx(16).value;
var softBody = true;
var flexibility = 200;
var overlapDuration = 0.1;
var threshold = 0.1;

//adjust parameters
damping = damping / 10;
elasticity = elasticity / 10;
anticipationQuantity = anticipationQuantity / 100;
flexibility = flexibility / 100;
slowIn = 1 - slowIn / 100;
slowOut = slowOut / 100;
var isThisSpatial = isSpatial();
var isThisPosition = isPosition();
var simulate = false;
if (isThisSpatial) {
    doInterpolation = doInterpolation && spatialDoInterpolation;
    simulate = spatialMode == 2;
} else {
    overlapDuration = 0;
}

//adjust elasticity based on flexibility
if (simulate && softBody && isThisSpatial && !isThisPosition) {
    //get the distance from anchor ratio 
    var distanceRatio = length(valueAtTime(0), anchorPoint) / (thisLayer.width / 2);
    //adjust with flexibility
    distanceRatio = 1 + distanceRatio * flexibility;
    distanceRatio = distanceRatio / 2;

    //elasticity 
    elasticity = elasticity / distanceRatio;
    damping = damping / distanceRatio;
    overlapDuration = overlapDuration * distanceRatio;
}

// randomness
seedRandom(0, true);
elasticity = addNoise(elasticity, elasticityRandom);
damping = addNoise(damping, dampingRandom);
var overlapRandom = ( elasticityRandom + dampingRandom ) / 2;
if (overlapDuration != 0) overlapDuration = addNoise(overlapDuration, overlapRandom );

var result = value;

function distanceFromAnchor() {

}

function addNoise(val, quantity) {
    // a true random value to make sure all properties have a differente noise
    // even with the same start value
    var randomValue = random(0.9, 1.1);
    // generate a noise from the start value
    // (which means properties with a similar value won't be to far away from each other)
    var noiseValue = noise(valueAtTime(0) * randomValue);
    noiseValue = noiseValue * (quantity / 100);
    return val * (noiseValue + 1);
}

function bezier(t, tMin, tMax, value1, value2, bezierPoints) {
    if (arguments.length !== 6) return value;
    var a = value2 - value1;
    var b = tMax - tMin;
    if (b == 0) return t;
    var c = clamp((t - tMin) / b, 0, 1);
    if (!(bezierPoints instanceof Array) || bezierPoints.length !== 4) bezierPoints = [0, 0, 1, 1];
    return a * h(c, bezierPoints) + value1;

    function h(f, g) {
        var x = 3 * g[0];
        var j = 3 * (g[2] - g[0]) - x;
        var k = 1 - x - j;
        var l = 3 * g[1];
        var m = 3 * (g[3] - g[1]) - l;
        var n = 1 - l - m;
        var d = f;
        for (var i = 0; i < 5; i++) {
            var z = d * (x + d * (j + d * k)) - f;
            if (Math.abs(z) < 1e-3) break;
            d -= z / (x + d * (2 * j + 3 * k * d));
        }
        return d * (l + d * (m + d * n));
    }
}

function isSpatial(prop) {
    if (typeof prop === 'undefined') prop = thisProperty;
    if (!(prop.value instanceof Array)) return false;
    if (prop.value.length != 2 && prop.length != 3) return false;
    try {
        sp = prop.speed;
        return true;
    } catch (e) {
        return false;
    }
}

function isPosition(prop) {
    if (typeof prop === "undefined") prop = thisProperty;
    if (!(thisProperty.value instanceof Array)) return false;
    if (thisProperty.value.length > 3) return false;
    //compare the value to world with the anchor point to world
    var apWorld = thisLayer.toWorld(thisLayer.anchorPoint.valueAtTime(0), 0);
    var posWorld = thisProperty.valueAtTime(0);
    if (thisLayer.hasParent) posWorld = thisLayer.parent.toWorld(posWorld, 0);
    var result = true;
    for (var i = 0, num = thisProperty.value.length; i < num; i++) {
        if (posWorld[i] != apWorld[i]) return false;
    }
    return true;
}

function getLayerWorldPos(t, l) {
    if (typeof l === "undefined") l = thisLayer;
    if (typeof t === "undefined") t = time;
    return l.toWorld(l.anchorPoint, t);
}

function getLayerWorldVelocity(t, l) {
    if (typeof t === "undefined") t = time;
    return (getWorldPos(t, l) - getWorldPos(t - 0.01, l)) * 100;
}

function getLayerWorldSpeed(t, l) {
    return length(getWorldVelocity(t, l));
}

function getPropWorldValue(t, prop) {
    if (typeof prop === "undefined") prop = thisProperty;
    if (typeof t === "undefined") t = time;
    if (isPosition(prop)) return getLayerWorldPos(t);
    return thisLayer.toWorld(prop.valueAtTime(t), t);
}

function getPropWorldVelocity(t, prop) {
    if (typeof t === "undefined") t = time;
    return (getPropWorldValue(t, prop) - getPropWorldValue(t - 0.01, prop)) * 100;
}

function getPropWorldSpeed(t, prop) {
    return length(getPropWorldVelocity(t, prop));
}

function isStill(t, threshold) {
    if (typeof t === "undefined") t = time;
    if (typeof threshold === "undefined") threshold = 0.1;
    var d = valueAtTime(t) - valueAtTime(t + framesToTime(1));

    if (d instanceof Array) {
        for (var i = 0; i < d.length; i++) {
            d[i] = Math.abs(d[i]);
            if (d[i] >= threshold) {
                return false;
            }
        }
        return true;
    } else {
        d = Math.abs(d);
        return d < threshold;
    }
}

function getNextKey(t) {
    if (typeof t === "undefined") t = time;
    if (numKeys == 0) return null;
    var nKey = nearestKey(t);
    if (nKey.time >= t) return nKey;
    if (nKey.index < numKeys) return key(nKey.index + 1);
    return null;
}

function getPrevKey(t) {
    if (typeof t === "undefined") t = time;
    if (numKeys == 0) return null;
    var nKey = nearestKey(t);
    if (nKey.time <= t) return nKey;
    if (nKey.index > 1) return key(nKey.index - 1);
    return null;
}

function isAfterLastKey() {
    if (numKeys == 0) return false;
    var nKey = nearestKey(time);
    return nKey.time <= time && nKey.index == numKeys;
}

function isKeyTop(k, axis) {
    if (typeof axis === "undefined") axis = 0;
    var prevSpeed = velocityAtTime(k.time - threshold);
    var nextSpeed = velocityAtTime(k.time + threshold);
    if (value instanceof Array) {
        prevSpeed = prevSpeed[axis];
        nextSpeed = nextSpeed[axis];
    }
    if (Math.abs(prevSpeed) < 0.01 || Math.abs(nextSpeed) < 0.01) return true;
    return prevSpeed * nextSpeed < 0;
}

function anticipate() {
    var result = 0;
    if (value instanceof Array) {
        result = [0];
        for (var i = 1; i < value.length; i++)
            result.push(0);
    }

    if (isAfterLastKey()) return result;
    if (numKeys < 2) return result;

    var nextKey = getNextKey();
    var aKey = nextKey;
    if (!isStill(aKey.time - threshold)) {
        aKey = getPrevKey();
        if (!isStill(aKey.time - threshold)) return result;
    }
    if (aKey.index == numKeys) return result;

    var anticipationMiddle = aKey.time;
    var anticipationStart = anticipationMiddle - anticipationDuration;
    var anticipationEnd = key(aKey.index + 1).time;
    var startValue = result;
    var midValue = -valueAtTime(anticipationMiddle + anticipationDuration) + aKey.value;
    midValue = midValue * anticipationQuantity;
    var endValue = result;

    if (time < anticipationStart) {
        return result;
    } else if (time < anticipationMiddle) {
        if (value instanceof Array) {
            for (var i = 0; i < value.length; i++) {
                result[i] = bezier(time, anticipationStart, anticipationMiddle, startValue[i], midValue[i], [slowOut, 0, slowIn, 1]);
            }
            return result;
        } else {
            return bezier(time, anticipationStart, anticipationMiddle, startValue, midValue, [slowOut, 0, slowIn, 1]);
        }
    } else if (time <= anticipationEnd) {
        if (value instanceof Array) {
            for (var i = 0; i < value.length; i++) {
                result[i] = bezier(time, anticipationMiddle, anticipationEnd, midValue[i], endValue[i], [slowOut, 0, slowIn, 1]);
            }
            return result;
        } else {
            return bezier(time, anticipationMiddle, anticipationEnd, midValue, endValue, [slowOut, 0, slowIn, 1]);
        }
    } else {
        return result;
    }
}

function followThroughAtTime(t) {
    if (typeof t === "undefined") t = time;

    var fThrough = 0;
    if (value instanceof Array) {
        fThrough = [0];
        for (var i = 1; i < value.length; i++)
            fThrough.push(0);
    }

    //checks
    if (elasticity == 0) return fThrough;
    if (!simulate) {
        if (numKeys < 2) return fThrough;
        if (nearestKey(t).index == 1) return fThrough;
        var propSpeed = length(velocityAtTime(t));
        if (propSpeed >= threshold) return fThrough;
    } else {
        var propSpeed = getPropWorldSpeed(t);
        if (propSpeed >= threshold) return fThrough;
    }

    //check state and time
    var fThroughStart = 0;
    var fThroughTime = 0;

    if (simulate) {
        var speedI = getPropWorldSpeed(t);
        var i = t;
        //search for the time when the layer last moved
        while (speedI < threshold && i > 0) {
            i = i - thisComp.frameDuration / moBlurPrecision;
            speedI = getPropWorldSpeed(i);
        }
        fThroughStart = i;
    } else {
        //follow through starts at previous key
        var fThroughKey = getPrevKey(t);
        fThroughStart = fThroughKey.time;
    }

    if (fThroughStart == 0) return fThrough;

    fThroughTime = t - fThroughStart;

    //from velocity
    if (simulate) fThrough = getPropWorldVelocity(fThroughStart - thisComp.frameDuration) / 2;
    else fThrough = velocityAtTime(fThroughStart - thisComp.frameDuration) / 2;
    // damping ratio 
    var damp = Math.exp(fThroughTime * damping);
    var bounceDamp = Math.exp(fThroughTime * .12 * damping);
    // sinus evolution 
    var sinus = elasticity * fThroughTime * 2 * Math.PI;
    if (bounce) sinus = sinus * (bounceDamp);
    //sinus
    sinus = Math.sin(sinus);
    // elasticity
    sinus = .3 / elasticity * sinus;
    // damping
    sinus = sinus / damp;
    if (Math.abs(sinus) < threshold / 100) return 0;
    // result
    fThrough = fThrough * sinus;

    if (threshold > 0) {
        fThrough = fThrough * (1 - propSpeed / threshold);
    }

    if (bounce) {
        var prevValue = valueAtTime(fThroughStart - thisComp.frameDuration);
        var startValue = valueAtTime(fThroughStart);
        if (value instanceof Array) {
            for (var i = 0; i < prevValue.length; i++) {
                if (prevValue[i] > startValue[i]) fThrough[i] = Math.abs(fThrough[i]);
                if (prevValue[i] < startValue[i]) fThrough[i] = -Math.abs(fThrough[i]);
            }
        } else {
            if (prevValue > startValue) fThrough = Math.abs(fThrough);
            if (prevValue < startValue) fThrough = -Math.abs(fThrough);
        }
    }

    if (simulate) {
        if (!isThisPosition) {
            fThrough = fThrough + getLayerWorldPos();
            fThrough = thisLayer.fromWorld(fThrough) - thisLayer.anchorPoint;
        } else if (thisLayer.hasParent) {
            fThrough = fThrough + getLayerWorldPos(time, thisLayer.parent);
            fThrough = thisLayer.parent.fromWorld(fThrough) - thisLayer.parent.anchorPoint;
        }
    }

    return fThrough;
}

function followThrough() {
    var propSpeed = length(velocity);
    if (simulate) propSpeed = getPropWorldSpeed(time - overlapDuration);

    if (propSpeed < threshold) return followThroughAtTime(time - overlapDuration);

    //need to get back in time get the last follow-through value to fade it
    var fThrough = 0;
    var zero = 0;
    if (value instanceof Array) {
        fThrough = [0];
        for (var i = 1; i < value.length; i++)
            fThrough.push(0);
        zero = fThrough;
    }

    var t = time;
    while (t > 0) {
        t = t - thisComp.frameDuration;
        if (simulate) propSpeed = getPropWorldSpeed(t - overlapDuration);
        else propSpeed = length(velocityAtTime(t));
        if (propSpeed < threshold) {
            fThrough = followThroughAtTime(t - overlapDuration);
            break;
        }
    }

    return linear(time, t, t + anticipationDuration * 2, fThrough, zero);
}

function smartSmooth(axis) {
    if (typeof axis === "undefined") axis = 0;
    var startKey = nearestKey(time);
    var endKey = startKey;
    if (time == startKey.time) return 0;
    if (time < startKey.time && startKey.index == 1) return 0;
    if (time > startKey.time && startKey.index == numKeys) return 0;
    if (time < startKey.time) startKey = key(startKey.index - 1);
    if (time > startKey.time) endKey = key(startKey.index + 1);
    var sI = .66;
    var sO = .33;
    var sIV = 1;
    var sOV = 0;
    var sVal = startKey.value;
    var eVal = endKey.value;
    if (value instanceof Array) {
        sVal = sVal[axis];
        eVal = eVal[axis];
    }
    var sTime = startKey.time;
    var eTime = endKey.time;
    if (isKeyTop(startKey, axis)) sO = slowOut;
    else {
        var prevKey = key(startKey.index - 1);
        var pVal = prevKey.value;
        if (value instanceof Array) pVal = pVal[axis];
        sOV = (sVal - pVal) / (eVal - pVal);
    }
    if (isKeyTop(endKey, axis)) {
        sI = slowIn;
        if (endKey.index != numKeys) {
            var nextKey = key(endKey.index + 1);
            var nVal = nextKey.value;
            if (value instanceof Array) nVal = nVal[axis];
            if (Math.abs(nVal - eVal) < 0.01 && doFollowThrough) sI = 1;
        }
    } else {
        var nextKey = key(endKey.index + 1);
        var nVal = nextKey.value;
        if (value instanceof Array) nVal = nVal[axis];
        sIV = (eVal - sVal) / (nVal - sVal);
    }
    if (endKey.index == numKeys && doFollowThrough) {
        sI = 1;
    }

    var val = value;
    if (value instanceof Array) val = val[axis];
    return bezier(time, sTime, eTime, sVal, eVal, [sO, sOV, sI, sIV]) - val;
}

function overlap() {
    var ol = getPropWorldValue(time - overlapDuration) - getPropWorldValue() + getLayerWorldPos();
    ol = thisLayer.fromWorld(ol) - thisLayer.anchorPoint;
    return ol;
}

var okToGo = false;
if (simulate && fx.enabled) okToGo = true;
else if (numKeys > 1 && fx.enabled) okToGo = true;

if (okToGo) {
    var smartSmoothResult = 0;
    if (doInterpolation) smartSmoothResult = smartSmooth();
    if (value instanceof Array) {
        smartSmoothResult = [smartSmoothResult];
        for (var i = 1; i < value.length; i++) {
            if (doInterpolation) smartSmoothResult.push(smartSmooth(i));
            else smartSmoothResult.push(0);
        }
    }
    if (doAnticipation) result += anticipate();
    result = result + smartSmoothResult;
    if (doFollowThrough) result += followThrough();
    if (overlapDuration != 0) result += overlap();
}
result;
Nico-Duduf commented 4 years ago

done