100 lines
4.2 KiB
Swift
100 lines
4.2 KiB
Swift
|
|
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
|
||
|
|
}
|