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
72 changes: 72 additions & 0 deletions src/silky/contexts.nim
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,78 @@ proc draw9Patch*(
## Queues a 9-patch image draw.
sk.draw9Patch(name, patch, patch, patch, patch, pos, size, color)

proc drawRoundedImage*(
sk: Silky,
name: string,
pos: Vec2,
size: Vec2,
radius: float32,
color = rgbx(255, 255, 255, 255)
) {.measure.} =
## Queues an atlas image draw stretched to size with rounded corners.
## Builds the shape from an inner rect, four edge rects and four
## triangle fans, keeping UVs mapped to the destination rect throughout.
if name notin sk.atlas.entries:
echo "[Warning] Sprite not found in atlas: " & name
return
if size.x <= 0.001 or size.y <= 0.001:
return
let
entry = sk.atlas.entries[name]
uvOrigin = vec2(entry.x.float32, entry.y.float32)
uvScale = vec2(
entry.width.float32 / size.x,
entry.height.float32 / size.y
)
r = clamp(radius, 0.0'f, min(size.x, size.y) / 2)

template uvAt(p: Vec2): Vec2 =
uvOrigin + (p - pos) * uvScale

template piece(piecePos, pieceSize: Vec2) =
if pieceSize.x > 0.001 and pieceSize.y > 0.001:
sk.drawQuad(
piecePos,
pieceSize,
uvAt(piecePos),
pieceSize * uvScale,
color
)

if r < 0.5:
piece(pos, size)
return

# Inner rect plus the four edge rects between the corners.
piece(pos + vec2(r, r), size - vec2(r, r) * 2)
piece(pos + vec2(r, 0), vec2(size.x - r * 2, r))
piece(pos + vec2(r, size.y - r), vec2(size.x - r * 2, r))
piece(pos + vec2(0, r), vec2(r, size.y - r * 2))
piece(pos + vec2(size.x - r, r), vec2(r, size.y - r * 2))

# Quarter-circle fans, one segment per ~2px of arc on screen.
let
arcLength = r * PI.float32 / 2 * max(sk.uiScale, 1.0'f)
segments = max(4, ceil(arcLength / 2).int)
corners = [
(pos + vec2(r, r), PI.float32),
(pos + vec2(size.x - r, r), PI.float32 * 1.5),
(pos + vec2(size.x - r, size.y - r), 0.0'f),
(pos + vec2(r, size.y - r), PI.float32 * 0.5)
]
for (pivot, startAngle) in corners:
var previous = pivot + vec2(cos(startAngle), sin(startAngle)) * r
for i in 1 .. segments:
let
angle = startAngle + PI.float32 / 2 * i.float32 / segments.float32
current = pivot + vec2(cos(angle), sin(angle)) * r
sk.drawTriangle(
[pivot, previous, current],
[uvAt(pivot), uvAt(previous), uvAt(current)],
[color, color, color]
)
previous = current

proc contains*(sk: Silky, name: string): bool =
## Returns true if the atlas contains one image entry.
name in sk.atlas.entries
Expand Down
11 changes: 11 additions & 0 deletions src/silky/semantics.nim
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,17 @@ proc drawTriangle*(
) {.inline.} =
discard

proc drawRoundedImage*(
sk: Silky,
name: string,
pos: Vec2,
size: Vec2,
radius: float32,
color = rgbx(255, 255, 255, 255)
) {.inline.} =
## Stub for drawing an image with rounded corners.
discard

proc draw9Patch*(sk: Silky, name: string, patch: int, pos: Vec2, size: Vec2, color = rgbx(255, 255, 255, 255)) {.inline.} =
## Stub for drawing a 9-patch image.
discard
Expand Down
126 changes: 126 additions & 0 deletions tests/manual_rounded_image.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
## Manual test for drawRoundedImage with adjustable radius sliders.
## Pass --screenshot to render one frame to tmp/rounded_screenshot.png and exit.

import
std/[strformat, os],
opengl, windy, vmath, chroma, pixie,
silky

let builder = newAtlasBuilder(1024, 4)
builder.addDir("tests/data/", "tests/data/")
builder.addFont("tests/data/IBMPlexSans-Regular.ttf", "H1", 32.0)
builder.addFont("tests/data/IBMPlexSans-Regular.ttf", "Default", 18.0)
builder.write("tests/dist/atlas.png")

let window = newWindow(
"Rounded Image Test",
ivec2(900, 700),
vsync = false
)
makeContextCurrent(window)
loadExtensions()

let sk = newSilky(window, "tests/dist/atlas.png")

var
cornerRadius = 32.0f
drawWidth = 300.0f
drawHeight = 200.0f

proc drawFrame() =
sk.beginUI(window, window.size)
sk.clearScreen(rgbx(40, 40, 40, 255))

const
Margin = 20.0f
LabelWidth = 80.0f
SliderWidth = 300.0f

ui:
text "title":
box Margin, Margin, 400, 24
characters "Rounded Image Manual Test"

let refX = Margin + LabelWidth + SliderWidth + Margin
text "original label":
box refX, 55, 200, 24
characters "Original:"
sk.drawImage("debug.9patch", vec2(refX, 80))

group "controls":
box Margin, 55, LabelWidth + SliderWidth, 140
layout TopToBottom
itemSpacing 12

group "width row":
box LabelWidth + SliderWidth, 24
layout LeftToRight
text "width label":
box LabelWidth, 24
characters "Width:"
group "width slider":
box SliderWidth, 24
scrubber("width", drawWidth, 32.0, 600.0, &"{drawWidth:.0f}")

group "height row":
box LabelWidth + SliderWidth, 24
layout LeftToRight
text "height label":
box LabelWidth, 24
characters "Height:"
group "height slider":
box SliderWidth, 24
scrubber("height", drawHeight, 32.0, 600.0, &"{drawHeight:.0f}")

group "radius row":
box LabelWidth + SliderWidth, 24
layout LeftToRight
text "radius label":
box LabelWidth, 24
characters "Radius:"
group "radius slider":
box SliderWidth, 24
scrubber("radius", cornerRadius, 0.0, 300.0, &"{cornerRadius:.0f}")

# Draw the rounded image below the controls.
sk.drawRoundedImage(
"debug.9patch",
vec2(Margin, 220),
vec2(drawWidth, drawHeight),
cornerRadius
)

let ms = sk.avgFrameTime * 1000
text "frame time":
box sk.size.x - 250, 20, 230, 22
characters &"frame time: {ms:>7.3f}ms"

sk.endUi()

window.onFrame = proc() =
drawFrame()
window.swapBuffers()

if "--screenshot" in commandLineParams():
pollEvents()
drawFrame()
let fb = window.size
var pixels = newSeq[uint8](fb.x * fb.y * 4)
glPixelStorei(GL_PACK_ALIGNMENT, 1)
glReadPixels(
0, 0, fb.x, fb.y,
GL_RGBA, GL_UNSIGNED_BYTE,
addr pixels[0]
)
let shot = newImage(fb.x, fb.y)
for y in 0 ..< fb.y.int:
for x in 0 ..< fb.x.int:
let i = ((fb.y.int - 1 - y) * fb.x.int + x) * 4
shot.data[y * fb.x.int + x] =
rgbx(pixels[i], pixels[i + 1], pixels[i + 2], 255)
shot.writeFile("tmp/rounded_screenshot.png")
echo "Wrote tmp/rounded_screenshot.png"
quit(0)

while not window.closeRequested:
pollEvents()
Loading