Public Access
Give the desktop head the mark the phone has carried all along
The window had no icon and the executable had no icon, so Windows drew its generic one in the taskbar, in alt-tab and in Explorer, next to an application that draws its own titlebar precisely so that nothing about it looks borrowed. The geometry is the launcher vector's, mapped so the middle 72 of its 108-unit viewport fills the canvas: that 72 is what an adaptive icon actually shows, and rendering the full 108 would draw the mark at 39% of the width. Nine sizes, because Windows asks for several and downsamples unkindly when one is missing, and a floor of one pixel on the strokes so 16px is a mark rather than a smudge. Windows takes an .ico and nothing else, so the raster the phone does not need exists here. dodossh-icon.ps1 sits beside it and draws it from those same numbers, so the two heads still have one geometry between them.
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
# Regenerates dodossh.ico from the same geometry the Android launcher icon draws.
|
||||||
|
#
|
||||||
|
# The phone's mark is a vector — Resources/drawable/ic_launcher_foreground.xml — and the whole
|
||||||
|
# reason it is a vector is that there is then one geometry to change and no set of PNG densities
|
||||||
|
# to forget one of. Windows will not take a vector: <ApplicationIcon> wants an .ico and nothing
|
||||||
|
# else, and Window.Icon wants a bitmap. So the raster exists, and this script is how it stays
|
||||||
|
# honest: the numbers below are the ones in that XML, and regenerating is the whole edit.
|
||||||
|
#
|
||||||
|
# pwsh -File src/DodoSSH.Client.App/Assets/dodossh-icon.ps1
|
||||||
|
#
|
||||||
|
# Coordinates are the launcher's 108-unit viewport, mapped so the middle 72 fills the canvas.
|
||||||
|
# That 72 is not an arbitrary crop: it is the part of an adaptive icon a launcher actually shows,
|
||||||
|
# the outer 18 on each edge being what it eats for masking and parallax. Rendering the whole 108
|
||||||
|
# here would draw the mark at 39% of the width — correct arithmetic, and a stamp lost in a field
|
||||||
|
# of background. Matching what the phone displays means matching the 72.
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
|
||||||
|
$window = [System.Drawing.ColorTranslator]::FromHtml('#0E1220') # dodo_window / CanvasColor
|
||||||
|
$accent = [System.Drawing.ColorTranslator]::FromHtml('#5B8CFF') # AccentColor
|
||||||
|
|
||||||
|
# Every size Windows asks for: 16 in a titlebar and a tree, 32 on the desktop, 48 in a large-icon
|
||||||
|
# view, 256 for the preview pane. Shipping fewer means Windows downsamples one of these to fill
|
||||||
|
# the gap, and its downsampler is not kind to a hairline.
|
||||||
|
$sizes = @(16, 20, 24, 32, 40, 48, 64, 128, 256)
|
||||||
|
|
||||||
|
function New-MarkPng([int]$size)
|
||||||
|
{
|
||||||
|
$bitmap = New-Object System.Drawing.Bitmap($size, $size, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||||
|
$g = [System.Drawing.Graphics]::FromImage($bitmap)
|
||||||
|
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
|
||||||
|
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
||||||
|
|
||||||
|
# The background, rounded as a launcher mask rounds it. A square-cornered tile would be the
|
||||||
|
# one icon on the taskbar with corners, which reads as unfinished rather than as deliberate.
|
||||||
|
$radius = [double]$size * 0.22
|
||||||
|
$d = $radius * 2.0
|
||||||
|
$path = New-Object System.Drawing.Drawing2D.GraphicsPath
|
||||||
|
$path.AddArc(0.0, 0.0, $d, $d, 180, 90)
|
||||||
|
$path.AddArc($size - $d, 0.0, $d, $d, 270, 90)
|
||||||
|
$path.AddArc($size - $d, $size - $d, $d, $d, 0, 90)
|
||||||
|
$path.AddArc(0.0, $size - $d, $d, $d, 90, 90)
|
||||||
|
$path.CloseFigure()
|
||||||
|
$brush = New-Object System.Drawing.SolidBrush($window)
|
||||||
|
$g.FillPath($brush, $path)
|
||||||
|
|
||||||
|
# 108-viewport units to pixels, with the outer 18 dropped on each edge.
|
||||||
|
$scale = [double]$size / 72.0
|
||||||
|
function P([double]$x, [double]$y) { New-Object System.Drawing.PointF((($x - 18.0) * $scale), (($y - 18.0) * $scale)) }
|
||||||
|
|
||||||
|
# A stroke thinner than a pixel renders as a grey suggestion of itself, which at 16px is the
|
||||||
|
# difference between a mark and a smudge. The phone's file already bumps these widths for the
|
||||||
|
# same reason at 48dp; the floor is that argument carried down to the sizes Windows asks for.
|
||||||
|
function Pen([double]$units)
|
||||||
|
{
|
||||||
|
$pen = New-Object System.Drawing.Pen($accent, [float][Math]::Max($units * $scale, 1.0))
|
||||||
|
$pen.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
|
||||||
|
$pen.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
|
||||||
|
$pen.LineJoin = [System.Drawing.Drawing2D.LineJoin]::Round
|
||||||
|
return $pen
|
||||||
|
}
|
||||||
|
|
||||||
|
# The box: 42 across, centred, square-cornered as the Border in the app is.
|
||||||
|
$box = Pen 2.2
|
||||||
|
$g.DrawPolygon($box, @((P 33 33), (P 75 33), (P 75 75), (P 33 75)))
|
||||||
|
|
||||||
|
# The chevron of >_ and the underscore on the baseline it bottoms out at.
|
||||||
|
$ink = Pen 2.8
|
||||||
|
$g.DrawLines($ink, @((P 44.8 48.75), (P 51.9 54), (P 44.8 59.25)))
|
||||||
|
$g.DrawLine($ink, (P 54 59.7), (P 63.2 59.7))
|
||||||
|
|
||||||
|
$stream = New-Object System.IO.MemoryStream
|
||||||
|
$bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
|
||||||
|
$box.Dispose(); $ink.Dispose(); $brush.Dispose(); $path.Dispose(); $g.Dispose(); $bitmap.Dispose()
|
||||||
|
return $stream.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
# ICO is a six-byte header, a sixteen-byte directory entry per image, then the images. The entries
|
||||||
|
# carry PNG payloads rather than the older BMP-with-AND-mask form, which every Windows since Vista
|
||||||
|
# reads and which is what keeps a 256px entry from costing 256KB.
|
||||||
|
#
|
||||||
|
# The cast on each frame is load-bearing. A byte[] returned through PowerShell's output collector
|
||||||
|
# comes back as Object[] of boxed bytes, which BinaryWriter has no overload for — it binds to one
|
||||||
|
# of the scalar Write()s instead and puts a single byte on the stream. The first run of this
|
||||||
|
# script produced a 159-byte .ico that way, header and directory intact and nine one-byte images.
|
||||||
|
$frames = New-Object 'System.Collections.Generic.List[byte[]]'
|
||||||
|
foreach ($size in $sizes)
|
||||||
|
{
|
||||||
|
[byte[]]$png = New-MarkPng $size
|
||||||
|
$frames.Add($png)
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = New-Object System.IO.MemoryStream
|
||||||
|
$w = New-Object System.IO.BinaryWriter($out)
|
||||||
|
$w.Write([uint16]0) # reserved
|
||||||
|
$w.Write([uint16]1) # type: icon
|
||||||
|
$w.Write([uint16]$sizes.Count)
|
||||||
|
|
||||||
|
$offset = 6 + (16 * $sizes.Count)
|
||||||
|
for ($i = 0; $i -lt $sizes.Count; $i++)
|
||||||
|
{
|
||||||
|
$size = $sizes[$i]
|
||||||
|
$w.Write([byte]($(if ($size -ge 256) { 0 } else { $size }))) # 0 means 256
|
||||||
|
$w.Write([byte]($(if ($size -ge 256) { 0 } else { $size })))
|
||||||
|
$w.Write([byte]0) # palette entries: none, this is truecolour
|
||||||
|
$w.Write([byte]0) # reserved
|
||||||
|
$w.Write([uint16]1) # colour planes
|
||||||
|
$w.Write([uint16]32) # bits per pixel
|
||||||
|
$w.Write([uint32]$frames[$i].Length)
|
||||||
|
$w.Write([uint32]$offset)
|
||||||
|
$offset += $frames[$i].Length
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($frame in $frames) { $w.Write($frame) }
|
||||||
|
$w.Flush()
|
||||||
|
|
||||||
|
$target = Join-Path $PSScriptRoot 'dodossh.ico'
|
||||||
|
[System.IO.File]::WriteAllBytes($target, $out.ToArray())
|
||||||
|
$w.Dispose(); $out.Dispose()
|
||||||
|
|
||||||
|
Write-Output "Wrote $target ($($sizes.Count) sizes, $((Get-Item $target).Length) bytes)"
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
@@ -5,6 +5,14 @@
|
|||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The icon on the executable itself — what Explorer, the Start menu and a pinned taskbar button
|
||||||
|
draw, all of which read it from the PE resource and never start the process. Window.Icon in
|
||||||
|
MainWindow.axaml is a separate thing that only exists once the application is running; both are
|
||||||
|
needed, and both point at this file.
|
||||||
|
-->
|
||||||
|
<ApplicationIcon>Assets/dodossh.ico</ApplicationIcon>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
False here, unlike every server project. The root Directory.Build.props sets it true because
|
False here, unlike every server project. The root Directory.Build.props sets it true because
|
||||||
the API is container-hosted, UTC-only and has no business formatting anything for a human.
|
the API is container-hosted, UTC-only and has no business formatting anything for a human.
|
||||||
@@ -15,6 +23,14 @@
|
|||||||
<InvariantGlobalization>false</InvariantGlobalization>
|
<InvariantGlobalization>false</InvariantGlobalization>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!--
|
||||||
|
Named rather than globbed as Assets/**, because the folder also holds the script that draws the
|
||||||
|
icon and a build has no reason to carry a copy of it around inside the binary.
|
||||||
|
-->
|
||||||
|
<AvaloniaResource Include="Assets/dodossh.ico" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" />
|
<PackageReference Include="Avalonia" />
|
||||||
<PackageReference Include="Avalonia.Desktop" />
|
<PackageReference Include="Avalonia.Desktop" />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
x:Class="DodoSSH.Client.App.Views.MainWindow"
|
x:Class="DodoSSH.Client.App.Views.MainWindow"
|
||||||
x:DataType="vm:MainWindowViewModel"
|
x:DataType="vm:MainWindowViewModel"
|
||||||
Title="DodoSSH"
|
Title="DodoSSH"
|
||||||
|
Icon="/Assets/dodossh.ico"
|
||||||
Width="1180"
|
Width="1180"
|
||||||
Height="760"
|
Height="760"
|
||||||
MinWidth="1016"
|
MinWidth="1016"
|
||||||
|
|||||||
Reference in New Issue
Block a user