/* * SOFT LOOP v3 · AE 2022 (22.x) / ExtendScript * v2 修复:pathShape 追加 Stroke 后返回失效引用 * v3 修复:footSpecs 下标越界(fs[2] → fs[1]);已用模拟 AE 运行时全量跑通 * 1920x1080 / 30fps / 180 frames (6 seconds) / 1 continuous looping shot * Original character motion study inspired by BV1kq4y1c7aJ. * Editable vector artwork, sparse transform/path keys and native effects only. * No frame-by-frame image baking, external assets, plugins or project saving. */ (function () { var CFG = { width: 1920, height: 1080, fps: 30, duration: 6 }; var END = Math.round(CFG.fps * CFG.duration); var C = { blue: "1F4BD2", yellow: "F7DE38", shade: "E5B927", cream: "FFF6B3", ink: "172C5C", ground: "1643BB" }; var comp = null; var notes = []; var builtOK = false; // ---------- Toolkit: time, color and property access ---------- function T(frame) { return frame / CFG.fps; } function rgb(hex) { hex = String(hex).replace("#", ""); return [parseInt(hex.substr(0, 2), 16) / 255, parseInt(hex.substr(2, 2), 16) / 255, parseInt(hex.substr(4, 2), 16) / 255]; } function tg(layer) { return layer.property("ADBE Transform Group"); } function P(l) { return tg(l).property("ADBE Position"); } function S(l) { return tg(l).property("ADBE Scale"); } function A(l) { return tg(l).property("ADBE Anchor Point"); } function O(l) { return tg(l).property("ADBE Opacity"); } function R(l) { return tg(l).property("ADBE Rotate Z"); } function safeEase(prop, keyIndex, speed, influence) { var dim = 1; try { var v = prop.value; if (v && typeof v.length === "number" && v.length > 0) dim = v.length; } catch (e) {} var tries = [dim, 2, 3, 1]; for (var t = 0; t < tries.length; t++) { var arr = [], d; for (d = 0; d < tries[t]; d++) arr.push(new KeyframeEase(speed, influence)); try { prop.setTemporalEaseAtKey(keyIndex, arr, arr); return true; } catch (e2) {} } return false; } function kf(prop, frame, value, influence) { prop.setValueAtTime(T(frame), value); var i = prop.nearestKeyIndex(T(frame)); if (!i || i < 1) return 0; if (Math.abs(prop.keyTime(i) - T(frame)) > 0.001) return 0; try { prop.setInterpolationTypeAtKey(i, KeyframeInterpolationType.BEZIER, KeyframeInterpolationType.BEZIER); } catch (e) {} safeEase(prop, i, 0, influence == null ? 70 : influence); return i; } function effect(layer, matchName, name) { try { var fx = layer.property("ADBE Effect Parade").addProperty(matchName); fx.name = name; return fx; } catch (e) { notes.push("Effect skipped: " + name); return null; } } function blur(layer, amount) { var fx = effect(layer, "ADBE Gaussian Blur 2", "FX-Softness"); if (fx) { try { fx.property("ADBE Gaussian Blur 2-0001").setValue(amount); fx.property("ADBE Gaussian Blur 2-0002").setValue(1); fx.property("ADBE Gaussian Blur 2-0003").setValue(1); } catch (e) { notes.push("Blur settings skipped"); } } } // ---------- Toolkit: editable vector elements ---------- function shape(name) { var l = comp.layers.addShape(); l.name = name; l.inPoint = 0; l.outPoint = T(END); return l; } function group(layer, name) { var g = layer.property("ADBE Root Vectors Group").addProperty("ADBE Vector Group"); g.name = name; return g; } function contents(g) { return g.property("ADBE Vectors Group"); } function ellipse(layer, name, x, y, w, h, hex, opacity) { var g = group(layer, name); var it = contents(g); var e = it.addProperty("ADBE Vector Shape - Ellipse"); e.name = name + "-ellipse"; e.property("ADBE Vector Ellipse Size").setValue([w, h]); e.property("ADBE Vector Ellipse Position").setValue([x, y]); var f = it.addProperty("ADBE Vector Graphic - Fill"); f.name = name + "-fill"; f.property("ADBE Vector Fill Color").setValue(rgb(hex)); f.property("ADBE Vector Fill Opacity").setValue(opacity == null ? 100 : opacity); return g; } // 向同一个矢量组再添加属性后,先前取得的组代理会失效(AE 的经典坑), // 所以路径属性必须先取出来,并在结尾验证一次,失效就按类型重取。 function resolvePathProp(it) { for (var i = 1; i <= it.numProperties; i++) { var c = it.property(i); if (!c) continue; try { if (c.matchName === "ADBE Vector Shape - Group") { return c.property("ADBE Vector Shape"); } } catch (e) {} } return null; } function pathShape(layer, name, vertices, inT, outT, hex, width) { var g = group(layer, name); var it = contents(g); var p = it.addProperty("ADBE Vector Shape - Group"); p.name = name + "-path"; var pathProp = p.property("ADBE Vector Shape"); // 先取,避免后面失效 var sh = new Shape(); sh.vertices = vertices; sh.inTangents = inT; sh.outTangents = outT; sh.closed = false; pathProp.setValue(sh); var st = it.addProperty("ADBE Vector Graphic - Stroke"); st.name = name + "-stroke"; st.property("ADBE Vector Stroke Color").setValue(rgb(hex)); st.property("ADBE Vector Stroke Width").setValue(width); try { st.property("ADBE Vector Stroke Line Cap").setValue(2); } catch (e) {} try { st.property("ADBE Vector Stroke Line Join").setValue(2); } catch (e) {} // 验证引用是否还有效;失效则重取 try { pathProp.value; } catch (e2) { pathProp = resolvePathProp(it); } if (!pathProp) throw new Error("pathShape: 无法取得 " + name + " 的路径属性"); return pathProp; } function ring(name, x, y) { var l = shape(name); var g = group(l, "Expanding-ring"); var it = contents(g); var e = it.addProperty("ADBE Vector Shape - Ellipse"); e.property("ADBE Vector Ellipse Size").setValue([180, 28]); var st = it.addProperty("ADBE Vector Graphic - Stroke"); st.property("ADBE Vector Stroke Color").setValue(rgb(C.cream)); st.property("ADBE Vector Stroke Width").setValue(5); P(l).setValue([x, y]); return l; } function attach(layer, parent) { layer.parent = parent; P(layer).setValue([0, 0]); return layer; } // ---------- Shot 01: body performance, six seconds, seamless at frame 180 ---------- // x, y, rotation, scaleX, scaleY, pupilX, pupilY var poses = [ [0, 960, 445, 0, 100, 100, 0, 0], [10, 960, 462, 0, 109, 91, 5, 2], [21, 960, 430, 0, 96, 106, 8, -2], [30, 1030, 325, -10, 88, 116, 9, -3], [36, 1060, 335, -6, 92, 110, 8, -2], [45, 1090, 455, 5, 115, 84, 5, 4], [51, 1090, 434, -3, 97, 105, 3, -1], [60, 1090, 445, 0, 100, 100, 0, 0], [69, 1090, 462, 0, 108, 92,-5, 2], [78, 1055, 325, 9, 88, 116,-9,-3], [86, 920, 320, -10, 89, 114,-8,-3], [96, 885, 455, -5, 115, 84,-4, 4], [102, 885, 435, 3, 97, 105,-3,-1], [110, 885, 445, 0, 100, 100, 0, 0], [117, 885, 457, 0, 107, 93, 0, 3], [125, 885, 351, -5, 91, 112, 2,-3], [133, 885, 448, 4, 110, 88, 2, 4], [139, 885, 435, -2, 97, 104, 1,-1], [146, 885, 446, 0, 102, 98, 0, 1], [152, 885, 349, 6, 89, 115, 3,-3], [160, 885, 452, -3, 112, 86, 2, 4], [167, 907, 426, 3, 98, 104, 1,-1], [180, 960, 445, 0, 100, 100, 0, 0] ]; // Foot x/y and bend values sampled only at performance poses. // The editable cubic Bezier keys interpolate in AE, not baked frames. var rightFoot = [ [0,1030,780], [10,1030,780], [21,1050,770], [30,1135,685], [36,1138,720], [45,1150,780], [51,1150,780], [60,1150,780], [69,1150,780], [78,1130,770], [86,950,730], [96,955,780], [102,955,780], [110,955,780], [117,955,780], [125,945,750], [133,955,780], [139,955,780], [146,955,780], [152,950,755], [160,955,780], [167,970,778], [180,1030,780] ]; var leftFoot = [ [0,890,780], [10,890,780], [21,880,772], [30,950,748], [36,970,758], [45,1020,780], [51,1020,780], [60,1020,780], [69,1020,780], [78,955,690], [86,800,715], [96,815,780], [102,815,780], [110,815,780], [117,815,780], [125,825,728], [133,815,780], [139,815,780], [146,815,780], [152,825,718], [160,815,780], [167,825,775], [180,890,780] ]; var rightBend = [ [0,0,0,0], [21,10,-5,0], [30,36,-24,0], [36,25,-12,0], [45,0,0,0], [69,0,0,0], [78,15,-3,0], [86,-30,-24,0], [96,0,0,0], [117,0,0,0], [125,12,-10,0], [133,0,0,0], [152,10,-10,0], [160,0,0,0], [180,0,0,0] ]; var leftBend = [ [0,0,0,0], [21,-10,-5,0], [30,-25,-14,0], [45,0,0,0], [69,0,0,0], [78,-36,-24,0], [86,-32,-18,0], [96,0,0,0], [117,0,0,0], [125,-15,-14,0], [133,0,0,0], [146,0,0,0], [152,-12,-18,0], [160,0,0,0], [180,0,0,0] ]; function bodyRig() { var ctrl = comp.layers.addNull(T(END)); ctrl.name = "CTRL-身体"; ctrl.source.width = 280; ctrl.source.height = 280; A(ctrl).setValue([140, 140]); for (var i = 0; i < poses.length; i++) { var q = poses[i]; kf(P(ctrl), q[0], [q[1], q[2]], 65); kf(R(ctrl), q[0], q[3], 65); kf(S(ctrl), q[0], [q[4], q[5]], 65); } ctrl.motionBlur = true; return ctrl; } function footRig(name, data) { var ctrl = comp.layers.addNull(T(END)); ctrl.name = name; ctrl.source.width = 24; ctrl.source.height = 24; A(ctrl).setValue([12, 12]); for (var i = 0; i < data.length; i++) { kf(P(ctrl), data[i][0], [data[i][1], data[i][2]], 65); } return ctrl; } function leg(name, foot, side, bendData) { var l = shape(name); var path = pathShape(l, "Soft-leg", [[0,0],[0,130],[0,260]], [[0,0],[0,0],[0,0]], [[0,0],[0,0],[0,0]], C.ink, 13); // Sparse, editable cubic Bezier poses. No per-frame path baking. for (var i = 0; i < poses.length; i++) { var q = poses[i]; var rad = q[3] * Math.PI / 180; var hx = side * 62 * q[4] / 100; var hy = 108 * q[5] / 100; var top = [q[1] + hx * Math.cos(rad) - hy * Math.sin(rad), q[2] + hx * Math.sin(rad) + hy * Math.cos(rad)]; var end = P(foot).valueAtTime(T(q[0]), false); var bend = 0, lift = 0; for (var j = 0; j < bendData.length; j++) { if (bendData[j][0] === q[0]) { bend = bendData[j][1]; lift = bendData[j][2]; break; } if (bendData[j][0] > q[0]) { var prev = bendData[j - 1], next = bendData[j]; var mix = (q[0] - prev[0]) / (next[0] - prev[0]); mix = mix * mix * (3 - 2 * mix); bend = prev[1] + (next[1] - prev[1]) * mix; lift = prev[2] + (next[2] - prev[2]) * mix; break; } } var dx = end[0] - top[0], dy = end[1] - top[1]; var len = Math.max(1, Math.sqrt(dx * dx + dy * dy)); var c1 = [top[0] + dx / 3 - dy / len * bend, top[1] + dy / 3 + dx / len * bend + lift]; var c2 = [top[0] + dx * 2 / 3 - dy / len * bend, top[1] + dy * 2 / 3 + dx / len * bend + lift]; var sh = new Shape(); sh.vertices = [top, c1, c2, [end[0], end[1]]]; sh.inTangents = [[0,0],[0,0],[0,0],[0,0]]; sh.outTangents = [[c1[0]-top[0],c1[1]-top[1]], [c2[0]-c1[0],c2[1]-c1[1]], [end[0]-c2[0],end[1]-c2[1]],[0,0]]; sh.closed = false; kf(path, q[0], sh, 60); } l.motionBlur = true; return l; } function eyePair(body) { var whites = attach(shape("04-眼白"), body); ellipse(whites, "Left-eye", -43, -31, 43, 58, C.cream, 100); ellipse(whites, "Right-eye", 43, -31, 43, 58, C.cream, 100); var pupils = attach(shape("05-瞳孔-跟随"), body); ellipse(pupils, "Left-pupil", -43, -28, 18, 27, C.ink, 100); ellipse(pupils, "Right-pupil", 43, -28, 18, 27, C.ink, 100); for (var i = 0; i < poses.length; i++) { kf(P(pupils), poses[i][0], [poses[i][6], poses[i][7]], 60); } var mouth = attach(shape("06-微笑"), body); var path = pathShape(mouth, "Smile", [[-21,38],[0,43],[21,38]], [[0,0],[-11,0],[0,0]], [[0,0],[11,0],[0,0]], C.ink, 8); for (var j = 0; j < poses.length; j++) { var smile = new Shape(); var happy = (poses[j][5] < 96) ? 13 : 7; smile.vertices = [[-21,38],[0,43],[21,38]]; smile.inTangents = [[0,0],[-happy,0],[0,0]]; smile.outTangents = [[0,0],[happy,0],[0,0]]; smile.closed = false; path.setValueAtTime(T(poses[j][0]), smile); } for (var k = 1; k <= path.numKeys; k++) safeEase(path, k, 0, 65); } // ---------- Main build: one undo group, no deletion or saving ---------- app.beginSuppressDialogs(); app.beginUndoGroup("SOFT LOOP - Create editable six-second loop"); try { comp = app.project.items.addComp("01-软弹循环-6s", CFG.width, CFG.height, 1, CFG.duration, CFG.fps); comp.bgColor = rgb(C.blue); comp.workAreaStart = 0; comp.workAreaDuration = CFG.duration; comp.motionBlur = true; comp.shutterAngle = 110; comp.shutterPhase = -55; // Sparse timing markers explain the acting without adding visible text. var cues = [[0,"LOOP IN"],[21,"Anticipation"],[30,"Stretch"], [45,"Land / ring"],[78,"Return jump"],[96,"Land"], [125,"Skip 01"],[152,"Skip 02"],[180,"LOOP OUT"]]; for (var c = 0; c < cues.length; c++) { try { comp.markerProperty.setValueAtTime(T(cues[c][0]), new MarkerValue(cues[c][1])); } catch (e) {} } var floor = shape("08-地面弧线"); pathShape(floor, "Ground", [[440,784],[960,784],[1480,784]], [[0,0],[-240,0],[0,0]], [[0,0],[240,0],[0,0]], C.ground, 4); O(floor).setValue(68); var hits = [[45,1120],[96,850],[133,885],[160,850]]; for (var h = 0; h < hits.length; h++) { var ripple = ring("FX-落地冲击环-" + (h + 1), hits[h][1], 780); kf(S(ripple), hits[h][0], [20,20,100], 45); kf(S(ripple), hits[h][0] + 18, [190,190,100], 65); kf(O(ripple), hits[h][0], 0, 45); kf(O(ripple), hits[h][0] + 3, 55, 45); kf(O(ripple), hits[h][0] + 18, 0, 65); } var shadow = shape("07-柔化投影"); ellipse(shadow, "Shadow", 0, 0, 285, 30, C.ink, 26); blur(shadow, 13); for (var i = 0; i < poses.length; i++) { var b = poses[i]; var height = Math.max(0, 445 - b[2]); var spread = 100 + Math.min(32, height / 8); var flatten = Math.max(55, 100 - height / 5); kf(P(shadow), b[0], [b[1] + 22, 780], 60); kf(S(shadow), b[0], [spread, flatten, 100], 60); kf(O(shadow), b[0], Math.max(35, 100 - height / 4), 60); } var body = bodyRig(); var rf = footRig("CTRL-右脚", rightFoot); var lf = footRig("CTRL-左脚", leftFoot); leg("03-右腿-弹性路径", rf, 1, rightBend); leg("02-左腿-弹性路径", lf, -1, leftBend); var footSpecs = [[rf, "09-右脚"], [lf, "11-左脚"]]; for (var f = 0; f < footSpecs.length; f++) { var fs = footSpecs[f]; var footLayer = shape(fs[1]); // 修:fs[2] 越界,应为 fs[1] ellipse(footLayer, "Rounded-foot", 0, 0, 43, 22, C.ink, 100); footLayer.parent = fs[0]; P(footLayer).setValue([0, 0]); } var torso = attach(shape("01-黄色身体"), body); ellipse(torso, "Body", 0, 0, 280, 280, C.yellow, 100); var shadeLayer = attach(shape("12-柔和暗面"), body); ellipse(shadeLayer, "Shade", 38, 76, 150, 90, C.shade, 20); blur(shadeLayer, 9); var shineLayer = attach(shape("10-柔和高光"), body); var shine = ellipse(shineLayer, "Highlight", -61, -68, 84, 45, C.cream, 47); try { shine.property("ADBE Vector Transform Group").property("ADBE Vector Rotation").setValue(-35); } catch (e) {} blur(shineLayer, 5); torso.motionBlur = true; shineLayer.motionBlur = true; eyePair(body); // Arrange controllers above the artwork; they never render. body.moveToBeginning(); rf.moveToBeginning(); lf.moveToBeginning(); var legLayers = [comp.layer("03-右腿-弹性路径"), comp.layer("02-左腿-弹性路径")]; for (var n = 0; n < legLayers.length; n++) { legLayers[n].comment = "Sparse editable cubic Bezier poses. No expressions required."; } body.comment = "Position / Rotation / Scale = acting and squash-stretch."; shadow.comment = "Independent from body parent: stays on the floor."; comp.openInViewer(); comp.time = 0; builtOK = true; } catch (err) { alert("SOFT LOOP stopped: " + err.toString() + "\nLine: " + err.line + "\nYour existing project items were not deleted or saved."); } finally { app.endUndoGroup(); app.endSuppressDialogs(false); if (builtOK && notes.length > 0) { alert("SOFT LOOP created. Optional effect fallback:\n" + notes.join("\n")); } } })();