Files
networkcity/Sources/NetworkCityApp/Visuals.swift
T

100 lines
4.2 KiB
Swift
Raw Normal View History

import SpriteKit
import NetworkCityCore
/// How a car of light looks for a given traffic class + magnitude.
struct CarStyle {
let color: SKColor
let scale: CGFloat
let elongation: CGFloat // 1 = round dot; >1 = streak along motion
let speed: CGFloat // points/sec
}
/// Colour per traffic class direction is read from motion, so hue is free to
/// mean protocol.
enum TrafficPalette {
static func color(_ c: TrafficClass) -> SKColor {
switch c {
case .dns: return SKColor(red: 1.00, green: 0.82, blue: 0.25, alpha: 1) // gold
case .https: return SKColor(red: 0.22, green: 0.88, blue: 1.00, alpha: 1) // cyan
case .http: return SKColor(red: 1.00, green: 0.48, blue: 0.24, alpha: 1) // orange
case .quic: return SKColor(red: 0.71, green: 0.48, blue: 1.00, alpha: 1) // violet
case .other: return SKColor(red: 0.55, green: 0.60, blue: 0.72, alpha: 1) // slate
}
}
}
/// Translate a class + this-tick byte volume into a car's personality. Heavy
/// flows become "freight": bigger, slower, stretched into a glowing comet.
func carStyle(_ cls: TrafficClass, bytes: UInt64) -> CarStyle {
let heavy = bytes > 196_608 // ~96 KB/s over a 2s tick
var scale: CGFloat
var elongation: CGFloat = 1
var speed: CGFloat = 230
switch cls {
case .dns: scale = 0.055; speed = 360 // tiny, fast sparks
case .https: scale = 0.085
case .http: scale = 0.085
case .quic: scale = 0.085; elongation = 2.4; speed = 285 // streaks
case .other: scale = 0.065
}
if heavy {
scale *= 1.7
elongation = max(elongation, 3.0) // freight comet
speed *= 0.7 // slow and heavy
}
return CarStyle(color: TrafficPalette.color(cls), scale: scale, elongation: elongation, speed: speed)
}
/// Neon-night palette. Bright, saturated colors on near-black so additive
/// blending reads as glow.
enum Palette {
static let background = SKColor(red: 0.039, green: 0.055, blue: 0.102, alpha: 1) // #0a0e1a
static let grid = SKColor(red: 0.13, green: 0.20, blue: 0.38, alpha: 1)
static let hub = SKColor(red: 1.00, green: 0.85, blue: 0.62, alpha: 1) // warm downtown
static let building = SKColor(red: 0.50, green: 0.82, blue: 0.78, alpha: 1) // teal
static let road = SKColor(red: 0.17, green: 0.26, blue: 0.46, alpha: 1)
static let download = SKColor(red: 0.22, green: 0.88, blue: 1.00, alpha: 1) // cyan, inbound
static let upload = SKColor(red: 1.00, green: 0.62, blue: 0.30, alpha: 1) // amber, outbound
}
extension CGFloat {
func clamped(_ lo: CGFloat, _ hi: CGFloat) -> CGFloat { Swift.min(Swift.max(self, lo), hi) }
}
enum Textures {
/// A white radial-gradient disc (opaque center transparent edge). Tint it
/// per-node with `colorBlendFactor` + `.add` blend mode to get a glow.
// Immutable after creation and only read; safe to share across actors.
nonisolated(unsafe) static let softCircle: SKTexture = makeSoftCircle(diameter: 128)
private static func makeSoftCircle(diameter: Int) -> SKTexture {
let space = CGColorSpaceCreateDeviceRGB()
let ctx = CGContext(
data: nil, width: diameter, height: diameter,
bitsPerComponent: 8, bytesPerRow: 0, space: space,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)!
let colors = [
SKColor.white.cgColor,
SKColor.white.withAlphaComponent(0).cgColor,
] as CFArray
let gradient = CGGradient(colorsSpace: space, colors: colors, locations: [0, 1])!
let center = CGPoint(x: diameter / 2, y: diameter / 2)
ctx.drawRadialGradient(
gradient, startCenter: center, startRadius: 0,
endCenter: center, endRadius: CGFloat(diameter) / 2, options: []
)
return SKTexture(cgImage: ctx.makeImage()!)
}
}
/// Deterministic FNV-1a hash so a given host always lands in the same place,
/// independent of Swift's per-process `Hasher` seed.
func stableHash(_ s: String) -> UInt64 {
var h: UInt64 = 1469598103934665603
for byte in s.utf8 { h = (h ^ UInt64(byte)) &* 1099511628211 }
return h
}