1/* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {assertDefined} from 'common/assert_utils'; 18import {Rect} from 'common/geometry/rect'; 19import {CanvasDrawer} from './canvas_drawer'; 20 21describe('CanvasDrawer', () => { 22 let actualCanvas: HTMLCanvasElement; 23 let expectedCanvas: HTMLCanvasElement; 24 let canvasDrawer: CanvasDrawer; 25 26 const testRect = new Rect(10, 10, 10, 10); 27 const hexColor = '#333333'; 28 const expectedRgbaColor = 'rgba(51,51,51,1)'; 29 const expectedTransparentColor = 'rgba(51,51,51,0)'; 30 31 beforeEach(() => { 32 actualCanvas = createCanvas(100, 100); 33 expectedCanvas = createCanvas(100, 100); 34 canvasDrawer = new CanvasDrawer(); 35 canvasDrawer.setCanvas(actualCanvas); 36 }); 37 38 it('erases the canvas', async () => { 39 canvasDrawer.drawRect(testRect, hexColor, 1.0); 40 expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeFalse(); 41 42 canvasDrawer.clear(); 43 expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue(); 44 }); 45 46 it('can draw opaque rect', () => { 47 canvasDrawer.drawRect(testRect, hexColor, 1.0); 48 49 const expectedCtx = assertDefined(expectedCanvas.getContext('2d')); 50 expectedCtx.fillStyle = hexColor; 51 expectedCtx.rect(testRect.x, testRect.y, testRect.w, testRect.h); 52 expectedCtx.fill(); 53 54 expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue(); 55 }); 56 57 it('can draw translucent rect', () => { 58 canvasDrawer.drawRect(testRect, hexColor, 0.5); 59 60 const expectedCtx = assertDefined(expectedCanvas.getContext('2d')); 61 expectedCtx.fillStyle = 'rgba(51,51,51,0.5)'; 62 expectedCtx.rect(testRect.x, testRect.y, testRect.w, testRect.h); 63 expectedCtx.fill(); 64 65 expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue(); 66 }); 67 68 it('can draw rect with start gradient and full ellipsis', () => { 69 const testGradientRect = new Rect(50, 10, 50, 10); 70 canvasDrawer.drawRect(testGradientRect, hexColor, 1, true, false); 71 72 const expectedCtx = assertDefined(expectedCanvas.getContext('2d')); 73 fillGradient(expectedCtx, testGradientRect, 0.5, true, false); 74 addEllipsis(expectedCtx, testGradientRect, true); 75 76 expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue(); 77 }); 78 79 it('can draw rect with partial start ellipsis', () => { 80 canvasDrawer.drawRect(testRect, hexColor, 1, true, false); 81 82 const expectedCtx = assertDefined(expectedCanvas.getContext('2d')); 83 expectedCtx.fillStyle = hexColor; 84 expectedCtx.rect(testRect.x, testRect.y, testRect.w, testRect.h); 85 expectedCtx.fill(); 86 87 addEllipsis(expectedCtx, testRect, true); 88 89 expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue(); 90 }); 91 92 it('can draw rect with end gradient and full ellipsis', () => { 93 const testGradientRect = new Rect(50, 10, 50, 10); 94 canvasDrawer.drawRect(testGradientRect, hexColor, 1, false, true); 95 96 const expectedCtx = assertDefined(expectedCanvas.getContext('2d')); 97 fillGradient(expectedCtx, testGradientRect, 0.5, false, true); 98 addEllipsis(expectedCtx, testGradientRect, false); 99 100 expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue(); 101 }); 102 103 it('can draw rect with partial end ellipsis', () => { 104 canvasDrawer.drawRect(testRect, hexColor, 1, false, true); 105 106 const expectedCtx = assertDefined(expectedCanvas.getContext('2d')); 107 fillGradient(expectedCtx, testRect, 1, false, true); 108 addEllipsis(expectedCtx, testRect, false); 109 110 expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue(); 111 }); 112 113 it('can draw rect border', () => { 114 canvasDrawer.drawRectBorder(testRect); 115 116 const expectedCtx = assertDefined(expectedCanvas.getContext('2d')); 117 118 expectedCtx.rect(9, 9, 12, 3); 119 expectedCtx.fill(); 120 expectedCtx.rect(9, 9, 3, 12); 121 expectedCtx.fill(); 122 expectedCtx.rect(9, 18, 12, 3); 123 expectedCtx.fill(); 124 expectedCtx.rect(18, 9, 3, 12); 125 expectedCtx.fill(); 126 127 expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue(); 128 }); 129 130 it('can draw rect outside bounds', () => { 131 canvasDrawer.drawRect(new Rect(200, 200, 10, 10), hexColor, 1.0); 132 canvasDrawer.drawRect(new Rect(95, 95, 50, 50), hexColor, 1.0); 133 134 const expectedCtx = assertDefined(expectedCanvas.getContext('2d')); 135 expectedCtx.fillStyle = hexColor; 136 expectedCtx.rect(95, 95, 5, 5); 137 expectedCtx.fill(); 138 139 expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue(); 140 }); 141 142 function createCanvas(width: number, height: number): HTMLCanvasElement { 143 const canvas = document.createElement('canvas') as HTMLCanvasElement; 144 canvas.width = width; 145 canvas.height = height; 146 return canvas; 147 } 148 149 function pixelsAllMatch( 150 canvasA: HTMLCanvasElement, 151 canvasB: HTMLCanvasElement, 152 ): boolean { 153 if (canvasA.width !== canvasB.width || canvasA.height !== canvasB.height) { 154 return false; 155 } 156 157 const imgA = assertDefined(canvasA.getContext('2d')).getImageData( 158 0, 159 0, 160 canvasA.width, 161 canvasA.height, 162 ).data; 163 const imgB = assertDefined(canvasB.getContext('2d')).getImageData( 164 0, 165 0, 166 canvasB.width, 167 canvasB.height, 168 ).data; 169 170 for (let i = 0; i < imgA.length; i++) { 171 if (imgA[i] !== imgB[i]) { 172 return false; 173 } 174 } 175 176 return true; 177 } 178 179 function fillGradient( 180 ctx: CanvasRenderingContext2D, 181 testGradientRect: Rect, 182 gradientRatio: number, 183 startGradient: boolean, 184 endGradient: boolean, 185 ) { 186 const gradient = ctx.createLinearGradient( 187 testGradientRect.x, 188 0, 189 testGradientRect.x + testGradientRect.w, 190 0, 191 ); 192 gradient.addColorStop( 193 0, 194 startGradient ? expectedTransparentColor : expectedRgbaColor, 195 ); 196 gradient.addColorStop( 197 1, 198 endGradient ? expectedTransparentColor : expectedRgbaColor, 199 ); 200 gradient.addColorStop(gradientRatio, expectedRgbaColor); 201 gradient.addColorStop(1 - gradientRatio, expectedRgbaColor); 202 ctx.fillStyle = gradient; 203 ctx.rect( 204 testGradientRect.x, 205 testGradientRect.y, 206 testGradientRect.w, 207 testGradientRect.h, 208 ); 209 ctx.fill(); 210 } 211 212 function addEllipsis( 213 ctx: CanvasRenderingContext2D, 214 testGradientRect: Rect, 215 forwards: boolean, 216 ) { 217 ctx.fillStyle = 'black'; 218 const centerY = testGradientRect.y + testGradientRect.h / 2; 219 const xLim = forwards 220 ? testGradientRect.x + testGradientRect.w 221 : testGradientRect.x; 222 let centerX = forwards 223 ? testGradientRect.x + 5 224 : testGradientRect.x + testGradientRect.w - 5; 225 let i = 0; 226 const radius = 2; 227 while (i < 3) { 228 if (forwards && centerX + radius >= xLim) { 229 break; 230 } 231 if (!forwards && centerX + radius <= xLim) { 232 break; 233 } 234 ctx.beginPath(); 235 ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI); 236 ctx.fill(); 237 centerX = forwards ? centerX + 7 : centerX - 7; 238 i++; 239 } 240 } 241}); 242