Module: Everywhere::Icon
- Defined in:
- lib/everywhere/icon.rb
Overview
Turns one source PNG into platform-native app icons — no Tauri, no sips, no iconutil, no ImageMagick. Pure Ruby, so it runs the same locally and on the cloud build runners.
The important part is macОS shaping. macOS renders an .icns literally: it does not round the corners or add margins the way iOS does. A full-bleed square PNG therefore shows up as a hard square, out of place next to every native app. So we bake the platform's look into the artwork ourselves — inset the icon to the Big Sur grid (~824px inside a 1024px canvas) and clip it to a superellipse ("squircle") with transparent corners.
Windows (.ico) and Linux (hicolor PNGs) don't get the squircle — those desktops present square icons — so they're generated straight from the full-bleed source.
Constant Summary collapse
- CANVAS =
Big Sur icon-grid geometry, all as fractions of the 1024px canvas so it scales to any master size.
1024- INSET =
icon body is 824/1024 of the canvas
0.8047- CORNER_RATIO =
corner radius as a fraction of the body's side
0.2237- EXPONENT =
superellipse power — the iOS/macOS "squircle" corner
5.0- ICNS_TYPES =
.icns type codes -> pixel size. Mirrors what
iconutilemits from a standard .iconset (retina variants included), so the result is indistinguishable from an Apple-toolchain icns. [ ["icp4", 16], ["icp5", 32], ["ic07", 128], ["ic08", 256], ["ic09", 512], ["ic11", 32], ["ic12", 64], ["ic13", 256], ["ic14", 512], ["ic10", 1024] ].freeze
- ICO_SIZES =
[16, 32, 48, 64, 128, 256].freeze
- LINUX_SIZES =
[16, 32, 48, 64, 128, 256, 512].freeze
Class Method Summary collapse
-
.macos_master(source, dest, canvas: CANVAS) ⇒ Object
Write a macOS-shaped 1024px RGBA master PNG (padding + squircle) from a source PNG.
-
.sized_png(master, size) ⇒ Object
PNG bytes of
masteratsizexsize(no-op resample when already sized). -
.squircle_coverage(x, y, centre, half, straight, radius, exponent) ⇒ Object
Coverage (0.0..1.0) of pixel (x, y) by the squircle: a rounded rectangle with continuous (superelliptical) corners.
-
.squircle_inside?(sx, sy, centre, half, straight, radius, exponent) ⇒ Boolean
Is the continuous point (sx, sy) inside the squircle? Straight edges up to
straightfrom centre, superelliptical corners ofradiusbeyond that. -
.write_icns(master_png, dest) ⇒ Object
Build a macOS .icns from an already-shaped master PNG (ideally 1024px).
-
.write_ico(source, dest, sizes: ICO_SIZES) ⇒ Object
Build a Windows .ico (PNG-compressed entries, Vista+) from a full-bleed source PNG.
-
.write_png_set(source, dest_dir, sizes: LINUX_SIZES) ⇒ Object
Write a set of square PNGs (Linux/desktop use) named "
x .png" into dest_dir from a full-bleed source.
Class Method Details
.macos_master(source, dest, canvas: CANVAS) ⇒ Object
Write a macOS-shaped 1024px RGBA master PNG (padding + squircle) from a source PNG. Returns true, or false if the source PNG can't be read.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/everywhere/icon.rb', line 44 def macos_master(source, dest, canvas: CANVAS) raster = Raster.decode_png(source) or return false body = (canvas * INSET).round offset = (canvas - body) / 2 # Fit the artwork inside the body square preserving aspect ratio (square # sources fill it exactly; non-square ones are centred, not stretched). fit = [body.to_f / raster.width, body.to_f / raster.height].min sw = (raster.width * fit).round.clamp(1, body) sh = (raster.height * fit).round.clamp(1, body) scaled = raster.resample(sw, sh) out = Raster.transparent(canvas, canvas) out.paste!(scaled, offset + (body - sw) / 2, offset + (body - sh) / 2) centre = canvas / 2.0 half = body / 2.0 radius = [body * CORNER_RATIO, half].min straight = half - radius # half-extent of the straight edges out.mask_region!(offset, offset, offset + body, offset + body) do |x, y| squircle_coverage(x, y, centre, half, straight, radius, EXPONENT) end out.write_png(dest) true end |
.sized_png(master, size) ⇒ Object
PNG bytes of master at sizexsize (no-op resample when already sized).
121 122 123 |
# File 'lib/everywhere/icon.rb', line 121 def sized_png(master, size) (size == master.width && size == master.height ? master : master.resample(size, size)).encode_png end |
.squircle_coverage(x, y, centre, half, straight, radius, exponent) ⇒ Object
Coverage (0.0..1.0) of pixel (x, y) by the squircle: a rounded rectangle with continuous (superelliptical) corners. Interior pixels return 1.0, exterior 0.0, and boundary pixels are antialiased by 4x4 supersampling.
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/everywhere/icon.rb', line 128 def squircle_coverage(x, y, centre, half, straight, radius, exponent) c0 = squircle_inside?(x, y, centre, half, straight, radius, exponent) c1 = squircle_inside?(x + 1, y, centre, half, straight, radius, exponent) c2 = squircle_inside?(x, y + 1, centre, half, straight, radius, exponent) c3 = squircle_inside?(x + 1, y + 1, centre, half, straight, radius, exponent) return 1.0 if c0 && c1 && c2 && c3 return 0.0 unless c0 || c1 || c2 || c3 hit = 0 4.times do |i| 4.times do |j| hit += 1 if squircle_inside?(x + (i + 0.5) / 4.0, y + (j + 0.5) / 4.0, centre, half, straight, radius, exponent) end end hit / 16.0 end |
.squircle_inside?(sx, sy, centre, half, straight, radius, exponent) ⇒ Boolean
Is the continuous point (sx, sy) inside the squircle? Straight edges up to
straight from centre, superelliptical corners of radius beyond that.
148 149 150 151 152 153 154 155 156 157 |
# File 'lib/everywhere/icon.rb', line 148 def squircle_inside?(sx, sy, centre, half, straight, radius, exponent) u = (sx - centre).abs v = (sy - centre).abs return false if u > half || v > half return true if u <= straight || v <= straight du = (u - straight) / radius dv = (v - straight) / radius (du**exponent) + (dv**exponent) <= 1.0 end |
.write_icns(master_png, dest) ⇒ Object
Build a macOS .icns from an already-shaped master PNG (ideally 1024px). Returns true on success.
74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/everywhere/icon.rb', line 74 def write_icns(master_png, dest) master = Raster.decode_png(master_png) or return false body = ICNS_TYPES.map do |type, size| png = sized_png(master, size) type.b + [png.bytesize + 8].pack("N") + png end.join File.binwrite(dest, "icns".b + [body.bytesize + 8].pack("N") + body) true end |
.write_ico(source, dest, sizes: ICO_SIZES) ⇒ Object
Build a Windows .ico (PNG-compressed entries, Vista+) from a full-bleed source PNG. Returns true on success.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/everywhere/icon.rb', line 88 def write_ico(source, dest, sizes: ICO_SIZES) master = Raster.decode_png(source) or return false images = sizes.map { |s| [s, sized_png(master, s)] } offset = 6 + images.size * 16 entries = +"".b blob = +"".b images.each do |size, png| dim = size >= 256 ? 0 : size # 0 means 256 in the ICONDIRENTRY entries << [dim, dim, 0, 0, 1, 32, png.bytesize, offset].pack("CCCCvvVV") blob << png offset += png.bytesize end header = [0, 1, images.size].pack("vvv") # reserved, type=icon, count File.binwrite(dest, header + entries + blob) true end |
.write_png_set(source, dest_dir, sizes: LINUX_SIZES) ⇒ Object
Write a set of square PNGs (Linux/desktop use) named "
109 110 111 112 113 114 115 116 117 118 |
# File 'lib/everywhere/icon.rb', line 109 def write_png_set(source, dest_dir, sizes: LINUX_SIZES) master = Raster.decode_png(source) or return [] FileUtils.mkdir_p(dest_dir) sizes.map do |size| path = File.join(dest_dir, "#{size}x#{size}.png") File.binwrite(path, sized_png(master, size)) path end end |