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