fix(desktop): match Apple HIG squircle margins so dock icon isn't oversized

Previous impl filled the entire 1024x1024 canvas with the squircle.
Apple's macOS app icon template leaves ~100px of transparent margin
around a centered 824x824 squircle (content area is 80.5% of the
canvas width) — which is the size the dock normalizes to when laying
out next to first-party apps. Filling the whole canvas made Amethyst
render ~20% larger than its neighbors in the dock and Cmd+Tab.

Now matches Apple's reference geometry:
  - Canvas: 1024x1024 (transparent margins of 100px)
  - Squircle: 824x824 centered
  - Corner radius: ~185px (22.45% of the squircle, per HIG)
  - Mark: 10% padding inside the squircle

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
This commit is contained in:
Claude
2026-04-24 13:59:29 +00:00
parent 979c262d87
commit be8fef15db
@@ -51,38 +51,47 @@ object PlatformAppIcon {
} }
/** /**
* Renders [source] inside a 1024x1024 white squircle with padding around * Renders [source] inside a white squircle following Apple's macOS app icon
* the mark. Matches Apple's HIG macOS app icon template (Big Sur+). * template (Big Sur+). The squircle fills ~80.5% of the canvas width
* (824/1024 per Apple's HIG) — the rest is transparent margin, matching
* the size at which first-party dock icons render so ours doesn't appear
* oversized next to them.
* *
* Implementation notes: * Implementation notes:
* - Apple's "squircle" is a superellipse, not a standard rounded rectangle; * - Apple's "squircle" is a superellipse, not a standard rounded rectangle;
* Java2D doesn't ship a superellipse primitive, so [RoundRectangle2D] * Java2D doesn't ship a superellipse primitive, so [RoundRectangle2D]
* with a 22.5% corner radius is the practical approximation used by most * with a ~22.37% corner radius (185/824) is the practical approximation
* third-party tooling. At dock-icon sizes the difference is invisible. * used by most third-party tooling. At dock-icon sizes the difference
* - 10% padding on each side keeps the mark from touching the squircle * is invisible.
* corners, matching Apple's template content area (824/1024 ≈ 80%). * - The mark is padded ~10% inside the squircle so it doesn't touch the
* rounded corners.
*/ */
private fun macOsSquircle(source: BufferedImage): BufferedImage { private fun macOsSquircle(source: BufferedImage): BufferedImage {
val size = 1024 val canvas = 1024
val cornerDiameter = (size * 0.45f) // diameter = 2 * radius; radius = 22.5% // Apple's reference template: 824x824 squircle centered in a 1024x1024
val padding = (size * 0.10f).toInt() // canvas (100px transparent margin each side).
val innerSize = size - 2 * padding val squircleSize = 824
val squircleMargin = (canvas - squircleSize) / 2
// Apple's reference corner radius on the 824-box is ~185px (≈22.45%).
val cornerDiameter = (squircleSize * 0.4490f)
// Mark padding inside the squircle: 10% of the squircle size.
val markPadding = (squircleSize * 0.10f).toInt()
val markOrigin = squircleMargin + markPadding
val markSize = squircleSize - 2 * markPadding
val out = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB) val out = BufferedImage(canvas, canvas, BufferedImage.TYPE_INT_ARGB)
val g = out.createGraphics() val g = out.createGraphics()
try { try {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC) g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY) g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
// Paint the squircle background in white. Clip must be set BEFORE
// the fill so the fill respects the rounded corners.
val squircle = val squircle =
RoundRectangle2D.Float( RoundRectangle2D.Float(
0f, squircleMargin.toFloat(),
0f, squircleMargin.toFloat(),
size.toFloat(), squircleSize.toFloat(),
size.toFloat(), squircleSize.toFloat(),
cornerDiameter, cornerDiameter,
cornerDiameter, cornerDiameter,
) )
@@ -92,8 +101,8 @@ object PlatformAppIcon {
// Composite the source logo on top, scaled into the padded area. // Composite the source logo on top, scaled into the padded area.
g.composite = AlphaComposite.SrcOver g.composite = AlphaComposite.SrcOver
g.clip = squircle // keep the logo within the squircle on overhang g.clip = squircle
g.drawImage(source, padding, padding, innerSize, innerSize, null) g.drawImage(source, markOrigin, markOrigin, markSize, markSize, null)
} finally { } finally {
g.dispose() g.dispose()
} }