121 lines
3.5 KiB
TypeScript
121 lines
3.5 KiB
TypeScript
import { describe, expect, mock, test } from 'bun:test'
|
||
|
||
import type { Fill, SceneNode } from '@open-pencil/scene-graph'
|
||
|
||
import { makeImageFillLocalMatrix, patternTileLayout } from '#core/canvas/fills'
|
||
import type { SkiaRenderer } from '#core/canvas/renderer'
|
||
|
||
function createRenderer() {
|
||
return {
|
||
ck: {
|
||
Matrix: {
|
||
identity: mock(() => ['identity']),
|
||
invert: mock((matrix) => ['invert', matrix]),
|
||
multiply: mock((...matrices) => ['multiply', ...matrices]),
|
||
scaled: mock((x, y) => ['scaled', x, y]),
|
||
translated: mock((x, y) => ['translated', x, y])
|
||
}
|
||
}
|
||
} as SkiaRenderer
|
||
}
|
||
|
||
const node = {
|
||
width: 120,
|
||
height: 80
|
||
} as SceneNode
|
||
|
||
describe('canvas pattern fills', () => {
|
||
test('uses scale, spacing, and alignment for rectangular pattern tiles', () => {
|
||
const layout = patternTileLayout(
|
||
{ width: 20, height: 10 } as SceneNode,
|
||
{
|
||
type: 'PATTERN',
|
||
scale: 2,
|
||
patternSpacing: { x: 0.25, y: 0.5 },
|
||
horizontalAlignment: 'CENTER',
|
||
verticalAlignment: 'END',
|
||
color: { r: 0, g: 0, b: 0, a: 1 },
|
||
opacity: 1,
|
||
visible: true
|
||
} as Fill
|
||
)
|
||
|
||
expect(layout).toEqual({
|
||
rect: { x: 0, y: 0, width: 50, height: 30 },
|
||
scale: 2,
|
||
positions: [{ x: -5, y: -10 }]
|
||
})
|
||
})
|
||
|
||
test('adds an offset source copy for hexagonal pattern tiles', () => {
|
||
const layout = patternTileLayout(
|
||
{ width: 20, height: 10 } as SceneNode,
|
||
{
|
||
type: 'PATTERN',
|
||
patternTileType: 'HORIZONTAL_HEXAGONAL',
|
||
patternSpacing: { x: 0, y: 0 },
|
||
color: { r: 0, g: 0, b: 0, a: 1 },
|
||
opacity: 1,
|
||
visible: true
|
||
} as Fill
|
||
)
|
||
|
||
expect(layout.positions).toEqual([
|
||
{ x: 0, y: 0 },
|
||
{ x: 10, y: 5 }
|
||
])
|
||
})
|
||
})
|
||
|
||
describe('canvas image fills', () => {
|
||
test.each([
|
||
{ width: 100, height: 100, scale: 0.8, x: 20, y: 0 },
|
||
{ width: 240, height: 80, scale: 0.5, x: 0, y: 20 },
|
||
{ width: 60, height: 40, scale: 2, x: 0, y: 0 }
|
||
])(
|
||
'centers FIT images without stretching ($width × $height)',
|
||
({ width, height, scale, x, y }) => {
|
||
const renderer = createRenderer()
|
||
const fill = { type: 'IMAGE', imageScaleMode: 'FIT' } as Fill
|
||
expect(makeImageFillLocalMatrix(renderer, fill, node, width, height)).toEqual([
|
||
'multiply',
|
||
['translated', x, y],
|
||
['scaled', scale, scale]
|
||
])
|
||
}
|
||
)
|
||
|
||
test('keeps untransformed tile fills in image pixel space', () => {
|
||
const renderer = createRenderer()
|
||
const fill = { type: 'IMAGE', imageScaleMode: 'TILE' } as Fill
|
||
|
||
const matrix = makeImageFillLocalMatrix(renderer, fill, node, 24, 16)
|
||
|
||
expect(matrix).toEqual(['identity'])
|
||
expect(renderer.ck.Matrix.identity).toHaveBeenCalled()
|
||
expect(renderer.ck.Matrix.scaled).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('uses the full imported affine transform for patterned image fills', () => {
|
||
const renderer = createRenderer()
|
||
const transform = { m00: 0.5, m01: 0.1, m02: 0.25, m10: -0.2, m11: 0.25, m12: 0.5 }
|
||
const fill = {
|
||
type: 'IMAGE',
|
||
imageScaleMode: 'TILE',
|
||
imageTransform: transform
|
||
} as Fill
|
||
|
||
const matrix = makeImageFillLocalMatrix(renderer, fill, node, 40, 40)
|
||
|
||
expect(renderer.ck.Matrix.invert).toHaveBeenCalledWith([
|
||
0.5, 0.1, 0.25, -0.2, 0.25, 0.5, 0, 0, 1
|
||
])
|
||
expect(matrix).toEqual([
|
||
'multiply',
|
||
['scaled', 120, 80],
|
||
['invert', [0.5, 0.1, 0.25, -0.2, 0.25, 0.5, 0, 0, 1]],
|
||
['scaled', 0.025, 0.025]
|
||
])
|
||
})
|
||
})
|