Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/figdraw/figbackend.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 14 additions & 15 deletions src/figdraw/fignodes.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type
dkRectangle
dkBezier
dkArc
dkEllipse

DrawableOp* = object
case kind*: DrawableKind
Expand All @@ -36,6 +37,9 @@ type
startAngle*: float32
sweepAngle*: float32
arcSteps*: uint16
of dkEllipse:
ellipseCenter*: Vec2
ellipseRadii*: Vec2

RenderList* = object
nodes*: seq[Fig]
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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) =
Expand Down Expand Up @@ -549,4 +549,3 @@ proc contains*(r: Renders, lvl: ZLevel): bool =
r.layers.contains(lvl)

{.pop.}

92 changes: 92 additions & 0 deletions src/figdraw/figrender.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion src/figdraw/metal/metal_context.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
28 changes: 27 additions & 1 deletion src/figdraw/metal/metal_shaders.metal
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -527,6 +546,7 @@ fragment float4 fs_mask(
texture2d<float> atlasTex [[texture(0)]],
texture2d<float> maskTex [[texture(1)]]) {
const int sdfModeAtlas = 0;
const int sdfModeEllipseAnnularAA = 22;

float alpha;
int packedSdfMode = int(in.sdfMode);
Expand All @@ -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);
}
Expand Down
5 changes: 4 additions & 1 deletion src/figdraw/opengl/glcontext.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
21 changes: 20 additions & 1 deletion src/figdraw/opengl/glsl/atlas.frag
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading