FieldLines
A planet's magnetic field drawn as lines in space, looping pole to pole with a pulse of light along each. The planet hides the far half of every loop, and the weight can be read on screen or in world units (drawPolyline over Vector3 with a color per point, drawGrid, drawAxes).
- Run
swift run --package-path Examples Example-3D-Geometry-FieldLines- Source
- Examples/3D/Geometry/FieldLines/Sketch.swift
import Ollin
/// **Field lines**: the magnetic field of a planet drawn as lines in space, looping
/// out of one pole and back into the other, with a pulse of light running along
/// each one.
///
/// A magnet the size of a planet has a simple field far from its surface: every
/// line of it keeps to a shell where the distance from the center is `L·sin²θ`, `θ`
/// measured from the magnetic axis, so each line leaves the surface at one latitude
/// and comes back at the same latitude on the other side. The sketch draws a few of
/// those shells at even steps round the axis, each line a `drawPolyline` through
/// points in space with a color for every point. The planet is a solid sphere, so it
/// hides the far half of every loop, and the axis is tilted the way a planet's
/// magnetic axis usually is, by a 3D `rotateZ` that moves the lines with it.
///
/// The lines take a weight measured on the canvas by default, so a line keeps its
/// width near the camera and far from it. Set **units** to world and the weight is
/// read in the scene's own units instead: the lines thin as they go back into the
/// scene, like thread would. **grid** and **axes** turn on the two helpers built
/// from the same lines, a floor across the equator and the magnetic axis itself.
///
/// Try it: raise **shells** and **lines** for a denser field, or slow the **pulse**
/// to zero to see the field still.
@main
final class FieldLines_Example: Sketch {
@Param(1 ... 6, icon: "circle.dotted") var shells = 4
@Param(4 ... 24, icon: "line.3.horizontal") var lines = 12
@Param(0.5 ... 6, icon: "lineweight") var weight = 2.0
@Param var units: StrokeUnits = .screen
@Param(0 ... 1, icon: "bolt.horizontal") var pulse = 0.35
@Param(icon: "grid") var grid = true
@Param(icon: "move.3d") var axes = true
/// How far each shell reaches at the magnetic equator, in planet radii.
private func reach(_ shell: Int) -> Double { 1.6 * pow(1.45, Double(shell)) }
override func draw() {
background(Color(hex: 0x05060B))
cameraShowcase(.turntable(period: 48), target: Vector3(0, -0.3, 0), radius: 12,
elevation: 0.22, fieldOfView: .pi / 3.6)
directionalLight(.white, direction: Vector3(-0.6, -0.4, -0.7), intensity: 1.1)
ambientLight(Color(white: 0.12))
if grid {
stroke(Color(hex: 0x2A3346))
strokeWeight(1)
drawGrid(size: 16, divisions: 16)
}
rotateZ(0.2) // the magnetic axis, tilted
fill(Color(hex: 0x1D3E6E))
specular(0.3); specularSharpness(32)
drawSphere(radius: 1, segments: 64, rings: 32)
if axes {
strokeWeight(2)
drawAxes(length: 2.4)
}
strokeWeight(units == .world ? weight * 0.012 : weight, in: units)
strokeCap(.round)
strokeJoin(.round)
for shell in 0 ..< shells {
let l = reach(shell)
// Where the shell meets the surface: L·sin²θ = 1.
let start = asin((1 / l).squareRoot())
let hue = 0.08 + 0.5 * Double(shell) / Double(max(shells - 1, 1))
for k in 0 ..< lines {
let around = (Double(k) + 0.5 * Double(shell % 2)) / Double(lines) * .tau
var points: [Vector3] = []
var colors: [Color] = []
let steps = 72 + 24 * shell
for i in 0 ... steps {
let along = Double(i) / Double(steps)
let theta = start + (.pi - 2 * start) * along
let r = l * sin(theta) * sin(theta)
points.append(Vector3(r * sin(theta) * cos(around), r * cos(theta),
r * sin(theta) * sin(around)))
colors.append(color(hue: hue, along: along, line: k + shell * lines))
}
drawPolyline(points, colors: colors)
}
}
}
/// The line's own color, brightened where the pulse is: a short bright stretch
/// running from the south pole to the north, each line at its own phase.
private func color(hue: Double, along: Double, line: Int) -> Color {
let phase = Double((line * 37) % 101) / 101
var gap = (along - (time * pulse * 0.25 + phase)).truncatingRemainder(dividingBy: 1)
if gap < 0 { gap += 1 }
let glow = pulse > 0 ? exp(-pow(min(gap, 1 - gap) / 0.05, 2)) : 0
return Color(hue: hue, saturation: 0.75 - 0.55 * glow, brightness: 0.55 + 0.45 * glow)
}
}