This is a very powerful expression that you can use to drive all kinds of very cool effects! Based on knowledge from The Power of Expression Book. The expression has now been updated to work with Bezier!

Now works with Bezier!

Now works with parenting as well as feather!

Tools used

v4 Expression now works with Bezier!

IMPORTANT: This expression requires the new 'Javascript' expressions engine introduced in AE 16.0 (CC2019)
You can set this in File -> Project Settings

Download

v4 Expression now works with bezier!


// Make Layers React to a Mask
// Based on knowledge from The Power of Expression Book
// https://aescripts.com/the-power-of-expression/
//
// Requires the new 'Javascript' expressions engine introduced in AE 16.0 (CC2019)
// You can set this in File -> Project Settings: https://drop.aescripts.com/NQurXWmG
// If you replace all "let" to "var" this expression will should work in previous versions
//
// v1 initial version
// v2 added feathering support
// v3 added parenting support
// v4 added bezier support

const numDivisions = 5; // Adjust this number for more or less precision
const maskLayer = thisComp.layer("MASK"); // Replace "MASK" with the name of your layer
const maskName = "Mask 1"; // Change this to the name of your mask

const maskPath = maskLayer.mask(maskName).maskPath;
const rawMaskPoints = maskPath.points();
const inTangents = maskPath.inTangents();
const outTangents = maskPath.outTangents();
const isMaskClosed = maskPath.isClosed();
const maskFeather = maskLayer.mask(maskName).maskFeather[0];
const fallOffSquared = Math.pow(maskFeather, 2);

function needsSubdivision(c1, c2) {
    const tangentThreshold = 0.1; 
    return (length(c1) > tangentThreshold || length(c2) > tangentThreshold);
}

function bezier(t, p1, c1, c2, p2) {
    var u = 1 - t, tt = t * t, uu = u * u, uuu = uu * u, ttt = tt * t;
    return [
        uuu * p1[0] + 3 * uu * t * c1[0] + 3 * u * tt * c2[0] + ttt * p2[0],
        uuu * p1[1] + 3 * uu * t * c1[1] + 3 * u * tt * c2[1] + ttt * p2[1]
    ];
}

function subdivideBezierSegment(p1, c1, c2, p2, numDivisions) {
    var newPoints = [];
    for (var i = 0; i <= numDivisions; i++) {
        var t = i / numDivisions;
        newPoints.push(bezier(t, p1, c1, c2, p2));
    }
    return newPoints;
}

function transformMaskPoints(layer, pathPoints, inTangents, outTangents, isClosed) {
    var allPoints = [];
    var count = isClosed ? pathPoints.length : pathPoints.length - 1;

    for (var i = 0; i < count; i++) {
        var nextIndex = (i + 1) % pathPoints.length;
        var c1 = pathPoints[i] + outTangents[i];
        var c2 = pathPoints[nextIndex] + inTangents[nextIndex];

        if (needsSubdivision(outTangents[i], inTangents[nextIndex])) {
            allPoints = allPoints.concat(subdivideBezierSegment(pathPoints[i], c1, c2, pathPoints[nextIndex], numDivisions));
        } else {
            allPoints.push(pathPoints[i], pathPoints[nextIndex]);
        }
    }

    return allPoints.map(pt => layer.toComp(pt));
}

const transformedMaskPoints = transformMaskPoints(maskLayer, rawMaskPoints, inTangents, outTangents, isMaskClosed);

function inside(point, path) {
    
    let [minX, maxX, minY, maxY] = [Infinity, -Infinity, Infinity, -Infinity];
    for (let i = 0; i < path.length; i++) {
        minX = Math.min(minX, path[i][0]);
        maxX = Math.max(maxX, path[i][0]);
        minY = Math.min(minY, path[i][1]);
        maxY = Math.max(maxY, path[i][1]);
    }
    if (point[0] < minX || point[0] > maxX || point[1] < minY || point[1] > maxY) {
        return false;
    }
    
    let inside = false;
    for (let i = 0, j = path.length - 1; i < path.length; j = i++) {
        let xi = path[i][0], yi = path[i][1];
        let xj = path[j][0], yj = path[j][1];
        let intersect = ((yi > point[1]) != (yj > point[1])) && (point[0] < (xj - xi) * (point[1] - yi) / (yj - yi) + xi);
        if (intersect) inside = !inside;
    }
    return inside;
}

function distanceToLineSquared(point, a, b) {
    let lineLengthSquared = Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2);
    if (lineLengthSquared == 0) return Math.pow(point[0] - a[0], 2) + Math.pow(point[1] - a[1], 2);
    let t = ((point[0] - a[0]) * (b[0] - a[0]) + (point[1] - a[1]) * (b[1] - a[1])) / lineLengthSquared;
    t = Math.max(0, Math.min(1, t));
    return Math.pow(point[0] - (a[0] + t * (b[0] - a[0])), 2) + Math.pow(point[1] - (a[1] + t * (b[1] - a[1])), 2);
}

function closestDistanceSquared(point, path) {
    let closestDistSquared = Infinity;
    for (let i = 0; i < path.length - 1; i++) {
        let distSquared = distanceToLineSquared(point, path[i], path[i + 1]);
        if (distSquared < closestDistSquared) closestDistSquared = distSquared;
    }
    return closestDistSquared;
}

const anchorPoint = thisLayer.transform.anchorPoint;
const toCompAnchor = thisLayer.toComp([anchorPoint[0], anchorPoint[1]]);
let effectValue = thisProperty.key(1).value;
let closestDistSquared = closestDistanceSquared(toCompAnchor, transformedMaskPoints);

if (isMaskClosed) {
    let isInside = inside(toCompAnchor, transformedMaskPoints);
    if (isInside) {
        effectValue = thisProperty.key(2).value;
    } else if (maskFeather > 0 && closestDistSquared <= fallOffSquared) {
        let normalizedDistance = Math.sqrt(closestDistSquared) / maskFeather;
        effectValue = linear(normalizedDistance, 0, 1, thisProperty.key(2).value, thisProperty.key(1).value);
    }
} else {
    if (maskFeather > 0 && closestDistSquared <= fallOffSquared) {
        let normalizedDistance = Math.sqrt(closestDistSquared) / maskFeather;
        effectValue = linear(normalizedDistance, 0, 1, thisProperty.key(2).value, thisProperty.key(1).value);
    }
}

effectValue;

v3 Expression now works with parenting as well as feather!

 /* Make Layers React to a Mask // Based on knowledge from The Power of Expression Book // https://aescripts.com/the-power-of-expression/ // // Requires the new 'Javascript' expressions engine introduced in AE 16.0 (CC2019) // You can set this in File -> Project Settings: https://drop.aescripts.com/NQurXWmG // If you replace all "let" to "var" this expression will should work in previous versions // // v1 initial version // v2 added feathering support // v3 added parenting support */  let maskToUse = thisComp.layer("MASK").mask("Mask 1").maskPath.points(); let positionLayer = thisLayer.toComp([transform.anchorPoint[0],transform.anchorPoint[1]]); let inside = function(point, path) { let x = point[0], y = point[1]; let result = 0; let inside = false; for (let i = 0, j = path.length - 1; i < path.length; j = i++) { let xi = path[i][0], yi = path[i][1]; let xj = path[j][0], yj = path[j][1]; let intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); if (intersect) inside = !inside; let t1 = length(path[i],point); let t2 = length(path[result],point); if (t1 < t2){ result = i;} } return [inside,result]; }; let lengthToLine = function(a, b, c) { let line = Math.pow(length(b,c),2); if (line === 0) return Math.pow(length(a,b),2); let d = ((a[0] - b[0]) * (c[0] - b[0]) + (a[1] - b[1]) * (c[1] - b[1])) / line; d = Math.max(0, Math.min(1, d)); let distance = Math.pow(length(a, [ b[0] + d * (c[0] - b[0]), b[1] + d * (c[1] - b[1]) ]),2); return Math.sqrt(distance); }; let r = inside(positionLayer, maskToUse); let final, distance1, distance2, result2; if (r[0] == true) { thisProperty.key(2).value; } else { let closest = r[1]; let fallOff = thisComp.layer("MASK").mask("Mask 1").maskFeather[0]; final = thisProperty.key(1).value; if (fallOff!=0){ if (closest==0){ distance1 = lengthToLine (positionLayer, maskToUse[closest], maskToUse[maskToUse.length-1]); distance2 = lengthToLine (positionLayer, maskToUse[closest], maskToUse[closest+1]); } else if ((closest+1)==maskToUse.length){ distance1 = lengthToLine (positionLayer, maskToUse[closest], maskToUse[closest-1]); distance2 = lengthToLine (positionLayer, maskToUse[closest], maskToUse[0]); } else { distance1 = lengthToLine (positionLayer, maskToUse[closest], maskToUse[closest-1]); distance2 = lengthToLine (positionLayer, maskToUse[closest], maskToUse[closest+1]); }; if (distance1 < distance2){ result2 = distance1; }else{ result2 = distance2; }; final = linear(result2,0,fallOff,thisProperty.key(2).value,thisProperty.key(1).value); }; final; }; 

Old v1 Expression (does not support feathering)

 maskToUse = thisComp.layer("MASK").mask("Mask 1").maskPath.points(); function inside(point, path) { let x = point[0], y = point[1]; let inside = false; for (let i = 0, j = path.length - 1; i < path.length; j = i++) { let xi = path[i][0], yi = path[i][1]; let xj = path[j][0], yj = path[j][1]; let intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); if (intersect) inside = !inside; } return inside; }; r = inside(position, maskToUse); if (r == true) { thisProperty.key(2).value } else { thisProperty.key(1).value }; 

 

Submit

If you would like to submit a piece for inclusion in our INSPIRATION category please submit it to us here or tag #aescripts on Instagram.