• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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