describe('Skottie behavior', () => { let container; beforeEach(async () => { await EverythingLoaded; container = document.createElement('div'); container.innerHTML = ` `; document.body.appendChild(container); }); afterEach(() => { document.body.removeChild(container); }); const expectArrayCloseTo = (a, b, precision) => { precision = precision || 14; // digits of precision in base 10 expect(a.length).toEqual(b.length); for (let i=0; i response.arrayBuffer()); const jsonPromise = fetch('/assets/animated_gif.json') .then((response) => response.text()); const washPromise = fetch('/assets/map-shield.json') .then((response) => response.text()); const slotPromise = fetch('/assets/skottie_basic_slots.json') .then((response) => response.text()); const editPromise = fetch('/assets/text_edit.json') .then((response) => response.text()); const inlineFontPromise = fetch('/assets/skottie_inline_font.json') .then((response) => response.text()); const notoSerifPromise = fetch('/assets/NotoSerif-Regular.ttf').then( (response) => response.arrayBuffer()); gm('skottie_animgif', (canvas, promises) => { if (!CanvasKit.skottie || !CanvasKit.managed_skottie) { console.warn('Skipping test because not compiled with skottie'); return; } expect(promises[1]).not.toBe('NOT FOUND'); const animation = CanvasKit.MakeManagedAnimation(promises[1], { 'flightAnim.gif': promises[0], }); expect(animation).toBeTruthy(); const bounds = CanvasKit.LTRBRect(0, 0, 500, 500); const size = animation.size(); expectArrayCloseTo(size, Float32Array.of(800, 600), 4); animation.render(canvas, bounds); // We intentionally make the length of this array 5 and add a sentinel value // of 999 so we can make sure the bounds are copied into this rect and a new // one is not allocated. const damageRect = Float32Array.of(0, 0, 0, 0, 999); // There was a bug, fixed in https://skia-review.googlesource.com/c/skia/+/241757 // that seeking again and drawing again revealed. animation.seek(0.5, damageRect); expectArrayCloseTo(damageRect, Float32Array.of(0, 0, 800, 600, 999), 4); canvas.clear(CanvasKit.WHITE); animation.render(canvas, bounds); animation.delete(); }, imgPromise, jsonPromise); gm('skottie_setcolor', (canvas, promises) => { if (!CanvasKit.skottie || !CanvasKit.managed_skottie) { console.warn('Skipping test because not compiled with skottie'); return; } expect(promises[0]).not.toBe('NOT FOUND'); const bounds = CanvasKit.LTRBRect(0, 0, 500, 500); const animation = CanvasKit.MakeManagedAnimation(promises[0]); expect(animation).toBeTruthy(); animation.setColor('$Icon Fill', CanvasKit.RED); animation.seek(0.5); animation.render(canvas, bounds); animation.delete(); }, washPromise); gm('skottie_slots', (canvas, promises) => { if (!CanvasKit.skottie || !CanvasKit.managed_skottie) { console.warn('Skipping test because not compiled with skottie'); return; } expect(promises[0]).not.toBe('NOT FOUND'); const bounds = CanvasKit.LTRBRect(0, 0, 500, 500); const animation = CanvasKit.MakeManagedAnimation(promises[0], { 'flightAnim.gif': promises[1], 'NotoSerif': promises[2], }); expect(animation).toBeTruthy(); const slotInfo = animation.getSlotInfo(); expect(slotInfo.colorSlotIDs).toEqual(['FillsGroup', 'StrokeGroup']); expect(slotInfo.scalarSlotIDs).toEqual(['Opacity']); expect(slotInfo.vec2SlotIDs).toEqual(['ScaleGroup']); expect(slotInfo.imageSlotIDs).toEqual(['ImageSource']); expect(slotInfo.textSlotIDs).toEqual(['TextSource']); expect(animation.getScalarSlot('Opacity')).toBe(100); const textProp = animation.getTextSlot('TextSource'); expect(textProp.text).toBe('text slots'); textProp.text = 'new text'; textProp.fillColor = CanvasKit.CYAN; textProp.strokeColor = CanvasKit.MAGENTA; expect(animation.setColorSlot('FillsGroup', CanvasKit.RED)).toBeTruthy(); expect(animation.setScalarSlot('Opacity', 25)).toBeTruthy(); expect(animation.setVec2Slot('ScaleGroup', [25, 50])).toBeTruthy(); expect(animation.setImageSlot('ImageSource', 'flighAnim.gif')).toBeTruthy(); expect(animation.setTextSlot('TextSource', textProp)).toBeTruthy(); expectArrayCloseTo(animation.getColorSlot('FillsGroup'), CanvasKit.RED, 4); expect(animation.getScalarSlot('Opacity')).toBe(25); expectArrayCloseTo(animation.getVec2Slot('ScaleGroup'), [25, 50], 4); const newTextSlot = animation.getTextSlot('TextSource'); expect(newTextSlot.text).toBe('new text'); expectArrayCloseTo(newTextSlot.fillColor, CanvasKit.CYAN, 4); expectArrayCloseTo(newTextSlot.strokeColor, CanvasKit.MAGENTA, 4); expect(animation.getColorSlot('Bad ID')).toBeFalsy(); expect(animation.getScalarSlot('Bad ID')).toBeFalsy(); expect(animation.getVec2Slot('Bad ID')).toBeFalsy(); expect(animation.getTextSlot('Bad ID')).toBeFalsy(); animation.seek(0.5); animation.render(canvas, bounds); animation.delete(); }, slotPromise, imgPromise, notoSerifPromise); gm('skottie_textedit', (canvas, promises) => { if (!CanvasKit.skottie || !CanvasKit.managed_skottie) { console.warn('Skipping test because not compiled with skottie'); return; } expect(promises[0]).not.toBe('NOT FOUND'); const bounds = CanvasKit.LTRBRect(0, 0, 600, 600); const animation = CanvasKit.MakeManagedAnimation(promises[0], { // The animation is looking for a font called ArialMT, but we just // provide it the data for an arbitrary typeface. "ArialMT": promises[1], }); expect(animation).toBeTruthy(); // The animation contains two text layers grouped under the "text_layer" ID, and one // descriptive text layer. { const texts = animation.getTextProps(); expect(texts.length).toEqual(2); expect(texts[0].key).toEqual('text_layer'); expect(texts[0].value.text).toEqual('foo'); } expect(animation.attachEditor('txt_layer', 0)).toBeFalse(); // nonexistent layer expect(animation.attachEditor('text_layer', 2)).toBeFalse(); // nonexistent index expect(animation.attachEditor('text_layer', 0)).toBeTrue(); expect(animation.attachEditor('text_layer', 1)).toBeTrue(); { // no effect, editor inactive expect(animation.dispatchEditorKey('Backspace')).toBeFalse(); const texts = animation.getTextProps(); expect(texts.length).toEqual(2); expect(texts[0].key).toEqual('text_layer'); expect(texts[0].value.text).toEqual('foo'); } animation.enableEditor(true); animation.setEditorCursorWeight(1.5); // To be fully functional, the editor requires glyph data issued during rendering callbacks. animation.seek(0); animation.render(canvas, bounds); { expect(animation.dispatchEditorKey('Backspace')).toBeTrue(); expect(animation.dispatchEditorKey('Backspace')).toBeTrue(); expect(animation.dispatchEditorKey('Backspace')).toBeTrue(); expect(animation.dispatchEditorKey('b')).toBeTrue(); expect(animation.dispatchEditorKey('a')).toBeTrue(); expect(animation.dispatchEditorKey('r')).toBeTrue(); const texts = animation.getTextProps(); expect(texts.length).toEqual(2); expect(texts[0].key).toEqual('text_layer'); expect(texts[0].value.text).toEqual('bar'); } // Final render, after edits. animation.seek(0); animation.render(canvas, bounds); animation.delete(); }, editPromise, notoSerifPromise); gm('skottie_inlinefont', (canvas, promises) => { if (!CanvasKit.skottie || !CanvasKit.managed_skottie) { console.warn('Skipping test because not compiled with skottie'); return; } expect(promises[0]).not.toBe('NOT FOUND'); const bounds = CanvasKit.LTRBRect(0, 0, 600, 600); const animation = CanvasKit.MakeManagedAnimation(promises[0]); expect(animation).toBeTruthy(); animation.seek(0); animation.render(canvas, bounds); animation.delete(); }, inlineFontPromise); it('can load audio assets', (done) => { if (!CanvasKit.skottie || !CanvasKit.managed_skottie) { console.warn('Skipping test because not compiled with skottie'); return; } const mockSoundMap = { map : new Map(), getPlayer : function(name) {return this.map.get(name)}, setPlayer : function(name, player) {this.map.set(name, player)}, }; function mockPlayer(name) { this.name = name; this.wasPlayed = false, this.seek = function(t) { this.wasPlayed = true; } } for (let i = 0; i < 20; i++) { var name = 'audio_' + i; mockSoundMap.setPlayer(name, new mockPlayer(name)); } fetch('/assets/audio_external.json') .then((response) => response.text()) .then((lottie) => { const animation = CanvasKit.MakeManagedAnimation(lottie, null, null, mockSoundMap); expect(animation).toBeTruthy(); // 190 frames in sample lottie for (let t = 0; t < 190; t++) { animation.seekFrame(t); } animation.delete(); for(const player of mockSoundMap.map.values()) { expect(player.wasPlayed).toBeTrue(player.name + " was not played"); } done(); }); }); it('can get logs', (done) => { if (!CanvasKit.skottie || !CanvasKit.managed_skottie) { console.warn('Skipping test because not compiled with skottie'); return; } const logger = { errors: [], warnings: [], reset: function() { this.errors = []; this.warnings = []; }, // Logger API onError: function(err) { this.errors.push(err) }, onWarning: function(wrn) { this.warnings.push(wrn) } }; { const json = `{ "v": "5.2.1", "w": 100, "h": 100, "fr": 10, "ip": 0, "op": 100, "layers": [{ "ty": 3, "nm": "null", "ind": 0, "ip": 0 }] }`; const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger); expect(animation).toBeTruthy(); expect(logger.errors.length).toEqual(0); expect(logger.warnings.length).toEqual(0); } { const json = `{ "v": "5.2.1", "w": 100, "h": 100, "fr": 10, "ip": 0, "op": 100, "layers": [{ "ty": 2, "nm": "image", "ind": 0, "ip": 0 }] }`; const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger); expect(animation).toBeTruthy(); expect(logger.errors.length).toEqual(1); expect(logger.warnings.length).toEqual(0); // Image layer missing refID expect(logger.errors[0].includes('missing ref')); logger.reset(); } { const json = `{ "v": "5.2.1", "w": 100, "h": 100, "fr": 10, "ip": 0, "op": 100, "layers": [{ "ty": 1, "nm": "solid", "sw": 100, "sh": 100, "sc": "#aabbcc", "ind": 0, "ip": 0, "ef": [{ "mn": "FOO" }] }] }`; const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger); expect(animation).toBeTruthy(); expect(logger.errors.length).toEqual(0); expect(logger.warnings.length).toEqual(1); // Unsupported effect FOO expect(logger.warnings[0].includes('FOO')); logger.reset(); } done(); }); it('can access dynamic props', () => { if (!CanvasKit.skottie || !CanvasKit.managed_skottie) { console.warn('Skipping test because not compiled with skottie'); return; } const json = `{ "v": "5.2.1", "w": 100, "h": 100, "fr": 10, "ip": 0, "op": 100, "fonts": { "list": [{ "fName": "test_font", "fFamily": "test-family", "fStyle": "TestFontStyle" }] }, "layers": [ { "ty": 4, "nm": "__shape_layer", "ind": 0, "ip": 0, "shapes": [ { "ty": "el", "p": { "a": 0, "k": [ 50, 50 ] }, "s": { "a": 0, "k": [ 50, 50 ] } },{ "ty": "fl", "nm": "__shape_fill", "c": { "a": 0, "k": [ 1, 0, 0] } },{ "ty": "tr", "nm": "__shape_opacity", "o": { "a": 0, "k": 50 } } ] },{ "ty": 5, "nm": "__text_layer", "ip": 0, "t": { "d": { "k": [{ "t": 0, "s": { "f": "test_font", "s": 100, "t": "Foo Bar Baz", "lh": 120, "ls": 12 } }] } } } ] }`; const animation = CanvasKit.MakeManagedAnimation(json, null, '__'); expect(animation).toBeTruthy(); { const colors = animation.getColorProps(); expect(colors.length).toEqual(1); expect(colors[0].key).toEqual('__shape_fill'); expect(colors[0].value).toEqual(CanvasKit.ColorAsInt(255,0,0,255)); const opacities = animation.getOpacityProps(); expect(opacities.length).toEqual(1); expect(opacities[0].key).toEqual('__shape_opacity'); expect(opacities[0].value).toEqual(50); const texts = animation.getTextProps(); expect(texts.length).toEqual(1); expect(texts[0].key).toEqual('__text_layer'); expect(texts[0].value.text).toEqual('Foo Bar Baz'); expect(texts[0].value.size).toEqual(100); } expect(animation.setColor('__shape_fill', [0,1,0,1])).toEqual(true); expect(animation.setOpacity('__shape_opacity', 100)).toEqual(true); expect(animation.setText('__text_layer', 'baz bar foo', 10)).toEqual(true); { const colors = animation.getColorProps(); expect(colors.length).toEqual(1); expect(colors[0].key).toEqual('__shape_fill'); expect(colors[0].value).toEqual(CanvasKit.ColorAsInt(0,255,0,255)); const opacities = animation.getOpacityProps(); expect(opacities.length).toEqual(1); expect(opacities[0].key).toEqual('__shape_opacity'); expect(opacities[0].value).toEqual(100); const texts = animation.getTextProps(); expect(texts.length).toEqual(1); expect(texts[0].key).toEqual('__text_layer'); expect(texts[0].value.text).toEqual('baz bar foo'); expect(texts[0].value.size).toEqual(10); } expect(animation.setColor('INVALID_KEY', [0,1,0,1])).toEqual(false); expect(animation.setOpacity('INVALID_KEY', 100)).toEqual(false); expect(animation.setText('INVALID KEY', '', 10)).toEqual(false); }); });