Here is another useful After Effects script that adds a marker-style highlight to selected text. This script is an improved version of SY HIGHLIGHT and still under progress.
JavaScript
(function (thisObj) {
function SY_Highlight(animRate, overlap) {
app.beginUndoGroup("SY_Highlight");
var comp = app.project.activeItem;
if (!(comp && comp instanceof CompItem)) {
alert("Please select a composition first");
return;
}
var selectedLayers = comp.selectedLayers;
if (selectedLayers.length < 1) {
alert("Please select a text layer");
return;
}
// RTL detection
var rtlRegex = /[\u0600-\u08FF]/;
for (var t = 0; t < selectedLayers.length; t++) {
var origTextLayer = selectedLayers[t];
if (!origTextLayer.property("Source Text")) continue;
var origTextProp = origTextLayer.property("Source Text");
var origDoc = origTextProp.value;
var origText = origDoc.text;
// Create search dialog
var searchDialog = new Window("dialog", "Highlight Text Search");
searchDialog.orientation = "column";
searchDialog.alignChildren = ["left", "top"];
searchDialog.spacing = 10;
searchDialog.margins = 16;
// Display text for reference
var textPreview = searchDialog.add("statictext", undefined, "Text: " + origText.substring(0, 50) + (origText.length > 50 ? "..." : ""));
textPreview.preferredSize.width = 350;
var searchGroup = searchDialog.add("group");
searchGroup.orientation = "row";
searchGroup.alignChildren = ["left", "center"];
searchGroup.add("statictext", undefined, "Text to highlight:");
var searchInput = searchGroup.add("edittext", undefined, "");
searchInput.characters = 30;
// Case sensitivity option
var caseSensitiveCheck = searchDialog.add("checkbox", undefined, "Case sensitive");
caseSensitiveCheck.value = false;
// Whole word option
var wholeWordCheck = searchDialog.add("checkbox", undefined, "Match whole word only");
wholeWordCheck.value = false;
var buttonGroup = searchDialog.add("group");
buttonGroup.orientation = "row";
buttonGroup.alignment = "center";
var cancelBtn = buttonGroup.add("button", undefined, "Cancel");
var okBtn = buttonGroup.add("button", undefined, "OK", {name: "ok"});
cancelBtn.onClick = function() {
searchDialog.close();
return;
};
if (searchDialog.show() != 1) {
// User canceled
return;
}
var searchText = searchInput.text;
if (searchText === "") {
alert("Please enter text to highlight");
return;
}
// Find all matches
var matches = [];
var searchFlags = caseSensitiveCheck.value ? "" : "i";
// Create the appropriate search pattern
var searchPattern;
if (wholeWordCheck.value) {
searchPattern = new RegExp("\\b" + escapeRegExp(searchText) + "\\b", searchFlags);
} else {
searchPattern = new RegExp(escapeRegExp(searchText), searchFlags);
}
var match;
var tempText = origText;
var offset = 0;
while ((match = searchPattern.exec(tempText)) !== null) {
matches.push({
start: match.index + offset,
end: match.index + match[0].length + offset
});
// Move past this match
offset += match.index + 1;
tempText = origText.substring(offset);
}
if (matches.length === 0) {
alert("No matches found for '" + searchText + "'");
return;
}
var dupTextLayer = origTextLayer.duplicate();
dupTextLayer.name = origTextLayer.name + " (Temp Measurement)";
dupTextLayer.enabled = false;
var lines = origText.split(/\r\n|\r|\n/);
var numLines = lines.length;
var fontSize = origDoc.fontSize;
var leading = origDoc.leading;
var xHeight = fontSize / 2;
var shapeLayer = comp.layers.addShape();
var layerPos = origTextLayer.transform.position.value;
shapeLayer.transform.position.setValue(layerPos);
var layerAnchor = origTextLayer.transform.anchorPoint.value;
// Convert text-layer local point to shape-layer space
function textLocalToShape(localPt) {
var compPt = [
layerPos[0] + (localPt[0] - layerAnchor[0]),
layerPos[1] + (localPt[1] - layerAnchor[1])
];
var shapePos = shapeLayer.transform.position.value;
return [compPt[0] - shapePos[0], compPt[1] - shapePos[1]];
}
var currentTime = comp.time;
var offsetTime = origTextLayer.inPoint;
var charCounter = 0;
var highlightCounter = 0;
// Process each text line
for (var i = 0; i < numLines; i++) {
var lineText = lines[i];
var lineLength = lineText.length;
// Determine if we have any matches in this line
var lineStartIdx = charCounter;
var lineEndIdx = lineStartIdx + lineLength;
var lineMatches = [];
for (var m = 0; m < matches.length; m++) {
var match = matches[m];
// Check if match overlaps with this line
if (match.end > lineStartIdx && match.start < lineEndIdx) {
// Calculate character positions relative to line start
var startCharInLine = Math.max(0, match.start - lineStartIdx);
var endCharInLine = Math.min(lineLength, match.end - lineStartIdx);
lineMatches.push({
start: startCharInLine,
end: endCharInLine
});
}
}
// Skip if no matches in this line
if (lineMatches.length === 0) {
charCounter += lineLength + 1; // +1 for newline
continue;
}
// Process each match in this line
for (var j = 0; j < lineMatches.length; j++) {
var lineMatch = lineMatches[j];
var startCharInLine = lineMatch.start;
var endCharInLine = lineMatch.end;
var tempDoc = new TextDocument(origDoc.text);
tempDoc.text = lineText;
dupTextLayer.property("Source Text").setValue(tempDoc);
var rLine = dupTextLayer.sourceRectAtTime(currentTime, false);
var strokeWidth = Math.abs(rLine.top);
// For partial line highlight, we need additional measurements
// First measure with the whole line to get the base position
var fullLineRect = dupTextLayer.sourceRectAtTime(currentTime, false);
// Then measure with text up to start of highlight
var partialStartText = lineText.substring(0, startCharInLine);
tempDoc.text = partialStartText;
dupTextLayer.property("Source Text").setValue(tempDoc);
var rStart = dupTextLayer.sourceRectAtTime(currentTime, false);
// Then measure with text up to end of highlight
var partialEndText = lineText.substring(0, endCharInLine);
tempDoc.text = partialEndText;
dupTextLayer.property("Source Text").setValue(tempDoc);
var rEnd = dupTextLayer.sourceRectAtTime(currentTime, false);
// Calculate partial highlight positions
var startOffset = (partialStartText === "") ? 0 : rStart.width;
var endOffset = (partialEndText === "") ? 0 : rEnd.width;
var topLeft = [fullLineRect.left + startOffset, -xHeight + i * leading];
var topRight = [fullLineRect.left + endOffset, -xHeight + i * leading];
var boxHeight = xHeight;
var extraOffset = (strokeWidth - xHeight) / 2;
var centerY = topLeft[1] + boxHeight / 2 - extraOffset;
// Determine endpoints in text-layer local coordinates
var leftLocal = [topLeft[0], centerY];
var rightLocal = [topRight[0], centerY];
// Convert endpoints to shape-layer space
var shapeLeft = textLocalToShape(leftLocal);
var shapeRight = textLocalToShape(rightLocal);
// RTL check
var isRTL = rtlRegex.test(lineText);
var vertices = isRTL ? [shapeRight, shapeLeft] : [shapeLeft, shapeRight];
highlightCounter++;
var groupName = "Highlight " + highlightCounter;
var shapeGroup = shapeLayer.property("ADBE Root Vectors Group")
.addProperty("ADBE Vector Group");
shapeGroup.name = groupName;
var pathGroup = shapeGroup.property("ADBE Vectors Group")
.addProperty("ADBE Vector Shape - Group");
var myShape = new Shape();
myShape.vertices = vertices;
myShape.inTangents = [[0, 0], [0, 0]];
myShape.outTangents = [[0, 0], [0, 0]];
myShape.closed = false;
pathGroup.property("ADBE Vector Shape").setValue(myShape);
var strokeProp = shapeGroup.property("ADBE Vectors Group")
.addProperty("ADBE Vector Graphic - Stroke");
strokeProp.property("ADBE Vector Stroke Width").setValue(strokeWidth);
strokeProp.property("ADBE Vector Stroke Color").setValue([1, 0.8, 0]);
// Calculate animation based on selected text portion only
var highlightedText = lineText.substring(startCharInLine, endCharInLine);
var nonSpaceCount = highlightedText.replace(/\s/g, "").length;
var totalFrames = Math.round(comp.frameRate / 10 * nonSpaceCount);
var dur = totalFrames / comp.frameRate;
var trimPath = shapeGroup.property("ADBE Vectors Group")
.addProperty("ADBE Vector Filter - Trim");
var trimEndProp = trimPath.property("ADBE Vector Trim End");
var ease = new KeyframeEase(0, 100);
if (animRate === 0) {
trimEndProp.setValueAtTime(offsetTime, 100);
} else {
var effectiveDur = dur * animRate;
trimEndProp.setValueAtTime(offsetTime, 0);
trimEndProp.setValueAtTime(offsetTime + effectiveDur, 100);
trimEndProp.setTemporalEaseAtKey(2, [ease], [ease]);
offsetTime += effectiveDur * overlap;
}
}
charCounter += lineLength + 1; // +1 for newline
}
var tempText = comp.layers.addText();
tempText.name = origTextLayer.name + " Temp Text";
tempText.transform.position.setValue(layerPos);
shapeLayer.parent = tempText;
tempText.transform.scale.setValue(origTextLayer.transform.scale.value);
tempText.transform.rotation.setValue(origTextLayer.transform.rotation.value);
shapeLayer.parent = null;
shapeLayer.inPoint = origTextLayer.inPoint;
shapeLayer.outPoint = origTextLayer.outPoint;
tempText.remove();
dupTextLayer.remove();
shapeLayer.moveAfter(origTextLayer);
shapeLayer.parent = origTextLayer;
shapeLayer.name = origTextLayer.name + " Highlight";
if (app.activeViewer && app.activeViewer.type === ViewerType.VIEWER_COMPOSITION) {
app.activeViewer.setActive();
}
}
app.endUndoGroup();
}
// Helper function to escape special characters in regular expressions
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
function buildUI(thisObj) {
var panel = (thisObj instanceof Panel)
? thisObj
: new Window("palette", "SY_Highlight.jsx", undefined, { resizable: true });
if (panel) {
panel.orientation = "column";
panel.alignChildren = ["center", "top"];
panel.spacing = 5;
panel.margins = 5;
var group1 = panel.add("group", undefined, { name: "group1" });
group1.orientation = "row";
group1.alignChildren = ["center", "center"];
group1.alignment = ["fill", "top"];
group1.add("statictext", undefined, "Animation");
var animSlider = group1.add("slider", undefined, 1, 0, 2);
animSlider.preferredSize.width = 150;
var animValueText = group1.add("edittext", undefined, "1.00");
animValueText.characters = 4;
animSlider.onChanging = function () {
animValueText.text = animSlider.value.toFixed(2);
};
animValueText.onChange = function () {
var val = parseFloat(animValueText.text);
if (isNaN(val)) { val = 1; }
if (val < 0) { val = 0; }
if (val > 2) { val = 2; }
animSlider.value = val;
animValueText.text = val.toFixed(2);
};
var group2 = panel.add("group", undefined, { name: "group2" });
group2.orientation = "row";
group2.alignChildren = ["center", "center"];
group2.alignment = ["fill", "top"];
group2.add("statictext", undefined, "Overlap");
var overlapSlider = group2.add("slider", undefined, 1, 0, 2);
overlapSlider.preferredSize.width = 161;
var overlapValueText = group2.add("edittext", undefined, "1.00");
overlapValueText.characters = 4;
overlapSlider.onChanging = function () {
overlapValueText.text = overlapSlider.value.toFixed(2);
};
overlapValueText.onChange = function () {
var val = parseFloat(overlapValueText.text);
if (isNaN(val)) { val = 1; }
if (val < 0) { val = 0; }
if (val > 2) { val = 2; }
overlapSlider.value = val;
overlapValueText.text = val.toFixed(2);
};
// Add info text
var infoText = panel.add("statictext", undefined,
"This script will highlight specific words or phrases in your text layer.",
{multiline: true});
infoText.preferredSize.width = 250;
var group3 = panel.add("group", undefined, { name: "group3" });
group3.orientation = "row";
group3.alignChildren = ["center", "center"];
group3.alignment = ["fill", "top"];
var createBtn = group3.add("button", undefined, "Highlight!");
createBtn.onClick = function () {
SY_Highlight(animSlider.value, overlapSlider.value);
};
panel.onResizing = panel.onResize = function () {
this.layout.resize();
};
panel.layout.layout(true);
}
return panel;
}
var myPanel = buildUI(thisObj);
if (myPanel instanceof Window) {
myPanel.center();
myPanel.show();
}
})(this);