react-lottie 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [2.2.0](https://github.com/chenqingspring/react-lottie/compare/v2.1.0...v2.2.0) (2026-11-05)
6
+
7
+ ### Features
8
+
9
+ * add a `colorFilters` prop that replaces the colors of fills, strokes and solid layers by keypath, mirroring lottie-react-native's API. The filters are applied to a copy of `animationData`; changing them reloads the animation while preserving the current frame and playback state ([#144](https://github.com/chenqingspring/react-lottie/issues/144))
10
+
11
+ ## [2.1.0](https://github.com/chenqingspring/react-lottie/compare/v2.0.1...v2.1.0) (2026-11-05)
12
+
13
+ ### Features
14
+
15
+ * add an `isSubframe` prop that is passed to lottie-web's `setSubframe()`, so `enterFrame` can fire only on whole After Effects frames, e.g. to trigger audio exactly on frame boundaries ([#71](https://github.com/chenqingspring/react-lottie/issues/71))
16
+ * expose `goToAndStop(frame, isFrame)` and `goToAndPlay(frame, isFrame)` on the component instance, which makes it possible to drive an animation from scroll position or jump to an exact frame/marker through a `ref` ([#40](https://github.com/chenqingspring/react-lottie/issues/40))
17
+
18
+ ### Bug Fixes
19
+
20
+ * keep a playing animation running when an unrelated prop changes; added a regression test for the state-change pause reported in [#43](https://github.com/chenqingspring/react-lottie/issues/43)
21
+
5
22
  ## [2.0.1](https://github.com/chenqingspring/react-lottie/compare/v2.0.0...v2.0.1) (2026-11-05)
6
23
 
7
24
  ### Bug Fixes
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/react-lottie.svg)](http://badge.fury.io/js/react-lottie)
4
4
 
5
+ > **This project is fully maintained automatically by [Relay](https://get-relay.cc).**
6
+
5
7
  ## Demo
6
8
  https://chenqingspring.github.io/react-lottie
7
9
 
@@ -31,10 +33,7 @@ Install through npm:
31
33
  npm install --save react-lottie
32
34
  ```
33
35
 
34
- > **Note:** react-lottie is maintained automatically by Relay. Learn more at
35
- > [get-relay.cc](https://get-relay.cc).
36
- >
37
- > Found a bug or have a feature request? Open an issue on
36
+ > **Note:** Found a bug or have a feature request? Open an issue on
38
37
  > [GitHub](https://github.com/chenqingspring/react-lottie/issues) and Relay
39
38
  > will review and fix it.
40
39
 
@@ -188,6 +187,50 @@ A segment also has to be reachable with `loop`/`autoplay` as usual: with
188
187
  `loop: false` and `autoplay: false` the range only plays once you flip
189
188
  `isStopped` or pass a new `segments` value.
190
189
 
190
+ ### Recolor an animation
191
+
192
+ `colorFilters` replaces the colors of fills, strokes and solid layers named by
193
+ a keypath, so one animation file can follow a theme (issue #144):
194
+
195
+ ```jsx
196
+ <Lottie
197
+ options={defaultOptions}
198
+ colorFilters={[
199
+ { keypath: 'Pin 1.Shape 2.Fill 1', color: '#4f46e5' },
200
+ { keypath: 'Background', color: 'rgb(15, 23, 42)' },
201
+ ]}
202
+ />
203
+ ```
204
+
205
+ A keypath is the dot-separated name chain from the layer down to the fill,
206
+ stroke or solid, e.g. `layer.group.fill`. For a layer inside a precomposition,
207
+ the composition name can be included (`Precomp.InnerLayer.Shape.Fill 1`) or
208
+ omitted. `*` matches one level and `**` matches any number of levels; dots in
209
+ a name can be escaped with a backslash. Colors accept hex, `rgb()`/`rgba()`,
210
+ `hsl()`/`hsla()` and the basic CSS color names. Invalid filters are ignored.
211
+
212
+ The filters are applied to `options.animationData`. Changing them reloads the
213
+ animation, because lottie-web cannot recolor a loaded animation; the current
214
+ frame and paused/playing state are preserved. Animations loaded from
215
+ `options.path` are not recolored, since the JSON is fetched by lottie-web:
216
+ load the JSON yourself and pass it as `animationData` instead.
217
+
218
+ ### Imperative control
219
+
220
+ A `ref` to the component exposes `play()`, `pause()`, `stop()`,
221
+ `playSegments()`, `goToAndStop(frame, isFrame)` and
222
+ `goToAndPlay(frame, isFrame)`, mirroring the lottie-web animation object. Use
223
+ `isFrame: true` for a frame number, or omit it to pass a time in seconds. This
224
+ is how you can sync an animation to scroll position (issue #40):
225
+
226
+ ```jsx
227
+ const lottie = useRef(null);
228
+
229
+ const onScroll = (frame) => lottie.current.goToAndStop(frame, true);
230
+
231
+ <Lottie ref={lottie} options={defaultOptions} />
232
+ ```
233
+
191
234
  ### props
192
235
  The `<Lottie />` Component supports the following props:
193
236
 
@@ -207,6 +250,11 @@ the object representing the animation settings that will be instantiated by body
207
250
  >
208
251
  >**rendererSettings** *required*
209
252
 
253
+ **colorFilters** *optional* [default: `[]`]
254
+
255
+ Array of `{ keypath, color }` objects that replace the colors of fills,
256
+ strokes and solid layers. See [Recolor an animation](#recolor-an-animation).
257
+
210
258
  **width** *optional* [default: `100%`]
211
259
 
212
260
  pixel value for containers width.
@@ -235,6 +283,13 @@ values play in reverse at that rate.
235
283
  `1` plays forward, `-1` plays backward. Changing it keeps the current frame,
236
284
  which is what you want for hover-in / hover-out effects.
237
285
 
286
+ **isSubframe** *optional* [default: `true`]
287
+
288
+ Passed to lottie-web's `setSubframe()`. When `true`, frames are interpolated
289
+ for smooth playback. Set it to `false` to make the `enterFrame` event fire only
290
+ on whole After Effects frames, for example to trigger audio exactly on frame
291
+ boundaries (issue #71).
292
+
238
293
  **segments** *optional* [default: none]
239
294
 
240
295
  `[fromFrame, toFrame]` to play only part of the animation, e.g. `[45, 90]`.
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.parseColor = exports.colorFiltersAreEqual = exports.applyColorFilters = void 0;
7
+ function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
8
+ function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
9
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
10
+ function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
11
+ function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
12
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
13
+ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
14
+ /* `colorFilters` mirrors lottie-react-native: every filter names a keypath in
15
+ the animation (layer → group → shape item) and the color to replace there.
16
+ lottie-web has no public API for recoloring a loaded animation, so the
17
+ animation JSON is cloned with the matching colors replaced and the animation
18
+ is loaded again. The caller's animationData is never mutated, because it may
19
+ be a frozen ESM import (#144). */
20
+
21
+ var NAMED_COLORS = {
22
+ aqua: [0, 255, 255],
23
+ black: [0, 0, 0],
24
+ blue: [0, 0, 255],
25
+ cyan: [0, 255, 255],
26
+ fuchsia: [255, 0, 255],
27
+ gray: [128, 128, 128],
28
+ green: [0, 128, 0],
29
+ grey: [128, 128, 128],
30
+ lime: [0, 255, 0],
31
+ magenta: [255, 0, 255],
32
+ maroon: [128, 0, 0],
33
+ navy: [0, 0, 128],
34
+ olive: [128, 128, 0],
35
+ purple: [128, 0, 128],
36
+ red: [255, 0, 0],
37
+ silver: [192, 192, 192],
38
+ teal: [0, 128, 128],
39
+ white: [255, 255, 255],
40
+ yellow: [255, 255, 0]
41
+ };
42
+ var clampChannel = function clampChannel(value) {
43
+ return Math.min(255, Math.max(0, value));
44
+ };
45
+ var toFractions = function toFractions(channels) {
46
+ return channels.map(function (channel) {
47
+ return clampChannel(channel) / 255;
48
+ });
49
+ };
50
+ var parseHex = function parseHex(value) {
51
+ var match = /^#?([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(value);
52
+ if (!match) return null;
53
+ var digits = match[1].length <= 4 ? match[1].split('').map(function (digit) {
54
+ return digit + digit;
55
+ }) : match[1].match(/.{2}/g);
56
+
57
+ /* The fourth value is alpha; lottie keeps the animation's own alpha, so it
58
+ is ignored here. */
59
+ return toFractions(digits.slice(0, 3).map(function (digit) {
60
+ return parseInt(digit, 16);
61
+ }));
62
+ };
63
+ var parseChannel = function parseChannel(token) {
64
+ var value = parseFloat(token);
65
+ if (Number.isNaN(value)) return null;
66
+ return token.endsWith('%') ? value * 255 / 100 : value;
67
+ };
68
+ var parseRgb = function parseRgb(value) {
69
+ var match = /^rgba?\(\s*([^)]+)\)$/i.exec(value);
70
+ if (!match) return null;
71
+ var channels = match[1].split(/[\s,/]+/).filter(Boolean).slice(0, 3).map(parseChannel);
72
+ if (channels.length !== 3 || channels.some(function (channel) {
73
+ return channel === null;
74
+ })) return null;
75
+ return toFractions(channels);
76
+ };
77
+ var hueToRgb = function hueToRgb(p, q, t) {
78
+ var hue = t;
79
+ if (hue < 0) hue += 1;
80
+ if (hue > 1) hue -= 1;
81
+ if (hue < 1 / 6) return p + (q - p) * 6 * hue;
82
+ if (hue < 1 / 2) return q;
83
+ if (hue < 2 / 3) return p + (q - p) * (2 / 3 - hue) * 6;
84
+ return p;
85
+ };
86
+ var parseHsl = function parseHsl(value) {
87
+ var match = /^hsla?\(\s*([^)]+)\)$/i.exec(value);
88
+ if (!match) return null;
89
+ var parts = match[1].split(/[\s,/]+/).filter(Boolean);
90
+ if (parts.length < 3) return null;
91
+ var hue = (parseFloat(parts[0]) % 360 + 360) % 360 / 360;
92
+ var saturation = parseFloat(parts[1]) / 100;
93
+ var lightness = parseFloat(parts[2]) / 100;
94
+ if (Number.isNaN(hue) || Number.isNaN(saturation) || Number.isNaN(lightness)) {
95
+ return null;
96
+ }
97
+ if (saturation === 0) return [lightness, lightness, lightness];
98
+ var q = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation;
99
+ var p = 2 * lightness - q;
100
+ return [hueToRgb(p, q, hue + 1 / 3), hueToRgb(p, q, hue), hueToRgb(p, q, hue - 1 / 3)];
101
+ };
102
+
103
+ /* Accepts the CSS color forms an app is likely to use for theming. Anything
104
+ else (including CSS variables) is ignored rather than crashing the render. */
105
+ var parseColor = exports.parseColor = function parseColor(value) {
106
+ if (typeof value !== 'string') return null;
107
+ var color = value.trim().toLowerCase();
108
+ if (NAMED_COLORS[color]) return toFractions(NAMED_COLORS[color]);
109
+ return parseHex(color) || parseRgb(color) || parseHsl(color);
110
+ };
111
+ var setColorArray = function setColorArray(target, color) {
112
+ if (!Array.isArray(target) || target.length < 3) return false;
113
+ target[0] = color[0];
114
+ target[1] = color[1];
115
+ target[2] = color[2];
116
+ return true;
117
+ };
118
+
119
+ /* A color property is either static (`k: [r, g, b, a]`) or animated
120
+ (`k: [{ s: [...], e: [...] }, ...]`). Replace both so an animated fill stays
121
+ recolored throughout its keyframes. */
122
+ var setColorProperty = function setColorProperty(property, color) {
123
+ if (!property || !Array.isArray(property.k) || property.k.length === 0) return false;
124
+ if (typeof property.k[0] === 'number') {
125
+ return setColorArray(property.k, color);
126
+ }
127
+ var applied = false;
128
+ property.k.forEach(function (keyframe) {
129
+ if (!keyframe || _typeof(keyframe) !== 'object') return;
130
+ if (setColorArray(keyframe.s, color)) applied = true;
131
+ if (setColorArray(keyframe.e, color)) applied = true;
132
+ });
133
+ return applied;
134
+ };
135
+ var toHex = function toHex(color) {
136
+ return "#".concat(color.map(function (channel) {
137
+ return Math.round(channel * 255).toString(16).padStart(2, '0');
138
+ }).join(''));
139
+ };
140
+
141
+ /* Solid layers store their color as a hex string instead of a color array. */
142
+ var setSolidColor = function setSolidColor(layer, color) {
143
+ if (typeof layer.sc === 'string') {
144
+ layer.sc = toHex(color);
145
+ return true;
146
+ }
147
+ return setColorArray(layer.sc, color);
148
+ };
149
+
150
+ /* Split on unescaped dots so layer names that contain a dot (`icon.dark`) can
151
+ be targeted with `icon\.dark`. */
152
+ var splitKeypath = function splitKeypath(keypath) {
153
+ var parts = [];
154
+ var current = '';
155
+ for (var index = 0; index < keypath.length; index += 1) {
156
+ var character = keypath[index];
157
+ if (character === '\\' && index + 1 < keypath.length) {
158
+ current += keypath[index + 1];
159
+ index += 1;
160
+ } else if (character === '.') {
161
+ parts.push(current);
162
+ current = '';
163
+ } else {
164
+ current += character;
165
+ }
166
+ }
167
+ parts.push(current);
168
+ return parts;
169
+ };
170
+
171
+ /* `*` matches one level and `**` matches any number of levels, matching the
172
+ keypath conventions used by lottie-ios. */
173
+ var _matchesKeypath = function matchesKeypath(pattern, path) {
174
+ if (pattern.length === 0) return path.length === 0;
175
+ if (pattern[0] === '**') {
176
+ for (var index = 0; index <= path.length; index += 1) {
177
+ if (_matchesKeypath(pattern.slice(1), path.slice(index))) return true;
178
+ }
179
+ return false;
180
+ }
181
+ if (path.length === 0) return false;
182
+ if (pattern[0] !== '*' && pattern[0] !== path[0]) return false;
183
+ return _matchesKeypath(pattern.slice(1), path.slice(1));
184
+ };
185
+ var colorFiltersAreEqual = exports.colorFiltersAreEqual = function colorFiltersAreEqual(first, second) {
186
+ if (first === second) return true;
187
+ if (!Array.isArray(first) || !Array.isArray(second) || first.length !== second.length) {
188
+ return false;
189
+ }
190
+ return first.every(function (filter, index) {
191
+ var other = second[index];
192
+ return Boolean(filter && other) && filter.keypath === other.keypath && filter.color === other.color;
193
+ });
194
+ };
195
+ var applyColorFilters = exports.applyColorFilters = function applyColorFilters(animationData, colorFilters) {
196
+ if (!animationData || !Array.isArray(colorFilters) || colorFilters.length === 0) {
197
+ return animationData;
198
+ }
199
+ var filters = colorFilters.filter(function (filter) {
200
+ return filter && typeof filter.keypath === 'string';
201
+ }).map(function (filter) {
202
+ return {
203
+ parts: splitKeypath(filter.keypath.trim()),
204
+ color: parseColor(filter.color)
205
+ };
206
+ }).filter(function (filter) {
207
+ return filter.color;
208
+ });
209
+ if (filters.length === 0) return animationData;
210
+ var data = JSON.parse(JSON.stringify(animationData));
211
+ var assets = new Map();
212
+ if (Array.isArray(data.assets)) {
213
+ data.assets.forEach(function (asset) {
214
+ if (asset && asset.id) assets.set(asset.id, asset);
215
+ });
216
+ }
217
+ var applied = false;
218
+ var recolor = function recolor(node, path) {
219
+ filters.forEach(function (filter) {
220
+ if (!_matchesKeypath(filter.parts, path)) return;
221
+ if (node.c && setColorProperty(node.c, filter.color)) applied = true;
222
+ if (node.ty === 1 && setSolidColor(node, filter.color)) applied = true;
223
+ });
224
+ };
225
+ var _walkShapes = function walkShapes(shapes, path) {
226
+ if (!Array.isArray(shapes)) return;
227
+ shapes.forEach(function (shape) {
228
+ if (!shape || _typeof(shape) !== 'object') return;
229
+ var shapePath = shape.nm ? [].concat(_toConsumableArray(path), [shape.nm]) : path;
230
+ recolor(shape, shapePath);
231
+ _walkShapes(shape.it, shapePath);
232
+ _walkShapes(shape.shapes, shapePath);
233
+ });
234
+ };
235
+
236
+ /* Precompositions are walked twice: once from the asset (so a keypath can
237
+ start at the inner layer name) and once from each layer that references
238
+ them (so `Precomp.InnerLayer...` also works). `refStack` prevents a
239
+ self-referencing composition from recursing forever. */
240
+ var _walkLayers = function walkLayers(layers, prefix, refStack) {
241
+ if (!Array.isArray(layers)) return;
242
+ layers.forEach(function (layer) {
243
+ if (!layer || _typeof(layer) !== 'object') return;
244
+ var path = layer.nm ? [].concat(_toConsumableArray(prefix), [layer.nm]) : prefix;
245
+ recolor(layer, path);
246
+ _walkShapes(layer.shapes, path);
247
+ var asset = layer.refId ? assets.get(layer.refId) : null;
248
+ if (layer.ty === 0 && asset && !refStack.includes(layer.refId)) {
249
+ _walkLayers(asset.layers, path, [].concat(_toConsumableArray(refStack), [layer.refId]));
250
+ }
251
+ });
252
+ };
253
+ _walkLayers(data.layers, [], []);
254
+ if (Array.isArray(data.assets)) {
255
+ data.assets.forEach(function (asset) {
256
+ return _walkLayers(asset.layers, [], []);
257
+ });
258
+ }
259
+ return applied ? data : animationData;
260
+ };
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ exports["default"] = void 0;
7
7
  var _react = _interopRequireDefault(require("react"));
8
8
  var _propTypes = _interopRequireDefault(require("prop-types"));
9
9
  var _lottieWeb = _interopRequireDefault(require("lottie-web"));
10
+ var _colorFilters = require("./colorFilters");
10
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
11
12
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
12
13
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
@@ -56,7 +57,8 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
56
57
  value: function componentDidMount() {
57
58
  var _this$props = this.props,
58
59
  options = _this$props.options,
59
- eventListeners = _this$props.eventListeners;
60
+ eventListeners = _this$props.eventListeners,
61
+ colorFilters = _this$props.colorFilters;
60
62
  var loop = options.loop,
61
63
  autoplay = options.autoplay,
62
64
  animationData = options.animationData,
@@ -64,20 +66,33 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
64
66
  renderer = _options$renderer === void 0 ? 'svg' : _options$renderer,
65
67
  rendererSettings = options.rendererSettings,
66
68
  segments = options.segments;
69
+
70
+ /* Keep the caller's data and filter list around so a later update can tell
71
+ whether either actually changed. */
72
+ this.animationData = animationData;
73
+ this.colorFilters = colorFilters;
74
+ var filteredAnimationData = (0, _colorFilters.applyColorFilters)(animationData, colorFilters);
67
75
  this.options = {
68
76
  container: this.el,
69
77
  renderer: renderer,
70
78
  loop: loop !== false,
71
79
  autoplay: autoplay !== false,
72
80
  segments: segments !== false,
73
- animationData: animationData,
81
+ animationData: filteredAnimationData,
74
82
  rendererSettings: rendererSettings
75
83
  };
76
84
  this.options = _objectSpread(_objectSpread({}, this.options), options);
85
+ /* Spreading `options` restored the unfiltered data; use the filtered copy
86
+ after the merge. */
87
+ this.options.animationData = filteredAnimationData;
88
+ if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production' && Array.isArray(colorFilters) && colorFilters.length > 0 && !animationData) {
89
+ console.warn('[react-lottie] colorFilters requires options.animationData; ' + 'animations loaded from options.path are not recolored.');
90
+ }
77
91
  this.anim = _lottieWeb["default"].loadAnimation(this.options);
78
92
  this.registerEvents(eventListeners);
79
93
  this.setSpeed();
80
94
  this.setDirection();
95
+ this.setSubframe();
81
96
 
82
97
  /* Apply a segment passed on mount. The component otherwise only reacts to
83
98
  `segments` changes, so the animation used to play the whole timeline
@@ -100,17 +115,35 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
100
115
  }, {
101
116
  key: "componentDidUpdate",
102
117
  value: function componentDidUpdate(prevProps) {
103
- /* Recreate the animation handle if the data or its remote path changed.
104
- Done here rather than in componentWillUpdate, which React has deprecated
105
- and which is unsafe in concurrent rendering (#77, #100, #119). */
106
- var dataChanged = this.options.animationData !== this.props.options.animationData;
118
+ /* Recreate the animation handle if the data, its remote path or the color
119
+ filters changed. Done here rather than in componentWillUpdate, which
120
+ React has deprecated and which is unsafe in concurrent rendering
121
+ (#77, #100, #119). */
122
+ var dataChanged = this.animationData !== this.props.options.animationData;
107
123
  var pathChanged = this.options.path !== this.props.options.path;
108
- if (dataChanged || pathChanged) {
124
+ var colorFiltersChanged = !(0, _colorFilters.colorFiltersAreEqual)(this.colorFilters, this.props.colorFilters);
125
+ if (dataChanged || pathChanged || colorFiltersChanged) {
126
+ var currentFrame = this.anim ? this.anim.currentFrame : 0;
127
+ var wasPaused = this.anim ? this.anim.isPaused : true;
128
+ /* lottie-web cannot recolor a loaded animation, so a filter change has to
129
+ load the data again. Only a filter-only change restores the frame. */
130
+ var colorOnlyChange = colorFiltersChanged && !dataChanged && !pathChanged;
109
131
  this.deRegisterEvents(prevProps.eventListeners);
110
132
  this.destroy();
133
+ this.animationData = this.props.options.animationData;
134
+ this.colorFilters = this.props.colorFilters;
111
135
  this.options = _objectSpread(_objectSpread({}, this.options), this.props.options);
136
+ this.options.animationData = (0, _colorFilters.applyColorFilters)(this.props.options.animationData, this.props.colorFilters);
112
137
  this.anim = _lottieWeb["default"].loadAnimation(this.options);
113
138
  this.registerEvents(this.props.eventListeners);
139
+
140
+ /* Keep the frame and playback state so a theme change does not restart
141
+ a paused or half-played animation. */
142
+ if (colorOnlyChange) {
143
+ if (this.props.segments) this.anim.playSegments(this.props.segments, true);
144
+ if (Number.isFinite(currentFrame)) this.anim.goToAndStop(currentFrame, true);
145
+ if (!wasPaused) this.anim.play();
146
+ }
114
147
  }
115
148
 
116
149
  /* Only drive playback when a playback prop asks for it. Re-rendering used
@@ -132,6 +165,7 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
132
165
  }
133
166
  this.setSpeed();
134
167
  this.setDirection();
168
+ this.setSubframe();
135
169
  }
136
170
  }, {
137
171
  key: "componentWillUnmount",
@@ -151,11 +185,33 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
151
185
  value: function setDirection() {
152
186
  this.anim.setDirection(this.props.direction);
153
187
  }
188
+
189
+ /* `isSubframe` mirrors lottie-web's setSubframe(): with it enabled (the
190
+ default) frames are interpolated, with it disabled `enterFrame` fires only
191
+ on whole After Effects frames (issue #71). */
192
+ }, {
193
+ key: "setSubframe",
194
+ value: function setSubframe() {
195
+ this.anim.setSubframe(this.props.isSubframe);
196
+ }
154
197
  }, {
155
198
  key: "play",
156
199
  value: function play() {
157
200
  this.anim.play();
158
201
  }
202
+
203
+ /* Frame/marker control from a ref, e.g. to sync an animation to scroll.
204
+ `isFrame` false treats the value as a time in seconds (issue #40). */
205
+ }, {
206
+ key: "goToAndStop",
207
+ value: function goToAndStop(value, isFrame) {
208
+ this.anim.goToAndStop(value, isFrame);
209
+ }
210
+ }, {
211
+ key: "goToAndPlay",
212
+ value: function goToAndPlay(value, isFrame) {
213
+ this.anim.goToAndPlay(value, isFrame);
214
+ }
159
215
  }, {
160
216
  key: "playSegments",
161
217
  value: function playSegments() {
@@ -251,6 +307,10 @@ var Lottie = exports["default"] = /*#__PURE__*/function (_React$Component) {
251
307
  Lottie.propTypes = {
252
308
  eventListeners: _propTypes["default"].arrayOf(_propTypes["default"].object),
253
309
  options: _propTypes["default"].object.isRequired,
310
+ colorFilters: _propTypes["default"].arrayOf(_propTypes["default"].shape({
311
+ keypath: _propTypes["default"].string.isRequired,
312
+ color: _propTypes["default"].string.isRequired
313
+ })),
254
314
  height: _propTypes["default"].oneOfType([_propTypes["default"].string, _propTypes["default"].number]),
255
315
  width: _propTypes["default"].oneOfType([_propTypes["default"].string, _propTypes["default"].number]),
256
316
  isStopped: _propTypes["default"].bool,
@@ -258,6 +318,7 @@ Lottie.propTypes = {
258
318
  speed: _propTypes["default"].number,
259
319
  segments: _propTypes["default"].arrayOf(_propTypes["default"].number),
260
320
  direction: _propTypes["default"].number,
321
+ isSubframe: _propTypes["default"].bool,
261
322
  ariaRole: _propTypes["default"].string,
262
323
  ariaLabel: _propTypes["default"].string,
263
324
  isClickToPauseDisabled: _propTypes["default"].bool,
@@ -267,9 +328,11 @@ Lottie.propTypes = {
267
328
  };
268
329
  Lottie.defaultProps = {
269
330
  eventListeners: [],
331
+ colorFilters: [],
270
332
  isStopped: false,
271
333
  isPaused: false,
272
334
  speed: 1,
335
+ isSubframe: true,
273
336
  ariaRole: 'button',
274
337
  ariaLabel: 'animation',
275
338
  isClickToPauseDisabled: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-lottie",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "funding": "https://opencollective.com/react-lottie",
5
5
  "description": "lottie animation view for React",
6
6
  "repository": {
@@ -51,7 +51,8 @@
51
51
  "react": ">=16.8.0"
52
52
  },
53
53
  "overrides": {
54
- "cheerio": "1.0.0-rc.12"
54
+ "cheerio": "1.0.0-rc.12",
55
+ "minimatch@^10.2.2": "11.0.4"
55
56
  },
56
57
  "resolutions": {
57
58
  "cheerio": "1.0.0-rc.12"