diff --git a/README.md b/README.md index 8f67d93..ed599d5 100644 --- a/README.md +++ b/README.md @@ -503,6 +503,23 @@ nim r examples/siwin_renderfragments.nim nim r examples/windy_renderfragments.nim ``` +## Drawable operations + +`nkDrawable` nodes support lines, circles, ellipses, rectangles, Bézier curves, and circular arcs. +Ellipse centers and radii use coordinates local to the drawable node: + +```nim +let ellipse = Fig( + kind: nkDrawable, + screenBox: rect(20, 20, 160, 100), + fill: rgba(43, 159, 234, 255), + drawStroke: RenderStroke(weight: 2, fill: rgba(20, 40, 60, 255)), + drawOps: @[ + drawableEllipse(vec2(80, 50), vec2(70, 35)), + ], +) +``` + ## Transform Nodes Use `nkTransform` as a non-drawing container to apply transforms to descendants. diff --git a/src/figdraw/figbackend.nim b/src/figdraw/figbackend.nim index bf4b646..6ba4029 100644 --- a/src/figdraw/figbackend.nim +++ b/src/figdraw/figbackend.nim @@ -50,6 +50,8 @@ type SdfMode* {.pure.} = enum sdfModeBezierStrokeAA = 18 sdfModeBezierStrokeButtAA = 19 sdfModeBezierStrokeSquareAA = 20 + sdfModeEllipseAA = 21 + sdfModeEllipseAnnularAA = 22 func bezierStrokeSdfMode*(cap: StrokeCap): SdfMode = case cap diff --git a/src/figdraw/fignodes.nim b/src/figdraw/fignodes.nim index 2e4fe69..efc522b 100644 --- a/src/figdraw/fignodes.nim +++ b/src/figdraw/fignodes.nim @@ -16,6 +16,7 @@ type dkRectangle dkBezier dkArc + dkEllipse DrawableOp* = object case kind*: DrawableKind @@ -36,6 +37,9 @@ type startAngle*: float32 sweepAngle*: float32 arcSteps*: uint16 + of dkEllipse: + ellipseCenter*: Vec2 + ellipseRadii*: Vec2 RenderList* = object nodes*: seq[Fig] @@ -235,6 +239,13 @@ proc drawableCircle*(center: Vec2, radius: float32): DrawableOp = proc drawableCircle*(x: float32, y: float32, radius: float32): DrawableOp = drawableCircle(vec2(x, y), radius) +proc drawableEllipse*(center, radii: Vec2): DrawableOp = + ## Creates a filled and/or stroked axis-aligned ellipse drawable op. + DrawableOp(kind: dkEllipse, ellipseCenter: center, ellipseRadii: radii) + +proc drawableEllipse*(x, y, radiusX, radiusY: float32): DrawableOp = + drawableEllipse(vec2(x, y), vec2(radiusX, radiusY)) + proc drawableRect*( box: Rect, corners: CornerRadii = [0'u16, 0'u16, 0'u16, 0'u16] ): DrawableOp = @@ -279,27 +290,16 @@ proc drawableArc*( ) proc drawableArc*( - center: Vec2, - radius: float32, - startAngle: float32, - sweepAngle: float32, + center: Vec2, radius: float32, startAngle: float32, sweepAngle: float32 ): DrawableOp = - drawableArc( - center, - radius, - startAngle, - sweepAngle, - 0'u16, - ) + drawableArc(center, radius, startAngle, sweepAngle, 0'u16) proc drawableArc*( x, y, radius, startAngle, sweepAngle: float32, steps: uint16 ): DrawableOp = drawableArc(vec2(x, y), radius, startAngle, sweepAngle, steps) -proc drawableArc*( - x, y, radius, startAngle, sweepAngle: float32 -): DrawableOp = +proc drawableArc*(x, y, radius, startAngle, sweepAngle: float32): DrawableOp = drawableArc(vec2(x, y), radius, startAngle, sweepAngle, 0) proc clear*(list: var RenderList) = @@ -549,4 +549,3 @@ proc contains*(r: Renders, lvl: ZLevel): bool = r.layers.contains(lvl) {.pop.} - diff --git a/src/figdraw/figrender.nim b/src/figdraw/figrender.nim index f583631..bb312bf 100644 --- a/src/figdraw/figrender.nim +++ b/src/figdraw/figrender.nim @@ -1555,6 +1555,96 @@ proc renderDrawableArc( else: ctx.renderDrawableArcSegments(origin, op, stroke, nodeSteps) +when defined(useFigDrawTextures): + func ellipsePoint(center, radii: Vec2, angle: float32): Vec2 = + center + vec2(cos(angle) * radii.x, sin(angle) * radii.y) + + proc renderDrawableEllipseApprox( + ctx: BackendContext, + origin: Vec2, + op: DrawableOp, + radii: Vec2, + fill: Fill, + stroke: RenderStroke, + ) = + let + sweep = PI.float32 * 2.0'f32 + steps = adaptiveArcStepCount(max(radii.x, radii.y), sweep) + center = origin + op.ellipseCenter + + if fillAlphaMax(fill) > 0'u8: + let fillColor = fillCenterColor(fill).rgba() + for step in 0 ..< steps: + let + angle0 = sweep * step.float32 / steps.float32 + angle1 = sweep * (step + 1).float32 / steps.float32 + p0 = origin + ellipsePoint(op.ellipseCenter, radii, angle0) + p1 = origin + ellipsePoint(op.ellipseCenter, radii, angle1) + ctx.drawFilledQuad( + [center.scaled(), p0.scaled(), p1.scaled(), p1.scaled()], + [fillColor, fillColor, fillColor, fillColor], + ) + + if stroke.weight > 0.0'f32 and fillAlphaMax(stroke.fill) > 0'u8: + let + join = stroke.resolveCurveJoin() + joinRadius = max(0.0'f32, stroke.weight) * 0.5'f32 + segmentStroke = stroke.withCap(scButt) + for step in 0 ..< steps: + let + previousAngle = sweep * (step - 1).float32 / steps.float32 + angle = sweep * step.float32 / steps.float32 + nextAngle = sweep * (step + 1).float32 / steps.float32 + previous = ellipsePoint(op.ellipseCenter, radii, previousAngle) + point = ellipsePoint(op.ellipseCenter, radii, angle) + next = ellipsePoint(op.ellipseCenter, radii, nextAngle) + ctx.renderDrawableLine(origin, drawableLine(point, next), segmentStroke) + ctx.renderDrawableStrokeJoin( + origin, point, point - previous, next - point, joinRadius, stroke.fill, join + ) + +proc renderDrawableEllipse( + ctx: BackendContext, origin: Vec2, op: DrawableOp, fill: Fill, stroke: RenderStroke +) = + let radii = vec2(max(0.0'f32, op.ellipseRadii.x), max(0.0'f32, op.ellipseRadii.y)) + if radii.x <= 0.0'f32 or radii.y <= 0.0'f32: + return + + when not defined(useFigDrawTextures): + let + box = rect( + origin.x + op.ellipseCenter.x - radii.x, + origin.y + op.ellipseCenter.y - radii.y, + radii.x * 2.0'f32, + radii.y * 2.0'f32, + ) + .scaled() + corners = zeroCorners().scaledCorners() + + if fillAlphaMax(fill) > 0'u8: + ctx.drawRoundedRectSdf( + rect = box, + fill = fill.toBackendFill(), + radii = corners, + mode = figbackend.SdfMode.sdfModeEllipseAA, + factor = 4.0'f32, + spread = 0.0'f32, + shapeSize = vec2(0.0'f32, 0.0'f32), + ) + + if stroke.weight > 0.0'f32 and fillAlphaMax(stroke.fill) > 0'u8: + ctx.drawRoundedRectSdf( + rect = box, + fill = stroke.fill.toBackendFill(), + radii = corners, + mode = figbackend.SdfMode.sdfModeEllipseAnnularAA, + factor = stroke.weight.scaled(), + spread = 0.0'f32, + shapeSize = vec2(0.0'f32, 0.0'f32), + ) + else: + ctx.renderDrawableEllipseApprox(origin, op, radii, fill, stroke) + proc renderDrawableOps(ctx: BackendContext, node: Fig) = let origin = node.screenBox.xy @@ -1573,6 +1663,8 @@ proc renderDrawableOps(ctx: BackendContext, node: Fig) = ctx.renderDrawableBezier(origin, op, stroke, nodeSteps) of dkArc: ctx.renderDrawableArc(origin, op, stroke, nodeSteps) + of dkEllipse: + ctx.renderDrawableEllipse(origin, op, fill, stroke) proc renderDrawable*(ctx: BackendContext, node: Fig) = if node.drawAa <= 0.0'f32: diff --git a/src/figdraw/metal/metal_context.nim b/src/figdraw/metal/metal_context.nim index 180cf9c..fb0b344 100644 --- a/src/figdraw/metal/metal_context.nim +++ b/src/figdraw/metal/metal_context.nim @@ -1455,7 +1455,10 @@ method drawRoundedRectSdf*( shapeSize: Vec2 = vec2(0.0'f32, 0.0'f32), ) = if fill.kind == figbackend.bfLinear3 and - mode in {sdfModeClipAA, sdfModeAnnular, sdfModeAnnularAA}: + mode in { + sdfModeClipAA, sdfModeAnnular, sdfModeAnnularAA, sdfModeEllipseAA, + sdfModeEllipseAnnularAA, + }: ctx.drawRoundedRectSdfMetal( rect = rect, colors = [fill.lin3Start, fill.lin3Start, fill.lin3Start, fill.lin3Start], diff --git a/src/figdraw/metal/metal_shaders.metal b/src/figdraw/metal/metal_shaders.metal index 5bfa721..abdaff0 100644 --- a/src/figdraw/metal/metal_shaders.metal +++ b/src/figdraw/metal/metal_shaders.metal @@ -74,6 +74,16 @@ float sdRoundedBox(float2 p, float2 b, float4 r) { return min(max(q.x, q.y), 0.0) + length(max(q, float2(0.0))) - rr; } +float sdEllipse(float2 p, float2 radii) { + float2 safeRadii = max(radii, float2(0.000001)); + float k0 = length(p / safeRadii); + if (k0 <= 0.000001) { + return -min(safeRadii.x, safeRadii.y); + } + float k1 = length(p / (safeRadii * safeRadii)); + return k0 * (k0 - 1.0) / max(k1, 0.000001); +} + float dot2(float2 v) { return dot(v, v); } @@ -127,6 +137,10 @@ bool isBezierStrokeMode(int sdfModeInt) { return sdfModeInt == 18 || sdfModeInt == 19 || sdfModeInt == 20; } +bool isEllipseMode(int sdfModeInt) { + return sdfModeInt == 21 || sdfModeInt == 22; +} + float cross2(float2 a, float2 b) { return a.x * b.y - a.y * b.x; } @@ -320,6 +334,8 @@ float4 evalMainFragment( const int sdfModeBezierStrokeAA = 18; const int sdfModeBezierStrokeButtAA = 19; const int sdfModeBezierStrokeSquareAA = 20; + const int sdfModeEllipseAA = 21; + const int sdfModeEllipseAnnularAA = 22; int packedSdfMode = int(sdfMode); int fillMode = packedSdfMode / 256; @@ -336,6 +352,8 @@ float4 evalMainFragment( float dist; if (isBezierStrokeMode(sdfModeInt)) { dist = sdBezier(p, sdfParams.zw, sdfRadii.xy, sdfRadii.zw); + } else if (isEllipseMode(sdfModeInt)) { + dist = sdEllipse(p, shapeHalfExtents); } else { dist = sdRoundedBox(float2(p.x, -p.y), shapeHalfExtents, sdfRadii); } @@ -404,7 +422,8 @@ float4 evalMainFragment( alpha = (sd < 0.0) ? 1.0 : 0.0; break; } - case sdfModeAnnularAA: { + case sdfModeAnnularAA: + case sdfModeEllipseAnnularAA: { float f = sdfFactor * 0.5; float sd = abs(dist + f) - f; float cl = clamp(u.aaFactor * sd + 0.5, 0.0, 1.0); @@ -527,6 +546,7 @@ fragment float4 fs_mask( texture2d atlasTex [[texture(0)]], texture2d maskTex [[texture(1)]]) { const int sdfModeAtlas = 0; + const int sdfModeEllipseAnnularAA = 22; float alpha; int packedSdfMode = int(in.sdfMode); @@ -553,6 +573,12 @@ fragment float4 fs_mask( max(in.sdfFactors.x, 0.0) * 0.5, sdfModeInt ); + } else if (isEllipseMode(sdfModeInt)) { + dist = sdEllipse(p, shapeHalfExtents); + if (sdfModeInt == sdfModeEllipseAnnularAA) { + float halfWidth = max(in.sdfFactors.x, 0.0) * 0.5; + dist = abs(dist + halfWidth) - halfWidth; + } } else { dist = sdRoundedBox(float2(p.x, -p.y), shapeHalfExtents, in.sdfRadii); } diff --git a/src/figdraw/opengl/glcontext.nim b/src/figdraw/opengl/glcontext.nim index 06b2aaa..64faeee 100644 --- a/src/figdraw/opengl/glcontext.nim +++ b/src/figdraw/opengl/glcontext.nim @@ -1526,7 +1526,10 @@ method drawRoundedRectSdf*( shapeSize: Vec2 = vec2(0.0'f32, 0.0'f32), ) = if fill.kind == figbackend.bfLinear3 and - mode in {sdfModeClipAA, sdfModeAnnular, sdfModeAnnularAA}: + mode in { + sdfModeClipAA, sdfModeAnnular, sdfModeAnnularAA, sdfModeEllipseAA, + sdfModeEllipseAnnularAA, + }: ctx.drawRoundedRectSdfOpenGl( rect = rect, colors = [fill.lin3Start, fill.lin3Start, fill.lin3Start, fill.lin3Start], diff --git a/src/figdraw/opengl/glsl/atlas.frag b/src/figdraw/opengl/glsl/atlas.frag index 8851447..cc13029 100644 --- a/src/figdraw/opengl/glsl/atlas.frag +++ b/src/figdraw/opengl/glsl/atlas.frag @@ -37,6 +37,8 @@ const int sdfModeBackdropBlur = 17; const int sdfModeBezierStrokeAA = 18; const int sdfModeBezierStrokeButtAA = 19; const int sdfModeBezierStrokeSquareAA = 20; +const int sdfModeEllipseAA = 21; +const int sdfModeEllipseAnnularAA = 22; float median(float a, float b, float c) { return max(min(a, b), min(max(a, b), c)); @@ -68,6 +70,16 @@ float sdRoundedBox(vec2 p, vec2 b, vec4 r) { return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; } +float sdEllipse(vec2 p, vec2 radii) { + vec2 safeRadii = max(radii, vec2(0.000001)); + float k0 = length(p / safeRadii); + if (k0 <= 0.000001) { + return -min(safeRadii.x, safeRadii.y); + } + float k1 = length(p / (safeRadii * safeRadii)); + return k0 * (k0 - 1.0) / max(k1, 0.000001); +} + float dot2(vec2 v) { return dot(v, v); } @@ -121,6 +133,10 @@ bool isBezierStrokeMode(int sdfModeInt) { ); } +bool isEllipseMode(int sdfModeInt) { + return sdfModeInt == sdfModeEllipseAA || sdfModeInt == sdfModeEllipseAnnularAA; +} + float cross2(vec2 a, vec2 b) { return a.x * b.y - a.y * b.x; } @@ -219,6 +235,8 @@ void main() { float dist; if (isBezierStrokeMode(sdfModeInt)) { dist = sdBezier(p, sdfParams.zw, sdfRadii.xy, sdfRadii.zw); + } else if (isEllipseMode(sdfModeInt)) { + dist = sdEllipse(p, shapeHalfExtents); } else { dist = sdRoundedBox(vec2(p.x, -p.y), shapeHalfExtents, sdfRadii); } @@ -288,7 +306,8 @@ void main() { alpha = (sd < 0.0) ? 1.0 : 0.0; break; } - case sdfModeAnnularAA: { + case sdfModeAnnularAA: + case sdfModeEllipseAnnularAA: { float f = sdfFactor * 0.5; float sd = abs(dist + f) - f; float cl = clamp(aaFactor * sd + 0.5, 0.0, 1.0); diff --git a/src/figdraw/opengl/glsl/atlas_rect_mask.frag b/src/figdraw/opengl/glsl/atlas_rect_mask.frag index 0264266..8f51345 100644 --- a/src/figdraw/opengl/glsl/atlas_rect_mask.frag +++ b/src/figdraw/opengl/glsl/atlas_rect_mask.frag @@ -41,6 +41,8 @@ const int sdfModeBackdropBlur = 17; const int sdfModeBezierStrokeAA = 18; const int sdfModeBezierStrokeButtAA = 19; const int sdfModeBezierStrokeSquareAA = 20; +const int sdfModeEllipseAA = 21; +const int sdfModeEllipseAnnularAA = 22; float median(float a, float b, float c) { return max(min(a, b), min(max(a, b), c)); @@ -72,6 +74,16 @@ float sdRoundedBox(vec2 p, vec2 b, vec4 r) { return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; } +float sdEllipse(vec2 p, vec2 radii) { + vec2 safeRadii = max(radii, vec2(0.000001)); + float k0 = length(p / safeRadii); + if (k0 <= 0.000001) { + return -min(safeRadii.x, safeRadii.y); + } + float k1 = length(p / (safeRadii * safeRadii)); + return k0 * (k0 - 1.0) / max(k1, 0.000001); +} + float dot2(vec2 v) { return dot(v, v); } @@ -125,6 +137,10 @@ bool isBezierStrokeMode(int sdfModeInt) { ); } +bool isEllipseMode(int sdfModeInt) { + return sdfModeInt == sdfModeEllipseAA || sdfModeInt == sdfModeEllipseAnnularAA; +} + float cross2(vec2 a, vec2 b) { return a.x * b.y - a.y * b.x; } @@ -237,6 +253,8 @@ void main() { float dist; if (isBezierStrokeMode(sdfModeInt)) { dist = sdBezier(p, sdfParams.zw, sdfRadii.xy, sdfRadii.zw); + } else if (isEllipseMode(sdfModeInt)) { + dist = sdEllipse(p, shapeHalfExtents); } else { dist = sdRoundedBox(vec2(p.x, -p.y), shapeHalfExtents, sdfRadii); } @@ -306,7 +324,8 @@ void main() { alpha = (sd < 0.0) ? 1.0 : 0.0; break; } - case sdfModeAnnularAA: { + case sdfModeAnnularAA: + case sdfModeEllipseAnnularAA: { float f = sdfFactor * 0.5; float sd = abs(dist + f) - f; float cl = clamp(aaFactor * sd + 0.5, 0.0, 1.0); diff --git a/src/figdraw/opengl/glsl/emscripten/atlas.frag b/src/figdraw/opengl/glsl/emscripten/atlas.frag index 1761eb6..b532f36 100644 --- a/src/figdraw/opengl/glsl/emscripten/atlas.frag +++ b/src/figdraw/opengl/glsl/emscripten/atlas.frag @@ -38,6 +38,8 @@ const int sdfModeBackdropBlur = 17; const int sdfModeBezierStrokeAA = 18; const int sdfModeBezierStrokeButtAA = 19; const int sdfModeBezierStrokeSquareAA = 20; +const int sdfModeEllipseAA = 21; +const int sdfModeEllipseAnnularAA = 22; float median(float a, float b, float c) { return max(min(a, b), min(max(a, b), c)); @@ -70,6 +72,16 @@ float sdRoundedBox(vec2 p, vec2 b, vec4 r) { return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; } +float sdEllipse(vec2 p, vec2 radii) { + vec2 safeRadii = max(radii, vec2(0.000001)); + float k0 = length(p / safeRadii); + if (k0 <= 0.000001) { + return -min(safeRadii.x, safeRadii.y); + } + float k1 = length(p / (safeRadii * safeRadii)); + return k0 * (k0 - 1.0) / max(k1, 0.000001); +} + float dot2(vec2 v) { return dot(v, v); } @@ -123,6 +135,10 @@ bool isBezierStrokeMode(int sdfModeInt) { ); } +bool isEllipseMode(int sdfModeInt) { + return sdfModeInt == sdfModeEllipseAA || sdfModeInt == sdfModeEllipseAnnularAA; +} + float cross2(vec2 a, vec2 b) { return a.x * b.y - a.y * b.x; } @@ -220,6 +236,8 @@ void main() { float dist; if (isBezierStrokeMode(sdfModeInt)) { dist = sdBezier(p, sdfParams.zw, sdfRadii.xy, sdfRadii.zw); + } else if (isEllipseMode(sdfModeInt)) { + dist = sdEllipse(p, shapeHalfExtents); } else { dist = sdRoundedBox(vec2(p.x, -p.y), shapeHalfExtents, sdfRadii); } @@ -283,7 +301,9 @@ void main() { float f = sdfFactor * 0.5; float sd = abs(dist + f) - f; alpha = (sd < 0.0) ? 1.0 : 0.0; - } else if (sdfModeInt == sdfModeAnnularAA) { + } else if ( + sdfModeInt == sdfModeAnnularAA || sdfModeInt == sdfModeEllipseAnnularAA + ) { float f = sdfFactor * 0.5; float sd = abs(dist + f) - f; float cl = clamp(aaFactor * sd + 0.5, 0.0, 1.0); diff --git a/src/figdraw/opengl/glsl/emscripten/atlas_rect_mask.frag b/src/figdraw/opengl/glsl/emscripten/atlas_rect_mask.frag index 96553a5..852b5d8 100644 --- a/src/figdraw/opengl/glsl/emscripten/atlas_rect_mask.frag +++ b/src/figdraw/opengl/glsl/emscripten/atlas_rect_mask.frag @@ -42,6 +42,8 @@ const int sdfModeBackdropBlur = 17; const int sdfModeBezierStrokeAA = 18; const int sdfModeBezierStrokeButtAA = 19; const int sdfModeBezierStrokeSquareAA = 20; +const int sdfModeEllipseAA = 21; +const int sdfModeEllipseAnnularAA = 22; float median(float a, float b, float c) { return max(min(a, b), min(max(a, b), c)); @@ -74,6 +76,16 @@ float sdRoundedBox(vec2 p, vec2 b, vec4 r) { return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; } +float sdEllipse(vec2 p, vec2 radii) { + vec2 safeRadii = max(radii, vec2(0.000001)); + float k0 = length(p / safeRadii); + if (k0 <= 0.000001) { + return -min(safeRadii.x, safeRadii.y); + } + float k1 = length(p / (safeRadii * safeRadii)); + return k0 * (k0 - 1.0) / max(k1, 0.000001); +} + float dot2(vec2 v) { return dot(v, v); } @@ -127,6 +139,10 @@ bool isBezierStrokeMode(int sdfModeInt) { ); } +bool isEllipseMode(int sdfModeInt) { + return sdfModeInt == sdfModeEllipseAA || sdfModeInt == sdfModeEllipseAnnularAA; +} + float cross2(vec2 a, vec2 b) { return a.x * b.y - a.y * b.x; } @@ -238,6 +254,8 @@ void main() { float dist; if (isBezierStrokeMode(sdfModeInt)) { dist = sdBezier(p, sdfParams.zw, sdfRadii.xy, sdfRadii.zw); + } else if (isEllipseMode(sdfModeInt)) { + dist = sdEllipse(p, shapeHalfExtents); } else { dist = sdRoundedBox(vec2(p.x, -p.y), shapeHalfExtents, sdfRadii); } @@ -301,7 +319,9 @@ void main() { float f = sdfFactor * 0.5; float sd = abs(dist + f) - f; alpha = (sd < 0.0) ? 1.0 : 0.0; - } else if (sdfModeInt == sdfModeAnnularAA) { + } else if ( + sdfModeInt == sdfModeAnnularAA || sdfModeInt == sdfModeEllipseAnnularAA + ) { float f = sdfFactor * 0.5; float sd = abs(dist + f) - f; float cl = clamp(aaFactor * sd + 0.5, 0.0, 1.0); diff --git a/src/figdraw/opengl/glsl/emscripten/mask.frag b/src/figdraw/opengl/glsl/emscripten/mask.frag index aec2e32..7b73080 100644 --- a/src/figdraw/opengl/glsl/emscripten/mask.frag +++ b/src/figdraw/opengl/glsl/emscripten/mask.frag @@ -21,6 +21,8 @@ const int sdfModeAtlas = 0; const int sdfModeBezierStrokeAA = 18; const int sdfModeBezierStrokeButtAA = 19; const int sdfModeBezierStrokeSquareAA = 20; +const int sdfModeEllipseAA = 21; +const int sdfModeEllipseAnnularAA = 22; float sdRoundedBox(vec2 p, vec2 b, vec4 r) { float rr; @@ -42,6 +44,16 @@ float sdRoundedBox(vec2 p, vec2 b, vec4 r) { return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; } +float sdEllipse(vec2 p, vec2 radii) { + vec2 safeRadii = max(radii, vec2(0.000001)); + float k0 = length(p / safeRadii); + if (k0 <= 0.000001) { + return -min(safeRadii.x, safeRadii.y); + } + float k1 = length(p / (safeRadii * safeRadii)); + return k0 * (k0 - 1.0) / max(k1, 0.000001); +} + float dot2(vec2 v) { return dot(v, v); } @@ -95,6 +107,10 @@ bool isBezierStrokeMode(int sdfModeInt) { ); } +bool isEllipseMode(int sdfModeInt) { + return sdfModeInt == sdfModeEllipseAA || sdfModeInt == sdfModeEllipseAnnularAA; +} + float cross2(vec2 a, vec2 b) { return a.x * b.y - a.y * b.x; } @@ -162,6 +178,12 @@ void main() { max(sdfFactors.x, 0.0) * 0.5, sdfModeInt ); + } else if (isEllipseMode(sdfModeInt)) { + dist = sdEllipse(p, shapeHalfExtents); + if (sdfModeInt == sdfModeEllipseAnnularAA) { + float halfWidth = max(sdfFactors.x, 0.0) * 0.5; + dist = abs(dist + halfWidth) - halfWidth; + } } else { dist = sdRoundedBox(vec2(p.x, -p.y), shapeHalfExtents, sdfRadii); } diff --git a/src/figdraw/opengl/glsl/mask.frag b/src/figdraw/opengl/glsl/mask.frag index 4413456..e3ee6dc 100644 --- a/src/figdraw/opengl/glsl/mask.frag +++ b/src/figdraw/opengl/glsl/mask.frag @@ -21,6 +21,8 @@ const int sdfModeAtlas = 0; const int sdfModeBezierStrokeAA = 18; const int sdfModeBezierStrokeButtAA = 19; const int sdfModeBezierStrokeSquareAA = 20; +const int sdfModeEllipseAA = 21; +const int sdfModeEllipseAnnularAA = 22; float sdRoundedBox(vec2 p, vec2 b, vec4 r) { float rr; @@ -42,6 +44,16 @@ float sdRoundedBox(vec2 p, vec2 b, vec4 r) { return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; } +float sdEllipse(vec2 p, vec2 radii) { + vec2 safeRadii = max(radii, vec2(0.000001)); + float k0 = length(p / safeRadii); + if (k0 <= 0.000001) { + return -min(safeRadii.x, safeRadii.y); + } + float k1 = length(p / (safeRadii * safeRadii)); + return k0 * (k0 - 1.0) / max(k1, 0.000001); +} + float dot2(vec2 v) { return dot(v, v); } @@ -95,6 +107,10 @@ bool isBezierStrokeMode(int sdfModeInt) { ); } +bool isEllipseMode(int sdfModeInt) { + return sdfModeInt == sdfModeEllipseAA || sdfModeInt == sdfModeEllipseAnnularAA; +} + float cross2(vec2 a, vec2 b) { return a.x * b.y - a.y * b.x; } @@ -162,6 +178,12 @@ void main() { max(sdfFactors.x, 0.0) * 0.5, sdfModeInt ); + } else if (isEllipseMode(sdfModeInt)) { + dist = sdEllipse(p, shapeHalfExtents); + if (sdfModeInt == sdfModeEllipseAnnularAA) { + float halfWidth = max(sdfFactors.x, 0.0) * 0.5; + dist = abs(dist + halfWidth) - halfWidth; + } } else { dist = sdRoundedBox(vec2(p.x, -p.y), shapeHalfExtents, sdfRadii); } diff --git a/src/figdraw/vulkan/shaders/sdf.frag.glsl b/src/figdraw/vulkan/shaders/sdf.frag.glsl index ec9f098..2e59cb3 100644 --- a/src/figdraw/vulkan/shaders/sdf.frag.glsl +++ b/src/figdraw/vulkan/shaders/sdf.frag.glsl @@ -40,6 +40,8 @@ const uint sdfModeBackdropBlur = 17u; const uint sdfModeBezierStrokeAA = 18u; const uint sdfModeBezierStrokeButtAA = 19u; const uint sdfModeBezierStrokeSquareAA = 20u; +const uint sdfModeEllipseAA = 21u; +const uint sdfModeEllipseAnnularAA = 22u; const uint sdfFillModeShift = 256u; float median(float a, float b, float c) { @@ -64,6 +66,16 @@ float sdRoundedBox(vec2 p, vec2 b, vec4 r) { return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; } +float sdEllipse(vec2 p, vec2 radii) { + vec2 safeRadii = max(radii, vec2(0.000001)); + float k0 = length(p / safeRadii); + if (k0 <= 0.000001) { + return -min(safeRadii.x, safeRadii.y); + } + float k1 = length(p / (safeRadii * safeRadii)); + return k0 * (k0 - 1.0) / max(k1, 0.000001); +} + float dot2(vec2 v) { return dot(v, v); } @@ -117,6 +129,10 @@ bool isBezierStrokeMode(uint sdfModeInt) { ); } +bool isEllipseMode(uint sdfModeInt) { + return sdfModeInt == sdfModeEllipseAA || sdfModeInt == sdfModeEllipseAnnularAA; +} + float cross2(vec2 a, vec2 b) { return a.x * b.y - a.y * b.x; } @@ -227,6 +243,8 @@ void main() { float dist; if (isBezierStrokeMode(sdfModeInt)) { dist = sdBezier(p, vSdfParams.zw, vSdfRadii.xy, vSdfRadii.zw); + } else if (isEllipseMode(sdfModeInt)) { + dist = sdEllipse(p, shapeHalfExtents); } else { dist = sdRoundedBox(vec2(p.x, -p.y), shapeHalfExtents, vSdfRadii); } @@ -285,7 +303,8 @@ void main() { alpha = (sd < 0.0) ? 1.0 : 0.0; break; } - case sdfModeAnnularAA: { + case sdfModeAnnularAA: + case sdfModeEllipseAnnularAA: { float f = sdfFactor * 0.5; float sd = abs(dist + f) - f; float cl = clamp(uFS.aaFactor * sd + 0.5, 0.0, 1.0); diff --git a/src/figdraw/vulkan/shaders/sdf.frag.spv b/src/figdraw/vulkan/shaders/sdf.frag.spv index bff0f11..1e138ff 100644 Binary files a/src/figdraw/vulkan/shaders/sdf.frag.spv and b/src/figdraw/vulkan/shaders/sdf.frag.spv differ diff --git a/src/figdraw/vulkan/vulkan_context.nim b/src/figdraw/vulkan/vulkan_context.nim index c240aeb..98c2a86 100644 --- a/src/figdraw/vulkan/vulkan_context.nim +++ b/src/figdraw/vulkan/vulkan_context.nim @@ -1659,11 +1659,12 @@ proc ensureGpuRuntime(ctx: VulkanContext) = vendorId = ctx.driverInfo.vendorId, swapchainProfile = $ctx.activeSwapchainProfile - var queueCreateInfos = @[ - newVkDeviceQueueCreateInfo( - queueFamilyIndex = ctx.queueFamily, queuePriorities = [1.0'f32] - ) - ] + var queueCreateInfos = + @[ + newVkDeviceQueueCreateInfo( + queueFamilyIndex = ctx.queueFamily, queuePriorities = [1.0'f32] + ) + ] if ctx.presentQueueFamily != ctx.queueFamily: queueCreateInfos.add( newVkDeviceQueueCreateInfo( @@ -3007,7 +3008,10 @@ method drawRoundedRectSdf*( shapeSize: Vec2 = vec2(0.0'f32, 0.0'f32), ) = if fill.kind == figbackend.bfLinear3 and - mode in {sdfModeClipAA, sdfModeAnnular, sdfModeAnnularAA}: + mode in { + sdfModeClipAA, sdfModeAnnular, sdfModeAnnularAA, sdfModeEllipseAA, + sdfModeEllipseAnnularAA, + }: ctx.drawRoundedRectSdfVulkan( rect = rect, colors = [fill.lin3Start, fill.lin3Start, fill.lin3Start, fill.lin3Start], diff --git a/tests/trender_extras.nim b/tests/trender_extras.nim index 52e6152..aa4f51a 100644 --- a/tests/trender_extras.nim +++ b/tests/trender_extras.nim @@ -159,6 +159,20 @@ suite "drawable helper render": check curve.controls[2].x.approxEq(24.0'f32) check curve.steps == 8'u16 + test "ellipse math": + let + vectorEllipse = + drawableEllipse(vec2(12.0'f32, 18.0'f32), vec2(24.0'f32, 10.0'f32)) + scalarEllipse = drawableEllipse(12.0'f32, 18.0'f32, 24.0'f32, 10.0'f32) + + check vectorEllipse.kind == dkEllipse + check vectorEllipse.ellipseCenter.x.approxEq(12.0'f32) + check vectorEllipse.ellipseCenter.y.approxEq(18.0'f32) + check vectorEllipse.ellipseRadii.x.approxEq(24.0'f32) + check vectorEllipse.ellipseRadii.y.approxEq(10.0'f32) + check scalarEllipse.ellipseCenter == vectorEllipse.ellipseCenter + check scalarEllipse.ellipseRadii == vectorEllipse.ellipseRadii + test "arc math": let defaultArc = drawableArc(vec2(12.0'f32, 18.0'f32), 24.0'f32, 0.0'f32, 3.1415927'f32) diff --git a/tests/ttransfer.nim b/tests/ttransfer.nim index 0711a5b..38b7027 100644 --- a/tests/ttransfer.nim +++ b/tests/ttransfer.nim @@ -150,6 +150,7 @@ suite "test layers": drawableLine(vec2(1.0'f32, 2.0'f32), vec2(3.0'f32, 4.0'f32)), drawableCircle(vec2(8.0'f32, 9.0'f32), 5.0'f32), drawableArc(vec2(12.0'f32, 13.0'f32), 7.0'f32, 0.0'f32, 1.0'f32), + drawableEllipse(vec2(15.0'f32, 16.0'f32), vec2(9.0'f32, 4.0'f32)), ] let converted = drawable.toRenderFig() @@ -159,11 +160,14 @@ suite "test layers": check converted.drawStroke.fill.color == rgba(200, 40, 70, 255) check converted.drawSteps == 36'u16 check converted.drawAa == 0.85'f32 - check converted.drawOps.len == 3 + check converted.drawOps.len == 4 check converted.drawOps[0].kind == dkLine check converted.drawOps[1].kind == dkCircle check converted.drawOps[2].kind == dkArc check converted.drawOps[2].arcCenter == vec2(12.0'f32, 13.0'f32) + check converted.drawOps[3].kind == dkEllipse + check converted.drawOps[3].ellipseCenter == vec2(15.0'f32, 16.0'f32) + check converted.drawOps[3].ellipseRadii == vec2(9.0'f32, 4.0'f32) test "transfer converts legacy drawable points to rect ops": var legacy = FigTest(kind: nkDrawable) diff --git a/tests/ttransform.nim b/tests/ttransform.nim index 7d3227f..80a10a3 100644 --- a/tests/ttransform.nim +++ b/tests/ttransform.nim @@ -8,6 +8,7 @@ type RecordingBackend = ref object of BackendContext mat: Mat4 mats: seq[Mat4] draws: seq[Rect] + sdfModes: seq[SdfMode] aaFactor: float32 aaChanges: seq[float32] @@ -31,7 +32,7 @@ method drawRoundedRectSdf*( ) = discard colors discard radii - discard mode + ctx.sdfModes.add mode discard factor discard spread discard shapeSize @@ -98,7 +99,12 @@ method setSdfAaFactor*(ctx: RecordingBackend, aaFactor: float32) = proc newRecordingBackend(): RecordingBackend = RecordingBackend( - mat: mat4(), mats: @[], draws: @[], aaFactor: DefaultSdfAaFactor, aaChanges: @[] + mat: mat4(), + mats: @[], + draws: @[], + sdfModes: @[], + aaFactor: DefaultSdfAaFactor, + aaChanges: @[], ) proc renderedDrawableDraws( @@ -338,6 +344,35 @@ suite "nkTransform render behavior": check smallDraws.len > 0 check largeDraws.len > smallDraws.len + test "renders ellipse fill and stroke with ellipse sdf modes": + var renders = Renders(layers: initOrderedTable[ZLevel, RenderList]()) + + discard renders.addRoot( + 0.ZLevel, + Fig( + kind: nkDrawable, + screenBox: rect(5.0'f32, 7.0'f32, 30.0'f32, 20.0'f32), + fill: fill(rgba(20, 40, 80, 255)), + drawStroke: RenderStroke(weight: 2.0'f32, fill: fill(rgba(255, 0, 0, 255))), + drawOps: @[drawableEllipse(vec2(10.0'f32, 8.0'f32), vec2(6.0'f32, 3.0'f32))], + ), + ) + + let ctx = newRecordingBackend() + ctx.renderRoot(renders) + + check ctx.draws.len == 2 + check ctx.sdfModes == @[sdfModeEllipseAA, sdfModeEllipseAnnularAA] + check abs(ctx.draws[0].x - 9.0'f32) < 0.0001'f32 + check abs(ctx.draws[0].y - 12.0'f32) < 0.0001'f32 + check abs(ctx.draws[0].w - 12.0'f32) < 0.0001'f32 + check abs(ctx.draws[0].h - 6.0'f32) < 0.0001'f32 + + test "ignores ellipse drawables with a zero radius": + check renderedDrawableDraws( + drawableEllipse(vec2(10.0'f32, 10.0'f32), vec2(8.0'f32, 0.0'f32)) + ).len == 0 + test "renders explicit bevel joins for decomposed arc drawable": var renders = Renders(layers: initOrderedTable[ZLevel, RenderList]())