#target aftereffects /* Flicker: export evaluated layer Transform > Opacity without changing the project. Select one layer, set the work area to one second, then File > Scripts > Run Script File. Samples at the composition rate AND 12 / 24 / 30 / 60 fps for the web prototype. No libraries, installation, network calls, or project modifications. */ (function () { function quote(s) { return '"' + String(s).replace(/[\\"\x00-\x1f\u2028\u2029]/g, function (c) { if (c === '"') return '\\"'; if (c === '\\') return '\\\\'; return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); }) + '"'; } // ExtendScript installations do not consistently provide JSON.stringify. function json(value) { if (value === null) return 'null'; if (typeof value === 'string') return quote(value); if (typeof value === 'number') { if (!isFinite(value)) throw new Error('A non-finite value cannot be exported.'); return String(value); } if (typeof value === 'boolean') return value ? 'true' : 'false'; var parts = [], i, key; if (value instanceof Array) { for (i = 0; i < value.length; i++) parts.push(json(value[i])); return '[' + parts.join(',') + ']'; } for (key in value) if (value.hasOwnProperty(key)) parts.push(quote(key) + ':' + json(value[key])); return '{' + parts.join(',\n') + '}'; } function round(value) { return Math.round(value * 1000000) / 1000000; } function opacityAt(prop, time, layerName) { var value = prop.valueAtTime(time, false); // false includes expressions and temporal easing. if (prop.expressionEnabled && prop.expressionError) { throw new Error('Expression error on "' + layerName + '":\n' + prop.expressionError); } if (typeof value !== 'number' || !isFinite(value)) { throw new Error('Invalid opacity on "' + layerName + '" at ' + time + ' seconds.'); } return round(Math.max(0, Math.min(100, value))); } function interpolationName(value) { if (String(value) === String(KeyframeInterpolationType.HOLD)) return 'HOLD'; if (String(value) === String(KeyframeInterpolationType.LINEAR)) return 'LINEAR'; if (String(value) === String(KeyframeInterpolationType.BEZIER)) return 'BEZIER'; throw new Error('Unsupported keyframe interpolation.'); } function easeData(eases) { var values = []; for (var n = 0; n < eases.length; n++) values.push({speed:eases[n].speed, influence:eases[n].influence}); return values; } function originalKeys(prop, origin) { var keys = []; // Capture every original key, including boundary neighbors: they can affect auto-Bezier curves. for (var n = 1; n <= prop.numKeys; n++) keys.push({ time: prop.keyTime(n) - origin, value: prop.keyValue(n), inInterpolation: interpolationName(prop.keyInInterpolationType(n)), outInterpolation: interpolationName(prop.keyOutInterpolationType(n)), inEase: easeData(prop.keyInTemporalEase(n)), outEase: easeData(prop.keyOutTemporalEase(n)), temporalContinuous: prop.keyTemporalContinuous(n), temporalAutoBezier: prop.keyTemporalAutoBezier(n) }); return keys; } var file = null, opened = false; try { var comp = app.project ? app.project.activeItem : null; if (!(comp instanceof CompItem)) throw new Error('Open a composition first.'); var layers = comp.selectedLayers; if (layers.length !== 1) throw new Error('Select exactly one layer to export.'); layers.sort(function (a, b) { return a.index - b.index; }); var start = comp.workAreaStart, duration = comp.workAreaDuration; if (Math.abs(duration - 1) > 0.000001) throw new Error('Set the composition work area to exactly one second.'); duration = 1; var rates = [12, 24, 30, 60], sourceRate = 1 / comp.frameDuration, found = false, i, j, k; for (i = 0; i < rates.length; i++) if (Math.abs(rates[i] - sourceRate) < 0.000001) found = true; if (!found) rates.push(sourceRate); var result = { format: 'flicker-opacity-loops', version: 2, composition: comp.name, sourceFrameRate: sourceRate, loopStartSeconds: start, loopDurationSeconds: duration, sampleTiming: 'relativeSeconds = sampleIndex / fps; loop end excluded; final sample holds until loopDurationSeconds', property: 'Layer Transform > Opacity', units: 'percent', scope: 'Evaluated opacity only; excludes effects, masks, mattes, source alpha, layer switches, and in/out-point visibility.', layers: [] }; for (i = 0; i < layers.length; i++) { var layer = layers[i], transform = layer.property('ADBE Transform Group'); var opacity = transform ? transform.property('ADBE Opacity') : null; if (!opacity) throw new Error('"' + layer.name + '" has no layer Transform > Opacity. Select visual layers only.'); if (opacity.expressionEnabled) throw new Error('Disable the opacity expression and use keyframes instead.'); if (!opacity.numKeys) throw new Error('Add opacity keyframes before exporting.'); var record = { name: layer.name, layerIndex: layer.index, inPointSeconds: layer.inPoint, outPointSeconds: layer.outPoint, coversWorkArea: layer.inPoint <= start && layer.outPoint >= start + duration - 0.000001, expressionEnabled: opacity.expressionEnabled, keyframeTimeOrigin: 'seconds relative to work-area start', keyframes: originalKeys(opacity, start), samples: [] }; for (j = 0; j < rates.length; j++) { var rate = rates[j], count = Math.max(1, Math.ceil(duration * rate - 0.0000001)); var values = []; for (k = 0; k < count; k++) values.push(opacityAt(opacity, start + k / rate, layer.name)); record.samples.push({fps: rate, frameCount: count, opacity: values}); } // Boundary is diagnostic, not an extra playback frame. record.loopBoundary = { startOpacity: opacityAt(opacity, start, layer.name), endOpacity: opacityAt(opacity, start + duration, layer.name) }; result.layers.push(record); } var safeName = comp.name.replace(/[\\\/:*?"<>|]/g, '-') || 'Lightwork'; file = new File(Folder.desktop.fsName + '/' + safeName + '-sequence.json'); file = file.saveDlg('Save your Sequence', 'JSON files:*.json'); if (!file) return; if (!/\.json$/i.test(file.name)) { file = new File(file.fsName + '.json'); if (file.exists && !confirm('Replace the existing file?\n' + file.fsName)) return; } var text = json(result); file.encoding = 'UTF-8'; file.lineFeed = 'Unix'; if (!file.open('w')) throw new Error('Cannot save the file. Check the destination and enable "Allow Scripts to Write Files and Access Network" in Scripting & Expressions settings.'); opened = true; if (!file.write(text)) throw new Error('Could not write the animation data: ' + file.error); var closed = file.close(); opened = false; if (!closed) throw new Error('Could not finish saving the animation data: ' + file.error); alert('Your Sequence is ready.\n\nReturn to lightwork.gallery and upload this JSON.\n\n' + file.fsName); } catch (error) { if (opened && file) { try { file.close(); } catch (ignored) {} } alert('Unable to export your Sequence.\n\n' + (error.message || error.toString())); } })();