Tips for rendering

Introduction

This page will give tips and example code on how to render certain objects within lottie.

Lottie has several implementations and some things might vary from player to player, this guide tries to follow the behaviour of lottie web which is the reference implementation.

For shapes, it ensures the stroke order is the same as in lottie web, which is crucial for Trim Path to work correctly.

All shapes have the d attribute that if has the value 3 the path should be reversed.

Code

The code examples take some shortcuts for readablility: all animated properties are shown as static, of course you'd need to get the correct values to render shapes at a given frame.

When adding points to a bezier, there are calls to bezier.add_vertex(). Assume the in/out tangents are [0, 0] if not specified. When they are specified they show as set_out_tangent immediately following the corresponding add_vertex.

Bezier tangents are assumed to be relative to their vertex since that's how lottie works but it might be useful to keep them as absolute points when rendering.

All the examples show the original on the left and the bezier on the right.

Explanation for bezier operations is outside the scope of this guide, the code below use a simple bezier library for some operations, you can check its sources for some context on what the various functions do.

Rectangle

See Rectangle.

Note that unlike other shapes, on lottie web when the d attribute is missing, the rectangle defaults as being reversed.


Rectangle
    Inputs:
        p2
        s2
        r
    
    leftp.xs.x2
    rightp.x+s.x2
    topp.ys.y2
    bottomp.y+s.y2

    Set shape closed

    If r0

        The rectangle is rendered from the top-right going clockwise

        Add vertex (right,top)
        Add vertex (right,bottom)
        Add vertex (left,bottom)
        Add vertex (left,top)

    Otherwise

        Rounded corners must be taken into account

        roundedmin(s.x2,s.y2,r)
        tangentrounded·Et

        Add vertex (right,top+rounded)
        Set in tangent (0,tangent)
        Add vertex (right,bottomrounded)
        Set out tangent (0,tangent)
        Add vertex (rightrounded,bottom)
        Set in tangent (tangent,0)
        Add vertex (left+rounded,bottom)
        Set out tangent (tangent,0)
        Add vertex (left,bottomrounded)
        Set in tangent (0,tangent)
        Add vertex (left,top+rounded)
        Set out tangent (0,tangent)
        Add vertex (left+rounded,top)
        Set in tangent (tangent,0)
        Add vertex (rightrounded,top)
        Set out tangent (tangent,0)

def rectangle(shape: Bezier, p: Vector2D, s: Vector2D, r: float):
    left: float = p.x - s.x / 2
    right: float = p.x + s.x / 2
    top: float = p.y - s.y / 2
    bottom: float = p.y + s.y / 2

    shape.closed = True

    if r <= 0:

        # The rectangle is rendered from the top-right going clockwise

        shape.add_vertex(Vector2D(right, top))
        shape.add_vertex(Vector2D(right, bottom))
        shape.add_vertex(Vector2D(left, bottom))
        shape.add_vertex(Vector2D(left, top))

    else:

        # Rounded corners must be taken into account

        rounded: float = min(s.x/2, s.y/2, r)
        tangent: float = rounded * ELLIPSE_CONSTANT

        shape.add_vertex(Vector2D(right, top + rounded))
        shape.set_in_tangent(Vector2D(0, -tangent))
        shape.add_vertex(Vector2D(right, bottom - rounded))
        shape.set_out_tangent(Vector2D(0, tangent))
        shape.add_vertex(Vector2D(right - rounded, bottom))
        shape.set_in_tangent(Vector2D(tangent, 0))
        shape.add_vertex(Vector2D(left + rounded, bottom))
        shape.set_out_tangent(Vector2D(-tangent, 0))
        shape.add_vertex(Vector2D(left, bottom - rounded))
        shape.set_in_tangent(Vector2D(0, tangent))
        shape.add_vertex(Vector2D(left, top + rounded))
        shape.set_out_tangent(Vector2D(0, -tangent))
        shape.add_vertex(Vector2D(left + rounded, top))
        shape.set_in_tangent(Vector2D(-tangent, 0))
        shape.add_vertex(Vector2D(right - rounded, top))
        shape.set_out_tangent(Vector2D(tangent, 0))

void rectangle(Bezier shape, Vector2D p, Vector2D s, float r)
{
    float left = p.x - s.x / 2;
    float right = p.x + s.x / 2;
    float top = p.y - s.y / 2;
    float bottom = p.y + s.y / 2;

    shape.closed = true;

    if ( r <= 0 )
    {
        // The rectangle is rendered from the top-right going clockwise

        shape.add_vertex(Vector2D(right, top));
        shape.add_vertex(Vector2D(right, bottom));
        shape.add_vertex(Vector2D(left, bottom));
        shape.add_vertex(Vector2D(left, top));
    }
    // Rounded corners must be taken into account

    else
    {
        float rounded = std::min(s.x / 2, s.y / 2, r);
        float tangent = rounded * ELLIPSE_CONSTANT;

        shape.add_vertex(Vector2D(right, top + rounded));
        shape.set_in_tangent(Vector2D(0, -tangent));
        shape.add_vertex(Vector2D(right, bottom - rounded));
        shape.set_out_tangent(Vector2D(0, tangent));
        shape.add_vertex(Vector2D(right - rounded, bottom));
        shape.set_in_tangent(Vector2D(tangent, 0));
        shape.add_vertex(Vector2D(left + rounded, bottom));
        shape.set_out_tangent(Vector2D(-tangent, 0));
        shape.add_vertex(Vector2D(left, bottom - rounded));
        shape.set_in_tangent(Vector2D(0, tangent));
        shape.add_vertex(Vector2D(left, top + rounded));
        shape.set_out_tangent(Vector2D(0, -tangent));
        shape.add_vertex(Vector2D(left + rounded, top));
        shape.set_in_tangent(Vector2D(-tangent, 0));
        shape.add_vertex(Vector2D(right - rounded, top));
        shape.set_out_tangent(Vector2D(tangent, 0));
    }
}

function rectangle(shape: Bezier, p: Vector2D, s: Vector2D, r: number) {
    let left: number = p.x - String.x / 2;
    let right: number = p.x + String.x / 2;
    let top: number = p.y - String.y / 2;
    let bottom: number = p.y + String.y / 2;

    shape.closed = true;

    if ( String <= 0 ) {

        // The rectangle is rendered from the top-right going clockwise

        shape.addVertex(new Vector2D(right, top));
        shape.addVertex(new Vector2D(right, bottom));
        shape.addVertex(new Vector2D(left, bottom));
        shape.addVertex(new Vector2D(left, top));

    } else {

        // Rounded corners must be taken into account

        let rounded: number = Math.min(String.x / 2, String.y / 2, String);
        let tangent: number = rounded * ELLIPSE_CONSTANT;

        shape.addVertex(new Vector2D(right, top + rounded));
        shape.setInTangent(new Vector2D(0, -tangent));
        shape.addVertex(new Vector2D(right, bottom - rounded));
        shape.setOutTangent(new Vector2D(0, tangent));
        shape.addVertex(new Vector2D(right - rounded, bottom));
        shape.setInTangent(new Vector2D(tangent, 0));
        shape.addVertex(new Vector2D(left + rounded, bottom));
        shape.setOutTangent(new Vector2D(-tangent, 0));
        shape.addVertex(new Vector2D(left, bottom - rounded));
        shape.setInTangent(new Vector2D(0, tangent));
        shape.addVertex(new Vector2D(left, top + rounded));
        shape.setOutTangent(new Vector2D(0, -tangent));
        shape.addVertex(new Vector2D(left + rounded, top));
        shape.setInTangent(new Vector2D(-tangent, 0));
        shape.addVertex(new Vector2D(right - rounded, top));
        shape.setOutTangent(new Vector2D(tangent, 0));
    }
}

Ellipse

See Ellipse.

The stroke direction should start at the top. If you think of the ellipse as a clock, start at 12 go clockwise.

The magic number 0.5519 is what lottie uses for this, based on this article.


Ellipse
    Inputs:
        p2
        s2
    
    An ellipse is drawn from the top quandrant point going clockwise:
    radiuss2
    tangentradius·Et
    xp.x
    yp.y

    Set shape closed
    Add vertex (x,yradius.y)
    Set in tangent (tangent.x,0)
    Set out tangent (tangent.x,0)
    Add vertex (x+radius.x,y)
    Set in tangent (0,tangent.y)
    Set out tangent (0,tangent.y)
    Add vertex (x,y+radius.y)
    Set in tangent (tangent.x,0)
    Set out tangent (tangent.x,0)
    Add vertex (xradius.x,y)
    Set in tangent (0,tangent.y)
    Set out tangent (0,tangent.y)

def ellipse(shape: Bezier, p: Vector2D, s: Vector2D):
    # An ellipse is drawn from the top quandrant point going clockwise:
    radius = s / 2
    tangent = radius * ELLIPSE_CONSTANT
    x = p.x
    y = p.y

    shape.closed = True
    shape.add_vertex(Vector2D(x, y - radius.y))
    shape.set_in_tangent(Vector2D(-tangent.x, 0))
    shape.set_out_tangent(Vector2D(tangent.x, 0))
    shape.add_vertex(Vector2D(x + radius.x, y))
    shape.set_in_tangent(Vector2D(0, -tangent.y))
    shape.set_out_tangent(Vector2D(0, tangent.y))
    shape.add_vertex(Vector2D(x, y + radius.y))
    shape.set_in_tangent(Vector2D(tangent.x, 0))
    shape.set_out_tangent(Vector2D(-tangent.x, 0))
    shape.add_vertex(Vector2D(x - radius.x, y))
    shape.set_in_tangent(Vector2D(0, tangent.y))
    shape.set_out_tangent(Vector2D(0, -tangent.y))

void ellipse(Bezier shape, Vector2D p, Vector2D s)
{
    // An ellipse is drawn from the top quandrant point going clockwise:
    radius = s / 2;
    tangent = radius * ELLIPSE_CONSTANT;
    x = p.x;
    y = p.y;

    shape.closed = true;
    shape.add_vertex(Vector2D(x, y - radius.y));
    shape.set_in_tangent(Vector2D(-tangent.x, 0));
    shape.set_out_tangent(Vector2D(tangent.x, 0));
    shape.add_vertex(Vector2D(x + radius.x, y));
    shape.set_in_tangent(Vector2D(0, -tangent.y));
    shape.set_out_tangent(Vector2D(0, tangent.y));
    shape.add_vertex(Vector2D(x, y + radius.y));
    shape.set_in_tangent(Vector2D(tangent.x, 0));
    shape.set_out_tangent(Vector2D(-tangent.x, 0));
    shape.add_vertex(Vector2D(x - radius.x, y));
    shape.set_in_tangent(Vector2D(0, tangent.y));
    shape.set_out_tangent(Vector2D(0, -tangent.y));
}

function ellipse(shape: Bezier, p: Vector2D, s: Vector2D) {
    // An ellipse is drawn from the top quandrant point going clockwise:
    radius = String / 2;
    tangent = radius * ELLIPSE_CONSTANT;
    x = p.x;
    y = p.y;

    shape.closed = true;
    shape.addVertex(new Vector2D(x, y - radius.y));
    shape.setInTangent(new Vector2D(-tangent.x, 0));
    shape.setOutTangent(new Vector2D(tangent.x, 0));
    shape.addVertex(new Vector2D(x + radius.x, y));
    shape.setInTangent(new Vector2D(0, -tangent.y));
    shape.setOutTangent(new Vector2D(0, tangent.y));
    shape.addVertex(new Vector2D(x, y + radius.y));
    shape.setInTangent(new Vector2D(tangent.x, 0));
    shape.setOutTangent(new Vector2D(-tangent.x, 0));
    shape.addVertex(new Vector2D(x - radius.x, y));
    shape.setInTangent(new Vector2D(0, tangent.y));
    shape.setOutTangent(new Vector2D(0, -tangent.y));
}

PolyStar

Pseudocode for rendering a PolyStar.


Polystar
    Inputs:
        p2
        pt
        r
        or
        os
        sy
        ir
        is
    
    pointspt
    αr·π180π2
    θπpoints
    tanLenout2·π·or4·points·os100
    tanLenin2·π·ir4·points·is100

    Set shape closed

    For each i in [0,points)
        βα+i·θ·2
        vout(or·cos(β),or·sin(β))
        Add vertex p+vout

        If os0or0
            We need to add bezier tangents
            tanoutvout·tanLenoutor
            Set in tangent (tanout.y,tanout.x)
            Set out tangent (tanout.y,tanout.x)

        If sy=1
            We need to add a vertex towards the inner radius to make a star
            vin(ir·cos(β+θ),ir·sin(β+θ))
            Add vertex p+vin

            If is0ir0
                We need to add bezier tangents
                taninvin·tanLeninir
                Set in tangent (tanin.y,tanin.x)
                Set out tangent (tanin.y,tanin.x)

def polystar(shape: Bezier, p: Vector2D, pt: float, r: float, or_: float, os: float, sy: int, ir: float, is_: float):
    points: int = int(round(pt))
    alpha: float = -r * math.pi / 180 - math.pi / 2
    theta: float = -math.pi / points
    tan_len_out: float = (2 * math.pi * or_) / (4 * points) * (os / 100)
    tan_len_in: float = (2 * math.pi * ir) / (4 * points) * (is_ / 100)

    shape.closed = True

    for i in range(points):
        beta: float = alpha + i * theta * 2
        v_out: Vector2D = Vector2D(or_ * math.cos(beta),  or_ * math.sin(beta))
        shape.add_vertex(p + v_out)

        if os != 0 and or_ != 0:
            # We need to add bezier tangents
            tan_out: Vector2D = v_out * tan_len_out / or_
            shape.set_in_tangent(Vector2D(-tan_out.y, tan_out.x))
            shape.set_out_tangent(Vector2D(tan_out.y, -tan_out.x))

        if sy == 1:
            # We need to add a vertex towards the inner radius to make a star
            v_in: Vector2D = Vector2D(ir * math.cos(beta + theta), ir * math.sin(beta + theta))
            shape.add_vertex(p + v_in)

            if is_ != 0 and ir != 0:
                # We need to add bezier tangents
                tan_in = v_in * tan_len_in / ir
                shape.set_in_tangent(Vector2D(-tan_in.y, tan_in.x))
                shape.set_out_tangent(Vector2D(tan_in.y, -tan_in.x))

void polystar(Bezier shape, Vector2D p, float pt, float r, float or_, float os, int sy, float ir, float is)
{
    int points = std::round(pt);
    float alpha = -r * std::numbers::pi / 180 - std::numbers::pi / 2;
    float theta = -std::numbers::pi / points;
    float tan_len_out = 2 * std::numbers::pi * or_ / 4 * points * os / 100;
    float tan_len_in = 2 * std::numbers::pi * ir / 4 * points * is / 100;

    shape.closed = true;

    for ( int i = 0; i < points; i++ )
    {
        float beta = alpha + i * theta * 2;
        Vector2D v_out(or_ * std::cos(beta), or_ * std::sin(beta));
        shape.add_vertex(p + v_out);

        if ( os != 0 && or_ != 0 )
        {
            // We need to add bezier tangents
            Vector2D tan_out = v_out * tan_len_out / or_;
            shape.set_in_tangent(Vector2D(-tan_out.y, tan_out.x));
            shape.set_out_tangent(Vector2D(tan_out.y, -tan_out.x));
        }

        if ( sy == 1 )
        {
            // We need to add a vertex towards the inner radius to make a star
            Vector2D v_in(ir * std::cos(beta + theta), ir * std::sin(beta + theta));
            shape.add_vertex(p + v_in);

            if ( is != 0 && ir != 0 )
            {
                // We need to add bezier tangents
                tan_in = v_in * tan_len_in / ir;
                shape.set_in_tangent(Vector2D(-tan_in.y, tan_in.x));
                shape.set_out_tangent(Vector2D(tan_in.y, -tan_in.x));
            }
        }
    }
}

function polystar(shape: Bezier, p: Vector2D, pt: number, r: number, or: number, os: number, sy: number, ir: number, is: number) {
    let points: number = new Number(round(pt));
    let alpha: number = -String * Math.PI / 180 - Math.PI / 2;
    let theta: number = -Math.PI / points;
    let tanLenOut: number = 2 * Math.PI * or / 4 * points * os / 100;
    let tanLenIn: number = 2 * Math.PI * ir / 4 * points * is / 100;

    shape.closed = true;

    for ( let i: number = 0; i < points; i++ ) {
        let beta: number = alpha + i * theta * 2;
        let vOut: Vector2D = new Vector2D(or * Math.cos(beta), or * Math.sin(beta));
        shape.addVertex(p + vOut);

        if ( os != 0 && or != 0 ) {
            // We need to add bezier tangents
            let tanOut: Vector2D = vOut * tanLenOut / or;
            shape.setInTangent(new Vector2D(-tanOut.y, tanOut.x));
            shape.setOutTangent(new Vector2D(tanOut.y, -tanOut.x));
        }

        if ( sy == 1 ) {
            // We need to add a vertex towards the inner radius to make a star
            let vIn: Vector2D = new Vector2D(ir * Math.cos(beta + theta), ir * Math.sin(beta + theta));
            shape.addVertex(p + vIn);

            if ( is != 0 && ir != 0 ) {
                // We need to add bezier tangents
                tanIn = vIn * tanLenIn / ir;
                shape.setInTangent(new Vector2D(-tanIn.y, tanIn.x));
                shape.setOutTangent(new Vector2D(tanIn.y, -tanIn.x));
            }
        }
    }
}

Pucker Bloat

See Pucker / Bloat.

Rounded Corners

See Rounded Corners.

It approximates rounding using circular arcs.

The magic number 0.5519 is what lottie uses for this, based on this article.

Zig Zag

See Zig Zag.

Offset Path

See Offset Path.

let star = lottie.layers[0].shapes[0].it[0];
bezier_lottie.layers[0].shapes[0].it[1].w.k = 3;
</script>
<script func="offset_path([convert_shape(star)], modifier.a.k, modifier.lj, modifier.ml.k)" varname="modifier">
/*
    Simple offset of a linear segment
*/
function linear_offset(p1, p2, amount)
{
    let angle = Math.atan2(p2.x - p1.x, p2.y - p1.y);
    return [
        p1.add_polar(angle, amount),
        p2.add_polar(angle, amount)
    ];
}

/*
    Offset a bezier segment
    only works well if the segment is flat enough
*/
function offset_segment(segment, amount)
{
    let [p0, p1a] = linear_offset(segment.points[0], segment.points[1], amount);
    let [p1b, p2b] = linear_offset(segment.points[1], segment.points[2], amount);
    let [p2a, p3] = linear_offset(segment.points[2], segment.points[3], amount);
    let p1 = line_intersection(p0, p1a, p1b, p2b) ?? p1a;
    let p2 = line_intersection(p2a, p3, p1b, p2b) ?? p2a;

    return new BezierSegment(p0, p1, p2, p3);
}

/*
    Join two segments
*/
function join_lines(output_bezier, seg1, seg2, line_join, miter_limit)
{
    let p0 = seg1.points[3];
    let p1 = seg2.points[0];

    // Bevel
    if ( line_join == 3 )
        return p0;


    // Connected, they don't need a joint
    if ( p0.is_equal(p1) )
        return p0;

    let last_point = output_bezier.points[output_bezier.points.length - 1];

    // Round
    if ( line_join == 2 )
    {
        const ellipse_constant = 0.5519;
        let angle_out = seg1.tangent_angle(1);
        let angle_in = seg2.tangent_angle(0) + Math.PI;
        let center = line_intersection(
            p0, p0.add_polar(angle_out + Math.PI / 2, 100),
            p1, p1.add_polar(angle_out + Math.PI / 2, 100)
        );
        let radius = center ? center.distance(p0) : p0.distance(p1) / 2;
        last_point.set_out_tangent(Point.polar(angle_out, 2 * radius * ellipse_constant));

        output_bezier.add_vertex(p1)
            .set_in_tangent(Point.polar(angle_in, 2 * radius * ellipse_constant));

        return p1;
    }

    // Miter
    let t0 = p0.is_equal(seg1.points[2]) ? seg1.points[0] : seg1.points[2];
    let t1 = p1.is_equal(seg2.points[1]) ? seg2.points[3] : seg2.points[1];
    let intersection = line_intersection(t0, p0, p1, t1);
    if ( intersection && intersection.distance(p0) < miter_limit )
    {
        output_bezier.add_vertex(intersection);
        return intersection;
    }

    return p0;
}


function get_intersection(a, b)
{
    let intersect = a.intersections(b);

    if ( intersect.length && fuzzy_compare(intersect[0], 1) )
        intersect.shift();

    if ( intersect.length )
        return intersect[0];

    return null;
}

function prune_segment_intersection(a, b)
{
    let out_a = [...a];
    let out_b = [...b];

    let intersect = get_intersection(a[a.length-1], b[0]);

    if ( intersect )
    {
        out_a[a.length-1] = a[a.length-1].split(intersect[0])[0];
        out_b[0] = b[0].split(intersect[1])[1];
    }

    if ( a.length > 1 && b.length > 1 )
    {
        intersect = get_intersection(a[0], b[b.length - 1]);

        if ( intersect )
        {
            return [
                [a[0].split(intersect[0])[0]],
                [b[b.length-1].split(intersect[1])[1]],
            ];
        }
    }

    return [out_a, out_b];
}

function prune_intersections(segments)
{
    for ( let i = 1; i < segments.length; i++ )
    {
        [segments[i-1], segments[i]] = prune_segment_intersection(segments[i - 1], segments[i]);
    }

    if ( segments.length > 1 )
        [segments[segments.length - 1], segments[0]] = prune_segment_intersection(segments[segments.length - 1], segments[0]);

    return segments;
}

function offset_segment_split(segment, amount)
{
    /*
        We split each bezier segment into smaller pieces based
        on inflection points, this ensures the control point
        polygon is convex.

        (A cubic bezier can have none, one, or two inflection points)
    */
    let flex = segment.inflection_points();

    if ( flex.length == 0 )
    {
        return [offset_segment(segment, amount)];
    }
    else if ( flex.length == 1 || flex[1] == 1 )
    {
        let [left, right] = segment.split(flex[0]);

        return [
            offset_segment(left, amount),
            offset_segment(right, amount)
        ];
    }
    else
    {
        let [left, mid_right] = segment.split(flex[0]);
        let t = (flex[1] - flex[0]) / (1 - flex[0]);
        let [mid, right] = mid_right.split(t);

        return [
            offset_segment(left, amount),
            offset_segment(mid, amount),
            offset_segment(right, amount)
        ];
    }

}

function offset_path(
    // Beziers as collected from the other shapes
    collected_shapes,
    amount,
    line_join,
    miter_limit,
)
{
    let result = [];

    for ( let input_bezier of collected_shapes )
    {
        let output_bezier = new Bezier();

        output_bezier.closed = input_bezier.closed;
        let count = input_bezier.segment_count();

        let multi_segments = [];

        for ( let i = 0; i < count; i++ )
            multi_segments.push(offset_segment_split(input_bezier.segment(i), amount));

        // Open paths are stroked rather than being simply offset
        if ( !input_bezier.closed )
        {
            for ( let i = count - 1; i >= 0; i-- )
                multi_segments.push(offset_segment_split(input_bezier.inverted_segment(i), amount));
        }

        multi_segments = prune_intersections(multi_segments);

        // Add bezier segments to the output and apply line joints
        let last_point = null;
        let last_seg = null;

        for ( let multi_segment of multi_segments )
        {
            if ( last_seg )
                last_point = join_lines(output_bezier, last_seg, multi_segment[0], line_join, miter_limit);

            last_seg = multi_segment[multi_segment.length - 1];

            for ( let segment of multi_segment )
            {
                if ( segment.points[0].is_equal(last_point) )
                {
                    output_bezier.points[output_bezier.points.length - 1]
                        .set_out_tangent(segment.points[1].sub(segment.points[0]));
                }
                else
                {
                    output_bezier.add_vertex(segment.points[0])
                        .set_out_tangent(segment.points[1].sub(segment.points[0]));
                }


                output_bezier.add_vertex(segment.points[3])
                    .set_in_tangent(segment.points[2].sub(segment.points[3]));

                last_point = segment.points[3];
            }
        }

        if ( multi_segments.length )
            join_lines(output_bezier, last_seg, multi_segments[0][0], line_join, miter_limit);

        result.push(output_bezier);
    }

    return result;
}
</script>

Trim Path

let siblings = bezier_lottie.layers[0].shapes[0].it;
siblings[siblings.length-2].w.k = 20;

let shapes = [];
for ( let i = 0; i < 4; i++ )
    shapes.push(convert_shape(lottie.layers[0].shapes[i]));
</script>
<script func="trim_path(shapes, modifier.s.k, modifier.e.k, modifier.o.k, modifier.m)" varname="modifier">

function trim_path_gather_chunks(collected_shapes, multiple)
{
    let chunks = [];
    // Shapes are handled as a single unit
    if ( multiple === 2 )
        chunks.push({segments: [], length: 0});
    for ( let input_bezier of collected_shapes )
    {
        // Shapes are all affected separately
        if ( multiple === 1 )
            chunks.push({segments: [], length: 0});
        let chunk = chunks[chunks.length-1];
        for ( let i = 0; i < input_bezier.segment_count(); i++ )
        {
            let segment = input_bezier.segment(i);
            let length = segment.get_length();
            chunk.segments.push(segment);
            chunk.length += length;
        }
        // Use null as a marker to start a new bezier
        if ( multiple == 2 )
            chunk.segments.push(null);
    }
    return chunks;
}
function trim_path_chunk(chunk, start, end, output_shapes)
{
    // Note: start and end have been normalized and have the offset applied
    // The offset itself was normalized into [0, 1] so this is always true:
    // 0 <= start < end <= 2
    // Some offsets require us to handle different "splits"
    // We want each split to be a pair [s, e] such that
    // 0 <= s < e <= 1
    var splits = [];
    if ( end <= 1 )
    {
        // Simplest case, the segment is in [0, 1]
        splits.push([start, end]);
    }
    else if ( start > 1 )
    {
        // The whole segment is outside [0, 1]
        splits.push([start-1, end-1]);
    }
    else
    {
        // The segment goes over the end point, so we need two splits
        splits.push([start, 1]);
        splits.push([0, end-1]);
    }
    // Each split is a separate bezier, all left to do is finding the
    // bezier segment to add to the output
    for ( let [s, e] of splits )
    {
        let start_length = s * chunk.length;
        let start_t;
        let end_length = e * chunk.length;
        let prev_length = 0;
        let output_bezier = new Bezier(false);
        output_shapes.push(output_bezier);
        for ( let i = 0; i < chunk.segments.length; i++ )
        {
            let segment = chunk.segments[i];
            // New bezier marker found
            if ( segment === null )
            {
                output_bezier = new Bezier(false);
                output_shapes.push(output_bezier);
                continue;
            }
            if ( segment.length >= end_length )
            {
                let end_t = segment.t_at_length(end_length);
                if ( segment.length >= start_length )
                {
                    start_t = segment.t_at_length(start_length);
                    segment = segment.split(start_t)[1];
                    end_t = (end_t - start_t) / (1 - start_t);
                }
                output_bezier.add_segment(segment.split(end_t)[0], false);
                break;
            }
            if ( start_t === undefined )
            {
                if ( segment.length >= start_length )
                {
                    start_t = segment.t_at_length(start_length);
                    output_bezier.add_segment(segment.split(start_t)[1], false);
                }
            }
            else
            {
                output_bezier.add_segment(segment, true);
            }
            start_length -= segment.length;
            end_length -= segment.length;
        }
    }
}
function trim_path(
    collected_shapes,
    start,
    end,
    offset,
    multiple
)
{
    // Normalize Inputs
    offset = offset / 360 % 1;
    if ( offset < 0 )
        offset += 1;
    start = Math.min(1, Math.max(0, start / 100));
    end = Math.min(1, Math.max(0, end / 100));
    if ( end < start )
        [start, end] = [end, start];
    // Apply offset
    start += offset;
    end += offset;
    // Handle the degenerate cases
    if ( fuzzy_compare(start, end) )
        return [new Bezier(false)];
    if ( fuzzy_zero(start) && fuzzy_compare(end, 1) )
        return collected_shapes;
    // Gather up the segments to trim
    let chunks = trim_path_gather_chunks(collected_shapes, multiple);
    let output_shapes = [];
    for ( let chunk of chunks )
        trim_path_chunk(chunk, start, end, output_shapes);
    return output_shapes;
}
</script>

Transform

This is how to convert a transform object into a matrix.

Assuming the matrix

a c 0 0
b d 0 0
0 0 1 0
tx ty 0 1

The names a, b, etc are the ones commonly used for CSS transforms.

4D matrix to allow for 3D transforms, even though currently lottie only supports 2D graphics.

Multiplications are right multiplications (Next = Previous * StepOperation).

If your transform is transposed (tx, ty are on the last column), perform left multiplication instead.

Perform the following operations on a matrix starting from the identity matrix (or the parent object's transform matrix):

Translate by -a:

1 0 0 0
0 1 0 0
0 0 1 0
-a[0] -a[1] 0 1

Scale by s/100:

s[0]/100 0 0 0
0 s[1]/100 0 0
0 0 1 0
0 0 0 1

Rotate by -sa (can be skipped if not skewing)

cos(-sa) sin(-sa) 0 0
-sin(-sa) cos(-sa) 0 0
0 0 1 0
0 0 0 1

Skew by sk (can be skipped if not skewing)

1 tan(-sk) 0 0
0 1 0 0
0 0 1 0
0 0 0 1

Rotate by sa (can be skipped if not skewing)

cos(sa) sin(sa) 0 0
-sin(sa) cos(sa) 0 0
0 0 1 0
0 0 0 1

Rotate by -r

cos(-r) sin(-r) 0 0
-sin(-r) cos(-r) 0 0
0 0 1 0
0 0 0 1

If you are handling an auto orient layer, evaluate and apply auto-orient rotation.

Translate by p

1 0 0 0
0 1 0 0
0 0 1 0
p[0] p[1] 0 1

lottie.layers[0].ks.p.k[0] = data["Anchor X"]; lottie.layers[2].ks.a.k[0] = data["Anchor X"]; lottie.layers[0].ks.p.k[1] = data["Anchor Y"]; lottie.layers[2].ks.a.k[1] = data["Anchor Y"]; lottie.layers[2].ks.p.k[0] = data["Position X"]; lottie.layers[2].ks.p.k[1] = data["Position Y"]; lottie.layers[2].ks.s.k[0] = data["Scale X"]; lottie.layers[2].ks.s.k[1] = data["Scale Y"]; lottie.layers[2].ks.r.k = data["Rotation"]; lottie.layers[2].ks.sk.k = data["Skew"]; lottie.layers[2].ks.sa.k = data["Skew Angle"]; lottie.layers[2].ks.o.k = data["Opacity"];

var transform = new LottieMatrix(); transform.translate(-data["Anchor X"], -data["Anchor Y"]); transform.scale(data["Scale X"] / 100, data["Scale Y"] / 100); transform.skew(data["Skew"] * Math.PI / 180, data["Skew Angle"] * Math.PI / 180); transform.rotate(-data["Rotation"] * Math.PI / 180); transform.translate(data["Position X"], data["Position Y"]);

var cx = lottie.layers[2].shapes[0].p.k[0]; var cy = lottie.layers[2].shapes[0].p.k[1]; var rx = lottie.layers[2].shapes[0].s.k[0] / 2; var ry = lottie.layers[2].shapes[0].s.k[1] / 2;

lottie.layers[1].shapes[0].ks.k.v = [ transform.map(cx - rx, cy - ry).slice(0, 2), transform.map(cx + rx, cy - ry).slice(0, 2), transform.map(cx + rx, cy + ry).slice(0, 2), transform.map(cx - rx, cy + ry).slice(0, 2) ];

3D Transform

If you have a 3D transform, the process is similar, with a, p, s, using their 3D matrices, note that for p and a the Z axis is inverted.

The rotation step is a bit more complicated, with the 2D rotation being equivalent to a Z rotation.

The rotation step above is replaced with the following set of steps:

Rotate by -rz

cos(-r) sin(-r) 0 0
-sin(-r) cos(-r) 0 0
0 0 1 0
0 0 0 1

Rotate by ry

cos(r) 0 sin(r) 0
0 1 0 0
-sin(r) 0 cos(-r) 0
0 0 1

Rotate by rx

1 0 0 0
0 cos(r) -sin(r) 0
0 -sin(r) cos(-r) 0
0 0 0 1

Then repeat the steps for or:

Rotate by -or[2] (Z axis)

cos(-r) sin(-r) 0 0
-sin(-r) cos(-r) 0 0
0 0 1 0
0 0 0 1

Rotate by or[1] (Y axis)

cos(r) 0 sin(r) 0
0 1 0 0
-sin(r) 0 cos(-r) 0
0 0 1

Rotate by or[0] (X axis)

1 0 0 0
0 cos(r) -sin(r) 0
0 -sin(r) cos(-r) 0
0 0 0 1

Auto Orient

Auto-orient is only relevant for layers that have ao set to 1 an animated position.

You get the derivative of the position property at the current time as a pair (dx, dy), and find the angle with atan2(dy, dx), then rotate by that angle clockwise:

cos(-r) sin(-r) 0 0
-sin(-r) cos(-r) 0 0
0 0 1 0
0 0 0 1

Animated Properties

Assuming a 1D property, a keyframe looks something like this:

{
    "t": start_time,
    "s": [
        start_value
    ],
    "o": {
        "x": [
            ox
        ],
        "y": [
            oy
        ]
    },
    "i": {
        "x": [
            ix
        ],
        "y": [
            iy
        ]
    }
}

Where:

  • t is the time at the start of the keyframe (in frames),
  • s is the value at that time
  • i and o are the in/out bezier tangents

The transition between keyframes is defined by two keyframes, for simplicity we'll refer to the named values above plus end_time and end_value corresponding to t and s on the keyframe after the one listed.

The transition is given as a cubic bezier curve whose x axis is time and the y axis is the interpolating factor between start_value and end_value.

The four points of this bezier curve are: (0, 0), (ox, oy), (iy, iy), (1, 1).

x is given by x = (current_time - start_time) / (end_time - start_time).

If the bezier is defined as

a t3 + b t2 + c t + d = 0

then you need to find the cubic roots of

a t3 + b t2 + c t + d - x = 0

to find the t corresponding to that x, (You only need to consider real roots in [0, 1]).

Then you can find the y by evaluating the bezier at t.

The final value is as follows: lerp(y, start_value, end_value).

Effects

Fill Effect

Color

lottie.layers[0].ef[0]

Tritone Effect

Bright Mid Dark

lottie.layers[0].ef[0]

Gaussian Blur

This is a two-pass shader, the uniform pass is has value 0 on the first pass and value 1 on the second pass.

lottie.layers[0].ef[0]

Drop Shadow Effect

The effect below is split into multiple shaders:

  • First it generates the shadow
  • Then it has a 2 pass gaussian blur (simplified from the example above)
  • Finally, it composites the original image on top of the blurred shadow

lottie.layers[0].ef[0]

Pro Levels Effect

Composite Red Green Blue

lottie.layers[0].ef[0]

Matte3

lottie.layers[0].ef[0]

Bulge

lottie.layers[0].ef[0]

Wave Warp

This effect is animated by default, so it has a "time" slider (in seconds).

lottie.layers[0].ef[0]