xref: /aosp_15_r20/external/skia/modules/canvaskit/tests/skottie_test.js (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1describe('Skottie behavior', () => {
2    let container;
3
4    beforeEach(async () => {
5        await EverythingLoaded;
6        container = document.createElement('div');
7        container.innerHTML = `
8            <canvas width=600 height=600 id=test></canvas>
9            <canvas width=600 height=600 id=report></canvas>`;
10        document.body.appendChild(container);
11    });
12
13    afterEach(() => {
14        document.body.removeChild(container);
15    });
16
17    const expectArrayCloseTo = (a, b, precision) => {
18        precision = precision || 14; // digits of precision in base 10
19        expect(a.length).toEqual(b.length);
20        for (let i=0; i<a.length; i++) {
21          expect(a[i]).toBeCloseTo(b[i], precision);
22        }
23    };
24
25    const imgPromise = fetch('/assets/flightAnim.gif')
26        .then((response) => response.arrayBuffer());
27    const jsonPromise = fetch('/assets/animated_gif.json')
28        .then((response) => response.text());
29    const washPromise = fetch('/assets/map-shield.json')
30        .then((response) => response.text());
31    const slotPromise = fetch('/assets/skottie_basic_slots.json')
32        .then((response) => response.text());
33    const editPromise = fetch('/assets/text_edit.json')
34        .then((response) => response.text());
35    const inlineFontPromise = fetch('/assets/skottie_inline_font.json')
36        .then((response) => response.text());
37    const notoSerifPromise = fetch('/assets/NotoSerif-Regular.ttf').then(
38        (response) => response.arrayBuffer());
39
40    gm('skottie_animgif', (canvas, promises) => {
41        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
42            console.warn('Skipping test because not compiled with skottie');
43            return;
44        }
45        expect(promises[1]).not.toBe('NOT FOUND');
46        const animation = CanvasKit.MakeManagedAnimation(promises[1], {
47            'flightAnim.gif': promises[0],
48        });
49        expect(animation).toBeTruthy();
50        const bounds = CanvasKit.LTRBRect(0, 0, 500, 500);
51
52        const size = animation.size();
53        expectArrayCloseTo(size, Float32Array.of(800, 600), 4);
54
55        animation.render(canvas, bounds);
56
57        // We intentionally make the length of this array 5 and add a sentinel value
58        // of 999 so we can make sure the bounds are copied into this rect and a new
59        // one is not allocated.
60        const damageRect = Float32Array.of(0, 0, 0, 0, 999);
61
62        // There was a bug, fixed in https://skia-review.googlesource.com/c/skia/+/241757
63        // that seeking again and drawing again revealed.
64        animation.seek(0.5, damageRect);
65        expectArrayCloseTo(damageRect, Float32Array.of(0, 0, 800, 600, 999), 4);
66
67        canvas.clear(CanvasKit.WHITE);
68        animation.render(canvas, bounds);
69        animation.delete();
70    }, imgPromise, jsonPromise);
71
72    gm('skottie_setcolor', (canvas, promises) => {
73        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
74            console.warn('Skipping test because not compiled with skottie');
75            return;
76        }
77        expect(promises[0]).not.toBe('NOT FOUND');
78        const bounds = CanvasKit.LTRBRect(0, 0, 500, 500);
79
80        const animation = CanvasKit.MakeManagedAnimation(promises[0]);
81        expect(animation).toBeTruthy();
82        animation.setColor('$Icon Fill', CanvasKit.RED);
83        animation.seek(0.5);
84        animation.render(canvas, bounds);
85        animation.delete();
86    }, washPromise);
87
88    gm('skottie_slots', (canvas, promises) => {
89        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
90            console.warn('Skipping test because not compiled with skottie');
91            return;
92        }
93        expect(promises[0]).not.toBe('NOT FOUND');
94        const bounds = CanvasKit.LTRBRect(0, 0, 500, 500);
95
96        const animation = CanvasKit.MakeManagedAnimation(promises[0], {
97            'flightAnim.gif': promises[1],
98            'NotoSerif': promises[2],
99        });
100        expect(animation).toBeTruthy();
101
102        const slotInfo = animation.getSlotInfo();
103        expect(slotInfo.colorSlotIDs).toEqual(['FillsGroup', 'StrokeGroup']);
104        expect(slotInfo.scalarSlotIDs).toEqual(['Opacity']);
105        expect(slotInfo.vec2SlotIDs).toEqual(['ScaleGroup']);
106        expect(slotInfo.imageSlotIDs).toEqual(['ImageSource']);
107        expect(slotInfo.textSlotIDs).toEqual(['TextSource']);
108
109        expect(animation.getScalarSlot('Opacity')).toBe(100);
110        const textProp = animation.getTextSlot('TextSource');
111        expect(textProp.text).toBe('text slots');
112
113        textProp.text = 'new text';
114        textProp.fillColor = CanvasKit.CYAN;
115        textProp.strokeColor = CanvasKit.MAGENTA;
116
117        expect(animation.setColorSlot('FillsGroup', CanvasKit.RED)).toBeTruthy();
118        expect(animation.setScalarSlot('Opacity', 25)).toBeTruthy();
119        expect(animation.setVec2Slot('ScaleGroup', [25, 50])).toBeTruthy();
120        expect(animation.setImageSlot('ImageSource', 'flighAnim.gif')).toBeTruthy();
121        expect(animation.setTextSlot('TextSource', textProp)).toBeTruthy();
122
123        expectArrayCloseTo(animation.getColorSlot('FillsGroup'), CanvasKit.RED, 4);
124        expect(animation.getScalarSlot('Opacity')).toBe(25);
125        expectArrayCloseTo(animation.getVec2Slot('ScaleGroup'), [25, 50], 4);
126
127        const newTextSlot = animation.getTextSlot('TextSource');
128        expect(newTextSlot.text).toBe('new text');
129        expectArrayCloseTo(newTextSlot.fillColor, CanvasKit.CYAN, 4);
130        expectArrayCloseTo(newTextSlot.strokeColor, CanvasKit.MAGENTA, 4);
131
132        expect(animation.getColorSlot('Bad ID')).toBeFalsy();
133        expect(animation.getScalarSlot('Bad ID')).toBeFalsy();
134        expect(animation.getVec2Slot('Bad ID')).toBeFalsy();
135        expect(animation.getTextSlot('Bad ID')).toBeFalsy();
136
137        animation.seek(0.5);
138        animation.render(canvas, bounds);
139        animation.delete();
140    }, slotPromise, imgPromise, notoSerifPromise);
141
142    gm('skottie_textedit', (canvas, promises) => {
143        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
144            console.warn('Skipping test because not compiled with skottie');
145            return;
146        }
147        expect(promises[0]).not.toBe('NOT FOUND');
148        const bounds = CanvasKit.LTRBRect(0, 0, 600, 600);
149
150        const animation = CanvasKit.MakeManagedAnimation(promises[0], {
151            // The animation is looking for a font called ArialMT, but we just
152            // provide it the data for an arbitrary typeface.
153            "ArialMT": promises[1],
154        });
155        expect(animation).toBeTruthy();
156
157        // The animation contains two text layers grouped under the "text_layer" ID, and one
158        // descriptive text layer.
159        {
160            const texts = animation.getTextProps();
161            expect(texts.length).toEqual(2);
162            expect(texts[0].key).toEqual('text_layer');
163            expect(texts[0].value.text).toEqual('foo');
164        }
165
166        expect(animation.attachEditor('txt_layer', 0)).toBeFalse();  // nonexistent layer
167        expect(animation.attachEditor('text_layer', 2)).toBeFalse(); // nonexistent index
168        expect(animation.attachEditor('text_layer', 0)).toBeTrue();
169        expect(animation.attachEditor('text_layer', 1)).toBeTrue();
170
171        {
172            // no effect, editor inactive
173            expect(animation.dispatchEditorKey('Backspace')).toBeFalse();
174
175            const texts = animation.getTextProps();
176            expect(texts.length).toEqual(2);
177            expect(texts[0].key).toEqual('text_layer');
178            expect(texts[0].value.text).toEqual('foo');
179        }
180
181        animation.enableEditor(true);
182        animation.setEditorCursorWeight(1.5);
183
184        // To be fully functional, the editor requires glyph data issued during rendering callbacks.
185        animation.seek(0);
186        animation.render(canvas, bounds);
187
188        {
189            expect(animation.dispatchEditorKey('Backspace')).toBeTrue();
190            expect(animation.dispatchEditorKey('Backspace')).toBeTrue();
191            expect(animation.dispatchEditorKey('Backspace')).toBeTrue();
192            expect(animation.dispatchEditorKey('b')).toBeTrue();
193            expect(animation.dispatchEditorKey('a')).toBeTrue();
194            expect(animation.dispatchEditorKey('r')).toBeTrue();
195
196            const texts = animation.getTextProps();
197            expect(texts.length).toEqual(2);
198            expect(texts[0].key).toEqual('text_layer');
199            expect(texts[0].value.text).toEqual('bar');
200        }
201
202        // Final render, after edits.
203        animation.seek(0);
204        animation.render(canvas, bounds);
205        animation.delete();
206    }, editPromise, notoSerifPromise);
207
208    gm('skottie_inlinefont', (canvas, promises) => {
209        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
210            console.warn('Skipping test because not compiled with skottie');
211            return;
212        }
213        expect(promises[0]).not.toBe('NOT FOUND');
214        const bounds = CanvasKit.LTRBRect(0, 0, 600, 600);
215
216        const animation = CanvasKit.MakeManagedAnimation(promises[0]);
217        expect(animation).toBeTruthy();
218
219        animation.seek(0);
220        animation.render(canvas, bounds);
221        animation.delete();
222    }, inlineFontPromise);
223
224    it('can load audio assets', (done) => {
225        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
226            console.warn('Skipping test because not compiled with skottie');
227            return;
228        }
229        const mockSoundMap = {
230            map : new Map(),
231            getPlayer : function(name) {return this.map.get(name)},
232            setPlayer : function(name, player) {this.map.set(name, player)},
233        };
234        function mockPlayer(name) {
235            this.name = name;
236            this.wasPlayed = false,
237            this.seek = function(t) {
238                this.wasPlayed = true;
239            }
240        }
241        for (let i = 0; i < 20; i++) {
242            var name = 'audio_' + i;
243            mockSoundMap.setPlayer(name, new mockPlayer(name));
244        }
245        fetch('/assets/audio_external.json')
246        .then((response) => response.text())
247        .then((lottie) => {
248            const animation = CanvasKit.MakeManagedAnimation(lottie, null, null, mockSoundMap);
249            expect(animation).toBeTruthy();
250            // 190 frames in sample lottie
251            for (let t = 0; t < 190; t++) {
252                animation.seekFrame(t);
253            }
254            animation.delete();
255            for(const player of mockSoundMap.map.values()) {
256                expect(player.wasPlayed).toBeTrue(player.name + " was not played");
257            }
258            done();
259        });
260    });
261
262    it('can get logs', (done) => {
263        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
264            console.warn('Skipping test because not compiled with skottie');
265            return;
266        }
267
268        const logger = {
269           errors:   [],
270           warnings: [],
271
272           reset: function() { this.errors = []; this.warnings = []; },
273
274           // Logger API
275           onError:   function(err) { this.errors.push(err)   },
276           onWarning: function(wrn) { this.warnings.push(wrn) }
277        };
278
279        {
280            const json = `{
281                "v": "5.2.1",
282                "w": 100,
283                "h": 100,
284                "fr": 10,
285                "ip": 0,
286                "op": 100,
287                "layers": [{
288                    "ty": 3,
289                    "nm": "null",
290                    "ind": 0,
291                    "ip": 0
292                }]
293            }`;
294            const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger);
295            expect(animation).toBeTruthy();
296            expect(logger.errors.length).toEqual(0);
297            expect(logger.warnings.length).toEqual(0);
298        }
299
300        {
301            const json = `{
302                "v": "5.2.1",
303                "w": 100,
304                "h": 100,
305                "fr": 10,
306                "ip": 0,
307                "op": 100,
308                "layers": [{
309                    "ty": 2,
310                    "nm": "image",
311                    "ind": 0,
312                    "ip": 0
313                }]
314            }`;
315            const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger);
316            expect(animation).toBeTruthy();
317            expect(logger.errors.length).toEqual(1);
318            expect(logger.warnings.length).toEqual(0);
319
320            // Image layer missing refID
321            expect(logger.errors[0].includes('missing ref'));
322            logger.reset();
323        }
324
325        {
326            const json = `{
327                "v": "5.2.1",
328                "w": 100,
329                "h": 100,
330                "fr": 10,
331                "ip": 0,
332                "op": 100,
333                "layers": [{
334                    "ty": 1,
335                    "nm": "solid",
336                    "sw": 100,
337                    "sh": 100,
338                    "sc": "#aabbcc",
339                    "ind": 0,
340                    "ip": 0,
341                    "ef": [{
342                      "mn": "FOO"
343                    }]
344                }]
345            }`;
346            const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger);
347            expect(animation).toBeTruthy();
348            expect(logger.errors.length).toEqual(0);
349            expect(logger.warnings.length).toEqual(1);
350
351            // Unsupported effect FOO
352            expect(logger.warnings[0].includes('FOO'));
353            logger.reset();
354        }
355
356        done();
357    });
358
359    it('can access dynamic props', () => {
360        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
361            console.warn('Skipping test because not compiled with skottie');
362            return;
363        }
364
365        const json = `{
366            "v": "5.2.1",
367            "w": 100,
368            "h": 100,
369            "fr": 10,
370            "ip": 0,
371            "op": 100,
372            "fonts": {
373              "list": [{
374                "fName": "test_font",
375                "fFamily": "test-family",
376                "fStyle": "TestFontStyle"
377              }]
378            },
379            "layers": [
380              {
381                "ty": 4,
382                "nm": "__shape_layer",
383                "ind": 0,
384                "ip": 0,
385                "shapes": [
386                  {
387                    "ty": "el",
388                    "p": { "a": 0, "k": [ 50, 50 ] },
389                    "s": { "a": 0, "k": [ 50, 50 ] }
390                  },{
391                    "ty": "fl",
392                    "nm": "__shape_fill",
393                    "c": { "a": 0, "k": [ 1, 0, 0] }
394                  },{
395                    "ty": "tr",
396                    "nm": "__shape_opacity",
397                    "o": { "a": 0, "k": 50 }
398                  }
399                ]
400              },{
401                "ty": 5,
402                "nm": "__text_layer",
403                "ip": 0,
404                "t": {
405                  "d": {
406                    "k": [{
407                      "t": 0,
408                      "s": {
409                        "f": "test_font",
410                        "s": 100,
411                        "t": "Foo Bar Baz",
412                        "lh": 120,
413                        "ls": 12
414                      }
415                    }]
416                  }
417                }
418              }
419            ]
420        }`;
421
422        const animation = CanvasKit.MakeManagedAnimation(json, null, '__');
423        expect(animation).toBeTruthy();
424
425        {
426            const colors = animation.getColorProps();
427            expect(colors.length).toEqual(1);
428            expect(colors[0].key).toEqual('__shape_fill');
429            expect(colors[0].value).toEqual(CanvasKit.ColorAsInt(255,0,0,255));
430
431            const opacities = animation.getOpacityProps();
432            expect(opacities.length).toEqual(1);
433            expect(opacities[0].key).toEqual('__shape_opacity');
434            expect(opacities[0].value).toEqual(50);
435
436            const texts = animation.getTextProps();
437            expect(texts.length).toEqual(1);
438            expect(texts[0].key).toEqual('__text_layer');
439            expect(texts[0].value.text).toEqual('Foo Bar Baz');
440            expect(texts[0].value.size).toEqual(100);
441        }
442
443        expect(animation.setColor('__shape_fill', [0,1,0,1])).toEqual(true);
444        expect(animation.setOpacity('__shape_opacity', 100)).toEqual(true);
445        expect(animation.setText('__text_layer', 'baz bar foo', 10)).toEqual(true);
446
447        {
448            const colors = animation.getColorProps();
449            expect(colors.length).toEqual(1);
450            expect(colors[0].key).toEqual('__shape_fill');
451            expect(colors[0].value).toEqual(CanvasKit.ColorAsInt(0,255,0,255));
452
453            const opacities = animation.getOpacityProps();
454            expect(opacities.length).toEqual(1);
455            expect(opacities[0].key).toEqual('__shape_opacity');
456            expect(opacities[0].value).toEqual(100);
457
458            const texts = animation.getTextProps();
459            expect(texts.length).toEqual(1);
460            expect(texts[0].key).toEqual('__text_layer');
461            expect(texts[0].value.text).toEqual('baz bar foo');
462            expect(texts[0].value.size).toEqual(10);
463        }
464
465        expect(animation.setColor('INVALID_KEY', [0,1,0,1])).toEqual(false);
466        expect(animation.setOpacity('INVALID_KEY', 100)).toEqual(false);
467        expect(animation.setText('INVALID KEY', '', 10)).toEqual(false);
468    });
469});
470