// bind.jsx — FlatRender data-driven binder (v1 convention). // // Reads a JSON "bind-spec" and writes the user's values INTO a template, then // saves a "bound" .aep that aerender renders. This is the data-driven successor // to the legacy JSXGenerator string-concat approach: render-svc emits the JSON, // this one generic script interprets it. // // Environment: // FR_BIND_AEP — path to the template .aep to open // FR_BIND_SPEC — path to the bind-spec JSON file // FR_BIND_SAVE — path to write the bound .aep (aerender input) // FR_BIND_QUIT — "0" to keep AE open (debug); default quits // // Bind-spec shape (v1): // { // "comp": "frfinal", "duration": 15, "fps": 30, // "colors": [ { "key": "frd_primary", "value": "#3366ff" } ], // frshare text layers // "data": [ { "key": "frd_toggle", "value": "1" } ], // frd_ data layers // "layers": [ // { "key":"frl_title","type":"text","value":"Hello", // "font":"Vazirmatn","size":72,"justify":"CENTER_JUSTIFY" }, // { "key":"frl_logo","type":"media","value":"C:/work/cdn/logo.png" }, // { "key":"frl_music","type":"audio","value":"C:/work/cdn/track.mp3" } // ] // } // // Conventions (see docs/aep-template-convention.md): // - colours live as TEXT layers inside the `frshare` comp; their Source Text IS // the colour value, and template expressions read them → set once, propagate. // - frl_ = editable visible layers (text / media / audio). // - frd_ = data/direction layers (hidden values, RTL companions). (function () { function getenv(n) { try { return $.getenv(n); } catch (e) { return null; } } function readFile(p) { var f = new File(p); f.open("r"); var s = f.read(); f.close(); return s; } function marker(p, txt) { try { var f = new File(p); f.open("w"); f.write(txt); f.close(); } catch (e) {} } var aep = getenv("FR_BIND_AEP"); var specPath = getenv("FR_BIND_SPEC"); var savePath = getenv("FR_BIND_SAVE"); var donePath = (savePath || "bind") + ".done"; var errPath = (savePath || "bind") + ".error"; try { if (aep) app.open(new File(aep)); var proj = app.project; var spec = {}; if (specPath) spec = eval("(" + readFile(specPath) + ")"); // bind-spec JSON // index the spec by layer key for O(1) lookups while walking the project var colorMap = {}, dataMap = {}, layerMap = {}, i; if (spec.colors) for (i = 0; i < spec.colors.length; i++) colorMap[spec.colors[i].key] = spec.colors[i]; if (spec.data) for (i = 0; i < spec.data.length; i++) dataMap[spec.data[i].key] = spec.data[i]; if (spec.layers) for (i = 0; i < spec.layers.length; i++) layerMap[spec.layers[i].key] = spec.layers[i]; // ── helpers ─────────────────────────────────────────────────────────── function setText(layer, value) { var p = layer.property("Source Text"); if (!p) return; var d = p.value; d.text = (value == null) ? "" : ("" + value); p.setValue(d); } function justifyOf(name) { switch (name) { case "LEFT_JUSTIFY": return ParagraphJustification.LEFT_JUSTIFY; case "RIGHT_JUSTIFY": return ParagraphJustification.RIGHT_JUSTIFY; case "FULL_JUSTIFY": return ParagraphJustification.FULL_JUSTIFY; default: return ParagraphJustification.CENTER_JUSTIFY; } } function textBind(layer, item) { var p = layer.property("Source Text"); if (!p) return; var d = p.value; d.text = (item.value == null) ? "" : ("" + item.value); if (item.font) d.font = item.font; if (item.size) d.fontSize = item.size; if (item.justify) d.justification = justifyOf(item.justify); p.setValue(d); // NOTE (v1 TODO): box auto-fit + positionMode 0-8 anchoring go here later. } function mediaBind(layer, item) { if (!item.value) return; try { var footage = proj.importFile(new ImportOptions(new File(item.value))); layer.replaceSource(footage, false); // NOTE (v1 TODO): scale-to-fit the box (legacy mediaBind) goes here later. } catch (e) {} } function bindLayer(layer) { var name = layer.name; if (layerMap[name]) { var it = layerMap[name]; if (it.type === "media" || it.type === "audio") mediaBind(layer, it); else textBind(layer, it); } else if (dataMap[name]) { setText(layer, dataMap[name].value); // frd_ data / direction layers } } // ── walk every comp; bind layers by name (frl_/frd_ live anywhere in the tree) ── for (i = 1; i <= proj.numItems; i++) { var item = proj.item(i); if (!(item instanceof CompItem)) continue; if (item.name === "frshare") { for (var c = 1; c <= item.numLayers; c++) { var L = item.layer(c); if (colorMap[L.name]) setText(L, colorMap[L.name].value); } } for (var l = 1; l <= item.numLayers; l++) bindLayer(item.layer(l)); } // ── comp timing ───────────────────────────────────────────────────────── var comp = null, target = spec.comp || "frfinal"; for (i = 1; i <= proj.numItems; i++) { var x = proj.item(i); if (x instanceof CompItem && x.name === target) { comp = x; break; } } if (comp) { if (spec.duration) comp.duration = spec.duration; if (spec.fps) comp.frameRate = spec.fps; } // ── save the bound project for aerender ─────────────────────────────────── if (savePath) proj.save(new File(savePath)); marker(donePath, "ok comp=" + (comp ? comp.name : "?")); } catch (e) { marker(errPath, "bind error: " + e.toString()); } try { if (getenv("FR_BIND_QUIT") !== "0") app.quit(); } catch (e) {} })();