【AE脚本/YTPMV】om_pv阵列快速定位工具
船见結衣
编辑于 2026年06月27日 12:15

使用AI大模型辅助生成(我们俩真厉害

一个简单的小脚本,功能:自定义阵列,快速给素材定位到对应单元格,支持多图层分别分配位置,三种素材处理方式,点击应用同步图层位置

建议在MotionToolsPro中使用,因为我只测试了这个,并且该脚本无法实时预览位置(有bug会崩溃不会修所以摆了qwq

如果未来有可以搭配使用的功能可能会整合进来

单个图层

多个图层

间距效果

缩放适配

缩放适配(但素材和合成比例不同)

拉伸填充

填满裁剪(创建形状图层作为遮罩)

代码块
JavaScript
自动换行
复制代码
// om_pv.jsx
// om_pv阵列快速定位工具 (常驻面板版,精简图层分配UI,按钮样式名称)

// ========== 可自定义参数(可通过齿轮菜单修改部分数值) ==========
var MAX_COLS = 20;
var MAX_ROWS = 20;
var DEFAULT_COLS = 3;
var DEFAULT_ROWS = 3;
var BTN_SIZE_MIN = 20;
var BTN_SIZE_MAX = 50;
var LAYER_BTN_MIN_W = 120;
var DEFAULT_SCALE = true;
var DEFAULT_STRETCH = false;
var DEFAULT_FILL_CLIP = false;
var DEFAULT_GAP_X = 0;
var DEFAULT_GAP_Y = 0;
var GAP_STEP = 5;
var CN_CHAR_W = 11;
var EN_CHAR_W = 7;
// =================================

(function() {
    var comp = app.project.activeItem;
    if (!comp || !(comp instanceof CompItem)) { alert("请先激活合成窗口", "BAKA"); return; }
    var layers = comp.selectedLayers;
    if (!layers.length) { alert("请先在时间轴中选择至少一个图层", "BAKA"); return; }

    // 筛选有效图层
    var validLayers = [];
    var validLayerNames = [];
    for (var i = 0; i < layers.length; i++) {
        var l = layers[i];
        if (l instanceof AVLayer || l instanceof ShapeLayer || l instanceof TextLayer) {
            if (l.name.indexOf("_遮罩") < 0) {
                validLayers.push(l);
                validLayerNames.push(l.name || ("图层 " + (i + 1)));
            }
        }
    }
    if (!validLayers.length) { alert("请选择至少一个可视图层(不含音频或遮罩)", "BAKA"); return; }

    function src(l) { return (l.source || l); }
    function tw(s) { for (var i = 0, w = 0; i < s.length; i++) w += (s.charCodeAt(i) > 127) ? CN_CHAR_W : EN_CHAR_W; return w; }

    // ===== 齿轮图标 =====
    function base64Decode(input) {
        var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
        var output = [];
        var i = 0;
        while (i < input.length) {
            var enc1 = chars.indexOf(input.charAt(i++));
            var enc2 = chars.indexOf(input.charAt(i++));
            var enc3 = chars.indexOf(input.charAt(i++));
            var enc4 = chars.indexOf(input.charAt(i++));
            var chr1 = (enc1 << 2) | (enc2 >> 4);
            var chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
            var chr3 = ((enc3 & 3) << 6) | enc4;
            output.push(String.fromCharCode(chr1));
            if (enc3 != 64) output.push(String.fromCharCode(chr2));
            if (enc4 != 64) output.push(String.fromCharCode(chr3));
        }
        return output.join('');
    }
    function createTempPng(base64, filename) {
        var f = new File(Folder.temp.fsName + "/" + filename);
        if (f && f.open("w")) {
            f.encoding = "binary";
            f.write(base64Decode(base64));
            f.close();
        }
        return f;
    }
    var gearBase64 = "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAEMklEQVRIib1WTUgjWRCudeemiK4N4iy6GjYHESHYZC8yvqcEVvxBUEQvUcQQzOYayDALe3IGc1ToGQysoBdJBvUQVi9BXoSAkDQEg+QQiBsRvXQiO8bAHpYsX6c7ZESduewWNP3yut6rqq/qq8o3jDH6Smk11G6/9kAsFqMXWAghnlTinPuIyCNJkgW/NU2DgY9E9EEIkXrmXHWBCCqVyqMPY0z2+XwVTdMqptzf31disViFMbb51DnjrH7/iy9E6Z6cnNQXGxsb+ttms9HQ0BBZrVY35zwghMg9d0HNAOe8GVAQEaCICiEAg9zT00Pn5+e0v78fxH46nQ7DQH9/P2WzWZlzDsjcRo4+CiHURw0Q0ZrX6/W0t7dTKpWCd6okSXJbWxudnp7iew5GOee5QqFgsVqt2PNLkhSem5ujpqYmOj4+9nPOXUKI32u3AivG2NutrS0d5cvLy9ob2OM9MzMDzFsNbP3hcLimg5zgMXUZY0nG2A9mDlCmrZIkFXd2dvSyCgQCqt1ul0dGRqhUKlEoFELluIhIMmCIEtH09PS0B1GYOpC6OwJE9BprGPB7vd618fFxWlhYwGXdxmWzRs03S5L0xoRBVVWKRqMwgpzIhg7Wfgg8Hxsbw953MICQw8lkUg/R5XIhxLd1pfYecACCeslkMiZsw4aeDWV7eHioa6G0UeJmDoahbOJpfPSZHICg7mEceuvr67oenIJzjLFmXA4dCByCY2YOvu3u7v6zXC5fHRwcfD8wMPByYmKCwuHwj8B7ZWVFBiwejydXLBZHyuXym0wm809LS8srh8NB8Xi8r1gs/uX1elE9wB75+AD8iejvfD5PDVRtFSirQDweJ5Sl0RZ0DpTLZajcoi0IIYBt6ubmRvcOXIAjcKJQKCA3qhDiFyHEp894wDlfBskGBwd1RU3TwE714uJC7uvr0y/hnNuIKA8yd3R06IfT6bRuHJXU2dlJDocDxHtvRPDJhGhYkqS9zc3Nl11dXbS6ukrX19fviGivVCq5p6amyGKxtF5dXa00NDT4R0dHX83Pz9PZ2Rnt7u6C7b8lEok2i8UiO51OamxstCcSiRYi+gMQ/edVBIjUXC4329vbS7e3eqsPcs4dRIQnryjKu1Ao9BQPfuac/2Ss9cPIWSKRwFo1DQRDoZBOtOXlZVTCHrCUZfkzJiuK8pDJ4Tomr5ltRydX1eD/04vMifYr5xzl5gEM29vbesmhm6K/AB5FUdCS0WPc6FNHR0d634IOvqMLRyIRMiZd3pxo9e36taIoeRMGIUSUc54sFAoyDmNOcM5n7Xa7BVzJZrPYC2iaFjWMgzvBJ+eBQY7Ag4FU48Li4qL77u7ObcwBkwMgFhL68NyjA+cxCUYiETcYvbS0VPt+cnKCCODts+PyiwYQLiaU0+nEv4pqd6z+s0CVoOc8L0T0L32CI0/szYESAAAAAElFTkSuQmCC";

    // ===== 设置弹窗 =====
    function showSettings() {
        var sd = new Window("dialog", "设置");
        sd.orientation = "column"; sd.alignChildren = "center"; sd.spacing = 8; sd.margins = 12;
        function row(label, val) {
            var g = sd.add("group"); g.orientation = "row"; g.alignChildren = "center"; g.spacing = 6;
            g.add("statictext", undefined, label).preferredSize = [100, 18];
            var inp = g.add("edittext", undefined, val.toString()); inp.characters = 6; inp.active = true; inp.justify = "center";
            return inp;
        }
        var maxC = row("最大列数", MAX_COLS);
        var maxR = row("最大行数", MAX_ROWS);
        var defC = row("默认列数", DEFAULT_COLS);
        var defR = row("默认行数", DEFAULT_ROWS);
        var gapStep = row("间距步长", GAP_STEP);
        var bg = sd.add("group"); bg.orientation = "row"; bg.alignChildren = "center"; bg.spacing = 10;
        bg.add("button", undefined, "取消").onClick = function() { sd.close(); };
        bg.add("button", undefined, "保存").onClick = function() {
            MAX_COLS = parseInt(maxC.text) || 20;
            MAX_ROWS = parseInt(maxR.text) || 20;
            DEFAULT_COLS = parseInt(defC.text) || 3;
            DEFAULT_ROWS = parseInt(defR.text) || 3;
            GAP_STEP = parseInt(gapStep.text) || 5;
            sd.close();
        };
        sd.center(); sd.show();
    }

    // ===== 构建非模态面板 =====
    var win = new Window("palette", "om_pv", undefined, {resizeable: true});
    win.orientation = "column"; win.alignChildren = "center"; win.spacing = 8; win.margins = 12;

    win.add("statictext", undefined, "阵列设置");

    var SPIN_BTN_SIZE = 22;

    function makeSpinControl(parent, label, value) {
        var g = parent.add("group"); g.orientation = "row"; g.alignChildren = "center"; g.spacing = 2;
        g.add("statictext", undefined, label).preferredSize = [36, 18];
        var btnDec = g.add("button", undefined, "◀"); btnDec.preferredSize = [SPIN_BTN_SIZE, SPIN_BTN_SIZE];
        var edit = g.add("edittext", undefined, value.toString()); edit.characters = 3; edit.active = true; edit.justify = "center";
        var btnInc = g.add("button", undefined, "▶"); btnInc.preferredSize = [SPIN_BTN_SIZE, SPIN_BTN_SIZE];
        return { g: g, dec: btnDec, inc: btnInc, edit: edit };
    }

    var row1 = win.add("group"); row1.orientation = "row"; row1.alignChildren = "center"; row1.spacing = 15;
    var ctrlW = makeSpinControl(row1, "宽:", DEFAULT_COLS);
    var ctrlH = makeSpinControl(row1, "高:", DEFAULT_ROWS);

    var row2 = win.add("group"); row2.orientation = "row"; row2.alignChildren = "center"; row2.spacing = 15;
    var ctrlGX = makeSpinControl(row2, "间距X:", DEFAULT_GAP_X);
    var ctrlGY = makeSpinControl(row2, "间距Y:", DEFAULT_GAP_Y);

    var info = win.add("statictext", undefined, ""); info.alignment = "center"; info.preferredSize = [200, 18];
    var preview = win.add("statictext", undefined, "", {multiline: true}); preview.alignment = "center"; preview.preferredSize = [200, 100];

    // 图层分配区域 —— 使用按钮样式显示图层名称
    win.add("statictext", undefined, "图层位置分配");
    var layerAssignPanel = win.add("group"); layerAssignPanel.orientation = "column"; layerAssignPanel.alignChildren = "fill"; layerAssignPanel.spacing = 4;

    var maxNameWidth = 0;
    for (var i = 0; i < validLayerNames.length; i++) {
        var w = tw(validLayerNames[i]);
        if (w > maxNameWidth) maxNameWidth = w;
    }
    var layerNameWidth = Math.max(120, maxNameWidth + 10);

    var slotNumberDropdowns = [];

    for (var i = 0; i < validLayers.length; i++) {
        (function(idx) {
            var lr = layerAssignPanel.add("group"); lr.orientation = "row"; lr.alignChildren = "center"; lr.spacing = 6;
            // 按钮样式显示图层名称,不可点击(或点击无动作)
            var nameBtn = lr.add("button", undefined, validLayerNames[idx]);
            nameBtn.preferredSize = [layerNameWidth, 22];
            nameBtn.alignment = ["left", "center"];
            // 可选:禁用按钮使其呈灰色不可点,但为了美观可以不禁用,只是不绑定事件
            // nameBtn.enabled = false; 

            lr.add("statictext", undefined, "→ 位置:").preferredSize = [48, 18];
            var ndd = lr.add("dropdownlist", undefined, []);
            ndd.preferredSize = [60, 22];
            ndd.add("item", "未分配");
            slotNumberDropdowns.push(ndd);
        })(i);
    }

    var optGrp = win.add("group"); optGrp.orientation = "row"; optGrp.alignChildren = "center"; optGrp.spacing = 10;
    var doS = DEFAULT_SCALE, doT = DEFAULT_STRETCH, doF = DEFAULT_FILL_CLIP;
    var scb = optGrp.add("checkbox", undefined, "缩放适配"); scb.value = doS; scb.enabled = !doF;
    var tcb = optGrp.add("checkbox", undefined, "拉伸填充"); tcb.value = doT; tcb.enabled = doS && !doF;
    var fcb = optGrp.add("checkbox", undefined, "填满裁剪"); fcb.value = doF;

    var bottomGrp = win.add("group");
    bottomGrp.orientation = "row";
    bottomGrp.alignChildren = ["left", "center"];
    bottomGrp.alignment = ["fill", "bottom"];
    bottomGrp.spacing = 0;

    var gearFile = createTempPng(gearBase64, "om_pv_gear.png");
    var gearBtn = bottomGrp.add("iconbutton", undefined, gearFile, {style: "toolbutton"});
    gearBtn.size = [26, 26];
    gearBtn.onClick = function() { showSettings(); };
    gearFile.remove();

    var spacer = bottomGrp.add("statictext", undefined, "");
    spacer.alignment = ["fill", "center"];

    var applyBtn = bottomGrp.add("button", undefined, "应用");
    applyBtn.preferredSize = [60, 26];
    applyBtn.alignment = ["right", "center"];

    // ===== 内部函数 =====
    function read() {
        var c = parseInt(ctrlW.edit.text) || 1, r = parseInt(ctrlH.edit.text) || 1;
        if (c > MAX_COLS) { c = MAX_COLS; ctrlW.edit.text = c; }
        if (r > MAX_ROWS) { r = MAX_ROWS; ctrlH.edit.text = r; }
        return {c: Math.max(1, c), r: Math.max(1, r)};
    }
    function readGap() {
        var gx = parseInt(ctrlGX.edit.text) || 0, gy = parseInt(ctrlGY.edit.text) || 0;
        if (gx < 0) gx = 0; if (gy < 0) gy = 0;
        return {gx: gx, gy: gy};
    }
    function setGap(gx, gy) { ctrlGX.edit.text = gx; ctrlGY.edit.text = gy; }

    function updateNumberDropdowns() {
        var v = read();
        var total = v.c * v.r;
        for (var i = 0; i < slotNumberDropdowns.length; i++) {
            var ndd = slotNumberDropdowns[i];
            var prevIdx = ndd.selection ? ndd.selection.index : 0;
            ndd.removeAll();
            ndd.add("item", "未分配");
            for (var n = 1; n <= total; n++) ndd.add("item", n.toString());
            if (prevIdx <= 0 || prevIdx > total) ndd.selection = (i + 1 <= total) ? i + 1 : 0;
            else ndd.selection = prevIdx;
        }
    }

    function upd() {
        var v = read(), dg = (v.c * v.r).toString().length, t = "";
        info.text = v.c + " × " + v.r + "(共 " + (v.c * v.r) + " 格)";
        for (var i = 0; i < v.r; i++, t += "\n") for (var j = 0; j < v.c; j++) {
            var n = (i * v.c + j + 1).toString(); while (n.length < dg) n = " " + n; t += "[" + n + "]";
        }
        preview.text = t;
        updateNumberDropdowns();
        win.layout.resize();
    }

    ctrlW.dec.onClick = function() { var v = read(); if (v.c > 1) { ctrlW.edit.text = v.c - 1; upd(); } };
    ctrlW.inc.onClick = function() { var v = read(); if (v.c < MAX_COLS) { ctrlW.edit.text = v.c + 1; upd(); } };
    ctrlH.dec.onClick = function() { var v = read(); if (v.r > 1) { ctrlH.edit.text = v.r - 1; upd(); } };
    ctrlH.inc.onClick = function() { var v = read(); if (v.r < MAX_ROWS) { ctrlH.edit.text = v.r + 1; upd(); } };

    ctrlGX.dec.onClick = function() { var g = readGap(); if (g.gx >= GAP_STEP) setGap(g.gx - GAP_STEP, g.gy); };
    ctrlGX.inc.onClick = function() { var g = readGap(); setGap(g.gx + GAP_STEP, g.gy); };
    ctrlGY.dec.onClick = function() { var g = readGap(); if (g.gy >= GAP_STEP) setGap(g.gx, g.gy - GAP_STEP); };
    ctrlGY.inc.onClick = function() { var g = readGap(); setGap(g.gx, g.gy + GAP_STEP); };

    ctrlW.edit.onChanging = ctrlH.edit.onChanging = upd;
    upd();

    scb.onClick = function() {
        doS = scb.value;
        tcb.enabled = doS && !doF;
        if (!doS) { tcb.value = false; doT = false; } else doT = tcb.value;
    };
    tcb.onClick = function() { doT = tcb.value; };
    fcb.onClick = function() {
        doF = fcb.value;
        if (doF) { scb.value = true; scb.enabled = false; tcb.value = false; tcb.enabled = false; doS = true; doT = false; }
        else { scb.enabled = true; doS = scb.value; tcb.enabled = doS; doT = tcb.value; }
    };

    applyBtn.onClick = function() {
        var v = read(), gap = readGap();
        var cols = v.c, rows = v.r, gx = gap.gx, gy = gap.gy;
        var totalGapX = gx * (cols - 1);
        var totalGapY = gy * (rows - 1);
        var cellW = (comp.width - totalGapX) / cols;
        var cellH = (comp.height - totalGapY) / rows;

        app.beginUndoGroup("om_pv");
        for (var i = 0; i < slotNumberDropdowns.length; i++) {
            var ndd = slotNumberDropdowns[i];
            if (ndd.selection && ndd.selection.index > 0) {
                var l = validLayers[i];
                var num = parseInt(ndd.selection.text);
                if (num >= 1 && num <= cols * rows) {
                    var col = (num - 1) % cols;
                    var row = Math.floor((num - 1) / cols);
                    var tx = (col + .5) * cellW + col * gx;
                    var ty = (row + .5) * cellH + row * gy;
                    try {
                        if (doS || doF) {
                            var s = src(l), sx = (cellW / s.width) * 100, sy = (cellH / s.height) * 100;
                            if (doF) { var sc = Math.max(sx, sy); l.scale.setValue([sc, sc]); }
                            else if (doT) l.scale.setValue([sx, sy]);
                            else { var sc = Math.min(sx, sy); l.scale.setValue([sc, sc]); }
                        } else l.scale.setValue([100, 100]);
                        l.position.setValue([tx, ty]);
                    } catch (e) {}
                }
            }
        }
        app.endUndoGroup();

        app.beginUndoGroup("om_pv_mask");
        for (var i = 0; i < validLayers.length; i++) validLayers[i].trackMatteType = TrackMatteType.NO_TRACK_MATTE;
        for (var j = comp.numLayers; j >= 1; j--) {
            var cl = comp.layer(j);
            if (cl.name.indexOf("_遮罩") > 0) cl.remove();
        }
        if (doF) {
            for (var i = 0; i < slotNumberDropdowns.length; i++) {
                var ndd = slotNumberDropdowns[i];
                if (ndd.selection && ndd.selection.index > 0) {
                    var l = validLayers[i];
                    var s = comp.layers.addSolid([1, 1, 1], l.name + "_遮罩", cellW, cellH, 1);
                    s.moveBefore(l); s.position.setValue(l.position.value); s.opacity.setValue(100);
                    l.trackMatteType = TrackMatteType.ALPHA;
                }
            }
        }
        app.endUndoGroup();
        app.refresh();
    };

    win.onResizing = win.onResize = function() { win.layout.resize(); };
    win.center();
    win.show();
})();
复制成功

6/27:优化ui,添加间距参数,可自定义默认参数