mac icon: render 36px retina backing
This commit is contained in:
@@ -31,7 +31,7 @@ struct CritterStatusLabel: View {
|
|||||||
Group {
|
Group {
|
||||||
if self.isPaused {
|
if self.isPaused {
|
||||||
Image(nsImage: CritterIconRenderer.makeIcon(blink: 0))
|
Image(nsImage: CritterIconRenderer.makeIcon(blink: 0))
|
||||||
.frame(width: 18, height: 16)
|
.frame(width: 18, height: 18)
|
||||||
} else {
|
} else {
|
||||||
Image(nsImage: CritterIconRenderer.makeIcon(
|
Image(nsImage: CritterIconRenderer.makeIcon(
|
||||||
blink: self.blinkAmount,
|
blink: self.blinkAmount,
|
||||||
@@ -39,7 +39,7 @@ struct CritterStatusLabel: View {
|
|||||||
earWiggle: self.earWiggle,
|
earWiggle: self.earWiggle,
|
||||||
earScale: self.earBoostActive ? 1.9 : 1.0,
|
earScale: self.earBoostActive ? 1.9 : 1.0,
|
||||||
earHoles: self.earBoostActive))
|
earHoles: self.earBoostActive))
|
||||||
.frame(width: 18, height: 16)
|
.frame(width: 18, height: 18)
|
||||||
.rotationEffect(.degrees(self.wiggleAngle), anchor: .center)
|
.rotationEffect(.degrees(self.wiggleAngle), anchor: .center)
|
||||||
.offset(x: self.wiggleOffset)
|
.offset(x: self.wiggleOffset)
|
||||||
.onReceive(self.ticker) { now in
|
.onReceive(self.ticker) { now in
|
||||||
@@ -211,7 +211,7 @@ struct CritterStatusLabel: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum CritterIconRenderer {
|
enum CritterIconRenderer {
|
||||||
private static let size = NSSize(width: 18, height: 16)
|
private static let size = NSSize(width: 18, height: 18)
|
||||||
|
|
||||||
static func makeIcon(
|
static func makeIcon(
|
||||||
blink: CGFloat,
|
blink: CGFloat,
|
||||||
@@ -220,124 +220,157 @@ enum CritterIconRenderer {
|
|||||||
earScale: CGFloat = 1,
|
earScale: CGFloat = 1,
|
||||||
earHoles: Bool = false) -> NSImage
|
earHoles: Bool = false) -> NSImage
|
||||||
{
|
{
|
||||||
|
// Force a 36×36px backing store (2× for the 18pt logical canvas) so the menu bar icon stays crisp on Retina.
|
||||||
|
let pixelsWide = 36
|
||||||
|
let pixelsHigh = 36
|
||||||
|
guard let rep = NSBitmapImageRep(
|
||||||
|
bitmapDataPlanes: nil,
|
||||||
|
pixelsWide: pixelsWide,
|
||||||
|
pixelsHigh: pixelsHigh,
|
||||||
|
bitsPerSample: 8,
|
||||||
|
samplesPerPixel: 4,
|
||||||
|
hasAlpha: true,
|
||||||
|
isPlanar: false,
|
||||||
|
colorSpaceName: .deviceRGB,
|
||||||
|
bitmapFormat: [],
|
||||||
|
bytesPerRow: 0,
|
||||||
|
bitsPerPixel: 0
|
||||||
|
) else {
|
||||||
|
return NSImage(size: size)
|
||||||
|
}
|
||||||
|
rep.size = size
|
||||||
|
|
||||||
|
NSGraphicsContext.saveGraphicsState()
|
||||||
|
if let context = NSGraphicsContext(bitmapImageRep: rep) {
|
||||||
|
NSGraphicsContext.current = context
|
||||||
|
defer { NSGraphicsContext.restoreGraphicsState() }
|
||||||
|
|
||||||
|
let w = size.width
|
||||||
|
let h = size.height
|
||||||
|
|
||||||
|
let bodyW = w * 0.78
|
||||||
|
let bodyH = h * 0.58
|
||||||
|
let bodyX = (w - bodyW) / 2
|
||||||
|
let bodyY = h * 0.36
|
||||||
|
let bodyCorner = w * 0.09
|
||||||
|
|
||||||
|
let earW = w * 0.22
|
||||||
|
let earH = bodyH * 0.54 * earScale * (1 - 0.08 * abs(earWiggle))
|
||||||
|
let earCorner = earW * 0.24
|
||||||
|
let leftEarRect = CGRect(
|
||||||
|
x: bodyX - earW * 0.55 + earWiggle,
|
||||||
|
y: bodyY + bodyH * 0.08 + earWiggle * 0.4,
|
||||||
|
width: earW,
|
||||||
|
height: earH)
|
||||||
|
let rightEarRect = CGRect(
|
||||||
|
x: bodyX + bodyW - earW * 0.45 - earWiggle,
|
||||||
|
y: bodyY + bodyH * 0.08 - earWiggle * 0.4,
|
||||||
|
width: earW,
|
||||||
|
height: earH)
|
||||||
|
|
||||||
|
let legW = w * 0.11
|
||||||
|
let legH = h * 0.26
|
||||||
|
let legSpacing = w * 0.085
|
||||||
|
let legsWidth = 4 * legW + 3 * legSpacing
|
||||||
|
let legStartX = (w - legsWidth) / 2
|
||||||
|
let legLift = legH * 0.35 * legWiggle
|
||||||
|
let legYBase = bodyY - legH + h * 0.05
|
||||||
|
|
||||||
|
let eyeOpen = max(0.05, 1 - blink)
|
||||||
|
let eyeW = bodyW * 0.2
|
||||||
|
let eyeH = bodyH * 0.26 * eyeOpen
|
||||||
|
let eyeY = bodyY + bodyH * 0.56
|
||||||
|
let eyeOffset = bodyW * 0.24
|
||||||
|
|
||||||
|
context.cgContext.setFillColor(NSColor.labelColor.cgColor)
|
||||||
|
|
||||||
|
context.cgContext.addPath(CGPath(
|
||||||
|
roundedRect: CGRect(x: bodyX, y: bodyY, width: bodyW, height: bodyH),
|
||||||
|
cornerWidth: bodyCorner,
|
||||||
|
cornerHeight: bodyCorner,
|
||||||
|
transform: nil))
|
||||||
|
context.cgContext.addPath(CGPath(
|
||||||
|
roundedRect: leftEarRect,
|
||||||
|
cornerWidth: earCorner,
|
||||||
|
cornerHeight: earCorner,
|
||||||
|
transform: nil))
|
||||||
|
context.cgContext.addPath(CGPath(
|
||||||
|
roundedRect: rightEarRect,
|
||||||
|
cornerWidth: earCorner,
|
||||||
|
cornerHeight: earCorner,
|
||||||
|
transform: nil))
|
||||||
|
for i in 0..<4 {
|
||||||
|
let x = legStartX + CGFloat(i) * (legW + legSpacing)
|
||||||
|
let lift = (i % 2 == 0 ? legLift : -legLift)
|
||||||
|
let rect = CGRect(
|
||||||
|
x: x,
|
||||||
|
y: legYBase + lift,
|
||||||
|
width: legW,
|
||||||
|
height: legH * (1 - 0.12 * legWiggle))
|
||||||
|
context.cgContext.addPath(CGPath(
|
||||||
|
roundedRect: rect,
|
||||||
|
cornerWidth: legW * 0.34,
|
||||||
|
cornerHeight: legW * 0.34,
|
||||||
|
transform: nil))
|
||||||
|
}
|
||||||
|
context.cgContext.fillPath()
|
||||||
|
|
||||||
|
context.cgContext.saveGState()
|
||||||
|
context.cgContext.setBlendMode(CGBlendMode.clear)
|
||||||
|
|
||||||
|
let leftCenter = CGPoint(x: w / 2 - eyeOffset, y: eyeY)
|
||||||
|
let rightCenter = CGPoint(x: w / 2 + eyeOffset, y: eyeY)
|
||||||
|
|
||||||
|
if earHoles || earScale > 1.05 {
|
||||||
|
let holeW = earW * 0.6
|
||||||
|
let holeH = earH * 0.46
|
||||||
|
let holeCorner = holeW * 0.34
|
||||||
|
let leftHoleRect = CGRect(
|
||||||
|
x: leftEarRect.midX - holeW / 2,
|
||||||
|
y: leftEarRect.midY - holeH / 2 + earH * 0.04,
|
||||||
|
width: holeW,
|
||||||
|
height: holeH)
|
||||||
|
let rightHoleRect = CGRect(
|
||||||
|
x: rightEarRect.midX - holeW / 2,
|
||||||
|
y: rightEarRect.midY - holeH / 2 + earH * 0.04,
|
||||||
|
width: holeW,
|
||||||
|
height: holeH)
|
||||||
|
|
||||||
|
context.cgContext.addPath(CGPath(
|
||||||
|
roundedRect: leftHoleRect,
|
||||||
|
cornerWidth: holeCorner,
|
||||||
|
cornerHeight: holeCorner,
|
||||||
|
transform: nil))
|
||||||
|
context.cgContext.addPath(CGPath(
|
||||||
|
roundedRect: rightHoleRect,
|
||||||
|
cornerWidth: holeCorner,
|
||||||
|
cornerHeight: holeCorner,
|
||||||
|
transform: nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
let left = CGMutablePath()
|
||||||
|
left.move(to: CGPoint(x: leftCenter.x - eyeW / 2, y: leftCenter.y - eyeH))
|
||||||
|
left.addLine(to: CGPoint(x: leftCenter.x + eyeW / 2, y: leftCenter.y))
|
||||||
|
left.addLine(to: CGPoint(x: leftCenter.x - eyeW / 2, y: leftCenter.y + eyeH))
|
||||||
|
left.closeSubpath()
|
||||||
|
|
||||||
|
let right = CGMutablePath()
|
||||||
|
right.move(to: CGPoint(x: rightCenter.x + eyeW / 2, y: rightCenter.y - eyeH))
|
||||||
|
right.addLine(to: CGPoint(x: rightCenter.x - eyeW / 2, y: rightCenter.y))
|
||||||
|
right.addLine(to: CGPoint(x: rightCenter.x + eyeW / 2, y: rightCenter.y + eyeH))
|
||||||
|
right.closeSubpath()
|
||||||
|
|
||||||
|
context.cgContext.addPath(left)
|
||||||
|
context.cgContext.addPath(right)
|
||||||
|
context.cgContext.fillPath()
|
||||||
|
context.cgContext.restoreGState()
|
||||||
|
} else {
|
||||||
|
NSGraphicsContext.restoreGraphicsState()
|
||||||
|
return NSImage(size: size)
|
||||||
|
}
|
||||||
|
|
||||||
let image = NSImage(size: size)
|
let image = NSImage(size: size)
|
||||||
image.lockFocus()
|
image.addRepresentation(rep)
|
||||||
defer { image.unlockFocus() }
|
|
||||||
|
|
||||||
guard let ctx = NSGraphicsContext.current?.cgContext else { return image }
|
|
||||||
|
|
||||||
let w = self.size.width
|
|
||||||
let h = self.size.height
|
|
||||||
|
|
||||||
let bodyW = w * 0.78
|
|
||||||
let bodyH = h * 0.58
|
|
||||||
let bodyX = (w - bodyW) / 2
|
|
||||||
let bodyY = h * 0.36
|
|
||||||
let bodyCorner = w * 0.09
|
|
||||||
|
|
||||||
let earW = w * 0.22
|
|
||||||
let earH = bodyH * 0.66 * earScale * (1 - 0.08 * abs(earWiggle))
|
|
||||||
let earCorner = earW * 0.24
|
|
||||||
let leftEarRect = CGRect(
|
|
||||||
x: bodyX - earW * 0.55 + earWiggle,
|
|
||||||
y: bodyY + bodyH * 0.08 + earWiggle * 0.4,
|
|
||||||
width: earW,
|
|
||||||
height: earH)
|
|
||||||
let rightEarRect = CGRect(
|
|
||||||
x: bodyX + bodyW - earW * 0.45 - earWiggle,
|
|
||||||
y: bodyY + bodyH * 0.08 - earWiggle * 0.4,
|
|
||||||
width: earW,
|
|
||||||
height: earH)
|
|
||||||
|
|
||||||
let legW = w * 0.11
|
|
||||||
let legH = h * 0.26
|
|
||||||
let legSpacing = w * 0.085
|
|
||||||
let legsWidth = 4 * legW + 3 * legSpacing
|
|
||||||
let legStartX = (w - legsWidth) / 2
|
|
||||||
let legLift = legH * 0.35 * legWiggle
|
|
||||||
let legYBase = bodyY - legH + h * 0.05
|
|
||||||
|
|
||||||
let eyeOpen = max(0.05, 1 - blink)
|
|
||||||
let eyeW = bodyW * 0.2
|
|
||||||
let eyeH = bodyH * 0.26 * eyeOpen
|
|
||||||
let eyeY = bodyY + bodyH * 0.56
|
|
||||||
let eyeOffset = bodyW * 0.24
|
|
||||||
|
|
||||||
ctx.setFillColor(NSColor.labelColor.cgColor)
|
|
||||||
|
|
||||||
ctx.addPath(CGPath(
|
|
||||||
roundedRect: CGRect(x: bodyX, y: bodyY, width: bodyW, height: bodyH),
|
|
||||||
cornerWidth: bodyCorner,
|
|
||||||
cornerHeight: bodyCorner,
|
|
||||||
transform: nil))
|
|
||||||
ctx.addPath(CGPath(
|
|
||||||
roundedRect: leftEarRect,
|
|
||||||
cornerWidth: earCorner,
|
|
||||||
cornerHeight: earCorner,
|
|
||||||
transform: nil))
|
|
||||||
ctx.addPath(CGPath(
|
|
||||||
roundedRect: rightEarRect,
|
|
||||||
cornerWidth: earCorner,
|
|
||||||
cornerHeight: earCorner,
|
|
||||||
transform: nil))
|
|
||||||
for i in 0..<4 {
|
|
||||||
let x = legStartX + CGFloat(i) * (legW + legSpacing)
|
|
||||||
let lift = (i % 2 == 0 ? legLift : -legLift)
|
|
||||||
let rect = CGRect(x: x, y: legYBase + lift, width: legW, height: legH * (1 - 0.12 * legWiggle))
|
|
||||||
ctx.addPath(CGPath(roundedRect: rect, cornerWidth: legW * 0.34, cornerHeight: legW * 0.34, transform: nil))
|
|
||||||
}
|
|
||||||
ctx.fillPath()
|
|
||||||
|
|
||||||
ctx.saveGState()
|
|
||||||
ctx.setBlendMode(.clear)
|
|
||||||
|
|
||||||
let leftCenter = CGPoint(x: w / 2 - eyeOffset, y: eyeY)
|
|
||||||
let rightCenter = CGPoint(x: w / 2 + eyeOffset, y: eyeY)
|
|
||||||
|
|
||||||
if earHoles || earScale > 1.05 {
|
|
||||||
let holeW = earW * 0.6
|
|
||||||
let holeH = earH * 0.46
|
|
||||||
let holeCorner = holeW * 0.34
|
|
||||||
let leftHoleRect = CGRect(
|
|
||||||
x: leftEarRect.midX - holeW / 2,
|
|
||||||
y: leftEarRect.midY - holeH / 2 + earH * 0.04,
|
|
||||||
width: holeW,
|
|
||||||
height: holeH)
|
|
||||||
let rightHoleRect = CGRect(
|
|
||||||
x: rightEarRect.midX - holeW / 2,
|
|
||||||
y: rightEarRect.midY - holeH / 2 + earH * 0.04,
|
|
||||||
width: holeW,
|
|
||||||
height: holeH)
|
|
||||||
|
|
||||||
ctx.addPath(CGPath(
|
|
||||||
roundedRect: leftHoleRect,
|
|
||||||
cornerWidth: holeCorner,
|
|
||||||
cornerHeight: holeCorner,
|
|
||||||
transform: nil))
|
|
||||||
ctx.addPath(CGPath(
|
|
||||||
roundedRect: rightHoleRect,
|
|
||||||
cornerWidth: holeCorner,
|
|
||||||
cornerHeight: holeCorner,
|
|
||||||
transform: nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
let left = CGMutablePath()
|
|
||||||
left.move(to: CGPoint(x: leftCenter.x - eyeW / 2, y: leftCenter.y - eyeH))
|
|
||||||
left.addLine(to: CGPoint(x: leftCenter.x + eyeW / 2, y: leftCenter.y))
|
|
||||||
left.addLine(to: CGPoint(x: leftCenter.x - eyeW / 2, y: leftCenter.y + eyeH))
|
|
||||||
left.closeSubpath()
|
|
||||||
|
|
||||||
let right = CGMutablePath()
|
|
||||||
right.move(to: CGPoint(x: rightCenter.x + eyeW / 2, y: rightCenter.y - eyeH))
|
|
||||||
right.addLine(to: CGPoint(x: rightCenter.x - eyeW / 2, y: rightCenter.y))
|
|
||||||
right.addLine(to: CGPoint(x: rightCenter.x + eyeW / 2, y: rightCenter.y + eyeH))
|
|
||||||
right.closeSubpath()
|
|
||||||
|
|
||||||
ctx.addPath(left)
|
|
||||||
ctx.addPath(right)
|
|
||||||
ctx.fillPath()
|
|
||||||
ctx.restoreGState()
|
|
||||||
|
|
||||||
image.isTemplate = true
|
image.isTemplate = true
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Wiring points
|
|||||||
|
|
||||||
Shapes & sizes
|
Shapes & sizes
|
||||||
- Base icon drawn in `CritterIconRenderer.makeIcon(blink:legWiggle:earWiggle:earScale:earHoles:)`.
|
- Base icon drawn in `CritterIconRenderer.makeIcon(blink:legWiggle:earWiggle:earScale:earHoles:)`.
|
||||||
- Ear scale defaults to `1.0`; voice boost sets `earScale=1.9` and toggles `earHoles=true` without changing overall frame (18×16pt template image).
|
- Ear scale defaults to `1.0`; voice boost sets `earScale=1.9` and toggles `earHoles=true` without changing overall frame (18×18 pt template image rendered into a 36×36 px Retina backing store).
|
||||||
- Scurry uses leg wiggle up to ~1.0 with a small horizontal jiggle; it’s additive to any existing idle wiggle.
|
- Scurry uses leg wiggle up to ~1.0 with a small horizontal jiggle; it’s additive to any existing idle wiggle.
|
||||||
|
|
||||||
Behavioral notes
|
Behavioral notes
|
||||||
|
|||||||
Reference in New Issue
Block a user