UNPKG

@lottielab/lottie-player

Version:

Versatile Lottie animation player based on lottie-web. Supports HTML (via Web Components) and React.

1 lines 800 kB
{"version":3,"file":"bundle.mjs","sources":["../../src/common/interactivity/definition.ts","../../src/common/event.ts","../../src/common/playback.ts","../../src/common/expressions/parser.ts","../../src/common/expressions/evaluator.ts","../../src/common/interactivity/variables.ts","../../src/common/morphing.ts","../../src/common/interactivity/driver.ts","../../src/common/interactivity/bezier.ts","../../src/common/interactivity/events.ts","../../src/common/interactivity/index.ts","../../node_modules/lottie-web/build/player/lottie_lottielab.js","../../src/common/renderer.ts","../../src/common/driven-renderer.ts","../../src/common/player.ts","../../src/react/index.tsx","../../src/web/index.ts"],"sourcesContent":["export type InteractiveEventType =\n | 'click'\n | 'mouseDown'\n | 'mouseUp'\n | 'mouseEnter'\n | 'mouseLeave'\n | 'finish'\n | 'custom';\n\nexport type InteractiveEvent = { event: InteractiveEventType } & (\n | { event: 'click' | 'mouseDown' | 'mouseUp' | 'mouseEnter' | 'mouseLeave'; target?: string }\n | { event: 'finish' }\n | { event: 'custom'; name: string }\n);\n\n/**\n * A string representation of an interactive event. Can be one of the following:\n * - 'click', 'mouseDown', 'mouseUp', 'mouseEnter', 'mouseLeave': when the user interacts with the lottie\n * - 'finish': when the animation of this state finishes (after all the looping has completed, if any)\n * - 'custom:<event-name>': a custom, user-defined event with the name\n * '<event-name>', manually triggerable from code using\n * LottielabInteractivity.trigger.\n *\n * By appending a `:<class>` suffix, you can target specific layers for the mouse\n * events, where `<class>` is the CSS class of the layer in the Lottie JSON, see\n * the \"cl\" member of the Lottie JSON. For example, `click:myButton` will only\n * trigger the transition if the user clicks on a layer with the class (`cl`\n * property in the JSON) of `myButton`.\n */\nexport type InteractiveEventStr = InteractiveEventType | string;\n\nexport function eventToString(e: InteractiveEvent): InteractiveEventStr {\n switch (e.event) {\n case 'finish':\n return 'finish';\n\n case 'click':\n case 'mouseDown':\n case 'mouseUp':\n case 'mouseEnter':\n case 'mouseLeave':\n if (e.target) {\n return `${e.event}:${e.target}`;\n } else {\n return e.event;\n }\n\n case 'custom':\n return 'custom:' + e.name;\n }\n}\n\nexport type State = {\n /**\n * Which segment of the animation to play when this state is active. Accepts a\n * [start, end] array of times, in seconds, from the beginning of the\n * animation.\n */\n segment: [number, number];\n /** Speed of the animation in this state. 1.0 (normal speed) is the default. */\n speed?: number | Formula;\n /**\n * Direction of the animation in this state. Can be 'forward' or 'reverse'.\n * 'forward' is the default.\n */\n direction?: 'forward' | 'reverse';\n /**\n * Whether to loop the animation when in this state, and/or how many times. If\n * set to true, loops indefinitely. A value of false means the same as 1 (no\n * looping).\n */\n loop?: boolean | number;\n /**\n * If set, the state will end after this many seconds, regardless of the\n * looping or playback state, and a 'finish' event will be triggered. If this\n * is not provided, the 'finish' event will trigger at the end of the segment\n * if looping is disabled, or at the end of the last loop if looping is\n * enabled. If no 'finish' transition is present, playback will pause.\n */\n duration?: number;\n\n /**\n * If set, activates morphing of this state with another state. Morphing means\n * that there is another state running \"in parallel\", and the final animation\n * can be smoothly morphed between this state and the other state. When\n * morphing is active, you can map the \"morph strength\" value to some user\n * input, like the mouse position, to achieve various effects.\n */\n morphing?: {\n /** Name of the other state to morph with. */\n otherState: string;\n /**\n * How to sync the playhead of this state to the playhead of the other state.\n * Can be one of the following:\n *\n * - 'proportional': the playhead of the other state will be at the\n * same relative position (percentage) in the other as in this segment. For\n * example, if this state's segment is 0s-1s and the other state's segment is\n * 1s-3s, and this state is at 0.5s (50%), the other state will be at 1.5s\n * (also 50%). In this case, states will always be in sync but the other\n * state might be playing at a different speed if the segment lengths differ.\n * - 'wrap': means that the playheads of both states will start at the beginning\n * of their segments, but will be allowed to proceed independently and loop\n * around. In this case, both states will be playing at the same speed, but\n * they might go out of sync with each other if one's segment's length is not\n * an exact multiple of the other's.\n * - 'clamp': means that the playheads of both states will start at the same\n * absolute time, but the second one will get clamped to the end of its segment.\n * The playheads will be in sync, but the second state will stop if its\n * segment is shorter than the first state's.\n */\n timeRemap?: 'proportional' | 'wrap' | 'clamp';\n\n /**\n * A formula describing the morph strength as a value between 0 and 1 where 0 means\n * behave as if there is no morphing, 1 means display the other state fully, 0.5\n * means to morph evenly between this and the other state, etc.\n */\n strength: number | Formula;\n };\n\n /**\n * Defines the transitions to other states. The key is the name of the event,\n * and the value is the details about the transition.\n *\n * Available events are:\n * - 'click': when the user clicks on the lottie\n * - 'mouseDown': when the user presses the mouse button\n * - 'mouseUp': when the user releases the mouse button\n * - 'mouseEnter': when the user's mouse enters the lottie\n * - 'mouseLeave': when the user's mouse leaves the lottie\n * - 'finish': when the animation of this state finishes (after all the\n * looping has completed, if any)\n *\n * Optionally, for all events except 'finish', the event can have a `:<target>`\n * suffix which targets specific layers defined by a CSS class, see the `.cl`\n * member of the Lottie JSON.\n *\n * For example, if the lottie JSON defines a layer with its `cl` property set\n * to `myButton`, you can define a transition that should only happen when this\n * layer is clicked by setting the event to `click:myButton`.\n */\n on?: Partial<Record<InteractiveEventStr, Transition>>;\n\n /**\n * Allows the playhead position in this state to be mapped to some user\n * input, possibly via a formula. This is sometimes called \"playback control\".\n * The result of the provided formula will be applied as a 0-1 progress within\n * the segment of this state.\n */\n playhead?: Formula;\n};\n\nexport type TransitionProperties = {\n /**\n * Duration of the transition to the new state. If 0 or not provided, the\n * transition is instant. Otherwise, the state will be smoothly morphed to the\n * other state.\n */\n duration?: number;\n /**\n * Describes the initial playhead position in the new state after switching.\n * Can be one of the following:\n *\n * - 'start': start from the beginning of the new state's segment\n * - 'end': start at the end of the new state's segment\n * - 'proportional': start at the same relative position of the playhead (by\n * percentage/progress) as the current time. For example, if the current state\n * is at 25% of its segment, the new state will start at 25% of its segment.\n * - 'wrap': the new state will start at the same absolute time as the current\n * one, wrapped around. This means that if the current state is at 0.75s and\n * the new state has a 2 second long segment, it will also start at 0.75s.\n * However, if the new state has a 0.5s long segment, it will start at 0.25s\n * (wrapping around as if it looped).\n * - 'clamp': the new state will start at the same absolute time as the current\n * one, but simply clamped to the new state's segment. This means that if the\n * new state has a segment 0.5s long, a 0.25s playhead will end up the same\n * (at 0.25s), but a 1.0s playhead will end up at 0.5s (clamped).\n */\n startAt?: 'start' | 'end' | 'proportional' | 'wrap' | 'clamp';\n /**\n * Easing function to use for the transition. Has an effect only if `duration`\n * is set (otherwise, the transition is instant). If not provided, a linear\n * easing function is used. Accepts a cubic Bezier easing in a similar format\n * as Lottie (two control points).\n */\n easing?: BezierEasing;\n};\n\nexport type Transition = {\n /** Name of the state to switch to. */\n goTo: string;\n} & TransitionProperties;\n\n/** Lottie-like Bezier easing with 2 control points. */\nexport type BezierEasing = { i: { x: number; y: number }; o: { x: number; y: number } };\n\n/**\n * A simple number or an expression whose result will be used to control the provided\n * variable. The expression can use the standard mathematical functions and constants\n * from the JavaScript Math module (without the `Math.` prefix), and can use a few built-in\n * variables which are mapped to the current user input state, or custom\n * variables provided by the user.\n *\n * The formula can be as simple as a single variable to map the user input\n * directly, or a complex expression that remaps values and integrates multiple\n * inputs for a complex effect.\n *\n * The variables that can be used in the formula are:\n * - `time`: Time in seconds since this state started.\n * - `time.abs`: Time in seconds since the animation first started.\n * - `playhead`: The current playhead position in this state, in seconds.\n * - `playhead.progress`: The current playhead position in this state, as a\n * value from 0 to 1 within the segment (0 = start, 1 = end).\n * - `playhead.abs`: Global position of the playhead in seconds.\n * - `mouse.x`, `mouse.y`: the current mouse position, relative to the top-left\n * of the lottie, in pixels\n * - `mouse.progress.x`, `mouse.progress.y`: the current mouse position, as a\n * value from 0 to 1 within the lottie's bounds (0 = left/top, 1 = right/bottom)\n * - `mouse.abs.x`, `mouse.abs.y`: the current mouse position, relative to the top-left\n * of the whole viewport rather than the lottie itself\n * - `mouse.buttons.left`, `mouse.buttons.right`, `mouse.buttons.middle`: whether the left,\n * right, or middle mouse buttons are currently pressed\n *\n * All other variable names are considered custom variables and can be freely\n * used in formulas, provided that `LottielabInteractivity.inputs.set()` is\n * called by the user to set their values.\n */\nexport type Formula = string;\n\n/**\n * A definition of states and transitions conforming to Lottielab Interactivity.\n *\n * This object is added to the lottie in `.metadata.lottielabInteractivity`.\n */\nexport type LottielabInteractivityDef = {\n __version: 'v1';\n\n /** List of states in the animation by their names. */\n states: Record<string, State>;\n /** Name of the state to start the animation in. */\n initialState: string;\n};\n","export type Listener<E> = (event: E) => void;\n\nexport class EventEmitter<E = undefined> {\n private listeners: Listener<E>[] = [];\n\n addListener(listener: (event: E) => void) {\n this.listeners.push(listener);\n }\n\n removeListener(listener: (event: E) => void) {\n this.listeners = this.listeners.filter((l) => l !== listener);\n }\n\n hasListeners() {\n return this.listeners.length > 0;\n }\n\n removeAllListeners() {\n this.listeners = [];\n }\n\n emit(event: E) {\n for (const listener of this.listeners) {\n listener(event);\n }\n }\n}\n","import type { ILottie, LottieJSON } from '..';\nimport type { LottieDriver, LottieState } from './driver';\nimport type { AnimationItem } from 'lottie-web/build/player/lottie_lottielab';\nimport { EventEmitter } from './event';\n\nfunction lottieFrameRate(lottie: any) {\n if (!lottie) return 100;\n return lottie.fr;\n}\n\nfunction lottieDuration(lottie: any) {\n if (!lottie) return 0;\n return (lottie.op - lottie.ip) / lottieFrameRate(lottie);\n}\n\nexport type PlaybackEvent = {\n type: 'loop' | 'finish';\n relativeTime: number; // time from [0, elapsed] (see advanceWithEvents()) when the event happened\n};\n\nexport class PlaybackDriver implements LottieDriver, ILottie {\n public playing: boolean = true;\n public time: number = 0;\n public speed: number = 1;\n public direction: 1 | -1 = 1;\n public segment?: [number, number];\n\n private _fps?: number;\n private _duration?: number;\n private _loop: boolean | number = true;\n private _loopsRemaining: number = Infinity;\n\n public readonly loopEvent = new EventEmitter();\n public readonly finishEvent = new EventEmitter();\n\n private get effectiveSegment() {\n if (this.segment) {\n return this.segment;\n } else {\n return [0, this._duration ?? 0];\n }\n }\n\n private globalTimeToSegmentTime(time: number) {\n const [from, to] = this.effectiveSegment;\n return Math.min(Math.max(time - from, 0), to - from);\n }\n\n private segmentTimeToGlobalTime(time: number) {\n const [from] = this.effectiveSegment;\n return from + time;\n }\n\n advance(ls: LottieState, elapsed: number, eventsOut?: PlaybackEvent[]): LottieState {\n const { lottie } = ls;\n this._fps = lottieFrameRate(lottie);\n this._duration = lottieDuration(lottie);\n if (!this.playing) {\n return { time: this.time, lottie };\n }\n\n const segmentTime = this.globalTimeToSegmentTime(this.time);\n\n let newTime;\n newTime = segmentTime + elapsed * this.speed * this.direction;\n\n const events: PlaybackEvent[] = [];\n if (this.durationOfSegment > 0) {\n const loops = Math.abs(Math.floor(newTime / this.durationOfSegment));\n\n // TODO: Check if this is correct\n const firstLoop = this.durationOfSegment - segmentTime;\n for (let i = 0; i < loops; i++) {\n this._loopsRemaining--;\n const relativeTime = firstLoop + i * this.durationOfSegment;\n if (this._loopsRemaining > 0) {\n events.push({ type: 'loop', relativeTime });\n if (newTime >= this.durationOfSegment) {\n newTime -= this.durationOfSegment;\n } else {\n newTime += this.durationOfSegment;\n }\n } else {\n events.push({ type: 'finish', relativeTime });\n newTime = Math.max(0, Math.min(newTime, this.durationOfSegment));\n this._loopsRemaining = 0;\n this.playing = false;\n\n break;\n }\n }\n } else {\n newTime = 0;\n events.push({ type: 'loop', relativeTime: 0 });\n }\n\n const globalTime = this.segmentTimeToGlobalTime(newTime);\n this.time = globalTime;\n\n if (events.length > 0) {\n if (eventsOut != undefined) {\n eventsOut.push(...events);\n }\n\n for (const event of events) {\n if (event.type === 'loop') {\n this.loopEvent.emit(undefined);\n } else if (event.type === 'finish') {\n this.finishEvent.emit(undefined);\n }\n }\n }\n\n return { time: globalTime, lottie };\n }\n\n play() {\n this.playing = true;\n }\n\n pause() {\n this.playing = false;\n }\n\n stop() {\n this.playing = false;\n this.seek(0);\n this.loop = this._loop; // reset loop count\n }\n\n seek(time: number) {\n this.time = time;\n }\n\n seekToFrame(frame: number) {\n this.time = frame / this.frameRate;\n }\n\n loopBetween(start: number, end: number) {\n this.segment = [start, end];\n }\n\n loopBetweenFrames(start: number, end: number) {\n this.segment = [start / this.frameRate, end / this.frameRate];\n }\n\n get loop() {\n return this._loop;\n }\n\n set loop(newLoop: number | boolean) {\n this._loop = newLoop;\n if (typeof newLoop === 'number') {\n this._loopsRemaining = newLoop;\n } else {\n if (newLoop) {\n this._loopsRemaining = Infinity;\n } else {\n this._loopsRemaining = 1;\n }\n }\n }\n\n get currentTime() {\n return this.time;\n }\n\n get currentFrame() {\n return this.time * this.frameRate;\n }\n\n get timeInSegment() {\n return this.globalTimeToSegmentTime(this.time);\n }\n\n set timeInSegment(time: number) {\n this.time = this.segmentTimeToGlobalTime(time);\n }\n\n get frameInSegment() {\n return this.timeInSegment * this.frameRate;\n }\n\n get frameRate() {\n return this._fps ?? 100;\n }\n\n get duration() {\n return this._duration ?? 0;\n }\n\n get durationFrames() {\n return this.duration * this.frameRate;\n }\n\n get durationOfSegment() {\n const [from, to] = this.effectiveSegment;\n return to - from;\n }\n\n get animation(): AnimationItem {\n throw new Error(\n \"This is just a driver and implements ILottie for clarity; you shouldn't directly call this function\"\n );\n }\n\n get animationData(): LottieJSON {\n throw new Error(\n \"This is just a driver and implements ILottie for clarity; you shouldn't directly call this function\"\n );\n }\n\n toInteractive() {\n throw new Error(\n \"This is just a driver and implements ILottie for clarity; you shouldn't directly call this function\"\n );\n }\n\n toPlayback() {}\n\n on(event: string, listener: any) {\n throw new Error(\n \"This is just a driver and implements ILottie for clarity; you shouldn't directly call this function\"\n );\n }\n\n off(event: string, listener: any) {\n throw new Error(\n \"This is just a driver and implements ILottie for clarity; you shouldn't directly call this function\"\n );\n }\n}\n","export const enum NodeType {\n NUMBER,\n IDENTIFIER,\n UNARY_OPERATOR,\n BINARY_OPERATOR,\n CONDITIONAL_OPERATOR,\n FUNCTION_CALL,\n}\n\nexport type Node =\n | {\n type: NodeType.NUMBER;\n value: number;\n }\n | {\n type: NodeType.IDENTIFIER;\n name: string;\n }\n | {\n type: NodeType.UNARY_OPERATOR;\n operator: '+' | '-' | '!';\n operand: Node;\n }\n | {\n type: NodeType.BINARY_OPERATOR;\n operator: '+' | '-' | '*' | '/' | '^' | '<' | '<=' | '>' | '>=' | '==' | '&&' | '||';\n left: Node;\n right: Node;\n }\n | {\n type: NodeType.CONDITIONAL_OPERATOR;\n condition: Node;\n thenBranch: Node;\n elseBranch: Node;\n }\n | {\n type: NodeType.FUNCTION_CALL;\n name: string;\n operands: Node[];\n };\n\nconst enum Precedence {\n MINIMUM,\n SEPARATOR,\n LITERAL,\n CONDITIONAL,\n LOGICAL_OR,\n LOGICAL_AND,\n COMPARISON,\n ADDITIVE,\n MULTIPLICATIVE,\n POWER,\n LOGICAL_NEGATION,\n BRACKET,\n}\n\ninterface Parser {\n peek: () => Token;\n pop: () => Token;\n parse: (minPrecedence: number) => Node;\n}\n\ntype TokenPrimitive = {\n name: string;\n lbp: number;\n detect?: (s: string) => boolean;\n nud?: (p: Parser, token: Token) => Node;\n led?: (p: Parser, left: Node, token: Token) => Node;\n};\n\ntype Token = TokenPrimitive & { value: string; position: number };\n\nfunction parseError(token: Token): never {\n throw new Error(`Unexpected ${token.name} at position ${token.position}.`);\n}\n\nconst TokenPrimitives = (() => {\n const tokenPrimitives: TokenPrimitive[] = [\n {\n name: 'number',\n lbp: Precedence.LITERAL,\n detect: (s: string) => /^([1-9]\\d*|0)(\\.\\d*)?$/.test(s),\n nud: (_: Parser, token: Token) => ({\n type: NodeType.NUMBER,\n value: parseFloat(token.value),\n }),\n },\n {\n name: 'identifier',\n lbp: Precedence.LITERAL,\n detect: (s: string) => /^[_a-zA-Z][_a-zA-Z0-9]*(\\.[a-zA-Z]+)*$/.test(s),\n nud: (_: Parser, token: Token) => ({\n type: NodeType.IDENTIFIER,\n name: token.value,\n }),\n },\n {\n // right associative\n name: '^',\n lbp: Precedence.POWER,\n led: (p: Parser, left: Node) => ({\n type: NodeType.BINARY_OPERATOR,\n operator: '^',\n left,\n right: p.parse(Precedence.POWER - 1),\n }),\n },\n {\n // unary only\n name: '!',\n lbp: Precedence.LOGICAL_NEGATION,\n nud: (p: Parser) => ({\n type: NodeType.UNARY_OPERATOR,\n operator: '!',\n operand: p.parse(Precedence.LOGICAL_NEGATION),\n }),\n },\n {\n name: 'conditional-operator',\n detect: (s: string) => s == '?',\n lbp: Precedence.CONDITIONAL,\n led: (p: Parser, left: Node) => {\n const thenBranch = p.parse(Precedence.CONDITIONAL - 1);\n const nextToken = p.pop();\n if (nextToken.name != ':') parseError(nextToken);\n const elseBranch = p.parse(Precedence.CONDITIONAL - 1);\n return {\n type: NodeType.CONDITIONAL_OPERATOR,\n condition: left,\n thenBranch,\n elseBranch,\n };\n },\n },\n {\n name: '(',\n lbp: Precedence.BRACKET,\n nud: (p: Parser) => {\n const expression = p.parse(Precedence.SEPARATOR);\n const nextToken = p.pop();\n if (nextToken.name != ')') parseError(nextToken);\n return expression;\n },\n led: (p: Parser, left: Node, token: Token) => {\n if (left.type != NodeType.IDENTIFIER) parseError(token);\n\n const operands: Node[] = [];\n if (p.peek().name == ')') {\n p.pop();\n } else {\n while (true) {\n operands.push(p.parse(Precedence.SEPARATOR));\n\n const nextToken = p.pop();\n if (nextToken.name == ')') break;\n if (nextToken.name == ',') continue;\n parseError(nextToken);\n }\n }\n\n return { type: NodeType.FUNCTION_CALL, name: left.name, operands };\n },\n },\n {\n name: 'end of input',\n lbp: Precedence.MINIMUM,\n },\n ];\n\n for (const operator of ['+', '-'] as const) {\n tokenPrimitives.push({\n name: operator,\n lbp: Precedence.ADDITIVE,\n nud: (p: Parser) => ({\n type: NodeType.UNARY_OPERATOR,\n operator,\n operand: p.parse(Precedence.ADDITIVE),\n }),\n led: (p: Parser, left: Node) => ({\n type: NodeType.BINARY_OPERATOR,\n operator,\n left,\n right: p.parse(Precedence.ADDITIVE),\n }),\n });\n }\n\n const binaryOperators = [\n ['*', Precedence.MULTIPLICATIVE],\n ['/', Precedence.MULTIPLICATIVE],\n ['<', Precedence.COMPARISON],\n ['<=', Precedence.COMPARISON],\n ['>', Precedence.COMPARISON],\n ['>=', Precedence.COMPARISON],\n ['==', Precedence.COMPARISON],\n ['&&', Precedence.LOGICAL_AND],\n ['||', Precedence.LOGICAL_OR],\n ] as const;\n for (const [operator, precedence] of binaryOperators) {\n tokenPrimitives.push({\n name: operator,\n lbp: precedence,\n led: (p: Parser, left: Node) => ({\n type: NodeType.BINARY_OPERATOR,\n operator,\n left,\n right: p.parse(precedence),\n }),\n });\n }\n\n for (const separator of [',', ')', ':'] as const) {\n tokenPrimitives.push({\n name: separator,\n lbp: Precedence.SEPARATOR,\n });\n }\n\n return tokenPrimitives;\n})();\n\nfunction tokenize(input: string): Token[] {\n const isWhitespace = /^\\s*$/;\n const segments = input.split(/([+\\-*/^(),?:!]|<=|>=|==|>(?!=)|<(?!=)|\\|\\||&&|\\s+)/g);\n segments.push('end of input');\n\n let position = 0;\n const tokens: Token[] = [];\n for (const segment of segments) {\n const detected = TokenPrimitives.some((tokenPrimitive) => {\n const detect = tokenPrimitive.detect ?? ((s: string) => s == tokenPrimitive.name);\n if (detect(segment)) {\n tokens.push({ ...tokenPrimitive, value: segment, position });\n return true;\n }\n });\n\n if (!detected && !isWhitespace.test(segment)) {\n throw new Error(`Invalid token ${segment} at position ${position}`);\n }\n\n position += segment.length;\n }\n\n return tokens;\n}\n\nexport function parse(input: string) {\n const tokens = tokenize(input);\n\n let tokenIndex = 0;\n\n const parser: Parser = {\n peek: () => tokens[tokenIndex],\n pop: () => tokens[tokenIndex++],\n parse: (minPrecedence) => {\n let currentToken = parser.pop();\n\n if (!currentToken.nud) parseError(currentToken);\n let left = currentToken.nud(parser, currentToken);\n\n while (parser.peek().lbp > minPrecedence) {\n currentToken = parser.pop();\n\n if (!currentToken.led) parseError(currentToken);\n left = currentToken.led(parser, left, currentToken);\n }\n\n return left;\n },\n };\n\n return parser.parse(Precedence.MINIMUM);\n}\n","import { Node, NodeType } from './parser';\n\nexport type SymbolMap = {\n [name in string]: number | boolean | ((...args: any) => number | boolean);\n};\n\n// @ts-ignore For now only the members of Math are built-in symbols.\nconst BuiltinSymbols: SymbolMap = Math;\n\nfunction undefinedSymbol(name: string): never {\n throw new Error(`Symbol ${name} is not defined and not a built in symbol.`);\n}\n\ntype UnaryOperator = (Node & { type: NodeType.UNARY_OPERATOR })['operator'];\ntype BinaryOperator = (Node & { type: NodeType.BINARY_OPERATOR })['operator'];\n\nfunction evaluateUnary(operator: UnaryOperator, operand: number | boolean): number | boolean {\n switch (operator) {\n case '+':\n return +operand;\n case '-':\n return -operand;\n case '!':\n return !operand;\n }\n}\n\nfunction evaluateBinary(\n operator: BinaryOperator,\n left: number | boolean,\n right: number | boolean\n): number | boolean {\n switch (operator) {\n case '+':\n // @ts-ignore\n return left + right;\n case '-':\n // @ts-ignore\n return left - right;\n case '*':\n // @ts-ignore\n return left * right;\n case '/':\n // @ts-ignore\n return left / right;\n case '^':\n // @ts-ignore\n return Math.pow(left, right);\n case '<':\n return left < right;\n case '<=':\n return left <= right;\n case '>':\n return left > right;\n case '>=':\n return left >= right;\n case '==':\n return left == right;\n case '&&':\n return left && right;\n case '||':\n return left || right;\n }\n}\n\nexport function evaluate(expression: Node, userDefinedSymbols: SymbolMap): number | boolean {\n switch (expression.type) {\n case NodeType.NUMBER: {\n return expression.value;\n }\n\n case NodeType.IDENTIFIER: {\n const name = expression.name;\n const value = userDefinedSymbols[name] ?? BuiltinSymbols[name];\n if (value == undefined) undefinedSymbol(name);\n\n if (typeof value != 'boolean' && typeof value != 'number') {\n throw new Error(`Symbol ${name} is a function and must be used in a function call.`);\n }\n\n return value;\n }\n\n case NodeType.UNARY_OPERATOR: {\n const operand = evaluate(expression.operand, userDefinedSymbols);\n return evaluateUnary(expression.operator, operand);\n }\n\n case NodeType.BINARY_OPERATOR: {\n const left = evaluate(expression.left, userDefinedSymbols);\n const right = evaluate(expression.right, userDefinedSymbols);\n return evaluateBinary(expression.operator, left, right);\n }\n\n case NodeType.CONDITIONAL_OPERATOR: {\n const condition = evaluate(expression.condition, userDefinedSymbols);\n return condition\n ? evaluate(expression.thenBranch, userDefinedSymbols)\n : evaluate(expression.elseBranch, userDefinedSymbols);\n }\n\n case NodeType.FUNCTION_CALL: {\n const name = expression.name;\n const value = userDefinedSymbols[name] ?? BuiltinSymbols[name];\n if (value == undefined) undefinedSymbol(name);\n\n if (typeof value != 'function') {\n throw new Error(`Symbol ${name} has value ${value} and is not callable.`);\n }\n\n if (value.length != expression.operands.length) {\n throw new Error(\n `Expected ${value.length} operands for ${name}, received ${expression.operands.length}`\n );\n }\n\n const operands = expression.operands.map((operand) => evaluate(operand, userDefinedSymbols));\n\n return value.apply(null, operands);\n }\n }\n}\n","export type Point = { x: number; y: number };\n\nexport type BuiltinVariables = {\n // Time in seconds since the animation first started.\n time: number;\n // Time in seconds that passed between the previous and the current frames\n 'time.diff': number;\n // The current playhead position in this state, in seconds.\n playhead: number;\n // The current playhead position in this state, as a value from 0 to 1\n // within the segment (0 = start, 1 = end).\n 'playhead.progress': number;\n // Global position of the playhead in seconds.\n 'playhead.abs': number;\n // The current mouse position, relative to the top-left of the lottie, in pixels\n 'mouse.x': number;\n 'mouse.y': number;\n // The current mouse position, as a value from 0 to 1 within the lottie's bounds\n // (0 = left/top, 1 = right/bottom)\n 'mouse.progress.x': number;\n 'mouse.progress.y': number;\n // The current mouse position, relative to the top-left of the whole viewport rather\n // than the lottie itself\n 'mouse.abs.x': number;\n 'mouse.abs.y': number;\n // Whether the left, right, or middle mouse buttons are currently pressed\n 'mouse.buttons.left': boolean;\n 'mouse.buttons.right': boolean;\n 'mouse.buttons.middle': boolean;\n};\n\nexport const defaultValues: BuiltinVariables = {\n time: 0,\n 'time.diff': 0,\n playhead: 0,\n 'playhead.progress': 0,\n 'playhead.abs': 0,\n 'mouse.x': 0,\n 'mouse.y': 0,\n 'mouse.progress.x': 0,\n 'mouse.progress.y': 0,\n 'mouse.abs.x': 0,\n 'mouse.abs.y': 0,\n 'mouse.buttons.left': false,\n 'mouse.buttons.right': false,\n 'mouse.buttons.middle': false,\n};\n\nexport type UserVariables = Record<string, number | boolean | Point>;\n\nexport type Variables = Record<string, number | boolean>;\n\nexport function isValidVariableName(name: string): boolean {\n return /^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(name);\n}\n\nexport function mergeVariables(builtin: BuiltinVariables, user: UserVariables): Variables {\n const vars: Variables = { ...builtin };\n for (const key in user) {\n if (!isValidVariableName(key)) {\n continue;\n }\n\n const val = user[key];\n if (\n typeof val === 'object' &&\n 'x' in val &&\n 'y' in val &&\n typeof val.x === 'number' &&\n typeof val.y === 'number'\n ) {\n vars[key + '.x'] = val.x;\n vars[key + '.y'] = val.y;\n } else if (typeof val === 'number' || typeof val === 'boolean') {\n vars[key] = val;\n } else {\n vars[key] = 0;\n }\n }\n\n return vars;\n}\n","import type { AnimationItem } from 'lottie-web';\n\n// lottie-web internal types\n\ninterface SvgRenderer {\n elements: SvgRendererElement[];\n}\n\ntype ValueCallback = (val: Lerpable) => Lerpable;\n\ninterface DynamicProp {\n addEffect(cb: ValueCallback): void;\n effectsSequence: ValueCallback[];\n _caching: unknown;\n offsetTime: number;\n interpolateValue?(frame: number, caching: unknown): Lerpable;\n interpolateShape?(frame: number, prevValue: Lerpable, caching: unknown): Lerpable;\n}\n\ninterface NestedDynamicProp {\n dynamicProperties: DynamicProp[];\n}\n\ninterface SvgRendererElement {\n elements?: SvgRendererElement[];\n dynamicProperties?: (DynamicProp | NestedDynamicProp)[];\n}\n\ntype ShapePath = {\n closed: boolean;\n i: number[];\n o: number[];\n v: number[];\n length?: number;\n _length?: number;\n};\n\ntype Lerpable = number | number[] | Float32Array | ShapePath;\n\n/**\n * A linear interpolation implementation which dynamically handles any type of\n * value that a lottie-web dynamic property (DynamicProp) might contain.\n */\nfunction universalLerp(a: Lerpable, b: Lerpable, t: number): Lerpable {\n if (a == undefined) {\n return b;\n } else if (b == undefined) {\n return a;\n } else if (typeof a === 'number') {\n // 1-d lerp\n\n if (typeof b !== 'number') {\n // Cannot interpolate between these, fallback to a step function\n return t > 0.5 ? b : a;\n }\n\n return (1 - t) * a + t * b;\n } else if (Array.isArray(a) || a instanceof Float32Array) {\n // k-d lerp\n\n if (!Array.isArray(b) && !(b instanceof Float32Array)) {\n // Cannot interpolate between these, fallback to a step function\n return t > 0.5 ? b : a;\n }\n\n // Recursively apply to elements\n return a.map((xa, i) => universalLerp(xa, b[i] ?? xa, t) as number);\n } else if ('i' in a && 'v' in a && 'o' in a) {\n // Lerp between vector paths\n\n if (typeof b !== 'object' || !('i' in b && 'v' in b && 'o' in b)) {\n // Cannot interpolate between these, fallback to a step function\n return t > 0.5 ? b : a;\n }\n\n return {\n closed: a.closed && b.closed,\n i: universalLerp(a.i, b.i, t) as number[],\n o: universalLerp(a.o, b.o, t) as number[],\n v: universalLerp(a.v, b.v, t) as number[],\n length: Math.max(a.length ?? 0, b.length ?? 0),\n _length: Math.max(a._length ?? 0, b._length ?? 0),\n };\n } else {\n // Unknown type, fall back to step function\n return t > 0.5 ? b : a;\n }\n}\n\n/**\n * A single inter-frame morph operation. It instructs the morphing system to\n * combine the current state of each property with what its state would be at\n * another frame, interpolated with the provided strength.\n *\n * For example, if the animation is at time 2.0s and a morph operation of the\n * following form is applied:\n *\n * { time: 5.0, strength: 0.25 }\n *\n * This means that, for each animatable property, its state at frame 5.0s will be\n * combined with the state at frame 2.0s with 25% strength. Sliding the strength\n * from 0 to 1 will result in a smooth linar transition between the time 5.0s and\n * 2.0s.\n */\nexport type MorphOperation = {\n time: number;\n strength: number;\n};\n\n/** Maximum number of concurrent ops, for performance reasons. */\nexport const MAX_MORPHS = 8;\n\n/**\n * An inter-frame morphing implementation for lottie-web.\n *\n * This class attaches to an existing lottie-web instance and allows the user to\n * manipulate the displayed state of the Lottie so that it does not display only\n * one frame, but an arbitrary linear combination of multiple frames.\n *\n * To use, instantiate the Morphing class and pass it a lottie-web instance.\n * Then, set the ops property to an array of MorphOperation objects. The\n * operations will be performed in sequence, each blending the state left by\n * the previous with some other frame of the animation. @see MorphOperation\n */\nexport class Morphing {\n public ops: MorphOperation[];\n private lottie: AnimationItem;\n\n constructor(lottie: AnimationItem) {\n this.ops = [];\n this.lottie = lottie;\n\n const renderer = lottie.renderer as SvgRenderer;\n const s = new WeakSet<object>();\n (renderer?.elements ?? []).forEach((el) => this.attach(el, s));\n }\n\n private attachToProp(p: DynamicProp) {\n const self = this;\n const s = p as any;\n if (!p.effectsSequence?.length) {\n return;\n }\n\n const vrs: any[] = [];\n for (let i = 0; i < MAX_MORPHS; i++) {\n vrs[i] = {\n _caching: structuredClone(p._caching),\n propType: s.propType,\n offsetTime: s.offsetTime,\n keyframes: s.keyframes,\n keyframesMetadata: s.keyframesMetadata,\n sh: s.sh,\n pv: structuredClone(s.pv),\n };\n }\n\n p.addEffect(function (val: Lerpable) {\n const activeMorphs = self.ops.slice(-MAX_MORPHS);\n\n if (p.interpolateShape && val == undefined) {\n val = structuredClone(s.pv);\n }\n\n for (let i = 0; i < activeMorphs.length; i++) {\n const morph = activeMorphs[i];\n const vr = vrs[i];\n\n if (!vr._caching && p._caching) vr._caching = structuredClone(p._caching);\n if (!vr.keyframes && s.keyframes) vr.keyframes = s.keyframes;\n if (!vr.keyframesMetadata && s.keyframesMetadata)\n vr.keyframesMetadata = s.keyframesMetadata;\n\n const otherFrame = Math.min(\n Math.max(Math.round(morph.time * self.lottie.frameRate), 0),\n self.lottie.getDuration(true) - 1e-5\n );\n\n try {\n vr.offsetTime = p.offsetTime;\n let otherVal;\n if (p.interpolateValue) {\n if (vr._caching.lastFrame >= otherFrame) {\n vr._caching._lastKeyframeIndex = -1;\n vr._caching.lastIndex = 0;\n }\n otherVal = p.interpolateValue.call(vr, otherFrame, vr._caching);\n vr.pv = otherVal;\n } else if (p.interpolateShape) {\n vr._caching.lastIndex = vr._caching.lastFrame < otherFrame ? vr._caching.lastIndex : 0;\n p.interpolateShape.call(vr, otherFrame, vr.pv, vr._caching);\n otherVal = vr.pv;\n } else {\n otherVal = 0;\n }\n\n // update caching\n vr._caching.lastFrame = otherFrame;\n\n val = universalLerp(val, otherVal, morph.strength);\n } catch (e) {\n console.warn(`[@lottielab/lottie-player:morph]`, e);\n }\n }\n\n return val;\n });\n }\n\n private attach(el: any, s = new WeakSet<object>()) {\n if (s.has(el)) {\n return;\n }\n\n if (typeof el === 'object' && el != null) {\n s.add(el);\n } else {\n return;\n }\n\n if (el.interpolateShape || el.interpolateValue || el.addEffect || el.effectsSequence) {\n this.attachToProp(el);\n return;\n }\n\n for (const v of Object.values(el)) {\n this.attach(v, s);\n }\n }\n\n detach() {\n // TODO\n }\n}\n","import * as def from './definition';\nimport * as bezier from './bezier';\nimport type { LottieDriver, LottieState } from '../driver';\nimport { PlaybackDriver, PlaybackEvent } from '../playback';\nimport { parse, evaluate } from '../expressions';\nimport {\n BuiltinVariables,\n UserVariables,\n Variables,\n defaultValues,\n mergeVariables,\n} from './variables';\nimport { MorphOperation, MAX_MORPHS } from '../morphing';\nimport type { InteractiveEventHandler } from './events';\nimport { EventEmitter } from '../event';\n\nexport type StateTransitionEvent = {\n from: def.State;\n to: def.State;\n transition: def.TransitionProperties;\n};\n\nfunction easingToBezier(easing: def.BezierEasing): bezier.CubicBezier {\n return {\n start: { x: 0, y: 0 },\n end: { x: 1, y: 1 },\n controlPoint1: easing.o,\n controlPoint2: easing.i,\n };\n}\n\nexport function ease(easing: def.BezierEasing, t: number): number {\n return bezier.evaluate(easingToBezier(easing), t).y;\n}\n\nexport type MorphingSetup = {\n other: StateState;\n strength: number | ((variables: Variables) => number);\n timeRemap?: 'proportional' | 'wrap' | 'clamp';\n};\n\ntype StateState = {\n type: 'state';\n def: def.State;\n playback: StatePlayback;\n morphing?: MorphingSetup;\n remainingDuration?: number;\n};\n\ntype TransitionState = {\n type: 'transition';\n def: def.TransitionProperties;\n prev: StateState | TransitionState;\n from: def.State;\n next?: StateState;\n progress: number;\n};\n\nfunction formulaToFunction(\n formula: string,\n defaultValue: number\n): (variables: Variables) => number {\n try {\n const parsedFormula = parse(formula);\n return (variables: Variables) => {\n try {\n return +evaluate(parsedFormula, variables);\n } catch (e) {\n return defaultValue;\n }\n };\n } catch (e) {\n return () => defaultValue;\n }\n}\n\ntype StatePlayback = {\n driver: PlaybackDriver;\n playheadControl?: (variables: Variables) => number;\n speedControl?: (variables: Variables) => number;\n};\n\nfunction playbackFor(state: def.State): StatePlayback {\n const driver = new PlaybackDriver();\n driver.segment = state.segment;\n driver.loop = state.loop ?? true;\n if (state.direction) driver.direction = state.direction === 'forward' ? 1 : -1;\n\n const pb: StatePlayback = { driver };\n if (typeof state.speed === 'number') {\n driver.speed = state.speed;\n } else if (typeof state.speed == 'string') {\n pb.speedControl = formulaToFunction(state.speed, 1);\n }\n\n if (state.playhead) {\n pb.playheadControl = formulaToFunction(state.playhead, 0);\n }\n\n return pb;\n}\n\nfunction advanceStatePlayback(\n state: StateState,\n ls: LottieState,\n elapsed: number,\n clock: number,\n variables: Variables,\n eventsOut?: PlaybackEvent[]\n) {\n const pb = state.playback;\n const newVars = {\n ...variables,\n time: clock,\n 'time.diff': elapsed,\n playhead: pb.driver.timeInSegment,\n 'playhead.progress':\n pb.driver.durationOfSegment > 0 ? pb.driver.timeInSegment / pb.driver.durationOfSegment : 0,\n 'playhead.abs': pb.driver.currentTime,\n };\n if (pb.speedControl) {\n pb.driver.speed = pb.speedControl(variables);\n }\n\n const newLs = pb.driver.advance(ls, elapsed, eventsOut);\n if (pb.playheadControl) {\n newLs.time =\n pb.playheadControl(newVars) * pb.driver.durationOfSegment + (pb.driver.segment?.[0] ?? 0);\n }\n\n const morph = calculateMorphForState(state, newLs, variables);\n if (morph) {\n newLs.morphs = [morph];\n }\n\n return newLs;\n}\n\nfunction proportionalTimeRemap(\n time: number,\n from: [number, number],\n onto: [number, number],\n speed = 1\n) {\n const fromDuration = from[1] - from[0];\n const ontoDuration = onto[1] - onto[0];\n let progress = fromDuration > 0 ? (time - from[0]) / fromDuration : 0;\n progress = Math.min(1, Math.max(0, progress)) * speed;\n return onto[0] + progress * ontoDuration;\n}\n\nfunction wrapTimeRemap(time: number, from: [number, number], onto: [number, number]) {\n const duration = onto[1] - onto[0];\n const t = time - from[0];\n const val = onto[0] + (duration > 0 ? t % (onto[1] - onto[0]) : 0);\n return Math.min(onto[1], Math.max(onto[0], val));\n}\n\nfunction clampTimeRemap(time: number, from: [number, number], onto: [number, number]) {\n const val = time - from[0] + onto[0];\n return Math.min(onto[1], Math.max(onto[0], val));\n}\n\nfunction calculateMorphForState(\n state: StateState,\n ls: LottieState,\n variables: Variables\n): MorphOperation | undefined {\n if (!state.morphing) return undefined;\n const timeRemap = state.def.morphing?.timeRemap ?? 'proportional';\n let newTime;\n const currTime = ls.morphs ? ls.morphs[ls.morphs.length - 1].time : ls.time;\n switch (timeRemap) {\n case 'proportional':\n newTime = proportionalTimeRemap(\n currTime,\n state.def.segment,\n state.morphing.other.def.segment\n );\n break;\n case 'wrap':\n newTime = wrapTimeRemap(currTime, state.def.segment, state.morphing.other.def.segment);\n break;\n case 'clamp':\n newTime = clampTimeRemap(currTime, state.def.segment, state.morphing.other.def.segment);\n break;\n default:\n console.warn(`[@lottielab/lottie-player:interactive] Unknown timeRemap: ${timeRemap}`);\n return undefined;\n }\n\n return {\n time: newTime,\n strength:\n typeof state.morphing.strength === 'number'\n ? state.morphing.strength\n : state.morphing.strength(variables),\n };\n}\n\nfunction applyTransition(\n ls: LottieState,\n transition: TransitionState,\n elapsed: number,\n clock: number,\n variables: Variables,\n advanceTransition: boolean = true,\n remainingMorphs: number = MAX_MORPHS\n): LottieState {\n let newLs = transition.next\n ? advanceStatePlayback(transition.next, ls, elapsed, clock, variables)\n : ls;\n if (transition && transition.def.duration) {\n let prevLs;\n if (transition.prev.type === 'state') {\n prevLs = advanceStatePlayback(transition.prev, ls, elapsed, clock, variables);\n } else {\n prevLs = applyTransition(\n ls,\n transition.prev,\n elapsed,\n clock,\n variables,\n false,\n remainingMorphs - 1\n );\n }\n\n if (advanceTransition) {\n transition.progress += elapsed / transition.def.duration;\n }\n transition.progress = Math.min(1, transition.progress);\n\n let alpha = transition.progress;\n if (transition.def.easing) {\n alpha = ease(transition.def.easing, alpha);\n }\n\n if (alpha === 1) {\n newLs = { ...prevLs, morphs: ls.morphs, time: newLs.time };\n } else {\n if (remainingMorphs > 0) {\n const newTime = newLs.time;\n newLs = { ...prevLs };\n newLs.morphs = (newLs.morphs ?? []).concat([{ time: newTime, strength: alpha }]);\n if (ls.morphs && ls.morphs.length > 0) {\n // This used to express (1-a)*prev+a*new, but now we need new=(1-b)*n1+b*n2\n // This is not expressible in the general case as a composition of\n // morphs\n //\n // In this case, we hack around it by just assuming that the initial\n // time is the most \"important\"\n //\n // This can be solved in a much more elegant/general way by simply\n // changing the representation that the morphing driver accepts to be a\n // list of frames and coefficients rather than a composition of linear\n // combinations\n newLs.morphs = [\n ...ls.morphs,\n { time: newLs.time, strength: 1.0 - newLs.morphs[0].strength },\n ];\n newLs.time = ls.time;\n }\n } else {\n // Don't create any new morphs, just flatten\n newLs = { ...prevLs, time: alpha > 0.5 ? newLs.time : prevLs.time };\n }\n }\n } else if (!transition.def.duration) {\n console.warn(\n '[@lottielab/lottie-player:interactive] Transition duration of 0/unset is not expected here'\n );\n }\n\n return newLs;\n}\n\nexport class InteractiveDriver implements LottieDriver, InteractiveEventHandler {\n private _definition: def.LottielabInteractivityDef;\n private builtinVariables: BuiltinVariables = { ...defaultValues };\n private userVariables: UserVariables = {};\n private variables: Variables = {};\n private clock: number = 0;\n\n private state!: StateState;\n private transition?: TransitionState;\n\n public readonly transitionStartEvent = new EventEmitter<StateTransitionEvent>();\n public readonly transitionEndEvent = new EventEmitter<StateTransitionEvent>();\n\n constructor(definition: def.LottielabInteractivityDef) {\n this._definition = definition;\n const initialState = definition.states[definition.initialState];\n if (!initialState) {\n throw new Error(`Initial state ${definition.initialState} does not exist`);\n }\n\n this.enterState(initialState);\n }\n\n private setupMorphingForCurrentState(opts?: { force: boolean }) {\n if (opts?.force) this.state.morphing = undefined;\n\n const sd = this.state.def;\n if (sd.morphing && !this.state.morphing) {\n const otherState = this._definition.states[sd.morphing.otherState];\n if (otherState) {\n this.state.morphing = {\n other: {\n type: 'state',\n def: otherState,\n playback: playbackFor(otherState),\n },\n strength:\n typeof sd.morphing.strength === 'number'\n ? sd.morphing.strength\n : formulaToFunction(sd.morphing.strength, 0),\n };\n } else {\n console.warn(\n `[@lottielab/lottie-player:interactivity] State '${sd.morphing.otherState}' to morph with does not exist`\n );\n }\n } else if (!sd.morphing && this.state.morphing) {\n this.state.morphing = undefined;\n }\n }\n\n private enterState(state: def.State) {\n const newPb = playbackFor(state);\n this.state = {\n type: 'state',\n def: state,\n playback: newPb,\n remainingDuration: state.duration,\n };\n\n this.setupMorphingForCurrentState({ force: true });\n }\n\n getCurrentState() {\n return this.state;\n }\n\n goToState(newState: def.State, transition?: def.TransitionProperties) {\n const prevState = this.state;\n const prevSegment = prevState.def.segment;\n\n if (this.transition) {\n this.transitionEndEvent.emit({\n from: this.transition.from,\n to: this.state.def,\n transition: this.transition.def,\n });\n }\n\n const currTime = this.state.playback.driver.time;\n this.enterState(newState);\n transition = transition ?? { startAt: 'start' };\n this.transitionStartEvent.emit({\n from: prevState.def,\n to: newState,\n transition,\n });\n\n const newSegment = newState.segment;\n switch (transition?.startAt) {\n case 'start':\n case undefined:\n this.state.playback.driver.time = newSegment[0];\n break;\n case 'end':\n this.state.playback.driver.time = newSegment[1];\n break;\n case 'proportional':\n this.state.playback.driver.time = proportionalTimeRemap(currTime, prevSegment, newSegment);\n break;\n case 'wrap':\n this.state.playback.driver.time = wrapTimeRemap(currTime, prevSegment, newSegment);\n