openpencil/packages/core/src/canvas/strokes.ts
2026-05-24 12:15:29 +03:00

356 lines
9.5 KiB
TypeScript

import type { Canvas, EmbindEnumEntity, Paint } from 'canvaskit-wasm'
import type { SceneNode, Stroke } from '#core/scene-graph'
import type { Color } from '#core/types'
import type { SkiaRenderer } from './renderer'
import { makeSmoothRRectPath, nodeHasSmoothCorners } from './shapes'
export function getStrokeCapEntity(r: SkiaRenderer, cap: string | undefined): EmbindEnumEntity {
switch (cap) {
case 'ROUND':
return r.ck.StrokeCap.Round
case 'SQUARE':
return r.ck.StrokeCap.Square
default:
return r.ck.StrokeCap.Butt
}
}
export function getStrokeJoinEntity(r: SkiaRenderer, join: string | undefined): EmbindEnumEntity {
switch (join) {
case 'ROUND':
return r.ck.StrokeJoin.Round
case 'BEVEL':
return r.ck.StrokeJoin.Bevel
default:
return r.ck.StrokeJoin.Miter
}
}
function strokeInset(stroke: Stroke): number {
if (stroke.align === 'INSIDE') return stroke.weight / 2
if (stroke.align === 'OUTSIDE') return -stroke.weight / 2
return 0
}
export function drawDashedRRectWithSolidCorners(
r: SkiaRenderer,
canvas: Canvas,
node: SceneNode,
stroke: Stroke,
color: Color,
cornerRadius: number,
dashPhase = 0
): void {
const dash = stroke.dashPattern ?? []
const inset = strokeInset(stroke)
const left = inset
const top = inset
const right = node.width - inset
const bottom = node.height - inset
const radius = Math.max(0, cornerRadius - inset)
r.strokePaint.setColor(r.ck.Color4f(color.r, color.g, color.b, color.a))
r.strokePaint.setStrokeWidth(stroke.weight)
r.strokePaint.setAlphaf(stroke.opacity)
r.strokePaint.setStrokeCap(r.ck.StrokeCap.Butt)
r.strokePaint.setStrokeJoin(getStrokeJoinEntity(r, stroke.join))
r.strokePaint.setPathEffect(null)
canvas.drawArc(
r.ck.LTRBRect(left, top, left + radius * 2, top + radius * 2),
180,
90,
false,
r.strokePaint
)
canvas.drawArc(
r.ck.LTRBRect(right - radius * 2, top, right, top + radius * 2),
270,
90,
false,
r.strokePaint
)
canvas.drawArc(
r.ck.LTRBRect(right - radius * 2, bottom - radius * 2, right, bottom),
0,
90,
false,
r.strokePaint
)
canvas.drawArc(
r.ck.LTRBRect(left, bottom - radius * 2, left + radius * 2, bottom),
90,
90,
false,
r.strokePaint
)
r.strokePaint.setPathEffect(dash.length > 0 ? r.ck.PathEffect.MakeDash(dash, dashPhase) : null)
canvas.drawLine(left + radius, top, right - radius, top, r.strokePaint)
canvas.drawLine(right, top + radius, right, bottom - radius, r.strokePaint)
canvas.drawLine(right - radius, bottom, left + radius, bottom, r.strokePaint)
canvas.drawLine(left, bottom - radius, left, top + radius, r.strokePaint)
r.strokePaint.setPathEffect(null)
}
export function drawStyledRRectStroke(
r: SkiaRenderer,
canvas: Canvas,
rrect: Float32Array,
node: SceneNode,
stroke: Stroke,
color: Color,
dashPhase = 0
): void {
const dash = stroke.dashPattern ?? []
r.strokePaint.setColor(r.ck.Color4f(color.r, color.g, color.b, color.a))
r.strokePaint.setStrokeWidth(stroke.weight)
r.strokePaint.setAlphaf(stroke.opacity)
r.strokePaint.setStrokeCap(getStrokeCapEntity(r, stroke.cap))
r.strokePaint.setStrokeJoin(getStrokeJoinEntity(r, stroke.join))
r.strokePaint.setPathEffect(dash.length > 0 ? r.ck.PathEffect.MakeDash(dash, dashPhase) : null)
r.drawRRectStrokeWithAlign(canvas, rrect, node, stroke)
r.strokePaint.setPathEffect(null)
}
export function drawNodeStroke(
r: SkiaRenderer,
canvas: Canvas,
node: SceneNode,
rect: Float32Array,
hasRadius: boolean
): void {
switch (node.type) {
case 'VECTOR': {
const vps = r.getVectorPaths(node)
if (vps) {
for (const vp of vps) canvas.drawPath(vp, r.strokePaint)
}
break
}
case 'ELLIPSE': {
const fg = r.getFillGeometry(node)
if (fg) {
for (const p of fg) canvas.drawPath(p, r.strokePaint)
} else if (node.arcData) {
r.drawArc(canvas, node, r.strokePaint)
} else {
canvas.drawOval(rect, r.strokePaint)
}
break
}
case 'POLYGON':
case 'STAR': {
const path = r.makePolygonPath(node)
canvas.drawPath(path, r.strokePaint)
path.delete()
break
}
default:
if (nodeHasSmoothCorners(node)) {
const path = makeSmoothRRectPath(r, node)
canvas.drawPath(path, r.strokePaint)
path.delete()
} else if (hasRadius) {
canvas.drawRRect(r.makeRRect(node), r.strokePaint)
} else {
canvas.drawRect(rect, r.strokePaint)
}
}
}
export function drawStrokeWithAlign(
r: SkiaRenderer,
canvas: Canvas,
node: SceneNode,
rect: Float32Array,
hasRadius: boolean,
align: 'INSIDE' | 'CENTER' | 'OUTSIDE'
): void {
if (align === 'INSIDE') {
canvas.save()
r.clipNodeShape(canvas, node, rect, hasRadius)
const origWidth = r.strokePaint.getStrokeWidth()
r.strokePaint.setStrokeWidth(origWidth * 2)
r.drawNodeStroke(canvas, node, rect, hasRadius)
r.strokePaint.setStrokeWidth(origWidth)
canvas.restore()
} else if (align === 'OUTSIDE') {
canvas.save()
const bigRect = r.ck.LTRBRect(-node.width, -node.height, node.width * 2, node.height * 2)
const outerPath = new r.ck.Path()
outerPath.addRect(bigRect)
const innerPath = r.makeNodeShapePath(node, rect, hasRadius)
outerPath.op(innerPath, r.ck.PathOp.Difference)
innerPath.delete()
canvas.clipPath(outerPath, r.ck.ClipOp.Intersect, true)
outerPath.delete()
const origWidth = r.strokePaint.getStrokeWidth()
r.strokePaint.setStrokeWidth(origWidth * 2)
r.drawNodeStroke(canvas, node, rect, hasRadius)
r.strokePaint.setStrokeWidth(origWidth)
canvas.restore()
} else {
r.drawNodeStroke(canvas, node, rect, hasRadius)
}
}
export function drawRRectStrokeWithAlign(
r: SkiaRenderer,
canvas: Canvas,
rrect: Float32Array,
node: SceneNode,
stroke: Stroke
): void {
if (nodeHasSmoothCorners(node)) {
drawStrokeWithAlign(
r,
canvas,
node,
r.ck.LTRBRect(0, 0, node.width, node.height),
true,
stroke.align
)
return
}
if (stroke.align === 'INSIDE') {
canvas.save()
canvas.clipRRect(rrect, r.ck.ClipOp.Intersect, true)
r.strokePaint.setStrokeWidth(stroke.weight * 2)
canvas.drawRRect(rrect, r.strokePaint)
r.strokePaint.setStrokeWidth(stroke.weight)
canvas.restore()
} else if (stroke.align === 'OUTSIDE') {
canvas.save()
const outerPath = new r.ck.Path()
outerPath.addRect(r.ck.LTRBRect(-node.width, -node.height, node.width * 2, node.height * 2))
const innerPath = new r.ck.Path()
innerPath.addRRect(rrect)
outerPath.op(innerPath, r.ck.PathOp.Difference)
innerPath.delete()
canvas.clipPath(outerPath, r.ck.ClipOp.Intersect, true)
outerPath.delete()
r.strokePaint.setStrokeWidth(stroke.weight * 2)
canvas.drawRRect(rrect, r.strokePaint)
r.strokePaint.setStrokeWidth(stroke.weight)
canvas.restore()
} else {
canvas.drawRRect(rrect, r.strokePaint)
}
}
export function drawIndividualSideStrokes(
r: SkiaRenderer,
canvas: Canvas,
node: SceneNode,
align: 'INSIDE' | 'CENTER' | 'OUTSIDE'
): void {
const w = node.width
const h = node.height
const inside = align === 'INSIDE'
const outside = align === 'OUTSIDE'
const tw = node.borderTopWeight
if (tw > 0) {
let y = 0
if (inside) y = tw / 2
else if (outside) y = -tw / 2
r.strokePaint.setStrokeWidth(tw)
canvas.drawLine(0, y, w, y, r.strokePaint)
}
const rw = node.borderRightWeight
if (rw > 0) {
let x = w
if (inside) x = w - rw / 2
else if (outside) x = w + rw / 2
r.strokePaint.setStrokeWidth(rw)
canvas.drawLine(x, 0, x, h, r.strokePaint)
}
const bw = node.borderBottomWeight
if (bw > 0) {
let y = h
if (inside) y = h - bw / 2
else if (outside) y = h + bw / 2
r.strokePaint.setStrokeWidth(bw)
canvas.drawLine(0, y, w, y, r.strokePaint)
}
const lw = node.borderLeftWeight
if (lw > 0) {
let x = 0
if (inside) x = lw / 2
else if (outside) x = -lw / 2
r.strokePaint.setStrokeWidth(lw)
canvas.drawLine(x, 0, x, h, r.strokePaint)
}
}
export function strokeNodeShape(
r: SkiaRenderer,
canvas: Canvas,
node: SceneNode,
paint: Paint
): void {
const rect = r.ck.LTRBRect(0, 0, node.width, node.height)
switch (node.type) {
case 'ELLIPSE':
canvas.drawOval(rect, paint)
return
case 'VECTOR': {
const vps = r.getVectorPaths(node)
if (vps) {
for (const vp of vps) canvas.drawPath(vp, paint)
}
return
}
case 'LINE':
canvas.drawLine(0, 0, node.width, node.height, paint)
return
case 'POLYGON':
case 'STAR': {
const path = r.makePolygonPath(node)
canvas.drawPath(path, paint)
path.delete()
return
}
}
const hasRadius =
node.cornerRadius > 0 ||
(node.independentCorners &&
(node.topLeftRadius > 0 ||
node.topRightRadius > 0 ||
node.bottomRightRadius > 0 ||
node.bottomLeftRadius > 0))
if (hasRadius) {
if (node.independentCorners) {
const rrect = new Float32Array([
0,
0,
node.width,
node.height,
node.topLeftRadius,
node.topLeftRadius,
node.topRightRadius,
node.topRightRadius,
node.bottomRightRadius,
node.bottomRightRadius,
node.bottomLeftRadius,
node.bottomLeftRadius
])
canvas.drawRRect(rrect, paint)
} else {
canvas.drawRRect(r.ck.RRectXY(rect, node.cornerRadius, node.cornerRadius), paint)
}
} else {
canvas.drawRect(rect, paint)
}
}