Use the official Monero mark for the app icon
@@ -1,23 +1,32 @@
|
|||||||
# Branding
|
# Branding
|
||||||
|
|
||||||
`logo.svg` is the source artwork: the Monero monogram in black and white —
|
The app icon is the **official Monero mark**, monochrome.
|
||||||
two outer legs, a central V descending onto a full-width base bar, one uniform
|
|
||||||
stroke width (118 of 1024) so it stays legible at 60 px.
|
|
||||||
|
|
||||||
iOS cannot use SVG for app icons, so the PNGs in
|
`logo-official.svg` is `meta/safari-pinned-tab.svg` from
|
||||||
`Koins/Assets.xcassets/AppIcon.appiconset` are rendered from the same geometry:
|
[monero-project/monero-site](https://github.com/monero-project/monero-site) —
|
||||||
|
the Monero Project's own traced, single-colour version of the logo, which is
|
||||||
|
exactly what a black-and-white icon needs. `logo-official.png` is that file
|
||||||
|
rasterised to 1024×1024:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
qlmanage -t -s 1024 -o /tmp Branding/logo-official.svg
|
||||||
|
```
|
||||||
|
|
||||||
|
iOS cannot use SVG for app icons, so `make-icons.swift` produces the PNG the
|
||||||
|
asset catalog needs — the mark as published (black badge, white M) inset to 90%
|
||||||
|
so the platform's squircle mask does not clip it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd apps/ios
|
cd apps/ios
|
||||||
swift Branding/make-icons.swift Koins/Assets.xcassets/AppIcon.appiconset
|
swift Branding/make-icons.swift Koins/Assets.xcassets/AppIcon.appiconset
|
||||||
```
|
```
|
||||||
|
|
||||||
That writes the 1024×1024 icon (black ground, white mark, no alpha channel —
|
That writes the opaque 1024×1024 app icon (no alpha channel — the App Store
|
||||||
the App Store rejects icons with alpha) plus `Branding/logo-light.png`, the
|
rejects icons with alpha) and `Branding/logo-inverted.png`, the same mark
|
||||||
inverted variant.
|
inverted for dark surfaces (white badge, black M, black ground).
|
||||||
|
|
||||||
The asset catalog uses the single-size format, so Xcode derives every size the
|
The asset catalog uses the single-size format, so Xcode derives every size the
|
||||||
device needs from the 1024.
|
device needs from the 1024.
|
||||||
|
|
||||||
Note: the Monero name and mark are the Monero Project's; check their brand
|
Note: the Monero name and mark belong to the Monero Project — check their brand
|
||||||
guidance before shipping this publicly.
|
guidance before shipping this publicly.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M3370 6894 c-410 -21 -760 -95 -1116 -235 -1129 -445 -1939 -1467
|
||||||
|
-2114 -2666 -69 -479 -37 -963 97 -1436 l36 -127 508 0 509 0 0 1427 0 1428
|
||||||
|
1105 -1105 1105 -1105 1105 1105 1105 1105 0 -1428 0 -1427 509 0 508 0 36
|
||||||
|
127 c134 473 166 957 97 1436 -212 1456 -1350 2616 -2800 2856 -207 34 -511
|
||||||
|
54 -690 45z"/>
|
||||||
|
<path d="M2028 2635 l-3 -900 -713 -5 -713 -5 79 -117 c526 -777 1341 -1305
|
||||||
|
2252 -1457 200 -34 338 -45 570 -45 305 0 533 27 805 93 824 202 1538 700
|
||||||
|
2016 1409 l80 117 -716 3 -715 2 0 903 0 902 -735 -735 -735 -735 -735 735
|
||||||
|
-735 735 -2 -900z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1,17 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024" role="img" aria-label="Monero">
|
|
||||||
<title>Koins — Monero mark (monochrome)</title>
|
|
||||||
<!--
|
|
||||||
Monero monogram: two outer legs, a central V descending onto a full-width
|
|
||||||
base bar. Every part is one uniform stroke width so the mark stays legible
|
|
||||||
when iOS scales it to 60 px.
|
|
||||||
|
|
||||||
Regenerate the PNGs in Koins/Assets.xcassets/AppIcon.appiconset from this
|
|
||||||
geometry (see Branding/README.md).
|
|
||||||
-->
|
|
||||||
<rect width="1024" height="1024" fill="#000000"/>
|
|
||||||
<g fill="none" stroke="#ffffff" stroke-width="118" stroke-linecap="butt" stroke-linejoin="miter">
|
|
||||||
<path d="M 279 758 L 279 280 L 512 640"/>
|
|
||||||
<path d="M 745 758 L 745 280 L 512 640"/>
|
|
||||||
<path d="M 220 699 L 804 699"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 790 B |
@@ -1,81 +1,134 @@
|
|||||||
// Regenerates the app icon PNGs from the same geometry as Branding/logo.svg.
|
// Regenerates the app icon PNGs from the official Monero mark.
|
||||||
//
|
//
|
||||||
// cd apps/ios
|
// cd apps/ios
|
||||||
// swift Branding/make-icons.swift Koins/Assets.xcassets/AppIcon.appiconset
|
// swift Branding/make-icons.swift Koins/Assets.xcassets/AppIcon.appiconset
|
||||||
//
|
//
|
||||||
// iOS cannot use SVG for app icons, so the SVG is the source artwork and this
|
// Input: Branding/logo-official.png — the monochrome mark published in
|
||||||
// script produces the raster the asset catalog needs. It also writes
|
// monero-project/monero-site (meta/safari-pinned-tab.svg), rasterised.
|
||||||
// Branding/logo-light.png (white ground, black mark).
|
// It is a black badge with the M knocked out, drawn on white.
|
||||||
|
// Output: AppIcon-1024.png - the inverse: white badge, black M, on black
|
||||||
|
// (reads well on the app's dark UI)
|
||||||
|
// AppIcon-light-1024.png - the mark as published: black badge, white M,
|
||||||
|
// on white
|
||||||
|
//
|
||||||
|
// The artwork is inset to 90% of the canvas so the iOS squircle mask does not
|
||||||
|
// clip the badge's edges. iOS cannot use SVG for app icons, which is why this
|
||||||
|
// rasterises instead of linking the SVG.
|
||||||
import AppKit
|
import AppKit
|
||||||
import CoreGraphics
|
import CoreGraphics
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
// Draws the same geometry as Branding/logo.svg with CoreGraphics, because iOS
|
struct Bitmap {
|
||||||
// app icons must be raster images.
|
let width: Int
|
||||||
func drawIcon(size: CGFloat, inverted: Bool = false) -> CGImage? {
|
let height: Int
|
||||||
let scale = size / 1024.0
|
var pixels: [UInt8] // RGBA, premultiplied
|
||||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
||||||
guard let context = CGContext(data: nil,
|
|
||||||
width: Int(size),
|
|
||||||
height: Int(size),
|
|
||||||
bitsPerComponent: 8,
|
|
||||||
bytesPerRow: 0,
|
|
||||||
space: colorSpace,
|
|
||||||
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue) else { return nil }
|
|
||||||
|
|
||||||
let background = inverted ? NSColor.white : NSColor.black
|
func luminance(_ offset: Int) -> Int {
|
||||||
let foreground = inverted ? NSColor.black : NSColor.white
|
(Int(pixels[offset]) * 299 + Int(pixels[offset + 1]) * 587 + Int(pixels[offset + 2]) * 114) / 1000
|
||||||
context.setFillColor(background.cgColor)
|
|
||||||
context.fill(CGRect(x: 0, y: 0, width: size, height: size))
|
|
||||||
|
|
||||||
context.setStrokeColor(foreground.cgColor)
|
|
||||||
context.setLineWidth(118 * scale)
|
|
||||||
context.setLineCap(.butt)
|
|
||||||
context.setLineJoin(.miter)
|
|
||||||
|
|
||||||
func point(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
|
|
||||||
// SVG y grows downwards, CoreGraphics grows upwards.
|
|
||||||
CGPoint(x: x * scale, y: size - y * scale)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left leg continuing into the central V (one path, so the top corner is a
|
|
||||||
// clean miter rather than two overlapping strokes).
|
|
||||||
context.beginPath()
|
|
||||||
context.move(to: point(279, 758))
|
|
||||||
context.addLine(to: point(279, 280))
|
|
||||||
context.addLine(to: point(512, 640))
|
|
||||||
context.strokePath()
|
|
||||||
|
|
||||||
// Right leg, mirrored.
|
|
||||||
context.beginPath()
|
|
||||||
context.move(to: point(745, 758))
|
|
||||||
context.addLine(to: point(745, 280))
|
|
||||||
context.addLine(to: point(512, 640))
|
|
||||||
context.strokePath()
|
|
||||||
|
|
||||||
// Full-width bar across the base.
|
|
||||||
context.beginPath()
|
|
||||||
context.move(to: point(220, 699))
|
|
||||||
context.addLine(to: point(804, 699))
|
|
||||||
context.strokePath()
|
|
||||||
|
|
||||||
return context.makeImage()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func write(_ image: CGImage, to path: String) {
|
func loadBitmap(_ path: String) -> Bitmap? {
|
||||||
let rep = NSBitmapImageRep(cgImage: image)
|
guard let image = NSImage(contentsOfFile: path),
|
||||||
guard let data = rep.representation(using: .png, properties: [:]) else { return }
|
let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }
|
||||||
|
let width = cgImage.width
|
||||||
|
let height = cgImage.height
|
||||||
|
var pixels = [UInt8](repeating: 0, count: width * height * 4)
|
||||||
|
guard let context = CGContext(data: &pixels,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bytesPerRow: width * 4,
|
||||||
|
space: CGColorSpaceCreateDeviceRGB(),
|
||||||
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil }
|
||||||
|
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
|
||||||
|
return Bitmap(width: width, height: height, pixels: pixels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeImage(_ bitmap: Bitmap) -> CGImage? {
|
||||||
|
guard let provider = CGDataProvider(data: Data(bitmap.pixels) as CFData) else { return nil }
|
||||||
|
return CGImage(width: bitmap.width,
|
||||||
|
height: bitmap.height,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bitsPerPixel: 32,
|
||||||
|
bytesPerRow: bitmap.width * 4,
|
||||||
|
space: CGColorSpaceCreateDeviceRGB(),
|
||||||
|
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
|
||||||
|
provider: provider,
|
||||||
|
decode: nil,
|
||||||
|
shouldInterpolate: false,
|
||||||
|
intent: .defaultIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func inverted(_ bitmap: Bitmap) -> Bitmap {
|
||||||
|
var pixels = bitmap.pixels
|
||||||
|
for offset in stride(from: 0, to: pixels.count, by: 4) {
|
||||||
|
let value = UInt8(255 - bitmap.luminance(offset))
|
||||||
|
pixels[offset] = value
|
||||||
|
pixels[offset + 1] = value
|
||||||
|
pixels[offset + 2] = value
|
||||||
|
}
|
||||||
|
return Bitmap(width: bitmap.width, height: bitmap.height, pixels: pixels)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Composes the mark onto a square background, inset so the platform mask has
|
||||||
|
/// room, and writes an opaque PNG (App Store icons must not carry alpha).
|
||||||
|
func writeIcon(_ source: Bitmap, size: Int, inset: CGFloat, background: NSColor, to path: String) {
|
||||||
|
guard let image = makeImage(source) else { return }
|
||||||
|
var pixels = [UInt8](repeating: 0, count: size * size * 4)
|
||||||
|
guard let context = CGContext(data: &pixels,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bytesPerRow: size * 4,
|
||||||
|
space: CGColorSpaceCreateDeviceRGB(),
|
||||||
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return }
|
||||||
|
|
||||||
|
context.setFillColor(background.cgColor)
|
||||||
|
context.fill(CGRect(x: 0, y: 0, width: size, height: size))
|
||||||
|
let side = CGFloat(size) * inset
|
||||||
|
let offset = (CGFloat(size) - side) / 2
|
||||||
|
context.interpolationQuality = .high
|
||||||
|
context.draw(image, in: CGRect(x: offset, y: offset, width: side, height: side))
|
||||||
|
|
||||||
|
var rgb = [UInt8](repeating: 0, count: size * size * 3)
|
||||||
|
for index in 0..<(size * size) {
|
||||||
|
rgb[index * 3] = pixels[index * 4]
|
||||||
|
rgb[index * 3 + 1] = pixels[index * 4 + 1]
|
||||||
|
rgb[index * 3 + 2] = pixels[index * 4 + 2]
|
||||||
|
}
|
||||||
|
guard let provider = CGDataProvider(data: Data(rgb) as CFData),
|
||||||
|
let flattened = CGImage(width: size,
|
||||||
|
height: size,
|
||||||
|
bitsPerComponent: 8,
|
||||||
|
bitsPerPixel: 24,
|
||||||
|
bytesPerRow: size * 3,
|
||||||
|
space: CGColorSpaceCreateDeviceRGB(),
|
||||||
|
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue),
|
||||||
|
provider: provider,
|
||||||
|
decode: nil,
|
||||||
|
shouldInterpolate: false,
|
||||||
|
intent: .defaultIntent) else { return }
|
||||||
|
let representation = NSBitmapImageRep(cgImage: flattened)
|
||||||
|
guard let data = representation.representation(using: .png, properties: [:]) else { return }
|
||||||
try? data.write(to: URL(fileURLWithPath: path))
|
try? data.write(to: URL(fileURLWithPath: path))
|
||||||
print("wrote \(path)")
|
print("wrote \(path)")
|
||||||
}
|
}
|
||||||
|
|
||||||
let outputDir = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "/tmp"
|
let arguments = CommandLine.arguments
|
||||||
try? FileManager.default.createDirectory(atPath: outputDir, withIntermediateDirectories: true)
|
let outputDirectory = arguments.count > 1 ? arguments[1] : "/tmp"
|
||||||
|
let sourcePath = arguments.count > 2 ? arguments[2] : "Branding/logo-official.png"
|
||||||
|
try? FileManager.default.createDirectory(atPath: outputDirectory, withIntermediateDirectories: true)
|
||||||
|
|
||||||
if let icon = drawIcon(size: 1024) {
|
guard let source = loadBitmap(sourcePath) else {
|
||||||
write(icon, to: "\(outputDir)/AppIcon-1024.png")
|
print("cannot read source artwork at \(sourcePath)")
|
||||||
}
|
exit(1)
|
||||||
// A light variant, handy for other surfaces (white background, black mark).
|
|
||||||
if let light = drawIcon(size: 1024, inverted: true) {
|
|
||||||
write(light, to: "\(outputDir)/AppIcon-light-1024.png")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let inset: CGFloat = 0.9
|
||||||
|
// Primary: the mark exactly as Monero publishes it (black badge, white M).
|
||||||
|
writeIcon(source, size: 1024, inset: inset, background: .white,
|
||||||
|
to: "\(outputDirectory)/AppIcon-1024.png")
|
||||||
|
// Alternate: inverted, for dark surfaces (white badge, black M, black ground).
|
||||||
|
writeIcon(inverted(source), size: 1024, inset: inset, background: .black,
|
||||||
|
to: "\(outputDirectory)/AppIcon-inverted-1024.png")
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 54 KiB |