Find the @keyframes rule that wins for a name, and get the keyframes of each animation on an element the way Chrome’s getKeyframes() reports them, with var() substituted and the element’s own values filled in.
From 1.4.0, the cascade collects @keyframes rules. The library runs no clock and interpolates nothing: it gives a renderer or a test the keyframes, and the timing is up to you.
The page shows a saved-settings message, toast.html, with two animations:
@keyframes slide-in { from { opacity: 0; margin-inline-start: var(--offset) } 60% { opacity: 1 } } @keyframes pulse { 50% { background-color: #f4c95d } } .toast { --offset: -2rem; background-color: #fffaf0; animation: slide-in 300ms ease-out, pulse 1s 300ms 2; }
using RecursiveAI.OneHtml; using RecursiveAI.OneHtml.Css; Document doc = Document.Parse(File.ReadAllBytes("toast.html")); DocumentStyles styles = DocumentStyles.Create(doc); // The rule as written: one block per selector, with its declarations. KeyframesRule rule = styles.GetKeyframesRule("slide-in")!; Console.WriteLine($"@keyframes {rule.Name}: {rule.Blocks.Count} blocks, {rule.Origin}"); foreach (KeyframeBlock block in rule.Blocks) { var declarations = block.Declarations.Select(d => $"{d.Property}: {d.Value}"); Console.WriteLine($" {block.Offset:P0}: {string.Join("; ", declarations)}"); } // Each animation on the element, as Chrome's getKeyframes() reports it. ComputedStyle toast = styles.GetStyle(doc.Root.QuerySelector(".toast")); foreach (ResolvedKeyframes? animation in styles.GetAnimationKeyframes(toast)) { if (animation is null) continue; // `none`, or a name with no @keyframes rule string animated = string.Join(", ", animation.AnimatedProperties); Console.WriteLine($"{animation.Name} ({animation.TimingFunction}): {animated}"); foreach (ResolvedKeyframe frame in animation.Keyframes) { var values = frame.Values.Select(v => $"{v.Property}: {v.Value}"); string mark = frame.IsImplicit ? " (implicit)" : ""; string head = $"{frame.Offset:P0}{mark} {frame.TimingFunction}"; Console.WriteLine($" {head}: {string.Join("; ", values)}"); } }
@keyframes slide-in: 2 blocks, Author 0%: opacity: 0; margin-inline-start: var(--offset) 60%: opacity: 1 slide-in (ease-out): margin-left, opacity 0% ease-out: margin-left: -2rem; opacity: 0 60% ease-out: opacity: 1 100% (implicit) ease-out: margin-left: 0; opacity: 1 pulse (ease): background-color 0% (implicit) ease: background-color: rgb(255, 250, 240) 50% ease: background-color: #f4c95d 100% (implicit) ease: background-color: rgb(255, 250, 240)
How to read it:
GetKeyframesRule(name)returns the winning rule for a name: the unprefixed one over@-webkit-keyframes, then the later origin, the later layer, then the last in the document.KeyframesRuleslists all of them.Blocksare as written: one per selector, withfromas 0 andtoas 1.GetAnimationKeyframes(style)returns one entry per name inanimation-name, in order:nullfornoneor a name with no rule. It’s computed once per style object, so reading it again allocates nothing.ResolveKeyframes(style, name, index)resolves one animation.var(--offset)was replaced from the element’s custom properties.margin-inline-startbecamemargin-left, because the page is left to right.- The 100% keyframe of
slide-inand both ends ofpulsewere added (IsImplicit) with the element’s own value of each property the first or last keyframe leaves out. Like Chrome, they take the animation’s own timing function. - Explicit values are the specified text. Implicit ones carry the computed value, which is why the background reads
rgb(255, 250, 240). Chrome reports computed values everywhere:20pxfor1em,rgb(255, 0, 0)forred.
After a sheet edit, RestyleResult.ChangedKeyframes names the rules that changed. Elements that use them keep their computed style, so read GetAnimationKeyframes again on the new styles. The full rules are in @keyframes.