diff --git a/src/silky/contexts.nim b/src/silky/contexts.nim index 402c777..0d623a3 100644 --- a/src/silky/contexts.nim +++ b/src/silky/contexts.nim @@ -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 diff --git a/src/silky/semantics.nim b/src/silky/semantics.nim index 36b0de5..b05e721 100644 --- a/src/silky/semantics.nim +++ b/src/silky/semantics.nim @@ -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 diff --git a/tests/manual_rounded_image.nim b/tests/manual_rounded_image.nim new file mode 100644 index 0000000..3bd7d76 --- /dev/null +++ b/tests/manual_rounded_image.nim @@ -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()