如何在JavaFX Canvas中用GraphicsContext实现形状内部描边?
在JavaFX Canvas实现内部描边的方案
方案一:利用Shape临时计算偏移路径(无需加入场景图)
虽然你不想将Shape作为Node加入场景,但可以临时创建Shape来计算向内偏移的路径,再提取路径数据到Canvas绘制——全程不用把Shape添加到节点树里,完全避免手动管理节点的麻烦。
具体步骤:
- 将点列表转换为JavaFX的
Path对象 - 使用
Shape.offset()生成向内偏移的路径(偏移距离为描边宽度的一半) - 计算原路径与偏移路径的差值区域(即内部描边的范围)
- 先填充原形状,再填充差值区域模拟内部描边
代码示例(Kotlin):
import javafx.scene.shape.* import javafx.geometry.Point2D import javafx.scene.paint.Paint class MyClass { fun drawInnerStroke(ctx: GraphicsContext, points: List<Point2D>, strokeWidth: Double, fillColor: Paint, strokeColor: Paint) { // 1. 创建原路径 val originalPath = Path().apply { elements.add(MoveTo(points.first().x, points.first().y)) points.drop(1).forEach { elements.add(LineTo(it.x, it.y)) } elements.add(ClosePath()) } // 2. 生成向内偏移的路径(偏移量为负,指向路径内部) val offsetDistance = -strokeWidth / 2.0 val offsetPath = Shape.offset(originalPath, offsetDistance, ArcType.ROUND) // 3. 计算描边区域:原路径减去偏移后的路径 val strokeArea = Shape.subtract(originalPath, offsetPath) // 4. 填充原形状 ctx.fill = fillColor fillPath(ctx, originalPath.elements) // 5. 填充描边区域 ctx.fill = strokeColor if (strokeArea is Path) { fillPath(ctx, strokeArea.elements) } } // 辅助方法:将PathElement列表绘制到Canvas private fun fillPath(ctx: GraphicsContext, elements: List<PathElement>) { ctx.beginPath() elements.forEach { element -> when (element) { is MoveTo -> ctx.moveTo(element.x, element.y) is LineTo -> ctx.lineTo(element.x, element.y) is ClosePath -> ctx.closePath() is QuadCurveTo -> ctx.quadraticCurveTo(element.controlX, element.controlY, element.x, element.y) is CubicCurveTo -> ctx.cubicCurveTo(element.controlX1, element.controlY1, element.controlX2, element.controlY2, element.x, element.y) is ArcTo -> ctx.arcTo(element.radiusX, element.radiusY, element.xAxisRotation, element.x, element.y, element.largeArcFlag, element.sweepFlag) } } ctx.fill() } }
这个方案的优势:
- 不依赖场景节点管理,仅做临时路径计算
- 支持任意复杂路径(只要PathElement能覆盖)
- 性能远优于
clip(),因为是矢量级路径计算而非像素级裁剪
方案二:针对简单形状的手动偏移(圆形、规则多边形)
如果你的形状是规则图形(如圆形、正多边形),可以直接手动计算偏移后的坐标,完全不用依赖Shape类,性能更优:
圆形的内部描边实现
fun drawCircleInnerStroke(ctx: GraphicsContext, centerX: Double, centerY: Double, radius: Double, strokeWidth: Double, fillColor: Paint, strokeColor: Paint) { // 先绘制内部描边的圆环区域 ctx.fill = strokeColor ctx.fillOval(centerX - radius, centerY - radius, radius * 2, radius * 2) ctx.clearRect(centerX - (radius - strokeWidth), centerY - (radius - strokeWidth), (radius - strokeWidth)*2, (radius - strokeWidth)*2) // 填充形状内部区域 ctx.fill = fillColor ctx.fillOval(centerX - (radius - strokeWidth), centerY - (radius - strokeWidth), (radius - strokeWidth)*2, (radius - strokeWidth)*2) }
正多边形的内部描边实现
先实现顶点向内偏移的工具方法,再绘制差值区域:
import kotlin.math.* fun regularPolygonInnerPoints(centerX: Double, centerY: Double, radius: Double, sides: Int, offset: Double): List<Point2D> { val angleStep = 2 * PI / sides return List(sides) { i -> val angle = i * angleStep - PI / 2 val originalX = centerX + radius * cos(angle) val originalY = centerY + radius * sin(angle) // 计算向内偏移的向量 val dx = centerX - originalX val dy = centerY - originalY val length = hypot(dx, dy) val offsetX = originalX + (dx / length) * offset val offsetY = originalY + (dy / length) * offset Point2D(offsetX, offsetY) } } // 使用示例 fun drawRegularPolygonInnerStroke(ctx: GraphicsContext, centerX: Double, centerY: Double, radius: Double, sides: Int, strokeWidth: Double, fillColor: Paint, strokeColor: Paint) { val originalPoints = regularPolygonInnerPoints(centerX, centerY, radius, sides, 0.0) val offsetPoints = regularPolygonInnerPoints(centerX, centerY, radius, sides, strokeWidth) // 填充原多边形 ctx.fill = fillColor ctx.beginPath() originalPoints.forEachIndexed { idx, point -> if (idx == 0) ctx.moveTo(point.x, point.y) else ctx.lineTo(point.x, point.y) } ctx.closePath() ctx.fill() // 填充描边区域(原多边形减去偏移后的多边形) ctx.fill = strokeColor ctx.beginPath() originalPoints.forEachIndexed { idx, point -> if (idx == 0) ctx.moveTo(point.x, point.y) else ctx.lineTo(point.x, point.y) } ctx.closePath() ctx.beginPath() offsetPoints.forEachIndexed { idx, point -> if (idx == 0) ctx.moveTo(point.x, point.y) else ctx.lineTo(point.x, point.y) } ctx.closePath() ctx.fill() }
注意事项
Shape.offset()方法仅在JavaFX 11及以上版本可用,若使用旧版本,建议优先采用方案二处理简单形状,或自行实现路径偏移算法- 动态更新的形状建议缓存计算好的偏移路径,避免重复计算损耗性能
内容的提问来源于stack exchange,提问作者David
相关产品推荐
相关产品推荐

