Copy link to clipboard
Copied
Hi everyone!
We're excited to announce that starting in version 24.5.0x41 of After Effects Beta introduces support for per-character control of text and paragraph styling in Expressions.
This long-requested addition to Expressions builds on recent per-character scripting API updates to unlock powerful text styling capabilities that are able to respond to changes in text content and can be saved and shared as presets. These new options can also streamline template design by making it possible to configure alignment and paragraph styling in a single layer rather than turning on and off multiple synced copies of a layer.
A benefit of per-character control is automatic reflowing of text such as when scaling letters, using superscript, and using a different font, just as you would expect from using substring styling from the Character panel.
Getting Started
Make sure you have your expression engine set to Javascript.
The Expression reference flyout menu provides an overview of available methods in the updated Text > Styling submenu; see the attached document for the full API.
Sample Expressions
Change the font for certain words
Using the string: “Change the fonts of some words” apply the following to the Source Text
text.sourceText.style
.setFont("Montserrat-Light")
.setFont("Gigantic", 0, 6).setFont("Gigantic", 10, 6).setFont("Gigantic", 20)
To customize this for yourself, change the font from the Text Expression flyout menu, and target the character index and amount of characters to make a different font.
Set the first line of a text layer to bold and make it larger
Using the string: “Change the first line of text to bold and make it larger” apply the following to the Source Text
text.sourceText.style.setFontSize(100, 0, 30).setFauxBold(true, 0, 30)
Set superscript for characters
Using the string: “1st and 2nd Place” apply the following to the Source Text
text.sourceText.style.setBaselineOption("superscript",1,2).setBaselineOption("superscript", 9, 2)
Try passing character index values and character amounts to expression sliders and using keyframes to create animation!
Known Issues
Limitations
The style object always returns the first character's style information, not the characters' individual substyles. Therefore, when reading the style of a Text layer containing multiple styles, you must use getStyleAt() with a character index to specify which character's style you need.
We've built an example AEP with the above expressions and few extras to get you started. Please give all of these new expressions a try and let us know how they’re working for you!
Copy link to clipboard
Copied
Excellent work on the API PDF.
I'm trying to think how the Range Definition can be made dynamic, to cater to dynamic Text Inputs?
I was hoping Text/Font Styling would go into the Text Animator Section where it's more feasible to code for such cases plus more users are familiar with Text Animators than Expressions. Is this still feasible; something for the near future?
Great to finally have Paragraph Justification and it will work well in text.sourceText. Thank You! 🙂
Copy link to clipboard
Copied
I think there's a typo here -
Using the string: “Change the fonts of some words” apply the following to the Source Text
text.sourceText.style .setFont("Montserrat-Light") .setFont("Gigantic", 0, 6).setFont("Gigantic", 10, 6).setFont("Gigantic", 20)
------------------------
Copy link to clipboard
Copied
Ah Fantastic. Looking forward to having a play.
Copy link to clipboard
Copied
In playing around with this wonderful new capability, it seems like there should be an easy way to copy another layer's text and all of its per-character styling, but it's not jumping out at me how you would do that. Am I missing something, and if so, can you provide an example?
Copy link to clipboard
Copied
Yes I was hoping this code would just duplicate the text layer and all its per character styling then turn off any fills and just do stroke the text as I hya number of templates that are setup to work that way with the old methods.
const otherLayer = layer("text");
const strokeWPercent = 3; //stroke is percentage of fontsize so that stroke scales with fontsize
const sourceTxt = otherLayer.text.sourceText.value;
const { fillColor, fontSize } = otherLayer.text.sourceText.style
const strokeW = (strokeWPercent/100)*fontSize
const origLayerStyle = otherLayer.text.sourceText.style;
origLayerStyle.setApplyFill(false).setApplyStroke(true).setStrokeColor(fillColor).setStrokeWidth(strokeW).setText(sourceTxt);
This for me would be the ideal solution as it means I don't need to change up my methods too much but others with better expression skills might see a problem with this implementation.
Copy link to clipboard
Copied
Hi @Dan Ebberts,
Unfortunately, this is the limitation.
Currently, style pulls the information at the 0 index, while getStyleAt can get whatever index you ask for (and sliders can be useful for this when going between multiple layers), but trying to get all of the substring ends up with a lot of expressions work on the other text layers. And trying to make that dynamic can be a chore.
Do you have ideas of how you'd like to see substring copying to work? Would love some thoughts to bring back to the team!
Theresa
Copy link to clipboard
Copied
Hmmm... I know this isn't quite right, but it seems like something like this would give you a way to at least copy another layer's styling, character by character:
txt = thisComp.layer("other layer").text.sourceText;
s = style;
for (i = 0; i < txt.length; i++){
sTemp = txt.getStyleAt(i);
s.setStyleAt(i,sTemp);
}
s.setText(txt);
Copy link to clipboard
Copied
Could you possibly add a text animator for font. I know text animators interpolate between values but you could hardcode a font so below 0.5 it's the original font and values above 0.5 it's the text animators font values. Keep the expression API and add the text animators. I think that will please a lot of people as they understand text animators easier than coding expressions. I'd also add a text animator for fill equals yes or no and a text trim paths. With all those you have a ton of possibilities
Copy link to clipboard
Copied
I just wanted to be clear too that i cannot agree more with Dan. We really need a way to refer to another text layer and replicate it through an expression. I've been having a play and think with the ability to set the text and attributes by an index we really need to interrogate the text attribute style at an index. It's in my mind a given we need this to implemet it properly.
And i agrree with him and see the value in a setTextAt() function would be a fantastic way. This is a high priority on my wishlist for this api development
Copy link to clipboard
Copied
Having a lot of fun playing with this so far - I also somewhat second that these would be really effective if they could be treated as text animators are - primarily because "word" index is much easier to use than "character" index for many uses. If you're trying to do things more dynamically. Having to parse and split and such to determine a words start/end point within a character index is somewhat laborious (in my particular implementations)
Copy link to clipboard
Copied
Some expressions that could help with laborious long expressions. Now you just need to fill in laborious Arrays!
3 expressions for those to play with. Note You may not have the same fonts installed as me. SO adjust your font arrays as neccesary. AE uses the Postript Name for Fonts in expressions.
1. Random Font Per Character
2. Set Multi Attributes in an array order split per word/line/delimiter
3. Set Multi Attributes in Radnom order split per line/word/delimiter
Numbers 2 and 3 should help you out @Jesse27755342iou7
// code for AE Beta 24.5 Per character styling
// https://be.net/volition - 2024
// Random Fonts per Character
//
// BEWARE as this is a very slow expression due to the nature of it.
// Could be made slightly more optimal by looking for whitespaces and character returns and grouping them within the iteration, even then i'm not confident in that.
// posterizeTime(0); // uncomment line and changeRate to 0 if you don't want the font changing over time - it should help with processing optimisation
var changeRate = 50; // change every x frames (I recomend adding )
var frequency = changeRate*thisComp.frameDuration;
var timeIndex = changeRate ? Math.floor(time/frequency) : 0;
var regularFontArray = ["KaushanScript-Regular", "Lato-Regular", "VT323-Regular", "GreatVibes-Regular", "FiraMono-Regular", "Exo2-Regular", "Raleway-Regular", "OpenSans-Regular", "Ubuntu-Regular", "Muli-Regular", "PlayfairDisplay-Regular", "Heebo-Regular", "CutiveMono-Regular", "BarlowCondensed-Regular", "ROGFontsv1.6-Regular", "NotoSansJP-Regular", "ArchivoNarrow-Regular", "PTMono-Regular", "FjallaOne-Regular", "JuliusSansOne-Regular", "Satisfy-Regular", "RobotoMono-Regular", "SourceSansPro-Regular", "Domine-Regular"];
var sourceText = text.sourceText.value; // Get the text value as a string
var startIndex = 0;
var expression = "text.sourceText.style"; // Initialize the expression
for (var i = 0; i < sourceText.length; i++) {
var charLength = 1;
seedRandom(index+i+timeIndex*sourceText.length,true);
var randomFont = regularFontArray[Math.floor(Math.random() * regularFontArray.length)];
expression += `.setFont("${randomFont}", ${startIndex}, ${charLength})`;
startIndex += 1;
}
expression += ";"
// Output the final expression
eval(expression)
Ok Now for Number 2
// code for AE Beta 24.5 Per character styling
// https://be.net/volition - 2024
// CHANGES TEXT ATTRIBUTES EVERY WORD/LINE/ OTHER DEPENDING ON DELIMITER ACCORDING TO THE ARRAY ORDERS
// quick guide for those not as familiar with expressions
// choose your delimiter variable [" "] will change per word ["\n","\r"] will change per line you can also use any other delimiters you want
// textProperties & textPropertyValues you can set how ever many or few text attributes and the trange of values it that attribute will cycle through.
// If you don't want the array to cycle through over the animation. you can remove the 3 lines from changeRate on and remove ",index+timeIndex" from output
// WARNING these expressions can slow your system way down if elaborate and lots of text
// posterizeTime(0); // use if not wanting ongoing change of attributes over time also uncomment this line
function getIndicesOf(text, chars) {
var indices = [];
for (var i = 0; i < text.length; i++) {
if (chars.indexOf(text[i]) !== -1) {
indices.push(i);
}
}
return indices;
}
function setTextPerPropsDelimiterArrayOrder(sourceText, delimiter, textProperties, textPropertyValues, indexOffset) {
var delimitIndices = getIndicesOf(sourceText, delimiter);
delimitIndices.push(sourceText.length);
var expression = "text.sourceText.style"; // Initialize the expression
var startIndex = 0;
for (var i = 0; i < textProperties.length; i++) { // step through the attributes
startIndex = 0;
for (var j = 0; j < delimitIndices.length; j++) { // step through the elements for each attribute
var endIndex = delimitIndices[j];
var wordLength = endIndex - startIndex;
if (wordLength > 0) {
var setAttribute = textProperties[i];
var setAttributeValue = textPropertyValues[i][(j+indexOffset)%textPropertyValues[i].length]; // cycle back
if (Array.isArray(setAttributeValue)) { // check if element has more then 1 value it needs to be applied as an array with square brackets
expression += `.${setAttribute}([${setAttributeValue}], ${startIndex}, ${wordLength})`;
} else {
expression += `.${setAttribute}("${setAttributeValue}", ${startIndex}, ${wordLength})`;
}
}
startIndex = endIndex + 1; // Move to next word
}
}
return expression += ";"
}
var sourceText = text.sourceText.value;
var delimiter = [" "];
var textProperties = ["setApplyStroke","setStrokeColor","setStrokeWidth","setFont"];
var textPropertyValues = [[1,1,0,1,1,0],
[[1.0, 0.0, 0.0],[0.0, 1.0, 0.0],[0.0, 0.0, 1.0],[1.0, 1.0, 0.0],[1.0, 0.0, 1.0],[0.0, 1.0, 1.0]],
[1,1.5,2,3,0.5,1,],
["Lato-Regular", "VT323-Regular", "GreatVibes-Regular", "FiraMono-Regular", "Exo2-Regular", "Raleway-Regular", "OpenSans-Regular", "Ubuntu-Regular", "Muli-Regular", "PlayfairDisplay-Regular", "Heebo-Regular", "CutiveMono-Regular", "BarlowCondensed-Regular", "ROGFontsv1.6-Regular", "NotoSansJP-Regular", "ArchivoNarrow-Regular", "PTMono-Regular", "FjallaOne-Regular", "JuliusSansOne-Regular", "Satisfy-Regular", "RobotoMono-Regular", "SourceSansPro-Regular", "Domine-Regular"]
]
var changeRate = 50; // change every x frames (I recomend adding )
var frequency = changeRate*thisComp.frameDuration;
var timeIndex = changeRate ? Math.floor(time/frequency) : 0;
output = setTextPerPropsDelimiterArrayOrder(sourceText, delimiter,textProperties,textPropertyValues,index+timeIndex);
// evaluate the expression string
eval(output)
Last one Hope they help someone
// code for AE Beta 24.5 Per character styling
// https://be.net/volition - 2024
// CHANGES TEXT ATTRIBUTES EVERY WORD/LINE/ OTHER DEPENDING ON DELIMITER DOES SO RANDOMNLY
// quick guide for those not as familiar with expressions
// choose your delimiter variable [" "] will change per word ["\n","\r"] will change per line you can also use any other delimiters you want
// textProperties & textPropertyValues you can set how ever many or few text attributes and the trange of values it that attribute will cycle through.
// If you don't want the array to cycle through over the animation. you can remove the 3 lines from changeRate on and remove ",index+timeIndex" from output
// WARNING these expressions can slow your system way down if elaborate and lots of text
// posterizeTime(0); // use if not wanting ongoing change of attributes over time also uncomment this line
function getIndicesOf(text, chars) {
var indices = [];
for (var i = 0; i < text.length; i++) {
if (chars.indexOf(text[i]) !== -1) {
indices.push(i);
}
}
return indices;
}
function setTextPerPropsDelimiterRandomOrder(sourceText, delimiter, textProperties, textPropertyValues, textSeed = 0) {
var delimitIndices = getIndicesOf(sourceText, delimiter);
delimitIndices.push(sourceText.length);
var expression = "text.sourceText.style"; // Initialize the expression
var startIndex = 0;
for (var i = 0; i < textProperties.length; i++) { // step through the attributes
startIndex = 0;
for (var j = 0; j < delimitIndices.length; j++) { // step through the elements for each attribute
var endIndex = delimitIndices[j];
var wordLength = endIndex - startIndex;
if (wordLength > 0) {
var setAttribute = textProperties[i];
seedRandom(index+i+j+textSeed*sourceText.length,true);
var setAttributeValue = textPropertyValues[i][Math.floor(Math.random() * textPropertyValues[i].length)]; // cycle back
if (Array.isArray(setAttributeValue)) { // check if element has more then 1 value it needs to be applied as an array with square brackets
expression += `.${setAttribute}([${setAttributeValue}], ${startIndex}, ${wordLength})`;
} else {
expression += `.${setAttribute}("${setAttributeValue}", ${startIndex}, ${wordLength})`;
}
}
startIndex = endIndex + 1; // Move to next word
}
}
return expression += ";"
}
var sourceText = text.sourceText.value;
var delimiter = [" "];
var textProperties = ["setApplyStroke","setStrokeColor","setStrokeWidth","setFont","setApplyFill"];
var textPropertyValues = [[1,0],
[[1.0, 0.0, 0.0],[0.0, 1.0, 0.0],[0.0, 0.5, 1.0],[1.0, 1.0, 0.0],[1.0, 0.0, 1.0],[0.0, 1.0, 1.0],[0.7, 0.5, 1.0],[0.8, 0.5, 0.6]],
[1,1.5,2,3,0.5,1,6],
["Lato-Regular", "VT323-Regular", "GreatVibes-Regular", "FiraMono-Regular", "Exo2-Regular", "Raleway-Regular", "OpenSans-Regular", "Ubuntu-Regular", "Muli-Regular", "PlayfairDisplay-Regular", "Heebo-Regular", "CutiveMono-Regular", "BarlowCondensed-Regular", "ROGFontsv1.6-Regular", "NotoSansJP-Regular", "ArchivoNarrow-Regular", "PTMono-Regular", "FjallaOne-Regular", "JuliusSansOne-Regular", "Satisfy-Regular", "RobotoMono-Regular", "SourceSansPro-Regular", "Domine-Regular"],
[1,1,1,1,1,1,1,1,1,0],
[[1.0, 0.0, 0.0],[0.0, 1.0, 0.0],[0.0, 0.5, 1.0],[1.0, 1.0, 0.0],[1.0, 0.0, 1.0],[0.0, 1.0, 1.0],[0.7, 0.5, 1.0],[0.8, 0.5, 0.6]]
]
var changeRate = 50; // change every x frames (I recomend adding )
var frequency = changeRate*thisComp.frameDuration;
var timeIndex = changeRate ? Math.floor(time/frequency) : 0;
output = setTextPerPropsDelimiterRandomOrder(sourceText, delimiter,textProperties,textPropertyValues,index+timeIndex);
// evaluate the expression string
eval(output)
Copy link to clipboard
Copied
@Volition74au Thanks for sharing these. Do these delimiters in your code work with multi-lined texts including Point Text and Paragraph Text layers?
One thing I've struggled with is to get delimiters to work on multi-lined text and I was thinking perhaps the AE Team could provide an Expression function to handle this.
Copy link to clipboard
Copied
The expressions only work where there are delimiters so with a paragraph text opposed to point text there is no delimiter that is in the string to indicate a new line. So no it won't work unless you actually hit a line return in the string. SO yes we would need a alternative object in the api that you could use rather then the chracter index we could use line indexes perhaps. e.g. setFont(character, "Cool Font"0,6).setFont(character, "Cool Font"6,10) for character indexing and setFont(line, "Cool Font"0,2) .setFont(line, "Cool Font"0,2)
or text .sourceStyleCharacter vs text .sourceStyleLine as ways it could be implemented.
Altough i think this expression api is great however, a "font" text animator in the same vain as say a character offset would be a great solution i'd love to see too.
Adobe have not released new animation funtionality for a long time in AFter effects like a new animator. Both that and a text trim paths text animator would make my year.
Copy link to clipboard
Copied
Thanks for your input @Jesse27755342iou7, glad you're having fun playing with the expressions.
We've talked about word and line ranges, but we needed to start somewhere with this large addition to expressions. I'll keep advocating for this addition since I know it would be extremely useful.
Thanks,
Theresa
Copy link to clipboard
Copied
AE's competitors are doing a good job with their Text Tools.
While wishing for more features, I am also wishfully hopeful for a much, much quicker AE Text Tool. I think it will be a good idea if AE's Text Tool Engine can be speed-improved by 3-5x - even a 6-month long project to make this happen will be worth the while. 🙂
The Paragraph Alignment is a great help in helping us be both more efficient in our workflow as well as develop more performant solutions. I'd also like to see Expressions link to the Based On property. This is another area that requires multiple set ups in order to provide user-expected features.
On the new Text Expressions, Word, Line and even Paragraph & ALL Ranges will be great and I am actually assuming these are in the works.
Copy link to clipboard
Copied
Thanks Theresa, per word changes would be extremely useful when making MOGRTs.
Copy link to clipboard
Copied
I finally got the chance to play a bit more with this and I have to say... I love it !
BUT, as I am trying to update my MoglyphFX tool to support these upcoming features, I notice that I can't use the newly added method for Paragraphs with the "setText()" function, which is a big limitation for me since I want to generate text procedurally AND to be able to force it to remain paragraph centered...
So if you try add the following expression to a centered or left aligned text:
s = text.sourceText ;
newStyle = style.setFontSize(200).setFillColor([1,0,1,1]).setJustification('alignRight');
newStyle.setText(s)
You'll see that the text is indeed managing to colorize all the text in pInk and to make its Font Size to 200 px, but that it fails to align the text on the right as requested.
If this is a bug, could we have it fixed soon please ?
If it wasn't well... please considere this as a feature request 🙂
Copy link to clipboard
Copied
(in case you wonder why I just don't use the "sourceText.style.setXXX" method you all use here, this is because I will create dynamic strings for the "s" variable, so it won't remain the initial sourceText value anymore ; it was a placeholder in order to demonstrate my problem)
Copy link to clipboard
Copied
I ran into similar limitations with my project as well, +1!
Copy link to clipboard
Copied
Hi @fremox, and @Jesse27755342iou7
Sorry for the long wait on the answer to this, but paragraph methods need to be called last, so
const s = text.sourceText;
newStyle = style
.setFontSize(200)
.setFillColor([1,0,1,1])
.setText(s)
.setJustification('alignRight');
(with many thanks to @JohnColombo)
-Theresa
Copy link to clipboard
Copied
Oh my... it works!
As nobody was replying to this, i thought it was a bug that would never be fixed...
but boy, now this is a cool thing !
We can FINALLY make responsive .mogrt templates for any paragraph alignement, without having to mess with different separated copies of text layers 🙂
Copy link to clipboard
Copied
I managed to create a cool animation with advanced expressions out of that, am I allowed to publish it on social medias or this all "secret" under NDA?
Copy link to clipboard
Copied
Go for it! This is a Public Beta - nothing is secret. 🙂
Looking forward to take a look at it. I've been busy with my recently-launched Advanced Responsive MoGRTs course. It's always good to see what others are doing.
Copy link to clipboard
Copied
Here you go 🙂
https://x.com/fremox59/status/1831616888649949577