Skip to content

Commit

Permalink
refactor(pattern): 使用setTransform方法,rotation作用于整个pattern画布 (#2789)
Browse files Browse the repository at this point in the history
* refactor(pattern): 使用setTransform方法,rotation作用于整个pattern画布

* refactor: 处理transformMatrix成纯函数

* refactor: 处理transformMatrix成纯函数

* test(dot and util): 添加dotPattern和util的单测,并添加getPixelColor获取像素点方法

* test(line-pattern): 添加linePattern的单测

* test(square-pattern): 添加squarePattern的单测

* refactor: 修改变换矩阵的写法

Co-authored-by: 酥云 <lisuwen.lsw@antgroup.com>
  • Loading branch information
2 people authored and visiky committed Aug 15, 2021
1 parent 1433c17 commit 7f58d3d
Show file tree
Hide file tree
Showing 11 changed files with 354 additions and 104 deletions.
73 changes: 73 additions & 0 deletions __tests__/unit/utils/pattern/dot-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { initCanvas } from '../../../../src/utils/pattern/util';
import { drawDot, defaultDotPatternCfg, createDotPattern } from '../../../../src/utils/pattern/dot';
import { DotPatternCfg } from '../../../../src/types/pattern';
import { getPixelColor } from '../../../utils/getPixelColor';
import { deepAssign } from '../../../../src/utils';

describe('utils: dot pattern', () => {
const width = 30,
height = 30;
const canvas = initCanvas(width, height);
const ctx = canvas.getContext('2d');
document.body.appendChild(canvas);

it('createDotPattern with defaultCfg', () => {
const pattern = createDotPattern(defaultDotPatternCfg as DotPatternCfg);
expect(pattern.toString()).toEqual('[object CanvasPattern]');
});

it('dotUnitPattern with fill', () => {
const cfg = deepAssign(defaultDotPatternCfg, {
// radius: 4, // 默认
fill: '#898989',
});
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawDot(ctx, cfg as DotPatternCfg, width / 2, height / 2);
// 传入的是呈现的位置
expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#898989');
expect(getPixelColor(canvas, width / 2 + cfg.radius, height / 2 + cfg.radius).hex).toEqual('#000000'); // 超出圆范围, 像素点在右下方
});

it('dotUnitPattern with fillOpacity', () => {
const cfg = deepAssign(defaultDotPatternCfg, {
// radius: 4, // 默认
fill: '#898989',
fillOpacity: 0.5,
});
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawDot(ctx, cfg as DotPatternCfg, width / 2, height / 2);
// 传入的是呈现的位置
expect(getPixelColor(canvas, width / 2, height / 2).alpha.toPrecision(1)).toEqual(`${0.5}`);
expect(getPixelColor(canvas, width / 2 + cfg.radius, height / 2 + cfg.radius).alpha.toPrecision(1)).toEqual(`${0}`); // 透明度为0
});

it('dotUnitPattern with radius', () => {
const radius = 15;
const cfg = deepAssign(defaultDotPatternCfg, {
fill: '#ff0000',
fillOpacity: 1,
radius,
});
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawDot(ctx, cfg as DotPatternCfg, width / 2, height / 2);
// 传入的是呈现的位置
expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#ff0000');
expect(getPixelColor(canvas, width / 2 + radius - 1, height / 2).hex).toEqual('#ff0000'); // 圆的边界
expect(getPixelColor(canvas, width / 2 + radius, height / 2).hex).toEqual('#000000');
});

it('dotUnitPattern with stroke and lineWidth', () => {
const cfg = deepAssign(defaultDotPatternCfg, {
radius: 10,
fill: '#ff0000',
stroke: '#00ff00',
lineWidth: 2,
});
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawDot(ctx, cfg as DotPatternCfg, width / 2, height / 2);
// 传入的是呈现的位置
expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#ff0000');
expect(getPixelColor(canvas, width / 2 + cfg.radius, height / 2).hex).toEqual('#00ff00');
expect(getPixelColor(canvas, width / 2 + cfg.radius, height / 2 + cfg.radius).hex).toEqual('#000000');
});
});
52 changes: 52 additions & 0 deletions __tests__/unit/utils/pattern/line-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { initCanvas } from '../../../../src/utils/pattern/util';
import { defaultLinePatternCfg, createLinePattern, drawLine } from '../../../../src/utils/pattern/line';
import { LinePatternCfg } from '../../../../src/types/pattern';
import { getPixelColor } from '../../../utils/getPixelColor';
import { deepAssign } from '../../../../src/utils';

describe('utils: line pattern', () => {
const width = 30,
height = 30;
const canvas = initCanvas(width, height);
const ctx = canvas.getContext('2d');
document.body.appendChild(canvas);

it('createLinePattern with defaultCfg', () => {
const pattern = createLinePattern(defaultLinePatternCfg as LinePatternCfg);
expect(pattern.toString()).toEqual('[object CanvasPattern]');
});

it('lineUnitPattern with stroke and strokeWidth', () => {
const cfg = deepAssign(defaultLinePatternCfg, {
stroke: '#ff0000',
lineWidth: 2,
});
const d = `
M 0 0 L ${width} 0
M 0 ${height} L ${width} ${height}
`;
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawLine(ctx, cfg as LinePatternCfg, d);
// 传入的是呈现的位置
expect(getPixelColor(canvas, 0, 0).hex).toEqual('#ff0000');
expect(getPixelColor(canvas, 0, height - 1).hex).toEqual('#ff0000');
});

it('lineUnitPattern with strokeOpacity', () => {
const cfg = deepAssign(defaultLinePatternCfg, {
stroke: '#ff0000',
lineWidth: 2,
strokeOpacity: 0.5,
});
const d = `
M 0 0 L ${width} 0
M 0 ${height} L ${width} ${height}
`;
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawLine(ctx, cfg as LinePatternCfg, d);
// 传入的是呈现的位置
expect(getPixelColor(canvas, 0, 0).alpha.toPrecision(1)).toEqual(`${0.5}`);
expect(getPixelColor(canvas, 0, height - 1).alpha.toPrecision(1)).toEqual(`${0.5}`);
expect(getPixelColor(canvas, width / 2, height / 2).alpha.toPrecision(1)).toEqual(`${0}`);
});
});
72 changes: 72 additions & 0 deletions __tests__/unit/utils/pattern/square-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { initCanvas } from '../../../../src/utils/pattern/util';
import { drawSquare, defaultSquarePatternCfg, createSquarePattern } from '../../../../src/utils/pattern/square';
import { SquarePatternCfg } from '../../../../src/types/pattern';
import { getPixelColor } from '../../../utils/getPixelColor';
import { deepAssign } from '../../../../src/utils';

describe('utils: square pattern', () => {
const width = 30,
height = 30;
const canvas = initCanvas(width, height);
const ctx = canvas.getContext('2d');
document.body.appendChild(canvas);

it('createSquarePattern with defaultCfg', () => {
const pattern = createSquarePattern(defaultSquarePatternCfg as SquarePatternCfg);
expect(pattern.toString()).toEqual('[object CanvasPattern]');
});

it('squareUnitPattern with fill', () => {
const cfg = deepAssign(defaultSquarePatternCfg, {
fill: '#898989',
});
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSquare(ctx, cfg as SquarePatternCfg, width / 2, height / 2);
// 传入的是呈现的位置
expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#898989');
expect(getPixelColor(canvas, width / 2 + cfg.size + 5, height / 2).hex).toEqual('#000000'); // 超出范围
});

it('squareUnitPattern with fillOpacity', () => {
const cfg = deepAssign(defaultSquarePatternCfg, {
// radius: 4, // 默认
fill: '#898989',
fillOpacity: 0.5,
});
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSquare(ctx, cfg as SquarePatternCfg, width / 2, height / 2);
// 传入的是呈现的位置
expect(getPixelColor(canvas, width / 2, height / 2).alpha.toPrecision(1)).toEqual(`${0.5}`);
expect(getPixelColor(canvas, width / 2 + cfg.size + 5, height / 2).alpha.toPrecision(1)).toEqual(`${0}`); // 透明度为0
});

it('squareUnitPattern with size', () => {
const size = 15;
const cfg = deepAssign(defaultSquarePatternCfg, {
fill: '#898989',
fillOpacity: 1,
size,
});
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSquare(ctx, cfg as SquarePatternCfg, width / 2, height / 2);
// 传入的是呈现的位置
expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#898989');
expect(getPixelColor(canvas, width / 2 + size / 2 - 1, height / 2).hex).toEqual('#898989'); // 边界
expect(getPixelColor(canvas, width / 2 + size / 2 + 5, height / 2).hex).toEqual('#000000'); // 超出边界
});

it('squareUnitPattern with stroke and lineWidth', () => {
const cfg = deepAssign(defaultSquarePatternCfg, {
size: 15,
fill: '#ff0000',
stroke: '#00ff00',
lineWidth: 2,
});
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSquare(ctx, cfg as SquarePatternCfg, width / 2, height / 2);
// 传入的是呈现的位置
expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#ff0000');
expect(getPixelColor(canvas, width / 2 + cfg.size / 2, height / 2).hex).toEqual('#00ff00');
expect(getPixelColor(canvas, width / 2 + cfg.size / 2 + 5, height / 2 + cfg.size / 2).hex).toEqual('#000000');
});
});
62 changes: 62 additions & 0 deletions __tests__/unit/utils/pattern/util-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
initCanvas,
getUnitPatternSize,
getSymbolsPosition,
drawBackground,
transformMatrix,
} from '../../../../src/utils/pattern/util';
import { DotPatternCfg } from '../../../../src/types/pattern';
import { getPixelColor } from '../../../utils/getPixelColor';

describe('utils', () => {
const width = 30,
height = 30;
let canvas = null;

it('initCanvas', () => {
canvas = initCanvas(width, height);
document.body.appendChild(canvas);
expect(canvas.width).toBe(60);
expect(canvas.height).toBe(60);
});

it('getPixelColor', () => {
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#989898';
ctx.fillRect(0, 0, canvas.width, canvas.height);
expect(getPixelColor(canvas, width / 2, height / 2).hex).toEqual('#989898');
expect(getPixelColor(canvas, width + 1, height + 1).hex).toEqual('#000000'); // 超出范围,黑色
});

it('drawBackground', () => {
const defaultDotPatternCfg = {
backgroundColor: '#eee', // 为了测试背景色填充
};
const ctx = canvas.getContext('2d');
drawBackground(ctx, defaultDotPatternCfg as DotPatternCfg, width, height);
const color = getPixelColor(canvas, width / 2, height / 2).hex;
expect(color).toEqual('#eeeeee');
});

it('getUnitPatternSize', () => {
expect(getUnitPatternSize(4, 6, false)).toBe(10);
expect(getUnitPatternSize(4, 6, true)).toBe(20);
});

it('getSymbolsPosition', () => {
expect(getSymbolsPosition(12, false)).toEqual([[6, 6]]);
expect(getSymbolsPosition(12, true)).toEqual([
[3, 3],
[9, 9],
]);
});

it('transformMatrix', () => {
expect(transformMatrix(2, 45).toString()).toEqual(
'matrix(0.3535533905932738, 0.35355339059327373, -0.35355339059327373, 0.3535533905932738, 0, 0)'
);
expect(transformMatrix(1, 45).toString()).toEqual(
'matrix(0.7071067811865476, 0.7071067811865475, -0.7071067811865475, 0.7071067811865476, 0, 0)'
);
});
});
26 changes: 26 additions & 0 deletions __tests__/utils/getPixelColor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @param canvas
* @param x和y 呈现的坐标位置
*/
export function getPixelColor(canvas: HTMLCanvasElement, x: number, y: number) {
const dpr = window.devicePixelRatio || 2;
const ctx = canvas.getContext('2d');
// ctx 获取的是实际渲染的图形的位置,所以需要:呈现的坐标位置 * dpr
const pixel = ctx.getImageData(x * dpr, y * dpr, 1, 1);
const data = pixel.data; // [r, g, b, a] 暂不处理透明度情况

// 如果 x, y 超出图像边界,则是透明黑色(全为零,即 rgba(0,0,0,0))
// 详见:https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas
const rHex = fixTwoDigit(data[0].toString(16));
const gHex = fixTwoDigit(data[1].toString(16));
const bHex = fixTwoDigit(data[2].toString(16));
const hex = `#${rHex}${gHex}${bHex}`;

const alpha = data[3] / 255;

return { hex, alpha };
}

function fixTwoDigit(str) {
return str.length < 2 ? `0${str}` : `${str}`;
}
10 changes: 5 additions & 5 deletions src/types/pattern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ export type PatternCfg = {
strokeOpacity?: number;
/** lines thickness. 描边粗细 */
lineWidth?: number;
/** 图案以及背景色 */
/** 整个pattern 透明度 */
opacity?: number;
/** 整个pattern 的旋转角度 */
rotation?: number;
/** 贴图模式 */
mode?: 'repeat' | 'no-repeat' | 'repeat-x' | 'repeat-y';
};
Expand All @@ -35,17 +37,15 @@ export type DotPatternCfg = PatternCfg & {
export type LinePatternCfg = PatternCfg & {
/** pacing between lines. 线之间的距离 */
spacing?: number;
/** lines rotation. */
rotation?: number;
};

/**
* square pattern
*/
export type SquarePatternCfg = PatternCfg & {
/** rotation */
rotation?: number;
/** 矩形的大小 */
size?: number;
/** 矩形之间的间隔 */
padding?: number;
/** 是否交错,默认: true. 即 staggered squares. */
isStagger?: boolean;
Expand Down
15 changes: 12 additions & 3 deletions src/utils/pattern/dot.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DotPatternCfg } from '../../types/pattern';
import { deepAssign } from '../../utils';
import { getUnitPatternSize, initCanvas, drawBackground, getSymbolsPosition } from './util';
import { getUnitPatternSize, initCanvas, drawBackground, getSymbolsPosition, transformMatrix } from './util';

/**
* dotPattern的默认配置
Expand All @@ -10,6 +10,7 @@ export const defaultDotPatternCfg = {
padding: 4,
backgroundColor: 'transparent',
opacity: 1,
rotation: 0,
fill: '#FFF',
fillOpacity: 1,
stroke: 'transparent',
Expand Down Expand Up @@ -51,7 +52,7 @@ export function drawDot(context: CanvasRenderingContext2D, cfg: DotPatternCfg, x
export function createDotPattern(cfg?: DotPatternCfg): CanvasPattern {
const dotCfg = deepAssign({}, defaultDotPatternCfg, cfg);

const { radius, padding, isStagger } = dotCfg;
const { radius, padding, isStagger, rotation } = dotCfg;

// 计算 画布大小,dots的位置
const unitSize = getUnitPatternSize(radius * 2, padding, isStagger);
Expand All @@ -67,5 +68,13 @@ export function createDotPattern(cfg?: DotPatternCfg): CanvasPattern {
drawDot(ctx, dotCfg, x, y);
}

return ctx.createPattern(canvas, dotCfg.mode);
const pattern = ctx.createPattern(canvas, dotCfg.mode);

if (pattern) {
const dpr = window?.devicePixelRatio || 2;
const matrix = transformMatrix(dpr, rotation);
pattern.setTransform(matrix);
}

return pattern;
}
4 changes: 0 additions & 4 deletions src/utils/pattern/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ export function getCanvasPattern(options: PatternOption): CanvasPattern | undefi
default:
break;
}
if (pattern) {
const dpr = window?.devicePixelRatio || 2;
pattern.setTransform({ a: 1 / dpr, b: 0, c: 0, d: 1 / dpr, e: 0, f: 0 });
}

return pattern;
}
Loading

0 comments on commit 7f58d3d

Please sign in to comment.