Back to Articles

2026-02-10

Introducing the Kaleidoscope Tool

By Anthony Dito

Turn any image into a symmetric pattern with adjustable segments, rotation, and warp.

BrushCue now has a Kaleidoscope tool that turns any image into a symmetric, reflective pattern. It is a fast way to create geometric art, abstract textures, or hypnotic backgrounds from photos, illustrations, or gradients.

Kaleidoscope tool animation

The three controls that shape the look

The Kaleidoscope tool exposes three parameters so you can dial in the look.

Segments

Segments control how many mirrored wedges are arranged around the center.

Rotation

Rotation spins the effect around the center. Use it to animate in a video or to align details where you want them.

Warp

Warp adds a subtle radial distortion. When it is set above 0, the shader applies a sine-wave ripple to the radius as the pattern is generated, which makes the result feel more organic and glass-like instead of perfectly geometric. The Warp Frequency control sets how many ripples appear across the radius, so higher values mean tighter, more frequent waves. Set Warp to 0 for a clean, static kaleidoscope.

Tips for great results

How the Shader Works

BrushCue aims to avoid keeping secrets about how our effects work. To that end, here is the shader code for this effect if you are interested. You (or the AI assistant) can modify this code within BrushCue to get a custom effect.

@group(0) @binding(0) var input_texture: texture_2d<f32>;
@group(0) @binding(1) var<uniform> rotation: f32;
@group(0) @binding(2) var<uniform> segments: f32;
@group(0) @binding(3) var<uniform> warp: f32;
@group(0) @binding(4) var<uniform> warp_frequency: f32;

fn do_transformation(input: vec4<f32>, position: vec2<u32>) -> vec4<f32> {
// @start_function
  // Get normalized coordinates (0.0 to 1.0)
  let dims_u = textureDimensions(input_texture);
  let dims = vec2<f32>(dims_u);

  // Pixel-center UV (reduces half-texel bias vs vec2(position)/dims)
  let uv = (vec2<f32>(position) + vec2<f32>(0.5)) / dims;

  // Center to [-0.5, 0.5] space
  var p = uv - vec2<f32>(0.5);

  p = rotate2(p, rotation);

  // Polar
  let angle_raw = atan2(p.y, p.x);
  var r = length(p);

  // Optional: subtle radial warp to feel more "glass-like"
  // (set warp=0.0 to disable)
  r = r + warp * sin(r * warp_frequency);

  // Kaleidoscope fold (clean wedge mirror)
  let TAU = 6.28318530718;
  let seg = TAU / segments;

  // Map angle into [0, seg), then mirror fold into [0, seg/2]
  var a = fract((angle_raw + TAU) / seg) * seg;
  a = abs(a - 0.5 * seg);

  // Radius scale (controls zoom/coverage)
  let r2 = r * 1.5;

  // Back to UV
  var k = vec2<f32>(cos(a), sin(a)) * r2 + vec2<f32>(0.5);

  // Prefer mirror wrap to avoid "stuck-to-edge" clamp rings
  k = vec2<f32>(mirror01(k.x), mirror01(k.y));

  return sample(input_texture, k);
// @end_function
}

// @start_helpers
// Helper function for safe texture sampling (nearest-neighbor)
fn sample_at(texture: texture_2d<f32>, uv: vec2<f32>) -> vec4<f32> {
  let dims = textureDimensions(texture);
  let clamped = clamp(uv, vec2<f32>(0.0), vec2<f32>(0.9999));
  let pos = vec2<u32>(clamped * vec2<f32>(dims));
  return textureLoad(texture, pos, 0);
}

// Mirror-wrap a scalar into [0, 1] with reflected repeats.
// This tends to look more "kaleidoscope-like" than clamping.
fn mirror01(x: f32) -> f32 {
  let t = fract(x);
  return 1.0 - abs(2.0 * t - 1.0);
}

// 2D rotation around origin
fn rotate2(v: vec2<f32>, radians: f32) -> vec2<f32> {
  let c = cos(radians);
  let s = sin(radians);
  return vec2<f32>(c * v.x - s * v.y, s * v.x + c * v.y);
}
// @end_helpers
fn sample(tex: texture_2d<f32>, uv: vec2<f32>) -> vec4<f32>; // function implemented for you

One key thing to notice is that we use the "sample" function for this effect. It is provided for you, and using it here keeps transitions smooth and avoids aliasing.