You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在JavaFX Canvas中用GraphicsContext实现形状内部描边?

在JavaFX Canvas实现内部描边的方案

方案一:利用Shape临时计算偏移路径(无需加入场景图)

虽然你不想将Shape作为Node加入场景,但可以临时创建Shape来计算向内偏移的路径,再提取路径数据到Canvas绘制——全程不用把Shape添加到节点树里,完全避免手动管理节点的麻烦。

具体步骤:

  1. 将点列表转换为JavaFX的Path对象
  2. 使用Shape.offset()生成向内偏移的路径(偏移距离为描边宽度的一半)
  3. 计算原路径与偏移路径的差值区域(即内部描边的范围)
  4. 先填充原形状,再填充差值区域模拟内部描边

代码示例(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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 20:05:26