Files
Camper-Monitor/Tools/make-vehicle-art.swift
BiasFandClaude Opus 5 49138312ea Neigung am Fahrzeug darstellen
Neben der Libelle gibt es jetzt eine zweite Darstellung: die Neigung am
Fahrzeug selbst, mit den Zeichnungen aus dem Ursprungsprojekt. Umschaltbar
und über Starts hinweg gemerkt.

Seitenansicht für längs, Heckansicht für quer. Die Heckansicht ist bewusst
gewählt, nicht die Frontansicht: sie teilt die Blickrichtung des Fahrers,
links im Bild ist also links am Fahrzeug. Von vorne betrachtet wäre es
seitenverkehrt und damit beim Ausrichten irreführend.

Die Zeichnungen sind weiss gefüllt mit dunklen Linien. Als Schablone
eingefärbt wurde daraus eine massive Fläche, weil alles Deckende zur
Tintfarbe wird - Fenster, Räder und Konturen verschwanden. Tools/
make-vehicle-art übersetzt deshalb die Helligkeit in Deckkraft: dunkle
Linien voll deckend, helle Karosserie nur angedeutet. Eingefärbt bleibt
die Zeichnung erhalten und passt sich Hell- wie Dunkeldarstellung an.

Die Neigung wird dreifach überhöht gezeigt, sonst wären zwei Grad am
Fahrzeug kaum auszumachen. Das steht als Hinweis darunter, zusammen mit
der Klarstellung, dass die Gradzahlen echt sind - sonst nähme man den
Bildwinkel für den tatsächlichen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 23:22:07 +02:00

62 lines
2.5 KiB
Swift

import AppKit
import CoreGraphics
import Foundation
// Die Zeichnungen sind weiss gefüllt mit dunklen Linien und Fenstern. Als
// Schablone eingefärbt geht beides verloren, weil alles Deckende zur
// Tintfarbe wird - übrig bleibt eine massive Fläche.
//
// Deshalb wird die Helligkeit in Deckkraft übersetzt: dunkle Linien werden
// voll deckend, die helle Karosserie nur angedeutet. Eingefärbt ergibt das
// eine gefüllte Form, in der die Details erhalten bleiben.
let arguments = CommandLine.arguments
guard arguments.count >= 3,
let source = NSImage(contentsOfFile: arguments[1]),
let image = source.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
print("Quelle nicht lesbar"); exit(1)
}
let width = image.width, height = image.height
let bytesPerRow = width * 4
var pixels = [UInt8](repeating: 0, count: bytesPerRow * height)
guard let context = CGContext(data: &pixels, width: width, height: height,
bitsPerComponent: 8, bytesPerRow: bytesPerRow,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else {
print("Puffer nicht anlegbar"); exit(1)
}
context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
/// Wie stark die Fläche noch durchscheint.
let fill = 0.22
var changed = 0
for index in stride(from: 0, to: pixels.count, by: 4) {
let alpha = Double(pixels[index + 3]) / 255
guard alpha > 0.02 else {
pixels[index] = 0; pixels[index + 1] = 0; pixels[index + 2] = 0; pixels[index + 3] = 0
continue
}
// Premultipliziert: erst zurückrechnen, um die echte Helligkeit zu sehen.
let r = Double(pixels[index]) / 255 / alpha
let g = Double(pixels[index + 1]) / 255 / alpha
let b = Double(pixels[index + 2]) / 255 / alpha
let brightness = min(1, max(0, 0.299 * r + 0.587 * g + 0.114 * b))
let newAlpha = alpha * (fill + (1 - fill) * (1 - brightness))
// Die Farbe selbst spielt keine Rolle, sie wird ohnehin eingefärbt.
let value = UInt8(newAlpha * 255)
pixels[index] = value; pixels[index + 1] = value; pixels[index + 2] = value
pixels[index + 3] = value
changed += 1
}
guard let result = context.makeImage() else { print("Bild leer"); exit(1) }
let rep = NSBitmapImageRep(cgImage: result)
guard let png = rep.representation(using: .png, properties: [:]) else {
print("PNG nicht erzeugbar"); exit(1)
}
try png.write(to: URL(fileURLWithPath: arguments[2]))
print("\(arguments[2]): \(changed) Bildpunkte umgesetzt")