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.PictureRecorder(); const rcanvas = spr.beginRecording( CanvasKit.LTRBRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)); const paint = new CanvasKit.Paint(); paint.setStrokeWidth(2.0); paint.setAntiAlias(true); paint.setColor(CanvasKit.Color(0, 0, 0, 1.0)); paint.setStyle(CanvasKit.PaintStyle.Stroke); rcanvas.drawRRect(CanvasKit.RRectXY([5, 35, 45, 80], 15, 10), paint); const font = new CanvasKit.Font(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); // test that file saving functionality throws no errors // Unfortunately jasmine spy objects can't fake their type so we can't verify it downloads // a nonzero sized file. pic.saveAsFile('foo.skp'); 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.length).toEqual(4); 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); }); it('can compute tonal colors with malloced values', () => { const ambientColor = CanvasKit.Malloc(Float32Array, 4); ambientColor.toTypedArray().set(CanvasKit.BLUE); const spotColor = CanvasKit.Malloc(Float32Array, 4); spotColor.toTypedArray().set(CanvasKit.RED); const input = { ambient: ambientColor, spot: spotColor, }; const out = CanvasKit.computeTonalColors(input); expect(new Float32Array(out.ambient)).toEqual(CanvasKit.BLACK); const expectedSpot = [0.173, 0, 0, 0.969]; expect(out.spot.length).toEqual(4); 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.Paint(); 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 Image', (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, colorSpace: CanvasKit.ColorSpace.SRGB, width: img.width(), height: img.height(), }; const rowBytes = 4 * img.width(); const pixels = img.readPixels(0, 0, imageInfo); // 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); // Make enough space for a 5x5 8888 surface (4 bytes for R, G, B, A) const rdsData = CanvasKit.Malloc(Uint8Array, 512 * 5*512 * 4); const pixels2 = rdsData.toTypedArray(); pixels2[0] = 127; // sentinel value, should be overwritten by readPixels. img.readPixels(0, 0, imageInfo, rdsData, rowBytes); expect(rdsData.toTypedArray()[0]).toEqual(pixels[0]); img.delete(); CanvasKit.Free(rdsData); 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); let img = aImg.makeImageAtCurrentFrame(); canvas.drawImage(img, 0, 0, null); img.delete(); // This is annoying, so we added drawImageAtCurrentFrame let c = aImg.decodeNextFrame(); expect(c).not.toEqual(-1); canvas.drawImageAtCurrentFrame(aImg, 300, 0, null); for(let i = 0; i < 10; i++) { c = aImg.decodeNextFrame(); expect(c).not.toEqual(-1); } canvas.drawImageAtCurrentFrame(aImg, 0, 300, null); for(let i = 0; i < 10; i++) { c = aImg.decodeNextFrame(); expect(c).not.toEqual(-1); } canvas.drawImageAtCurrentFrame(aImg, 300, 300, null); aImg.delete(); }, '/assets/flightAnim.gif'); gm('exif_orientation', (canvas, fetchedByteBuffers) => { canvas.clear(CanvasKit.WHITE); const paint = new CanvasKit.Paint(); const font = new CanvasKit.Font(null, 14); canvas.drawText('The following heart should be rotated 90 CCW due to exif.', 5, 25, paint, font); // TODO(kjlubick) it would be nice to also to test MakeAnimatedImageFromEncoded but // I could not create a sample animated image that worked. const img = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]); expect(img).toBeTruthy(); canvas.drawImage(img, 5, 35, null); img.delete(); paint.delete(); font.delete(); }, '/assets/exif_rotated_heart.jpg'); gm('1x4_from_scratch', (canvas) => { canvas.clear(CanvasKit.WHITE); const paint = new CanvasKit.Paint(); // This creates and draws an Image 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({ 'width': 1, 'height': 4, 'alphaType': CanvasKit.AlphaType.Unpremul, 'colorType': CanvasKit.ColorType.RGBA_8888, 'colorSpace': CanvasKit.ColorSpace.SRGB }, pixels, 4); canvas.drawImage(img, 1, 1, paint); const info = img.getImageInfo(); expect(info).toEqual({ 'width': 1, 'height': 4, 'alphaType': CanvasKit.AlphaType.Unpremul, 'colorType': CanvasKit.ColorType.RGBA_8888, }); const cs = img.getColorSpace(); expect(CanvasKit.ColorSpace.Equals(cs, CanvasKit.ColorSpace.SRGB)).toBeTruthy(); cs.delete(); img.delete(); paint.delete(); }); gm('draw_atlas_with_builders', (canvas, fetchedByteBuffers) => { const atlas = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]); expect(atlas).toBeTruthy(); canvas.clear(CanvasKit.WHITE); const paint = new CanvasKit.Paint(); paint.setColor(CanvasKit.Color(0, 0, 0, 0.8)); const srcs = new CanvasKit.RectBuilder(); // left top right bottom srcs.push( 0, 0, 256, 256); srcs.push(256, 0, 512, 256); srcs.push( 0, 256, 256, 512); srcs.push(256, 256, 512, 512); const dsts = new CanvasKit.RSXFormBuilder(); // scos, ssin, tx, ty dsts.push(0.5, 0, 20, 20); dsts.push(0.5, 0, 300, 20); dsts.push(0.5, 0, 20, 300); dsts.push(0.5, 0, 300, 300); const colors = new CanvasKit.ColorBuilder(); // note that the ColorBuilder expects int colors to be pushed. // pushing float colors to it only causes weird problems way downstream. // It does no type checking. colors.push(CanvasKit.ColorAsInt( 85, 170, 10, 128)); // light green colors.push(CanvasKit.ColorAsInt( 51, 51, 191, 128)); // light blue colors.push(CanvasKit.ColorAsInt( 0, 0, 0, 128)); colors.push(CanvasKit.ColorAsInt(256, 256, 256, 128)); canvas.drawAtlas(atlas, srcs, dsts, paint, CanvasKit.BlendMode.Modulate, colors); atlas.delete(); paint.delete(); }, '/assets/mandrill_512.png'); gm('draw_atlas_with_arrays', (canvas, fetchedByteBuffers) => { const atlas = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]); expect(atlas).toBeTruthy(); canvas.clear(CanvasKit.WHITE); const paint = new CanvasKit.Paint(); paint.setColor(CanvasKit.Color(0, 0, 0, 0.8)); const srcs = [ 0, 0, 256, 256, 256, 0, 512, 256, 0, 256, 256, 512, 256, 256, 512, 512, ]; const dsts = [ 0.5, 0, 20, 20, 0.5, 0, 300, 20, 0.5, 0, 20, 300, 0.5, 0, 300, 300, ]; const colors = Uint32Array.of( CanvasKit.ColorAsInt( 85, 170, 10, 128), // light green CanvasKit.ColorAsInt( 51, 51, 191, 128), // light blue CanvasKit.ColorAsInt( 0, 0, 0, 128), CanvasKit.ColorAsInt(256, 256, 256, 128), ); canvas.drawAtlas(atlas, srcs, dsts, paint, CanvasKit.BlendMode.Modulate, colors); atlas.delete(); paint.delete(); }, '/assets/mandrill_512.png'); gm('image_decoding_methods', async (canvas) => { canvas.clear(CanvasKit.WHITE); const IMAGE_FILE_PATHS = [ '/assets/brickwork-texture.jpg', '/assets/mandrill_512.png', '/assets/color_wheel.gif' ]; let row = 1; // Test 4 different methods of decoding an image for each of the three images in // IMAGE_FILE_PATHS. // Resulting Images are drawn to visually show that all methods decode correctly. for (const imageFilePath of IMAGE_FILE_PATHS) { const response = await fetch(imageFilePath); const arrayBuffer = await response.arrayBuffer(); // response.blob() is preferable when you don't need both a Blob *and* an ArrayBuffer. const blob = new Blob([ arrayBuffer ]); // Method 1 - decode TypedArray using wasm codecs: const skImage1 = CanvasKit.MakeImageFromEncoded(arrayBuffer); // Method 2 (slower and does not work in Safari) decode using ImageBitmap: const imageBitmap = await createImageBitmap(blob); // Testing showed that transferring an ImageBitmap to a canvas using the 'bitmaprenderer' // context and passing that canvas to CanvasKit.MakeImageFromCanvasImageSource() is // marginally faster than passing ImageBitmap to // CanvasKit.MakeImageFromCanvasImageSource() directly. const canvasBitmapElement = document.createElement('canvas'); canvasBitmapElement.width = imageBitmap.width; canvasBitmapElement.height = imageBitmap.height; const ctxBitmap = canvasBitmapElement.getContext('bitmaprenderer'); ctxBitmap.transferFromImageBitmap(imageBitmap); const skImage2 = CanvasKit.MakeImageFromCanvasImageSource(canvasBitmapElement); // Method 3 (slowest) decode using HTMLImageElement directly: const image = new Image(); // Testing showed that waiting for a load event is faster than waiting on image.decode() // HTMLImageElement.decode() reference: https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decode const promise1 = new Promise((resolve) => image.addEventListener('load', resolve)); image.src = imageFilePath; await promise1; const skImage3 = CanvasKit.MakeImageFromCanvasImageSource(image); // Method 4 (roundabout, but works if all you have is a Blob) decode from Blob using // HTMLImageElement: const imageObjectUrl = URL.createObjectURL( blob ); const image2 = new Image(); const promise2 = new Promise((resolve) => image2.addEventListener('load', resolve)); image2.src = imageObjectUrl; await promise2; const skImage4 = CanvasKit.MakeImageFromCanvasImageSource(image2); // Draw decoded images const sourceRect = CanvasKit.XYWHRect(0, 0, 150, 150); canvas.drawImageRect(skImage1, sourceRect, CanvasKit.XYWHRect(0, row * 100, 90, 90), null, false); canvas.drawImageRect(skImage2, sourceRect, CanvasKit.XYWHRect(100, row * 100, 90, 90), null, false); canvas.drawImageRect(skImage3, sourceRect, CanvasKit.XYWHRect(200, row * 100, 90, 90), null, false); canvas.drawImageRect(skImage4, sourceRect, CanvasKit.XYWHRect(300, row * 100, 90, 90), null, false); row++; } // Label images with the method used to decode them const paint = new CanvasKit.Paint(); const textFont = new CanvasKit.Font(null, 7); canvas.drawText('WASM Decoding', 0, 90, paint, textFont); canvas.drawText('ImageBitmap Decoding', 100, 90, paint, textFont); canvas.drawText('HTMLImageEl Decoding', 200, 90, paint, textFont); canvas.drawText('Blob Decoding', 300, 90, paint, textFont); }); gm('sweep_gradient', (canvas) => { const paint = new CanvasKit.Paint(); const shader = CanvasKit.Shader.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.Paint(); strokePaint.setStyle(CanvasKit.PaintStyle.Stroke); strokePaint.setColor(CanvasKit.BLACK); const paint = new CanvasKit.Paint(); paint.setStyle(CanvasKit.PaintStyle.Fill); const transparentGreen = CanvasKit.Color(0, 255, 255, 0); const lgs = CanvasKit.Shader.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.Shader.MakeLinearGradient( [100, 0], [150, 100], // start and stop points Uint32Array.of( CanvasKit.ColorAsInt(0, 255, 255, 0), CanvasKit.ColorAsInt(0, 0, 255, 255), CanvasKit.ColorAsInt(255, 0, 0, 255)), [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.Shader.MakeLinearGradient( [0, 100], [50, 200], // start and stop points Float32Array.of(...transparentGreen, ...CanvasKit.BLUE, ...CanvasKit.RED), [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, CanvasKit.Matrix.rotated(Math.PI/4, 0, 100), ); paint.setShader(lgs45); r = CanvasKit.LTRBRect(0, 100, 100, 200); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); // malloc'd color array const colors = CanvasKit.Malloc(Float32Array, 12); const typedColorsArray = colors.toTypedArray(); typedColorsArray.set(transparentGreen, 0); typedColorsArray.set(CanvasKit.BLUE, 4); typedColorsArray.set(CanvasKit.RED, 8); const lgs45Premul = CanvasKit.Shader.MakeLinearGradient( [100, 100], [150, 200], // start and stop points typedColorsArray, [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, CanvasKit.Matrix.rotated(Math.PI/4, 100, 100), 1 // interpolate colors in premul ); CanvasKit.Free(colors); 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.Paint(); strokePaint.setStyle(CanvasKit.PaintStyle.Stroke); strokePaint.setColor(CanvasKit.BLACK); const paint = new CanvasKit.Paint(); paint.setStyle(CanvasKit.PaintStyle.Fill); const transparentGreen = CanvasKit.Color(0, 255, 255, 0); const rgs = CanvasKit.Shader.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.Shader.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.Shader.MakeRadialGradient( [50, 150], 50, // center, radius [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, CanvasKit.Matrix.skewed(0.5, 0, 100, 100), null, // color space ); paint.setShader(rgsSkew); r = CanvasKit.LTRBRect(0, 100, 100, 200); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); const rgsSkewPremul = CanvasKit.Shader.MakeRadialGradient( [150, 150], 50, // center, radius [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, CanvasKit.Matrix.skewed(0.5, 0, 100, 100), 1, // interpolate colors in premul null, // color space ); 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.Paint(); strokePaint.setStyle(CanvasKit.PaintStyle.Stroke); strokePaint.setColor(CanvasKit.BLACK); const paint = new CanvasKit.Paint(); paint.setStyle(CanvasKit.PaintStyle.Fill); paint.setAntiAlias(true); const transparentGreen = CanvasKit.Color(0, 255, 255, 0); const cgs = CanvasKit.Shader.MakeTwoPointConicalGradient( [80, 10], 15, // start, radius [10, 110], 60, // end, radius [transparentGreen, CanvasKit.BLUE, CanvasKit.RED], [0, 0.65, 1.0], CanvasKit.TileMode.Mirror, null, // no local matrix ); paint.setShader(cgs); let r = CanvasKit.LTRBRect(0, 0, 100, 100); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); const cgsPremul = CanvasKit.Shader.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 null, // color space ); paint.setShader(cgsPremul); r = CanvasKit.LTRBRect(100, 0, 200, 100); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); const cgs45 = CanvasKit.Shader.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.Matrix.rotated(Math.PI/4, 0, 100), null, // color space ); paint.setShader(cgs45); r = CanvasKit.LTRBRect(0, 100, 100, 200); canvas.drawRect(r, paint); canvas.drawRect(r, strokePaint); const cgs45Premul = CanvasKit.Shader.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.Matrix.rotated(Math.PI/4, 100, 100), 1, // interpolate colors in premul null, // color space ); 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.Paint(); const textFont = new CanvasKit.Font(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.MaskFilter.MakeBlur(CanvasKit.BlurStyle.Normal, 5, true); paint.setMaskFilter(blurMask); canvas.drawPath(pathUL, paint); const blurIF = CanvasKit.ImageFilter.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.Paint(); paint.setAntiAlias(true); paint.setColor(CanvasKit.Color(0, 255, 0, 1.0)); const redCF = CanvasKit.ColorFilter.MakeBlend( CanvasKit.Color(255, 0, 0, 0.1), CanvasKit.BlendMode.SrcOver); const redIF = CanvasKit.ImageFilter.MakeColorFilter(redCF, null); const blurIF = CanvasKit.ImageFilter.MakeBlur(8, 0.2, CanvasKit.TileMode.Decal, null); const combined = CanvasKit.ImageFilter.MakeCompose(redIF, blurIF); // rotate 10 degrees centered on 200, 200 const m = CanvasKit.Matrix.rotated(Math.PI/18, 200, 200); const rotated = CanvasKit.ImageFilter.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.Paint(); paint.setAntiAlias(true); paint.setColor(CanvasKit.Color(0, 255, 0, 1.0)); const redCF = CanvasKit.ColorFilter.MakeBlend( CanvasKit.Color(255, 0, 0, 0.1), CanvasKit.BlendMode.SrcOver); const redIF = CanvasKit.ImageFilter.MakeColorFilter(redCF, null); const blurIF = CanvasKit.ImageFilter.MakeBlur(8, 0.2, CanvasKit.TileMode.Decal, null); const combined = CanvasKit.ImageFilter.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('drawImageVariants', (canvas, fetchedByteBuffers) => { const img = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]); expect(img).toBeTruthy(); canvas.clear(CanvasKit.WHITE); canvas.scale(2, 2); const paint = new CanvasKit.Paint(); const clipTo = (x, y) => { canvas.save(); canvas.clipRect(CanvasKit.XYWHRect(x, y, 128, 128), CanvasKit.ClipOp.Intersect); }; clipTo(0, 0); canvas.drawImage(img, 0, 0, paint); canvas.restore(); clipTo(128, 0); canvas.drawImageCubic(img, 128, 0, 1/3, 1/3, null); canvas.restore(); clipTo(0, 128); canvas.drawImageOptions(img, 0, 128, CanvasKit.FilterMode.Linear, CanvasKit.MipmapMode.None, null); canvas.restore(); const mipImg = img.makeCopyWithDefaultMipmaps(); clipTo(128, 128); canvas.drawImageOptions(mipImg, 128, 128, CanvasKit.FilterMode.Nearest, CanvasKit.MipmapMode.Nearest, null); canvas.restore(); paint.delete(); mipImg.delete(); img.delete(); }, '/assets/mandrill_512.png'); gm('drawImageRectVariants', (canvas, fetchedByteBuffers) => { const img = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]); expect(img).toBeTruthy(); canvas.clear(CanvasKit.WHITE); const paint = new CanvasKit.Paint(); const src = CanvasKit.XYWHRect(100, 100, 128, 128); canvas.drawImageRect(img, src, CanvasKit.XYWHRect(0, 0, 256, 256), paint); canvas.drawImageRectCubic(img, src, CanvasKit.XYWHRect(256, 0, 256, 256), 1/3, 1/3); canvas.drawImageRectOptions(img, src, CanvasKit.XYWHRect(0, 256, 256, 256), CanvasKit.FilterMode.Linear, CanvasKit.MipmapMode.None); const mipImg = img.makeCopyWithDefaultMipmaps(); canvas.drawImageRectOptions(mipImg, src, CanvasKit.XYWHRect(256, 256, 256, 256), CanvasKit.FilterMode.Nearest, CanvasKit.MipmapMode.Nearest); paint.delete(); mipImg.delete(); img.delete(); }, '/assets/mandrill_512.png'); gm('drawImage_skp', (canvas, fetchedByteBuffers) => { const pic = CanvasKit.MakePicture(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.Paint(); 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.Path(); 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 draw client-supplied dirty rects', (done) => { // dirty rects are only honored by software (CPU) canvases today. const surface = CanvasKit.MakeSWCanvasSurface('test'); expect(surface).toBeTruthy('Could not make surface'); if (!surface) { done(); return; } const drawFrame = (canvas) => { const paint = new CanvasKit.Paint(); 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.Path(); path.moveTo(20, 5); path.lineTo(30, 20); path.lineTo(40, 10); canvas.drawPath(path, paint); path.delete(); paint.delete(); done(); }; const dirtyRect = CanvasKit.XYWHRect(10, 10, 15, 15); surface.drawOnce(drawFrame, dirtyRect); // We simply ensure that passing a dirty rect doesn't crash. }); 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.Shader.Color(CanvasKit.Color(255, 0, 0, 1.0)); // deprecated const gShader = CanvasKit.Shader.MakeColor(CanvasKit.Color(0, 255, 0, 0.6)); const bShader = CanvasKit.Shader.MakeColor(CanvasKit.Color(0, 0, 255, 1.0)); const rgShader = CanvasKit.Shader.MakeBlend(CanvasKit.BlendMode.SrcOver, rShader, gShader); const p = new CanvasKit.Paint(); p.setShader(rgShader); canvas.drawPaint(p); const gbShader = CanvasKit.Shader.MakeLerp(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 color on a paint and get it as four floats', () => { const paint = new CanvasKit.Paint(); 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)); paint.setColorComponents(0.5, 0.6, 0.7, 0.8); expect(paint.getColor()).toEqual(Float32Array.of(0.5, 0.6, 0.7, 0.8)); paint.setColorInt(CanvasKit.ColorAsInt(50, 100, 150, 200)); let color = paint.getColor(); expect(color.length).toEqual(4); expect(color[0]).toBeCloseTo(50/255, 5); // Red expect(color[1]).toBeCloseTo(100/255, 5); // Green expect(color[2]).toBeCloseTo(150/255, 5); // Blue expect(color[3]).toBeCloseTo(200/255, 5); // Alpha paint.setColorInt(0xFF000000); expect(paint.getColor()).toEqual(Float32Array.of(0, 0, 0, 1.0)); }); gm('draw shadow', (canvas) => { const lightRadius = 20; const flags = 0; const lightPos = [500,500,20]; const zPlaneParams = [0,0,1]; const path = starPath(CanvasKit); canvas.drawShadow(path, zPlaneParams, lightPos, lightRadius, CanvasKit.BLACK, CanvasKit.MAGENTA, flags); }); gm('fractal_noise_shader', (canvas) => { const shader = CanvasKit.Shader.MakeFractalNoise(0.1, 0.05, 2, 0, 0, 0); const paint = new CanvasKit.Paint(); paint.setColor(CanvasKit.BLACK); paint.setShader(shader); canvas.drawPaint(paint); paint.delete(); shader.delete(); }); gm('turbulance_shader', (canvas) => { const shader = CanvasKit.Shader.MakeTurbulence(0.1, 0.05, 2, 117, 0, 0); const paint = new CanvasKit.Paint(); paint.setColor(CanvasKit.BLACK); paint.setShader(shader); canvas.drawPaint(paint); paint.delete(); shader.delete(); }); gm('fractal_noise_tiled_shader', (canvas) => { const shader = CanvasKit.Shader.MakeFractalNoise(0.1, 0.05, 2, 0, 80, 80); const paint = new CanvasKit.Paint(); paint.setColor(CanvasKit.BLACK); paint.setShader(shader); canvas.drawPaint(paint); paint.delete(); shader.delete(); }); gm('turbulance_tiled_shader', (canvas) => { const shader = CanvasKit.Shader.MakeTurbulence(0.1, 0.05, 2, 117, 80, 80); const paint = new CanvasKit.Paint(); paint.setColor(CanvasKit.BLACK); paint.setShader(shader); canvas.drawPaint(paint); paint.delete(); shader.delete(); }); gm('improved_noise_shader', (canvas) => { const shader = CanvasKit.Shader.MakeImprovedNoise(0.1, 0.05, 2, 10); const paint = new CanvasKit.Paint(); paint.setColor(CanvasKit.BLACK); paint.setShader(shader); canvas.drawPaint(paint); paint.delete(); shader.delete(); }); describe('ColorSpace Support', () => { it('Can create an SRGB 8888 surface', () => { const colorSpace = CanvasKit.ColorSpace.SRGB; const surface = CanvasKit.MakeCanvasSurface('test', CanvasKit.ColorSpace.SRGB); expect(surface).toBeTruthy('Could not make surface'); let info = surface.imageInfo(); expect(info.alphaType).toEqual(CanvasKit.AlphaType.Unpremul); expect(info.colorType).toEqual(CanvasKit.ColorType.RGBA_8888); expect(CanvasKit.ColorSpace.Equals(info.colorSpace, colorSpace)) .toBeTruthy("Surface not created with correct color space."); const mObj = CanvasKit.Malloc(Uint8Array, CANVAS_WIDTH * CANVAS_HEIGHT * 4); mObj.toTypedArray()[0] = 127; // sentinel value. Should be overwritten by readPixels. const canvas = surface.getCanvas(); canvas.clear(CanvasKit.TRANSPARENT); const pixels = canvas.readPixels(0, 0, { width: CANVAS_WIDTH, height: CANVAS_HEIGHT, colorType: CanvasKit.ColorType.RGBA_8888, alphaType: CanvasKit.AlphaType.Unpremul, colorSpace: colorSpace }, mObj, 4 * CANVAS_WIDTH); expect(pixels).toBeTruthy('Could not read pixels from surface'); expect(pixels[0] !== 127).toBeTruthy(); expect(pixels[0]).toEqual(mObj.toTypedArray()[0]); CanvasKit.Free(mObj); surface.delete(); }); it('Can create a Display P3 surface', () => { const colorSpace = CanvasKit.ColorSpace.DISPLAY_P3; const surface = CanvasKit.MakeCanvasSurface('test', CanvasKit.ColorSpace.DISPLAY_P3); expect(surface).toBeTruthy('Could not make surface'); if (!surface.reportBackendTypeIsGPU()) { console.log('Not expecting color space support in cpu backed suface.'); return; } let info = surface.imageInfo(); expect(info.alphaType).toEqual(CanvasKit.AlphaType.Unpremul); expect(info.colorType).toEqual(CanvasKit.ColorType.RGBA_F16); expect(CanvasKit.ColorSpace.Equals(info.colorSpace, colorSpace)) .toBeTruthy("Surface not created with correct color space."); const pixels = surface.getCanvas().readPixels(0, 0, { width: CANVAS_WIDTH, height: CANVAS_HEIGHT, colorType: CanvasKit.ColorType.RGBA_F16, alphaType: CanvasKit.AlphaType.Unpremul, colorSpace: colorSpace }); expect(pixels).toBeTruthy('Could not read pixels from surface'); }); it('Can create an Adobe RGB surface', () => { const colorSpace = CanvasKit.ColorSpace.ADOBE_RGB; const surface = CanvasKit.MakeCanvasSurface('test', CanvasKit.ColorSpace.ADOBE_RGB); expect(surface).toBeTruthy('Could not make surface'); if (!surface.reportBackendTypeIsGPU()) { console.log('Not expecting color space support in cpu backed surface.'); return; } let info = surface.imageInfo(); expect(info.alphaType).toEqual(CanvasKit.AlphaType.Unpremul); expect(info.colorType).toEqual(CanvasKit.ColorType.RGBA_F16); expect(CanvasKit.ColorSpace.Equals(info.colorSpace, colorSpace)) .toBeTruthy("Surface not created with correct color space."); const pixels = surface.getCanvas().readPixels(0, 0, { width: CANVAS_WIDTH, height: CANVAS_HEIGHT, colorType: CanvasKit.ColorType.RGBA_F16, alphaType: CanvasKit.AlphaType.Unpremul, colorSpace: colorSpace }); expect(pixels).toBeTruthy('Could not read pixels from surface'); }); it('combine draws from several color spaces', () => { const surface = CanvasKit.MakeCanvasSurface('test', CanvasKit.ColorSpace.ADOBE_RGB); expect(surface).toBeTruthy('Could not make surface'); if (!surface.reportBackendTypeIsGPU()) { console.log('Not expecting color space support in cpu backed suface.'); return; } const canvas = surface.getCanvas(); let paint = new CanvasKit.Paint(); paint.setColor(CanvasKit.RED, CanvasKit.ColorSpace.ADOBE_RGB); canvas.drawPaint(paint); paint.setColor(CanvasKit.RED, CanvasKit.ColorSpace.DISPLAY_P3); // 93.7 in adobeRGB canvas.drawRect(CanvasKit.LTRBRect(200, 0, 400, 600), paint); paint.setColor(CanvasKit.RED, CanvasKit.ColorSpace.SRGB); // 85.9 in adobeRGB canvas.drawRect(CanvasKit.LTRBRect(400, 0, 600, 600), paint); // this test paints three bands of red, each the maximum red that a color space supports. // They are each represented by skia by some color in the Adobe RGB space of the surface, // as floats between 0 and 1. // TODO(nifong) readpixels and verify correctness after f32 readpixels bug is fixed }); }); // end describe('ColorSpace Support') describe('DOMMatrix support', () => { gm('sweep_gradient_dommatrix', (canvas) => { const paint = new CanvasKit.Paint(); const shader = CanvasKit.Shader.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.Paint(); 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)); const localMatrix = canvas.getLocalToDevice(); expect4x4MatricesToMatch([ 0.693519, -0.137949, 0.707106, 91.944030, 0.698150, 0.370924, -0.612372, -209.445297, -0.177806, 0.918359, 0.353553, 53.342029, 0 , 0 , 0 , 1 ], localMatrix); // 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') it('can call subarray on a Malloced object', () => { const mThings = CanvasKit.Malloc(Float32Array, 6); mThings.toTypedArray().set([4, 5, 6, 7, 8, 9]); expectTypedArraysToEqual(Float32Array.of(4, 5, 6, 7, 8, 9), mThings.toTypedArray()); expectTypedArraysToEqual(Float32Array.of(4, 5, 6, 7, 8, 9), mThings.subarray(0)); expectTypedArraysToEqual(Float32Array.of(7, 8, 9), mThings.subarray(3)); expectTypedArraysToEqual(Float32Array.of(7), mThings.subarray(3, 4)); expectTypedArraysToEqual(Float32Array.of(7, 8), mThings.subarray(3, 5)); // mutations on the subarray affect the entire array (because they are backed by the // same memory) mThings.subarray(3)[0] = 100.5; expectTypedArraysToEqual(Float32Array.of(4, 5, 6, 100.5, 8, 9), mThings.toTypedArray()); CanvasKit.Free(mThings); }); function expectTypedArraysToEqual(expected, actual) { expect(expected.constructor.name).toEqual(actual.constructor.name); expect(expected.length).toEqual(actual.length); for (let i = 0; i < expected.length; i++) { expect(expected[i]).toBeCloseTo(actual[i], 5, `element ${i}`); } } it('can create a RasterDirectSurface', () => { // Make enough space for a 5x5 8888 surface (4 bytes for R, G, B, A) const rdsData = CanvasKit.Malloc(Uint8Array, 5 * 5 * 4); const surface = CanvasKit.MakeRasterDirectSurface({ 'width': 5, 'height': 5, 'colorType': CanvasKit.ColorType.RGBA_8888, 'alphaType': CanvasKit.AlphaType.Premul, 'colorSpace': CanvasKit.ColorSpace.SRGB, }, rdsData, 5 * 4); surface.getCanvas().clear(CanvasKit.Color(200, 100, 0, 0.8)); const pixels = rdsData.toTypedArray(); // Check that the first pixels colors are right. expect(pixels[0]).toEqual(160); // red (premul, 0.8 * 200) expect(pixels[1]).toEqual(80); // green (premul, 0.8 * 100) expect(pixels[2]).toEqual(0); // blue (premul, not that it matters) expect(pixels[3]).toEqual(204); // alpha (0.8 * 255) surface.delete(); CanvasKit.Free(rdsData); }); });