fix(canvas): render Figma-style corner smoothing
This commit is contained in:
parent
76ee122dee
commit
98616c8610
|
|
@ -15,7 +15,7 @@
|
|||
- Fix CanvasKit loading outside the browser when project paths contain spaces.
|
||||
- Render imported Figma layer and fill blend modes such as multiply, screen, overlay, difference, hue, saturation, color, and luminosity.
|
||||
- Render common imported Figma mask stacks so visible layers above a mask are clipped by the mask shape.
|
||||
- Render smoothed rectangle corners and effect blend modes from imported Figma files.
|
||||
- Render Figma-style smoothed rectangle corners, including independent corner radii, and effect blend modes from imported Figma files.
|
||||
|
||||
### Performance
|
||||
|
||||
|
|
|
|||
|
|
@ -18,11 +18,180 @@ export function nodeHasRadius(node: SceneNode): boolean {
|
|||
}
|
||||
|
||||
export function nodeHasSmoothCorners(node: SceneNode): boolean {
|
||||
return !node.independentCorners && node.cornerRadius > 0 && node.cornerSmoothing > 0
|
||||
if (!(node.cornerSmoothing > 0)) return false
|
||||
if (node.independentCorners) {
|
||||
return (
|
||||
node.topLeftRadius > 0 ||
|
||||
node.topRightRadius > 0 ||
|
||||
node.bottomRightRadius > 0 ||
|
||||
node.bottomLeftRadius > 0
|
||||
)
|
||||
}
|
||||
return node.cornerRadius > 0
|
||||
}
|
||||
|
||||
function clampCornerRadius(width: number, height: number, radius: number): number {
|
||||
return Math.max(0, Math.min(radius, width / 2, height / 2))
|
||||
type SmoothCornerKey = 'topLeft' | 'topRight' | 'bottomRight' | 'bottomLeft'
|
||||
|
||||
type SmoothCorner = {
|
||||
radius: number
|
||||
budget: number
|
||||
}
|
||||
|
||||
type SmoothCornerPathParams = {
|
||||
a: number
|
||||
b: number
|
||||
c: number
|
||||
d: number
|
||||
p: number
|
||||
radius: number
|
||||
arcSectionLength: number
|
||||
}
|
||||
|
||||
function smoothCornerRadii(node: SceneNode, width: number, height: number, spread: number): Record<SmoothCornerKey, SmoothCorner> {
|
||||
const radius = (value: number) => Math.max(0, value + spread)
|
||||
const radii: Record<SmoothCornerKey, number> = node.independentCorners
|
||||
? {
|
||||
topLeft: radius(node.topLeftRadius),
|
||||
topRight: radius(node.topRightRadius),
|
||||
bottomRight: radius(node.bottomRightRadius),
|
||||
bottomLeft: radius(node.bottomLeftRadius)
|
||||
}
|
||||
: {
|
||||
topLeft: radius(node.cornerRadius),
|
||||
topRight: radius(node.cornerRadius),
|
||||
bottomRight: radius(node.cornerRadius),
|
||||
bottomLeft: radius(node.cornerRadius)
|
||||
}
|
||||
|
||||
if (radii.topLeft === radii.topRight && radii.topRight === radii.bottomRight && radii.bottomRight === radii.bottomLeft) {
|
||||
const budget = Math.min(width, height) / 2
|
||||
const clampedRadius = Math.min(radii.topLeft, budget)
|
||||
return {
|
||||
topLeft: { radius: clampedRadius, budget },
|
||||
topRight: { radius: clampedRadius, budget },
|
||||
bottomRight: { radius: clampedRadius, budget },
|
||||
bottomLeft: { radius: clampedRadius, budget }
|
||||
}
|
||||
}
|
||||
|
||||
const budgets: Record<SmoothCornerKey, number> = {
|
||||
topLeft: -1,
|
||||
topRight: -1,
|
||||
bottomRight: -1,
|
||||
bottomLeft: -1
|
||||
}
|
||||
const adjacentByCorner: Record<SmoothCornerKey, Array<{ corner: SmoothCornerKey; sideLength: number }>> = {
|
||||
topLeft: [
|
||||
{ corner: 'topRight', sideLength: width },
|
||||
{ corner: 'bottomLeft', sideLength: height }
|
||||
],
|
||||
topRight: [
|
||||
{ corner: 'topLeft', sideLength: width },
|
||||
{ corner: 'bottomRight', sideLength: height }
|
||||
],
|
||||
bottomRight: [
|
||||
{ corner: 'bottomLeft', sideLength: width },
|
||||
{ corner: 'topRight', sideLength: height }
|
||||
],
|
||||
bottomLeft: [
|
||||
{ corner: 'bottomRight', sideLength: width },
|
||||
{ corner: 'topLeft', sideLength: height }
|
||||
]
|
||||
}
|
||||
|
||||
for (const corner of (Object.keys(radii) as SmoothCornerKey[]).sort((a, b) => radii[b] - radii[a])) {
|
||||
const cornerRadius = radii[corner]
|
||||
const budget = Math.min(
|
||||
...adjacentByCorner[corner].map((adjacent) => {
|
||||
const adjacentRadius = radii[adjacent.corner]
|
||||
if (cornerRadius === 0 && adjacentRadius === 0) return 0
|
||||
if (budgets[adjacent.corner] >= 0) return adjacent.sideLength - budgets[adjacent.corner]
|
||||
return (cornerRadius / (cornerRadius + adjacentRadius)) * adjacent.sideLength
|
||||
})
|
||||
)
|
||||
budgets[corner] = budget
|
||||
radii[corner] = Math.min(cornerRadius, budget)
|
||||
}
|
||||
|
||||
return {
|
||||
topLeft: { radius: radii.topLeft, budget: budgets.topLeft },
|
||||
topRight: { radius: radii.topRight, budget: budgets.topRight },
|
||||
bottomRight: { radius: radii.bottomRight, budget: budgets.bottomRight },
|
||||
bottomLeft: { radius: radii.bottomLeft, budget: budgets.bottomLeft }
|
||||
}
|
||||
}
|
||||
|
||||
function degreesToRadians(degrees: number): number {
|
||||
return (degrees * Math.PI) / 180
|
||||
}
|
||||
|
||||
function smoothCornerPathParams(corner: SmoothCorner, smoothing: number): SmoothCornerPathParams {
|
||||
let cornerSmoothing = smoothing
|
||||
let p = (1 + cornerSmoothing) * corner.radius
|
||||
if (corner.radius > 0) {
|
||||
const maxSmoothing = corner.budget / corner.radius - 1
|
||||
cornerSmoothing = Math.min(cornerSmoothing, maxSmoothing)
|
||||
p = Math.min(p, corner.budget)
|
||||
}
|
||||
|
||||
const arcMeasure = 90 * (1 - cornerSmoothing)
|
||||
const arcSectionLength = Math.sin(degreesToRadians(arcMeasure / 2)) * corner.radius * Math.sqrt(2)
|
||||
const angleAlpha = (90 - arcMeasure) / 2
|
||||
const p3ToP4Distance = corner.radius * Math.tan(degreesToRadians(angleAlpha / 2))
|
||||
const angleBeta = 45 * cornerSmoothing
|
||||
const c = p3ToP4Distance * Math.cos(degreesToRadians(angleBeta))
|
||||
const d = c * Math.tan(degreesToRadians(angleBeta))
|
||||
const b = (p - arcSectionLength - c - d) / 3
|
||||
|
||||
return {
|
||||
a: 2 * b,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
p,
|
||||
radius: corner.radius,
|
||||
arcSectionLength
|
||||
}
|
||||
}
|
||||
|
||||
function drawTopRightSmoothCorner(path: Path, corner: SmoothCornerPathParams, x: number, y: number) {
|
||||
if (corner.radius === 0) {
|
||||
path.lineTo(x + corner.p, y)
|
||||
return
|
||||
}
|
||||
path.cubicTo(x + corner.a, y, x + corner.a + corner.b, y, x + corner.a + corner.b + corner.c, y + corner.d)
|
||||
path.arcToRotated(corner.radius, corner.radius, 0, true, false, x + corner.p - corner.d, y + corner.p - corner.a - corner.b - corner.c)
|
||||
path.cubicTo(x + corner.p, y + corner.p - corner.a - corner.b, x + corner.p, y + corner.p - corner.a, x + corner.p, y + corner.p)
|
||||
}
|
||||
|
||||
function drawBottomRightSmoothCorner(path: Path, corner: SmoothCornerPathParams, x: number, y: number) {
|
||||
if (corner.radius === 0) {
|
||||
path.lineTo(x, y + corner.p)
|
||||
return
|
||||
}
|
||||
path.cubicTo(x, y + corner.a, x, y + corner.a + corner.b, x - corner.d, y + corner.a + corner.b + corner.c)
|
||||
path.arcToRotated(corner.radius, corner.radius, 0, true, false, x - corner.p + corner.a + corner.b + corner.c, y + corner.p - corner.d)
|
||||
path.cubicTo(x - corner.p + corner.a + corner.b, y + corner.p, x - corner.p + corner.a, y + corner.p, x - corner.p, y + corner.p)
|
||||
}
|
||||
|
||||
function drawBottomLeftSmoothCorner(path: Path, corner: SmoothCornerPathParams, x: number, y: number) {
|
||||
if (corner.radius === 0) {
|
||||
path.lineTo(x - corner.p, y)
|
||||
return
|
||||
}
|
||||
path.cubicTo(x - corner.a, y, x - corner.a - corner.b, y, x - corner.a - corner.b - corner.c, y - corner.d)
|
||||
path.arcToRotated(corner.radius, corner.radius, 0, true, false, x - corner.p + corner.d, y - corner.p + corner.a + corner.b + corner.c)
|
||||
path.cubicTo(x - corner.p, y - corner.p + corner.a + corner.b, x - corner.p, y - corner.p + corner.a, x - corner.p, y - corner.p)
|
||||
}
|
||||
|
||||
function drawTopLeftSmoothCorner(path: Path, corner: SmoothCornerPathParams, x: number, y: number) {
|
||||
if (corner.radius === 0) {
|
||||
path.lineTo(x, y - corner.p)
|
||||
return
|
||||
}
|
||||
path.cubicTo(x, y - corner.a, x, y - corner.a - corner.b, x + corner.d, y - corner.a - corner.b - corner.c)
|
||||
path.arcToRotated(corner.radius, corner.radius, 0, true, false, x + corner.p - corner.a - corner.b - corner.c, y - corner.p + corner.d)
|
||||
path.cubicTo(x + corner.p - corner.a - corner.b, y - corner.p, x + corner.p - corner.a, y - corner.p, x + corner.p, y - corner.p)
|
||||
}
|
||||
|
||||
export function makeSmoothRRectPath(
|
||||
|
|
@ -37,37 +206,38 @@ export function makeSmoothRRectPath(
|
|||
const top = offsetY - spread
|
||||
const right = offsetX + node.width + spread
|
||||
const bottom = offsetY + node.height + spread
|
||||
const radius = clampCornerRadius(right - left, bottom - top, node.cornerRadius + spread)
|
||||
|
||||
if (radius === 0) {
|
||||
path.addRect(r.ck.LTRBRect(left, top, right, bottom))
|
||||
const width = right - left
|
||||
const height = bottom - top
|
||||
if (width <= 0 || height <= 0) {
|
||||
path.addRect(r.ck.LTRBRect(left, top, Math.max(left, right), Math.max(top, bottom)))
|
||||
return path
|
||||
}
|
||||
|
||||
const smoothing = Math.max(0, Math.min(node.cornerSmoothing, 1))
|
||||
const exponent = 2 + smoothing * 3
|
||||
const samples = 12
|
||||
const addCorner = (cx: number, cy: number, startAngle: number, endAngle: number) => {
|
||||
for (let i = 1; i <= samples; i++) {
|
||||
const t = i / samples
|
||||
const angle = startAngle + (endAngle - startAngle) * t
|
||||
const cos = Math.cos(angle)
|
||||
const sin = Math.sin(angle)
|
||||
const x = cx + Math.sign(cos) * radius * Math.abs(cos) ** (2 / exponent)
|
||||
const y = cy + Math.sign(sin) * radius * Math.abs(sin) ** (2 / exponent)
|
||||
path.lineTo(x, y)
|
||||
}
|
||||
const corners = smoothCornerRadii(node, width, height, spread)
|
||||
const topLeftCorner = smoothCornerPathParams(corners.topLeft, smoothing)
|
||||
const topRightCorner = smoothCornerPathParams(corners.topRight, smoothing)
|
||||
const bottomRightCorner = smoothCornerPathParams(corners.bottomRight, smoothing)
|
||||
const bottomLeftCorner = smoothCornerPathParams(corners.bottomLeft, smoothing)
|
||||
|
||||
if (
|
||||
topLeftCorner.radius === 0 &&
|
||||
topRightCorner.radius === 0 &&
|
||||
bottomRightCorner.radius === 0 &&
|
||||
bottomLeftCorner.radius === 0
|
||||
) {
|
||||
path.addRect(r.ck.LTRBRect(left, top, right, bottom))
|
||||
return path
|
||||
}
|
||||
|
||||
path.moveTo(left + radius, top)
|
||||
path.lineTo(right - radius, top)
|
||||
addCorner(right - radius, top + radius, -Math.PI / 2, 0)
|
||||
path.lineTo(right, bottom - radius)
|
||||
addCorner(right - radius, bottom - radius, 0, Math.PI / 2)
|
||||
path.lineTo(left + radius, bottom)
|
||||
addCorner(left + radius, bottom - radius, Math.PI / 2, Math.PI)
|
||||
path.lineTo(left, top + radius)
|
||||
addCorner(left + radius, top + radius, Math.PI, (Math.PI * 3) / 2)
|
||||
path.moveTo(right - topRightCorner.p, top)
|
||||
drawTopRightSmoothCorner(path, topRightCorner, right - topRightCorner.p, top)
|
||||
path.lineTo(right, bottom - bottomRightCorner.p)
|
||||
drawBottomRightSmoothCorner(path, bottomRightCorner, right, bottom - bottomRightCorner.p)
|
||||
path.lineTo(left + bottomLeftCorner.p, bottom)
|
||||
drawBottomLeftSmoothCorner(path, bottomLeftCorner, left + bottomLeftCorner.p, bottom)
|
||||
path.lineTo(left, top + topLeftCorner.p)
|
||||
drawTopLeftSmoothCorner(path, topLeftCorner, left, top + topLeftCorner.p)
|
||||
path.close()
|
||||
return path
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ Figma's design documentation groups features into these areas:
|
|||
| Frames | ✅ | ✅ | ✅ | ✅ | ✅ | Includes clipping and auto-layout container behavior. |
|
||||
| Groups | ✅ | ✅ | ✅ | ✅ | ✅ | Grouping preserves visual positions. |
|
||||
| Sections | ✅ | ✅ | ✅ | ✅ | ✅ | Section rendering and title pills are OpenPencil-specific approximations. |
|
||||
| Rectangles / rounded rectangles | ✅ | ✅ | ✅ | ✅ | ✅ | Per-corner radii and smoothed uniform corners render; independent-corner smoothing remains approximate. |
|
||||
| Rectangles / rounded rectangles | ✅ | ✅ | ✅ | ✅ | ✅ | Per-corner radii and smoothed corners render for fills, strokes, clips, masks, and effects. |
|
||||
| Ellipses / arcs | ✅ | ✅ | ◐ | ✅ | ✅ | `arcData` renders/exports; no full inspector controls. |
|
||||
| Lines | ✅ | ✅ | ✅ | ✅ | ✅ | Stroke caps/joins render but are not fully exposed in UI. |
|
||||
| Polygons / stars | ✅ | ✅ | ◐ | ✅ | ✅ | `pointCount` and `starInnerRadius` modeled. |
|
||||
|
|
@ -204,7 +204,7 @@ OpenPencil deliberately preserves many Figma/Kiwi fields even when they are not
|
|||
These are parsed or visible in Figma docs and most likely to cause visible differences in real design files:
|
||||
|
||||
1. **Masks** — extend common sibling mask stacks with luminance masks and more exact `maskType` behavior.
|
||||
2. **Corner smoothing** — improve the current smoothed uniform-corner approximation for independent corners and stroke edge cases.
|
||||
2. **Corner smoothing** — compare smoothed-corner output against Figma fixtures and tune remaining edge cases.
|
||||
3. **Pattern fills** — render imported Figma pattern/image tiling semantics beyond current image scale modes.
|
||||
4. **Pattern fills/strokes** — support Figma pattern fills and their transforms.
|
||||
5. **Font variations** — apply variable-font axes from imported Figma metadata.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type { EditorStore } from '@/app/editor/session'
|
|||
export function createEffectsSection(store: EditorStore) {
|
||||
const { graph } = store
|
||||
|
||||
const effectsSectionId = store.createShape('SECTION', 60, 840, 920, 640)
|
||||
const effectsSectionId = store.createShape('SECTION', 60, 840, 920, 780)
|
||||
graph.updateNode(effectsSectionId, {
|
||||
name: 'Effects',
|
||||
fills: [solid(DEMO_COLORS.white)]
|
||||
|
|
@ -328,4 +328,100 @@ export function createEffectsSection(store: EditorStore) {
|
|||
rotation: -20,
|
||||
fills: [solid(DEMO_COLORS.teal)]
|
||||
})
|
||||
|
||||
const smoothingLabel = store.createShape('TEXT', 32, 644, 280, 20, effectsSectionId)
|
||||
graph.updateNode(smoothingLabel, {
|
||||
name: 'Label',
|
||||
text: 'Corner smoothing & effect blends',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const smoothingCard = store.createShape('FRAME', 32, 676, 256, 104, effectsSectionId)
|
||||
graph.updateNode(smoothingCard, {
|
||||
name: 'Smooth Corner Comparison',
|
||||
cornerRadius: 16,
|
||||
fills: [solid(DEMO_COLORS.gray50)],
|
||||
strokes: thinStroke(DEMO_COLORS.gray200)
|
||||
})
|
||||
const regularCorner = store.createShape('RECTANGLE', 24, 18, 58, 58, smoothingCard)
|
||||
graph.updateNode(regularCorner, {
|
||||
name: 'Regular Radius',
|
||||
cornerRadius: 18,
|
||||
fills: [solid(DEMO_COLORS.blue)]
|
||||
})
|
||||
const smoothCorner = store.createShape('RECTANGLE', 102, 18, 58, 58, smoothingCard)
|
||||
graph.updateNode(smoothCorner, {
|
||||
name: 'Smoothed Radius',
|
||||
cornerRadius: 18,
|
||||
cornerSmoothing: 0.9,
|
||||
fills: [solid(DEMO_COLORS.purple)]
|
||||
})
|
||||
const cornerLabel = store.createShape('TEXT', 176, 26, 56, 36, smoothingCard)
|
||||
graph.updateNode(cornerLabel, {
|
||||
name: 'Label',
|
||||
text: 'Radius\n+ smooth',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
lineHeight: 16,
|
||||
fills: [solid(DEMO_COLORS.gray500)]
|
||||
})
|
||||
|
||||
const independentCard = store.createShape('FRAME', 312, 676, 204, 104, effectsSectionId)
|
||||
graph.updateNode(independentCard, {
|
||||
name: 'Independent Smooth Corners',
|
||||
cornerRadius: 28,
|
||||
cornerSmoothing: 1,
|
||||
independentCorners: true,
|
||||
topLeftRadius: 36,
|
||||
topRightRadius: 10,
|
||||
bottomRightRadius: 36,
|
||||
bottomLeftRadius: 10,
|
||||
fills: [
|
||||
gradient([
|
||||
{ color: DEMO_COLORS.teal, position: 0 },
|
||||
{ color: DEMO_COLORS.blue, position: 1 }
|
||||
])
|
||||
],
|
||||
effects: [dropShadow(0, 8, 20, 0, { r: 0.08, g: 0.73, b: 0.73, a: 0.24 })]
|
||||
})
|
||||
const independentText = store.createShape('TEXT', 20, 34, 164, 24, independentCard)
|
||||
graph.updateNode(independentText, {
|
||||
name: 'Label',
|
||||
text: 'Independent corners',
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
fills: [solid(DEMO_COLORS.white)]
|
||||
})
|
||||
|
||||
const effectBlendCard = store.createShape('FRAME', 540, 676, 300, 104, effectsSectionId)
|
||||
graph.updateNode(effectBlendCard, {
|
||||
name: 'Effect Blend Card',
|
||||
cornerRadius: 18,
|
||||
fills: [solid({ r: 0.08, g: 0.1, b: 0.18, a: 1 })],
|
||||
clipsContent: true
|
||||
})
|
||||
const glowBase = store.createShape('ELLIPSE', 22, 14, 76, 76, effectBlendCard)
|
||||
graph.updateNode(glowBase, {
|
||||
name: 'Glow Base',
|
||||
fills: [solid({ r: 0.24, g: 0.46, b: 1, a: 1 })]
|
||||
})
|
||||
const glowShape = store.createShape('RECTANGLE', 70, 22, 96, 60, effectBlendCard)
|
||||
graph.updateNode(glowShape, {
|
||||
name: 'Screen Shadow Blend',
|
||||
cornerRadius: 22,
|
||||
cornerSmoothing: 0.85,
|
||||
fills: [solid({ r: 0.58, g: 0.27, b: 0.95, a: 0.7 })],
|
||||
effects: [{ ...dropShadow(0, 0, 28, 0, { r: 0.56, g: 0.33, b: 1, a: 0.72 }), blendMode: 'SCREEN' }]
|
||||
})
|
||||
const effectBlendText = store.createShape('TEXT', 184, 34, 88, 34, effectBlendCard)
|
||||
graph.updateNode(effectBlendText, {
|
||||
name: 'Label',
|
||||
text: 'Screen\nshadow',
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
lineHeight: 17,
|
||||
fills: [solid(DEMO_COLORS.white)]
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ function createRenderer() {
|
|||
addRect: ReturnType<typeof mock>
|
||||
moveTo: ReturnType<typeof mock>
|
||||
lineTo: ReturnType<typeof mock>
|
||||
cubicTo: ReturnType<typeof mock>
|
||||
arcToRotated: ReturnType<typeof mock>
|
||||
close: ReturnType<typeof mock>
|
||||
delete: ReturnType<typeof mock>
|
||||
}> = []
|
||||
|
|
@ -23,6 +25,8 @@ function createRenderer() {
|
|||
addRect = mock(() => undefined)
|
||||
moveTo = mock(() => undefined)
|
||||
lineTo = mock(() => undefined)
|
||||
cubicTo = mock(() => undefined)
|
||||
arcToRotated = mock(() => undefined)
|
||||
close = mock(() => undefined)
|
||||
delete = mock(() => undefined)
|
||||
|
||||
|
|
@ -51,7 +55,7 @@ function createCanvas() {
|
|||
}
|
||||
|
||||
describe('canvas corner smoothing', () => {
|
||||
test('builds a superellipse-style path for smoothed rectangular corners', () => {
|
||||
test('builds cubic paths for smoothed rectangular corners', () => {
|
||||
const graph = new SceneGraph()
|
||||
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
||||
width: 120,
|
||||
|
|
@ -64,12 +68,37 @@ describe('canvas corner smoothing', () => {
|
|||
makeSmoothRRectPath(renderer, node).delete()
|
||||
|
||||
expect(paths).toHaveLength(1)
|
||||
expect(paths[0].moveTo).toHaveBeenCalledWith(24, 0)
|
||||
expect(paths[0].lineTo).toHaveBeenCalledWith(96, 0)
|
||||
expect(paths[0].lineTo).toHaveBeenCalledTimes(52)
|
||||
expect(paths[0].moveTo).toHaveBeenCalledWith(80, 0)
|
||||
expect(paths[0].lineTo).toHaveBeenCalledWith(120, 40)
|
||||
expect(paths[0].arcToRotated).toHaveBeenCalledTimes(4)
|
||||
expect(paths[0].cubicTo).toHaveBeenCalledTimes(8)
|
||||
expect(paths[0].lineTo).toHaveBeenCalledTimes(3)
|
||||
expect(paths[0].close).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('supports independent smoothed corner radii', () => {
|
||||
const graph = new SceneGraph()
|
||||
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
||||
width: 120,
|
||||
height: 80,
|
||||
independentCorners: true,
|
||||
topLeftRadius: 28,
|
||||
topRightRadius: 12,
|
||||
bottomRightRadius: 32,
|
||||
bottomLeftRadius: 0,
|
||||
cornerSmoothing: 1
|
||||
})
|
||||
const { renderer, paths } = createRenderer()
|
||||
|
||||
makeSmoothRRectPath(renderer, node).delete()
|
||||
|
||||
expect(paths).toHaveLength(1)
|
||||
expect(paths[0].moveTo).toHaveBeenCalledWith(98.18181818181819, 0)
|
||||
expect(paths[0].arcToRotated).toHaveBeenCalledTimes(3)
|
||||
expect(paths[0].cubicTo).toHaveBeenCalledTimes(6)
|
||||
expect(paths[0].lineTo).toHaveBeenCalledWith(0, 80)
|
||||
})
|
||||
|
||||
test('draws smoothed rectangle fills as paths instead of regular rrects', () => {
|
||||
const graph = new SceneGraph()
|
||||
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
||||
|
|
|
|||
Loading…
Reference in a new issue