Ollin

Complex numbers

A Complex is a point of the plane that knows how to multiply. Adding two of them adds their coordinates, exactly as Vector2 does. Multiplying them multiplies their lengths and adds their angles. That is the one thing a plain vector cannot do, and the reason so much generative work is written in them. An escape-time fractal is z * z + c repeated. A domain coloring paints a function of z over the plane it acts on. A Möbius map turns the plane inside out around a point, and a conformal warp keeps every small square a square.

var z = Complex(0.3, 0.5)
let c: Complex = -0.8 + 0.156 * .i
for _ in 0 ..< 40 { z = z * z + c }      // one Julia orbit
let turned = z * .unit(.pi / 6)          // the same point, turned thirty degrees
drawCircle(center: Vector2(turned), radius: 4)

The same arithmetic runs on the GPU as the shader library's complex module, one function per operation with the same meaning. A curve worked out here in draw() lands on the pixels a shader paints. Examples/Shaders/ComplexPlane draws both halves over each other.

Contents

Making one

Complex(_ real: Double, _ imaginary: Double = 0)
Complex(real: Double, imaginary: Double)
Complex(magnitude: Double, argument: Double)    // the polar form
Complex(_ vector: Vector2)                      // vector.x + vector.y·i
Complex.zero, Complex.one, Complex.i
Complex.unit(_ angle: Double)                   // e^(i·angle), on the unit circle

A Complex is a Numeric value, so a plain number is one too. let z: Complex = 3 is the real number three, 2 * z doubles it, and an array of them sums under reduce(0, +). .i is the imaginary unit, so 1 + 2 * .i reads as it is written, and .unit(angle) is the number that turns whatever it multiplies by that angle.

let z = Complex(1, 2)                            // 1 + 2i
let w = Complex(magnitude: 1, argument: .pi / 4)  // a unit number at 45 degrees
let sum = [z, w, .i].reduce(0, +)

Arithmetic

+, -, *, and / work between two complex numbers and between a complex number and a Double on either side, with the compound forms +=, -=, *=, /=. Multiplication is the whole point: the lengths multiply and the angles add, so multiplying by .unit(a) turns a number by a without moving it in or out.

let a = Complex(2, 1), b = Complex(0, 1)
let product = a * b            // -1 + 2i: a turned by ninety degrees
let quotient = a / b           // 1 - 2i
let scaled = a * 0.5           // 1 + 0.5i
let shifted = a + 1            // 3 + i

A real number mixes in as a literal or as a Double. There is no conversion the other way, so a Complex never lands where a Double was expected.

Reading one

PropertyWhat it is
real, imaginarythe two parts
magnitudethe distance from the origin, `\
magnitudeSquared`\
argumentthe angle from the positive real axis, radians in (-pi, pi]
conjugatethe mirror image across the real axis, real - imaginary·i
reciprocal1 / z
vectorthe same point as a Vector2
isFinitewhether both parts are finite
rotated(by:)the number turned by an angle about the origin
squareRoot()the principal square root (the other root is its negative)
descriptionreads as written: 1 + 2i, 1 - 2i, 2i, -i, 3
let z = Complex(3, 4)
let r = z.magnitude            // 5
let theta = z.argument         // 0.927...
let back = Complex(magnitude: r, argument: theta)   // 3 + 4i again

Functions

The transcendental functions are static members, Complex.exp(z), the spelling the Swift numerics package uses. They are not free functions on purpose. A free pow(z, n) or sin(z) would join the candidates for every pow and sin in your sketch. A plain line of doubles such as pow(2, 20 * t - 10) * sin(x) would then have to rule this type out each time, which the compiler does slowly and sometimes not at all. Every multi-valued function takes the principal branch: the argument in (-pi, pi], so the values jump across the negative real axis. That cut is real, and a domain coloring shows it.

FunctionWhat it is
Complex.exp(z)e^z: the modulus is e to the real part, the argument is the imaginary part
Complex.log(z)the natural logarithm, `log\
Complex.pow(z, 2.5)z to a real power by the polar form; a whole number winds the plane cleanly, a fraction shows the seam
Complex.pow(z, w)z to a complex power w, exp(w·log z)
Complex.pow(z, 3)z to a whole power by repeated squaring: exact where the polar form would round, which is what an orbit iterated thousands of times wants
Complex.sqrt(z)the principal square root, the same as z.squareRoot()
Complex.sin(z), .cos(z), .tan(z)the trigonometric functions
Complex.sinh(z), .cosh(z), .tanh(z)the hyperbolic functions
let z = Complex(0.5, 1.2)
let back = Complex.log(Complex.exp(z))   // z again, since its imaginary part is inside (-pi, pi]
let cube = Complex.pow(z, 3)             // exact
let root = Complex.sqrt(Complex(-4))     // 2i
let euler = Complex.exp(Complex(0, .pi)) // -1, within rounding

The plane and the canvas

Mathematics draws the plane with the imaginary axis pointing up. A Vector2 is a canvas point, with y pointing down. Complex(v) and Vector2(z) copy the coordinates as they are and leave the flip to you. That keeps both conversions honest: a number read off a canvas point is that point, and the other way round. When a picture should read as mathematics writes it, flip the imaginary part once at the edge, and ComplexPlane is that edge as a value.

Every picture of the plane in Ollin is framed one way. A center sits in the middle of the picture, span units run across its shorter side, and the imaginary axis points up. The longer side shows more, so nothing stretches. The shader library's complexPlane frames a layer that way. So does every generator that paints the plane (.mandelbrot, .julia, .orbitTrap, .domainColoring, .newton), where the span is 3 / zoom. A ComplexPlane holds that framing and converts both ways:

CallWhat it is
ComplexPlane(center:span:)a framing by its span; the default is the origin in the middle and 3 units across
ComplexPlane(center:zoom:)the same framing by a generator's zoom, so zoom: 2 is a span of 1.5
center, span, zoomthe framing's parts, with zoom as 3 / span
canvasPoint(of:in:)where a number lands on a picture drawn undistorted in a Rectangle
planePoint(at:in:)the number under a canvas point of that picture
Generator.planea generator's own framing, or nil for one that paints no plane
let plane = ComplexPlane()                                    // the origin, 3 units across
let dot = plane.canvasPoint(of: Vector2(Complex.unit(time)), in: bounds)
drawCircle(center: dot, radius: 6)                            // a dot going round counter-clockwise

let julia = Generator.julia(c: Vector2(-0.79, 0.15), zoom: 1.2)
drawImage(generate(julia).image, 0, 0)
if let framing = julia.plane {
    let z = Complex(framing.planePoint(at: mouse, in: bounds))              // the number under the pointer
    drawCircle(center: framing.canvasPoint(of: .zero, in: bounds), radius: 4)   // the origin, marked
}

The rectangle is the one the picture was drawn into: bounds for a full-canvas layer, or the tile a smaller layer was drawn in. A point placed this way lands on the generator's picture of that number. A number read this way is the one the generator painted there.

See also

  • The shader library's complex module: the same functions on a float2 inside a shader, plus complexPlane to frame a layer as the plane and domainColor to paint a value.
  • Domain coloring: the built-in generator that paints a named function or a rational one over the plane.
  • Fractals: the escape-time generators, the Kleinian and Schottky limit sets, all of them this arithmetic iterated.
  • Geometry: Vector2, the value a Complex shares its coordinates with.
  • Worked examples: Examples/Shaders/ComplexPlane, a shader coloring the ratio of two moving points and draw() laying the circles that ratio is made of over it; ImaginaryLog, the imaginary part of that ratio's logarithm through a cosine palette, with the branch cut as a seam; and Meromorphic, a ratio of two cubics built from six roots that move as Complex values.

↑↓ move ↩ open