describe('Core canvas behavior', () => { let container; beforeEach(async () => { await LoadCanvasKit; container = document.createElement('div'); container.innerHTML = ` `; document.body.appendChild(container); }); afterEach(() => { document.body.removeChild(container); }); gm('picture_test', (canvas) => { const spr = new CanvasKit.SkPictureRecorder(); const rcanvas = spr.beginRecording( CanvasKit.LTRBRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)); const paint = new CanvasKit.SkPaint(); paint.setStrokeWidth(2.0); paint.setAntiAlias(true); paint.setColor(CanvasKit.Color(0, 0, 0, 1.0)); paint.setStyle(CanvasKit.PaintStyle.Stroke); rcanvas.drawRoundRect(CanvasKit.LTRBRect(5, 35, 45, 80), 15, 10, paint); const font = new CanvasKit.SkFont(null, 20); rcanvas.drawText('this picture has a round rect', 5, 100, paint, font); const pic = spr.finishRecordingAsPicture(); spr.delete(); paint.delete(); canvas.drawPicture(pic); pic.delete(); }); const uIntColorToCanvasKitColor = (c) => { return CanvasKit.Color( (c >> 16) & 0xFF, (c >> 8) & 0xFF, (c >> 0) & 0xFF, ((c >> 24) & 0xFF) / 255 ); } it('can compute tonal colors', () => { const input = { ambient: CanvasKit.BLUE, spot: CanvasKit.RED, }; const out = CanvasKit.computeTonalColors(input); expect(new Float32Array(out.ambient)).toEqual(CanvasKit.BLACK); const expectedSpot = [0.173, 0, 0, 0.969]; expect(out.spot[0]).toBeCloseTo(expectedSpot[0], 3); expect(out.spot[1]).toBeCloseTo(expectedSpot[1], 3); expect(out.spot[2]).toBeCloseTo(expectedSpot[2], 3); expect(out.spot[3]).toBeCloseTo(expectedSpot[3], 3); }); // This helper is used for all the MakeImageFromEncoded tests. // TODO(kjlubick): rewrite this and callers to use gm function decodeAndDrawSingleFrameImage(imgName, goldName, done) { const imgPromise = fetch(imgName) .then((response) => response.arrayBuffer()); Promise.all([imgPromise, LoadCanvasKit]).then((values) => { const imgData = values[0]; expect(imgData).toBeTruthy(); catchException(done, () => { let img = CanvasKit.MakeImageFromEncoded(imgData); expect(img).toBeTruthy(); const surface = CanvasKit.MakeCanvasSurface('test'); expect(surface).toBeTruthy('Could not make surface'); if (!surface) { done(); return; } const canvas = surface.getCanvas(); let paint = new CanvasKit.SkPaint(); canvas.drawImage(img, 0, 0, paint); paint.delete(); img.delete(); reportSurface(surface, goldName, done); })(); }); } it('can decode and draw a png', (done) => { decodeAndDrawSingleFrameImage('/assets/mandrill_512.png', 'drawImage_png', done); }); it('can decode and draw a jpg', (done) => { decodeAndDrawSingleFrameImage('/assets/mandrill_h1v1.jpg', 'drawImage_jpg', done); }); it('can decode and draw a (still) gif', (done) => { decodeAndDrawSingleFrameImage('/assets/flightAnim.gif', 'drawImage_gif', done); }); it('can decode and draw a still webp', (done) => { decodeAndDrawSingleFrameImage('/assets/color_wheel.webp', 'drawImage_webp', done); }); it('can readPixels from an SkImage', (done) => { const imgPromise = fetch('/assets/mandrill_512.png') .then((response) => response.arrayBuffer()); Promise.all([imgPromise, LoadCanvasKit]).then((values) => { const imgData = values[0]; expect(imgData).toBeTruthy(); catchException(done, () => { let img = CanvasKit.MakeImageFromEncoded(imgData); expect(img).toBeTruthy(); const imageInfo = { alphaType: CanvasKit.AlphaType.Unpremul, colorType: CanvasKit.ColorType.RGBA_8888, width: img.width(), height: img.height(), }; const pixels = img.readPixels(imageInfo, 0, 0); // We know the image is 512 by 512 pixels in size, each pixel // requires 4 bytes (R, G, B, A). expect(pixels.length).toEqual(512 * 512 * 4); img.delete(); done(); })(); }); }); gm('drawDrawable_animated_gif', (canvas, fetchedByteBuffers) => { let aImg = CanvasKit.MakeAnimatedImageFromEncoded(fetchedByteBuffers[0]); expect(aImg).toBeTruthy(); expect(aImg.getRepetitionCount()).toEqual(-1); // infinite loop expect(aImg.width()).toEqual(320); expect(aImg.height()).toEqual(240); expect(aImg.getFrameCount()).toEqual(60); // TODO(kjlubick): deprecate drawAnimatedImage and have it just snap off // an SkImage at the desired frame. canvas.drawAnimatedImage(aImg, 0, 0); let c = aImg.decodeNextFrame(); expect(c).not.toEqual(-1); canvas.drawAnimatedImage(aImg, 300, 0); for(let i = 0; i < 10; i++) { c = aImg.decodeNextFrame(); expect(c).not.toEqual(-1); } canvas.drawAnimatedImage(aImg, 0, 300); for(let i = 0; i < 10; i++) { c = aImg.decodeNextFrame(); expect(c).not.toEqual(-1); } canvas.drawAnimatedImage(aImg, 300, 300); aImg.delete(); }, '/assets/flightAnim.gif'); gm('1x4_from_scratch', (canvas) => { canvas.clear(CanvasKit.WHITE); const paint = new CanvasKit.SkPaint(); // This creates and draws an SkImage that is 1 pixel wide, 4 pixels tall with // the colors listed below. const pixels = Uint8Array.from([ 255, 0, 0, 255, // opaque red 0, 255, 0, 255, // opaque green 0, 0, 255, 255, // opaque blue 255, 0, 255, 100, // transparent purple ]); const img = CanvasKit.MakeImage(pixels, 1, 4, CanvasKit.AlphaType.Unpremul, CanvasKit.ColorType.RGBA_8888); canvas.drawImage(img, 1, 1, paint); img.delete(); }); gm('sweep_gradient', (canvas) => { const paint = new CanvasKit.SkPaint(); const shader = CanvasKit.SkShader.MakeSweepGradient( 100, 100, // X, Y coordinates [CanvasKit.GREEN, CanvasKit.BLUE], [0.0, 1.0], CanvasKit.TileMode.Clamp, ); expect(shader).toBeTruthy('Could not make shader'); paint.setShader(shader); canvas.drawPaint(paint); paint.delete(); shader.delete(); }); // TODO(kjlubick): There's a lot of shared code between the gradient gms // It would be best to deduplicate that in a nice DAMP way. // Inspired by https://fiddle.skia.org/c/b29ce50a341510784ac7d5281586d076 gm('linear_gradients', (canvas) => { canvas.clear(CanvasKit.WHITE); canvas.scale(2, 2); const strokePaint = new CanvasKit.SkPaint(); strokePaint.setStyle(CanvasKit.PaintStyle.Stroke); strokePaint.setColor(CanvasKit.BLACK); const paint = new CanvasKit.SkPaint(); paint.setStyle(CanvasKit.PaintStyle.Fill); const transparentGreen = CanvasKit.Color(0, 255, 255, 0); const lgs = CanvasKit.SkShader.MakeLinearGradient( [0, 0], [50, 100], // start and stop points [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror ); paint.setShader(lgs); let r = CanvasKit.LTRBRect(0, 0, 100, 100); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); const lgsPremul = CanvasKit.SkShader.MakeLinearGradient( [100, 0], [150, 100], // start and stop points [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, null, // no local matrix 1 // interpolate colors in premul ); paint.setShader(lgsPremul); r = CanvasKit.LTRBRect(100, 0, 200, 100); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); const lgs45 = CanvasKit.SkShader.MakeLinearGradient( [0, 100], [50, 200], // start and stop points [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, CanvasKit.SkMatrix.rotated(Math.PI/4, 0, 100), ); paint.setShader(lgs45); r = CanvasKit.LTRBRect(0, 100, 100, 200); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); const lgs45Premul = CanvasKit.SkShader.MakeLinearGradient( [100, 100], [150, 200], // start and stop points [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, CanvasKit.SkMatrix.rotated(Math.PI/4, 100, 100), 1 // interpolate colors in premul ); paint.setShader(lgs45Premul); r = CanvasKit.LTRBRect(100, 100, 200, 200); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); lgs.delete(); lgs45.delete(); lgsPremul.delete(); lgs45Premul.delete(); strokePaint.delete(); paint.delete(); }); gm('radial_gradients', (canvas) => { canvas.clear(CanvasKit.WHITE); canvas.scale(2, 2); const strokePaint = new CanvasKit.SkPaint(); strokePaint.setStyle(CanvasKit.PaintStyle.Stroke); strokePaint.setColor(CanvasKit.BLACK); const paint = new CanvasKit.SkPaint(); paint.setStyle(CanvasKit.PaintStyle.Fill); const transparentGreen = CanvasKit.Color(0, 255, 255, 0); const rgs = CanvasKit.SkShader.MakeRadialGradient( [50, 50], 50, // center, radius [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror ); paint.setShader(rgs); let r = CanvasKit.LTRBRect(0, 0, 100, 100); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); const rgsPremul = CanvasKit.SkShader.MakeRadialGradient( [150, 50], 50, // center, radius [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, null, // no local matrix 1 // interpolate colors in premul ); paint.setShader(rgsPremul); r = CanvasKit.LTRBRect(100, 0, 200, 100); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); const rgsSkew = CanvasKit.SkShader.MakeRadialGradient( [50, 150], 50, // center, radius [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, CanvasKit.SkMatrix.skewed(0.5, 0, 100, 100) ); paint.setShader(rgsSkew); r = CanvasKit.LTRBRect(0, 100, 100, 200); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); const rgsSkewPremul = CanvasKit.SkShader.MakeRadialGradient( [150, 150], 50, // center, radius [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, CanvasKit.SkMatrix.skewed(0.5, 0, 100, 100), 1 // interpolate colors in premul ); paint.setShader(rgsSkewPremul); r = CanvasKit.LTRBRect(100, 100, 200, 200); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); rgs.delete(); rgsPremul.delete(); rgsSkew.delete(); rgsSkewPremul.delete(); strokePaint.delete(); paint.delete(); }); gm('conical_gradients', (canvas) => { canvas.clear(CanvasKit.WHITE); canvas.scale(2, 2); const strokePaint = new CanvasKit.SkPaint(); strokePaint.setStyle(CanvasKit.PaintStyle.Stroke); strokePaint.setColor(CanvasKit.BLACK); const paint = new CanvasKit.SkPaint(); paint.setStyle(CanvasKit.PaintStyle.Fill); paint.setAntiAlias(true); const transparentGreen = CanvasKit.Color(0, 255, 255, 0); const cgs = CanvasKit.SkShader.MakeTwoPointConicalGradient( [80, 10], 15, // start, radius [10, 110], 60, // end, radius [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror ); paint.setShader(cgs); let r = CanvasKit.LTRBRect(0, 0, 100, 100); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); const cgsPremul = CanvasKit.SkShader.MakeTwoPointConicalGradient( [180, 10], 15, // start, radius [110, 110], 60, // end, radius [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, null, // no local matrix 1, // interpolate colors in premul ); paint.setShader(cgsPremul); r = CanvasKit.LTRBRect(100, 0, 200, 100); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); const cgs45 = CanvasKit.SkShader.MakeTwoPointConicalGradient( [80, 110], 15, // start, radius [10, 210], 60, // end, radius [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, CanvasKit.SkMatrix.rotated(Math.PI/4, 0, 100) ); paint.setShader(cgs45); r = CanvasKit.LTRBRect(0, 100, 100, 200); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); const cgs45Premul = CanvasKit.SkShader.MakeTwoPointConicalGradient( [180, 110], 15, // start, radius [110, 210], 60, // end, radius [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, CanvasKit.SkMatrix.rotated(Math.PI/4, 100, 100), 1 // interpolate colors in premul ); paint.setShader(cgs45Premul); r = CanvasKit.LTRBRect(100, 100, 200, 200); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); cgs.delete(); cgsPremul.delete(); cgs45.delete(); strokePaint.delete(); paint.delete(); }); gm('blur_filters', (canvas) => { const pathUL = starPath(CanvasKit, 100, 100, 80); const pathBR = starPath(CanvasKit, 400, 300, 80); const paint = new CanvasKit.SkPaint(); const textFont = new CanvasKit.SkFont(null, 24); canvas.drawText('Above: MaskFilter', 20, 220, paint, textFont); canvas.drawText('Right: ImageFilter', 20, 260, paint, textFont); paint.setColor(CanvasKit.BLUE); const blurMask = CanvasKit.SkMaskFilter.MakeBlur(CanvasKit.BlurStyle.Normal, 5, true); paint.setMaskFilter(blurMask); canvas.drawPath(pathUL, paint); const blurIF = CanvasKit.SkImageFilter.MakeBlur(8, 1, CanvasKit.TileMode.Decal, null); paint.setImageFilter(blurIF); canvas.drawPath(pathBR, paint); pathUL.delete(); pathBR.delete(); paint.delete(); blurMask.delete(); blurIF.delete(); textFont.delete(); }); gm('combined_filters', (canvas, fetchedByteBuffers) => { const img = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]); expect(img).toBeTruthy(); canvas.clear(CanvasKit.WHITE); const paint = new CanvasKit.SkPaint(); paint.setAntiAlias(true); paint.setColor(CanvasKit.Color(0, 255, 0, 1.0)); const redCF = CanvasKit.SkColorFilter.MakeBlend( CanvasKit.Color(255, 0, 0, 0.1), CanvasKit.BlendMode.SrcOver); const redIF = CanvasKit.SkImageFilter.MakeColorFilter(redCF, null); const blurIF = CanvasKit.SkImageFilter.MakeBlur(8, 0.2, CanvasKit.TileMode.Decal, null); const combined = CanvasKit.SkImageFilter.MakeCompose(redIF, blurIF); // rotate 10 degrees centered on 200, 200 const m = CanvasKit.SkMatrix.rotated(Math.PI/18, 200, 200); const rotated = CanvasKit.SkImageFilter.MakeMatrixTransform(m, CanvasKit.FilterQuality.Medium, combined); paint.setImageFilter(rotated); //canvas.rotate(10, 200, 200); canvas.drawImage(img, 0, 0, paint); canvas.drawRect(CanvasKit.LTRBRect(5, 35, 45, 80), paint); paint.delete(); redIF.delete(); redCF.delete(); blurIF.delete(); combined.delete(); rotated.delete(); img.delete(); }, '/assets/mandrill_512.png'); gm('animated_filters', (canvas, fetchedByteBuffers) => { const img = CanvasKit.MakeAnimatedImageFromEncoded(fetchedByteBuffers[0]); expect(img).toBeTruthy(); img.decodeNextFrame(); img.decodeNextFrame(); canvas.clear(CanvasKit.WHITE); const paint = new CanvasKit.SkPaint(); paint.setAntiAlias(true); paint.setColor(CanvasKit.Color(0, 255, 0, 1.0)); const redCF = CanvasKit.SkColorFilter.MakeBlend( CanvasKit.Color(255, 0, 0, 0.1), CanvasKit.BlendMode.SrcOver); const redIF = CanvasKit.SkImageFilter.MakeColorFilter(redCF, null); const blurIF = CanvasKit.SkImageFilter.MakeBlur(8, 0.2, CanvasKit.TileMode.Decal, null); const combined = CanvasKit.SkImageFilter.MakeCompose(redIF, blurIF); paint.setImageFilter(combined); const frame = img.getCurrentFrame(); canvas.drawImage(frame, 100, 50, paint); paint.delete(); redIF.delete(); redCF.delete(); blurIF.delete(); combined.delete(); frame.delete(); img.delete(); }, '/assets/flightAnim.gif'); gm('drawImage_skp', (canvas, fetchedByteBuffers) => { const pic = CanvasKit.MakeSkPicture(fetchedByteBuffers[0]); expect(pic).toBeTruthy(); canvas.clear(CanvasKit.TRANSPARENT); canvas.drawPicture(pic); }, '/assets/red_line.skp'); it('can draw once using drawOnce utility method', (done) => { const surface = CanvasKit.MakeCanvasSurface('test'); expect(surface).toBeTruthy('Could not make surface'); if (!surface) { done(); return; } const drawFrame = (canvas) => { const paint = new CanvasKit.SkPaint(); paint.setStrokeWidth(1.0); paint.setAntiAlias(true); paint.setColor(CanvasKit.Color(0, 0, 0, 1.0)); paint.setStyle(CanvasKit.PaintStyle.Stroke); const path = new CanvasKit.SkPath(); path.moveTo(20, 5); path.lineTo(30, 20); path.lineTo(40, 10); canvas.drawPath(path, paint); path.delete(); paint.delete(); // surface hasn't been flushed yet (nor should we call flush // ourselves), so reportSurface would likely be blank if we // were to call it. done(); } surface.drawOnce(drawFrame); // Reminder: drawOnce is async. In this test, we are just making // sure the drawOnce function is there and doesn't crash, so we can // just call done() when the frame is rendered. }); it('can use DecodeCache APIs', () => { const initialLimit = CanvasKit.getDecodeCacheLimitBytes(); expect(initialLimit).toBeGreaterThan(1024 * 1024); const newLimit = 42 * 1024 * 1024; CanvasKit.setDecodeCacheLimitBytes(newLimit); expect(CanvasKit.getDecodeCacheLimitBytes()).toEqual(newLimit); // We cannot make any assumptions about this value, // so we just make sure it doesn't crash. CanvasKit.getDecodeCacheUsedBytes(); }); gm('combined_shaders', (canvas) => { const rShader = CanvasKit.SkShader.Color(CanvasKit.Color(255, 0, 0, 1.0)); const gShader = CanvasKit.SkShader.Color(CanvasKit.Color(0, 255, 0, 0.6)); const bShader = CanvasKit.SkShader.Color(CanvasKit.Color(0, 0, 255, 1.0)); const rgShader = CanvasKit.SkShader.Blend(CanvasKit.BlendMode.SrcOver, rShader, gShader); const p = new CanvasKit.SkPaint(); p.setShader(rgShader); canvas.drawPaint(p); const gbShader = CanvasKit.SkShader.Lerp(0.5, gShader, bShader); p.setShader(gbShader); canvas.drawRect(CanvasKit.LTRBRect(5, 100, 300, 400), p); rShader.delete(); gShader.delete(); bShader.delete(); rgShader.delete(); gbShader.delete(); p.delete(); }); it('exports consts correctly', () => { expect(CanvasKit.TRANSPARENT).toEqual(Float32Array.of(0, 0, 0, 0)); expect(CanvasKit.RED).toEqual(Float32Array.of(1, 0, 0, 1)); expect(CanvasKit.QUAD_VERB).toEqual(2); expect(CanvasKit.CONIC_VERB).toEqual(3); expect(CanvasKit.SaveLayerInitWithPrevious).toEqual(4); expect(CanvasKit.SaveLayerF16ColorType).toEqual(16); }); it('can set and get a 4f color on a paint', () => { const paint = new CanvasKit.SkPaint(); paint.setColor(CanvasKit.Color4f(3.3, 2.2, 1.1, 0.5)); expect(paint.getColor()).toEqual(Float32Array.of(3.3, 2.2, 1.1, 0.5)); }); describe('DOMMatrix support', () => { gm('sweep_gradient_dommatrix', (canvas) => { const paint = new CanvasKit.SkPaint(); const shader = CanvasKit.SkShader.MakeSweepGradient( 100, 100, // x y coordinates [CanvasKit.GREEN, CanvasKit.BLUE], [0.0, 1.0], CanvasKit.TileMode.Clamp, new DOMMatrix().translate(-10, 100), ); expect(shader).toBeTruthy('Could not make shader'); if (!shader) { return; } paint.setShader(shader); canvas.drawPaint(paint); paint.delete(); shader.delete(); }); const radiansToDegrees = (rad) => { return (rad / Math.PI) * 180; } // this should draw the same as concat_with4x4_canvas gm('concat_dommatrix', (canvas) => { const path = starPath(CanvasKit, CANVAS_WIDTH/2, CANVAS_HEIGHT/2); const paint = new CanvasKit.SkPaint(); paint.setAntiAlias(true); canvas.clear(CanvasKit.WHITE); canvas.concat(new DOMMatrix().translate(CANVAS_WIDTH/2, 0, 0)); canvas.concat(new DOMMatrix().rotateAxisAngle(1, 0, 0, radiansToDegrees(Math.PI/3))); canvas.concat(new DOMMatrix().rotateAxisAngle(0, 1, 0, radiansToDegrees(Math.PI/4))); canvas.concat(new DOMMatrix().rotateAxisAngle(0, 0, 1, radiansToDegrees(Math.PI/16))); canvas.concat(new DOMMatrix().translate(-CANVAS_WIDTH/2, 0, 0)); // Draw some stripes to help the eye detect the turn const stripeWidth = 10; paint.setColor(CanvasKit.BLACK); for (let i = 0; i < CANVAS_WIDTH; i += 2*stripeWidth) { canvas.drawRect(CanvasKit.LTRBRect(i, 0, i + stripeWidth, CANVAS_HEIGHT), paint); } paint.setColor(CanvasKit.YELLOW); canvas.drawPath(path, paint); paint.delete(); path.delete(); }); }); // end describe('DOMMatrix support') });