Skip to main content

Colors and Coordinates

Learn detailed color manipulation and coordinate transformations.

Understanding Color Systems

Color Blending

Mix two colors together smoothly:

Live Editor
const fragment = () => {
      // Two colors to blend
      const color1 = vec3(1, 0.2, 0.3) // Reddish
      const color2 = vec3(0.2, 0.6, 1) // Bluish
      // X coordinate determines blend ratio
      const mixRatio = uv.x
      // Blend the colors
      const blendedColor = mix(color1, color2, mixRatio)
      return vec4(blendedColor, 1)
}

What mix() does:

  • When mixRatio = 0: returns color1
  • When mixRatio = 1: returns color2
  • When mixRatio = 0.5: returns halfway between both colors

RGB vs HSV

RGB (Red, Green, Blue) is like mixing colored lights, while HSV (Hue, Saturation, Value) is more intuitive for artists.

RGB: Mix red, green, and blue light HSV: Pick a color (hue), make it vibrant (saturation), make it bright (value)

Live Editor
const fragment = () => {
      // Proven HSV to RGB conversion
      const hsv2rgb = Fn(([h, s, v]) => {
              const k = vec4(1, 2 / 3, 1 / 3, 3)
              const p = k.xyz.add(h).fract().mul(6).sub(k.www).abs()
              return mix(k.xxx, p.sub(k.xxx).clamp(0, 1), s).mul(v)
      })
      // HSV-style color creation
      const h = uv.x // Hue from position
      const s = 0.75 // Saturation
      const v = uv.y // Value / brightness
      // Convert HSV to RGB using the function
      const rgb = hsv2rgb(h, s, v)
      return vec4(rgb, 1)
}

RGB vs XYZ

The CIE color space represents colors as chromaticity coordinates, independent of brightness. It's visualized as the famous "horseshoe" chromaticity diagram.

How it works:

  • x, y coordinates represent color hue and saturation
  • Pure colors form the curved boundary (spectral locus)
  • Mixed colors appear inside the horseshoe shape
  • White point is at the center
Live Editor
const fragment = () => {
      // XYZ to RGB conversion function
      const XYZ2RGB = mat3(
              3.2406, -1.5372, -0.4986,
              -0.9689, 1.8758, 0.0415,
              0.0557, -0.2040, 1.0570
      ).constant()
      const xyz2rgb = Fn(([x, y, z]) => {
              return XYZ2RGB.mul(vec3(x, y, z)).clamp(0, 1)
      })
      // CIE xy chromaticity coordinates from UV
      const x = uv.x
      const y = uv.y
      const Y = 1 // Full brightness
      const X = x.mul(Y).div(y)
      const Z = x.add(y).oneMinus().mul(Y).div(y)
      // Convert XYZ to RGB
      const rgb = xyz2rgb(X, Y, Z)
      return vec4(rgb, 1)
}

Coordinate Transformations

Moving, Scaling, and Rotating

Think of coordinates like a piece of paper you can move, resize, and turn:

Live Editor
const fragment = () => {
      // Move coordinates (translation)
      const offset = vec2(0.3, -0.2)
      const scale = 2
      // Rotate coordinates using rotation matrix
      const cosA = cos(0.5)
      const sinA = sin(0.5)
      const rotateMat = mat2(cosA, sinA, sinA.negate(), cosA)
      const rotatePos = rotateMat.mul(uv).add(offset).mul(scale)
      // Use transformed coordinates for color
      const color = vec3(rotatePos, 0.5)
      return vec4(color, 1)
}

Polar Coordinates

Sometimes it's easier to think in circles rather than squares:

Live Editor
const fragment = () => {
      // Convert to polar coordinates (radius and angle)
      const center = uv.sub(0.5)
      const angle = atan2(center.y, center.x) // Angle around center
      const radius = center.length() // Distance from center
      // Use radius for brightness
      const brightness = radius.oneMinus()
      const color = vec3(
              angle.sin(),
              angle.add(2).cos(),
              angle.add(4).sin()
      )
      return vec4(color.mul(brightness), 1)
}

What this creates: A circular rainbow that fades to black at the edges.

Creating Patterns

Checkerboard Pattern

Live Editor
const fragment = () => {
      // Divide screen into grid cells
      const scale = 8
      // Get cell indices as vec2
      const cell = uv.mul(scale).floor()
      // Checkerboard pattern
      const checker = mod(cell.x.add(cell.y), 2)
      // Alternate between white and black
      const color = vec3(checker)
      return vec4(color, 1)
}

Polka Dot Patterns

Live Editor
const fragment = () => {
      // Create repeating pattern and circular dots
      const scale = 10
      const dot = step(uv.mul(scale).fract().sub(0.5).length(), 0.3)
      // Create colored dots
      const color = vec3(0.4, 0.8, 1).mul(dot)
      return vec4(color, 1)
}

Circular Patterns

Live Editor
const fragment = () => {
      // Distance from center
      const center = uv.sub(0.5)
      const distance = center.length()
      // Concentric circles
      const ringFreq = 30
      const rings = distance.mul(ringFreq).sin()
      // Smooth ring edges
      const ringWidth = 0.1
      const smoothRings = smoothstep(0, ringWidth, rings)
      // Combine with gradient
      const gradient = distance.oneMinus()
      const pattern = smoothRings.mul(gradient)
      const color = vec3(1, 0.8, 1.2).mul(pattern)
      return vec4(color, 1)
}

Useful Effects

Vignette Effect

Darken the edges of the screen:

Live Editor
const fragment = () => {
      // Base color
      const baseColor = vec3(0.8, 0.9, 1)
      // Vignette based on distance from center
      const center = uv.sub(0.5)
      const distance = center.length()
      const vignette = smoothstep(0.8, 0.2, distance)
      const finalColor = baseColor.mul(vignette)
      return vec4(finalColor, 1)
}

Grid Lines

Live Editor
const fragment = () => {
      // Grid settings
      const gridSize = 0.1
      const lineWidth = 0.05
      // Calculate grid lines
      const grid = uv.div(gridSize).fract().sub(0.5).abs()
      const minGrid = min(grid.x, grid.y)
      const gridLines = smoothstep(0, lineWidth, minGrid)
      // Background color
      const backgroundColor = vec3(0.1, 0.1, 0.2)
      // Grid color
      const gridColor = vec3(0.3, 0.3, 0.5)
      // Blend background and grid
      const finalColor = mix(gridColor, backgroundColor, gridLines)
      return vec4(finalColor, 1)
}

Math Functions

Wave Functions

These create repeating patterns:

Live Editor
const fragment = () => {
      // Sine wave: smooth up and down motion
      const sine = uv.x.mul(8).sin()
      // Sawtooth wave: ramp up, then jump down
      const sawtooth = uv.x.mul(4).fract()
      // Square wave: switches between high and low
      const squareInput = uv.x.mul(4).fract()
      const square = step(0.5, squareInput)
      // Choose wave based on Y position
      const waveSelect = uv.y
      const selectThreshold1 = step(1 / 3, waveSelect)
      const wave1 = mix(sine, sawtooth, selectThreshold1)
      const selectThreshold2 = step(2 / 3, waveSelect)
      const wave2 = mix(wave1, square, selectThreshold2)
      const color = vec3(1, 0.8, 1.2).mul(wave2)
      return vec4(color, 1)
}

Random Patterns

Create seemingly random variations:

Live Editor
const fragment = () => {
      // Grid cells
      const scale = 10
      const cellPos = uv.mul(scale).floor()
      // Pseudo-random function
      const random = Fn(([xy]) => {
              return vec2(12.9898, 78.233).dot(xy).sin().mul(43758.5453123).fract()
      })
      // Random color per cell
      const color = vec3(
              random(cellPos),
              random(vec2(1, 0).add(cellPos)),
              random(vec2(0, 1).add(cellPos))
      )
      return vec4(color, 1)
}

Fractal Noise

Create complex organic patterns by layering simple noise:

Live Editor
const fragment = () => {
      const random = Fn(([xy]) => {
              return vec2(12.9898, 78.233).dot(xy).sin().mul(43758.5453).fract()
      })
      const noise = Fn(([xy]) => {
              const i = xy.floor().toVar()
              const j = xy.fract().toVar()
              const k = j.sub(2).negate()
              const u = j.mul(j).mul(k).toVar()
              return mix(
                      mix(random(i), random(vec2(1, 0).add(i)), u.x),
                      mix(random(vec2(0, 1).add(i)), random(vec2(1, 1).add(i)), u.x),
                      u.y
              )
      })
      const fbm = Fn(([xy]) => {
              return noise(xy).mul(0.5)
                      .add(noise(xy.mul(2)).mul(0.25))
                      .add(noise(xy.mul(4)).mul(0.125))
                      .add(noise(xy.mul(8)).mul(0.0625))
      })
      const fbmInput = uv.mul(8)
      const color = fbm(fbmInput)
      return vec4(vec3(color), 1)
}

What You've Learned

  • ✅ RGB and HSV color systems
  • ✅ Color blending and adjustments
  • ✅ Coordinate transformations (move, scale, rotate)
  • ✅ Polar coordinate system
  • ✅ Pattern generation
  • ✅ Mathematical function applications

Next Steps

Now that you understand colors and coordinates, learn about Time and Animation to make your visuals move and change dynamically.