What are the options for uniquely coloring the torus?

Firstly, apologies if this belongs on another site like the computer graphics stack exchange.

For some mathematical visualizations, I am trying to find a way to smoothly color the torus in such a way that each point gets a unique color. I tried starting with using the hue and lightness (based on a stackexchange post I can’t find anymore. On the square, this looks like:

#import "@preview/cetz:0.4.2"

#set page(
  width: auto,
  height: auto,
  margin: 1mm,
)

#cetz.canvas({
  import cetz.draw: *

  let steps = 25
  let step-size = 1.0 / steps

  // Pre-compute colors to avoid recomputation
  let colors = {
    let cols = ()
    for i in range(steps + 1) {
      let x = i * step-size
      let row = ()
      for j in range(steps + 1) {
        let y = j * step-size
        let hue = x * 360deg
        let saturation = 50% + 50% * calc.sin(2 * calc.pi * y)
        let value = 50% + 50% * calc.cos(2 * calc.pi * y)
        row.push(color.hsv(hue, saturation, value))
      }
      cols.push(row)
    }
    cols
  }

  // Draw rectangles with precomputed colors
  for i in range(steps) {
    for j in range(steps) {
      let x = i * step-size
      let y = j * step-size

      rect(
        (x, y),
        (x + step-size, y + step-size),
        fill: colors.at(i).at(j),
        stroke: none
      )
    }
  }
})

(as a sidenote, I’d also like to figure out why some white lines are still being made here despite me setting the stroke to none). Note that, on the square, smoothly coloring the torus requires that the right edge matches the left edge colors, and similarly between the top and bottom edges.

On the torus, this looks like:

#import "@preview/cetz:0.4.2"

#set page(
  width: auto,
  height: auto,
  margin: 1mm,
)

#cetz.canvas({
  import cetz.draw: *
  
  let R = 4      // Major radius
  let r = 1      // Minor radius
  let theta-divisions = 600
  let phi-divisions = 300
  
  let get-torus-point(theta, phi) = (
    (R + r * calc.cos(phi)) * calc.cos(theta),
    (R + r * calc.cos(phi)) * calc.sin(theta),
    r * calc.sin(phi)
  )
  
  // Pre-compute color grid
  let colors = {
    let cols = ()
    for i in range(theta-divisions + 1) {
      let x = i / theta-divisions  // normalized theta coordinate
      let row = ()
      for j in range(phi-divisions + 1) {
        let y = j / phi-divisions  // normalized phi coordinate
        
        // Your doubly periodic coloring
        let hue = x * 360deg
        let saturation = 50% + 50% * calc.sin(2 * calc.pi * y)
        let value = 50% + 50% * calc.cos(2 * calc.pi * y)
        
        row.push(color.hsv(hue, saturation, value))
      }
      cols.push(row)
    }
    cols
  }
  
  ortho(x: -70deg, y: 0deg, z: -90deg, sorted: true, {
    for i in range(theta-divisions) {
      for j in range(phi-divisions) {
        let theta1 = (2 * calc.pi * i) / theta-divisions
        let theta2 = (2 * calc.pi * (i + 1)) / theta-divisions
        let phi1 = (2 * calc.pi * j) / phi-divisions
        let phi2 = (2 * calc.pi * (j + 1)) / phi-divisions
        
        let point1 = get-torus-point(theta1, phi1)
        let point2 = get-torus-point(theta2, phi1)
        let point3 = get-torus-point(theta2, phi2)
        let point4 = get-torus-point(theta1, phi2)
        
        line(
          point1,
          point2,
          point3,
          point4,
          close: true,
          stroke: none,
          fill: colors.at(i).at(j)
        )
      }
    }
  })
})

This is close, but the white and black colors form a couple singular points where there are duplicate colors along lines.

I don’t know if the structure of color spaces simply prohibits such a 2d gradient setup for the square/torus, but I can’t find anything in the literature, especially if I try to add on the additional constraint of being CVD friendly.

Thus, does anyone have any ideas for making a 2D gradient for the torus that:

  1. gives each point a unique color,
  2. smoothly colors the torus, with no discontinuities along lines, (and, ideally)
  3. maintains CVD friendliness?

Thank you for your time.

For anyone wondering why I really don’t want even a single line of discontinuous colors, it’s because I would like to eventually provide smooth tilings of the torus by applying the square coloring multiple times, like below:

#import "@preview/cetz:0.4.2"

#set page(
  width: auto,
  height: auto,
  margin: 1mm,
)

#cetz.canvas({
  import cetz.draw: *
  
  let R = 4      // Major radius
  let r = 1      // Minor radius
  let theta-divisions = 600
  let phi-divisions = 300
  let theta-wraps = 12  // Number of times to wrap around theta (longitude)
  let phi-wraps = 5    // Number of times to wrap around phi (latitude)
  
  let get-torus-point(theta, phi) = (
    (R + r * calc.cos(phi)) * calc.cos(theta),
    (R + r * calc.cos(phi)) * calc.sin(theta),
    r * calc.sin(phi)
  )
  
  // Pre-compute color grid
  let colors = {
    let cols = ()
    for i in range(theta-divisions + 1) {
      let x = calc.rem(i / theta-divisions * theta-wraps, 1.0)
      let row = ()
      for j in range(phi-divisions + 1) {
        let y = calc.rem(j / phi-divisions * phi-wraps, 1.0)
        
        let hue = x * 360deg
        let saturation = 50% + 50% * calc.sin(2 * calc.pi * y)
        let value = 50% + 50% * calc.cos(2 * calc.pi * y)
        
        row.push(color.hsv(hue, saturation, value))
      }
      cols.push(row)
    }
    cols
  }
  
  ortho(x: -70deg, y: 0deg, z: -90deg, sorted: true, {
    for i in range(theta-divisions) {
      for j in range(phi-divisions) {
        let theta1 = (2 * calc.pi * i) / theta-divisions
        let theta2 = (2 * calc.pi * (i + 1)) / theta-divisions
        let phi1 = (2 * calc.pi * j) / phi-divisions
        let phi2 = (2 * calc.pi * (j + 1)) / phi-divisions
        
        let point1 = get-torus-point(theta1, phi1)
        let point2 = get-torus-point(theta2, phi1)
        let point3 = get-torus-point(theta2, phi2)
        let point4 = get-torus-point(theta1, phi2)
        
        line(
          point1,
          point2,
          point3,
          point4,
          close: true,
          stroke: none,
          fill: colors.at(i).at(j)
        )
      }
    }
    
    // Draw contour around a representative rectangle
    // This selects one tile from the wrapped pattern
    let contour-tile-theta = 11  // Which tile in theta direction (0 to theta-wraps-1)
    let contour-tile-phi = 0    // Which tile in phi direction (0 to phi-wraps-1)
    
    // Convert tile index to angular coordinates
    let tile-height-theta = (2 * calc.pi) / theta-wraps
    let tile-height-phi = (2 * calc.pi) / phi-wraps
    
    let contour-theta1 = contour-tile-theta * tile-height-theta
    let contour-theta2 = (contour-tile-theta + 1) * tile-height-theta
    let contour-phi1 = contour-tile-phi * tile-height-phi
    let contour-phi2 = (contour-tile-phi + 1) * tile-height-phi
    
    // Sample the contour boundary with many points for smoothness
    let contour-samples = 200
    let contour-points = ()
    
    // Bottom edge (constant phi1, varying theta)
    for k in range(contour-samples) {
      let theta = contour-theta1 + (contour-theta2 - contour-theta1) * k / contour-samples
      contour-points.push(get-torus-point(theta, contour-phi1))
    }
    
    // Right edge (constant theta2, varying phi)
    for k in range(contour-samples) {
      let phi = contour-phi1 + (contour-phi2 - contour-phi1) * k / contour-samples
      contour-points.push(get-torus-point(contour-theta2, phi))
    }
    
    // Top edge (constant phi2, varying theta backwards)
    for k in range(contour-samples) {
      let theta = contour-theta2 - (contour-theta2 - contour-theta1) * k / contour-samples
      contour-points.push(get-torus-point(theta, contour-phi2))
    }
    
    // Left edge (constant theta1, varying phi backwards)
    for k in range(contour-samples) {
      let phi = contour-phi2 - (contour-phi2 - contour-phi1) * k / contour-samples
      contour-points.push(get-torus-point(contour-theta1, phi))
    }
    
    // Draw the contour
    set-style(stroke: white + 1pt)
    line(..contour-points, close: true)
  })
})