Module: Unmagic::Color::Harmony

Included in:
Unmagic::Color
Defined in:
lib/unmagic/color/harmony.rb

Overview

Color harmony and variations module.

Provides methods for generating harmonious color palettes based on color theory principles. All calculations are performed in HSL color space for accurate hue-based relationships.

Included in the base Color class, making these methods available to RGB, HSL, and OKLCH color spaces via inheritance.

## Color Harmonies

Color harmonies are combinations of colors that are aesthetically pleasing based on their positions on the color wheel:

  • Complementary: Colors opposite on the wheel (180° apart)

  • Analogous: Colors adjacent on the wheel (typically 30° apart)

  • Triadic: Three colors evenly spaced (120° apart)

  • Split-complementary: Base color plus two colors adjacent to its complement

  • Tetradic: Four colors forming a rectangle or square on the wheel

## Color Variations

Create related colors by adjusting lightness or saturation:

  • Shades: Darker versions (reducing lightness)

  • Tints: Lighter versions (increasing lightness)

  • Tones: Less saturated versions (reducing saturation)

Examples:

Basic harmony usage

red = Unmagic::Color.parse("#FF0000")
red.complementary          # => #<RGB #00ffff>
red.triadic                # => [#<RGB ...>, #<RGB ...>]

Color variations

blue = Unmagic::Color.parse("#0000FF")
blue.shades(steps: 3)      # => [darker1, darker2, darker3]
blue.tints(steps: 3)       # => [lighter1, lighter2, lighter3]

Instance Method Summary collapse

Instance Method Details

#analogous(angle: 30) ⇒ Array<RGB, HSL, OKLCH>

Returns two analogous colors (adjacent on the color wheel).

Analogous colors create harmonious, cohesive designs. They’re often found in nature and produce a calm, comfortable feel.

Examples:

Default 30° separation

red = Unmagic::Color.parse("#FF0000")
red.analogous
# => [#<RGB ...>, #<RGB ...>] (red-violet, red-orange)

Custom 15° separation

red.analogous(angle: 15)

Parameters:

  • angle (Numeric) (defaults to: 30)

    Degrees of separation from the base color (default: 30)

Returns:

  • (Array<RGB, HSL, OKLCH>)

    Two colors [-angle, +angle] from the base



73
74
75
# File 'lib/unmagic/color/harmony.rb', line 73

def analogous(angle: 30)
  [rotate_hue(-angle), rotate_hue(angle)]
end

#complementaryRGB, ...

Returns the complementary color (180° opposite on the color wheel).

Complementary colors create high contrast and visual tension. They’re effective for creating emphasis and drawing attention.

Examples:

red = Unmagic::Color.parse("#FF0000")
red.complementary
# => #<RGB #00ffff> (cyan)

Returns:

  • (RGB, HSL, OKLCH)

    The complementary color (same type as self)



54
55
56
# File 'lib/unmagic/color/harmony.rb', line 54

def complementary
  rotate_hue(180)
end

#monochromatic(steps: 5) ⇒ Array<RGB, HSL, OKLCH>

Returns an array of colors with varying lightness (same hue).

Creates a monochromatic palette by generating colors across a lightness range while preserving hue and saturation.

Examples:

blue = Unmagic::Color.parse("#0000FF")
blue.monochromatic(steps: 5)
# => [very dark blue, dark blue, medium blue, light blue, very light blue]

Parameters:

  • steps (Integer) (defaults to: 5)

    Number of colors to generate (default: 5)

Returns:

  • (Array<RGB, HSL, OKLCH>)

    Colors with lightness from 15% to 85%

Raises:

  • (ArgumentError)


150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/unmagic/color/harmony.rb', line 150

def monochromatic(steps: 5)
  raise ArgumentError, "steps must be at least 1" if steps < 1

  hsl = to_hsl
  min_lightness = 15.0
  max_lightness = 85.0
  step_size = (max_lightness - min_lightness) / (steps - 1).to_f

  (0...steps).map do |i|
    lightness = min_lightness + (i * step_size)
    result = HSL.new(
      hue: hsl.hue.value,
      saturation: hsl.saturation.value,
      lightness: lightness,
      alpha: hsl.alpha.value,
    )
    convert_harmony_result(result)
  end
end

#shades(steps: 5, amount: 0.5) ⇒ Array<RGB, HSL, OKLCH>

Returns an array of progressively darker colors (shades).

Shades are created by reducing lightness, simulating the effect of adding black to the original color.

Examples:

red = Unmagic::Color.parse("#FF0000")
red.shades(steps: 3)
# => [slightly darker red, darker red, darkest red]

Parameters:

  • steps (Integer) (defaults to: 5)

    Number of shades to generate (default: 5)

  • amount (Float) (defaults to: 0.5)

    Total amount of darkening 0.0-1.0 (default: 0.5)

Returns:

  • (Array<RGB, HSL, OKLCH>)

    Progressively darker colors

Raises:

  • (ArgumentError)


183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/unmagic/color/harmony.rb', line 183

def shades(steps: 5, amount: 0.5)
  raise ArgumentError, "steps must be at least 1" if steps < 1

  hsl = to_hsl
  step_amount = amount / steps.to_f

  (1..steps).map do |i|
    new_lightness = hsl.lightness.value * (1 - (step_amount * i))
    result = HSL.new(
      hue: hsl.hue.value,
      saturation: hsl.saturation.value,
      lightness: new_lightness.clamp(0, 100),
      alpha: hsl.alpha.value,
    )
    convert_harmony_result(result)
  end
end

#split_complementary(angle: 30) ⇒ Array<RGB, HSL, OKLCH>

Returns two split-complementary colors.

Split-complementary uses the two colors adjacent to the complement, providing high contrast with less tension than pure complementary.

Examples:

red = Unmagic::Color.parse("#FF0000")
red.split_complementary
# => [#<RGB ...>, #<RGB ...>] (cyan-blue, cyan-green)

Parameters:

  • angle (Numeric) (defaults to: 30)

    Degrees from the complement (default: 30)

Returns:

  • (Array<RGB, HSL, OKLCH>)

    Two colors at (180-angle)° and (180+angle)°



104
105
106
# File 'lib/unmagic/color/harmony.rb', line 104

def split_complementary(angle: 30)
  [rotate_hue(180 - angle), rotate_hue(180 + angle)]
end

#tetradic_rectangle(angle: 60) ⇒ Array<RGB, HSL, OKLCH>

Returns three tetradic colors forming a rectangle on the color wheel.

Rectangular tetradic uses two complementary pairs with configurable spacing. This provides flexibility between harmony and contrast.

Examples:

red = Unmagic::Color.parse("#FF0000")
red.tetradic_rectangle(angle: 60)

Parameters:

  • angle (Numeric) (defaults to: 60)

    Degrees between first pair (default: 60)

Returns:

  • (Array<RGB, HSL, OKLCH>)

    Three colors at angle°, 180°, (180angle)°



134
135
136
# File 'lib/unmagic/color/harmony.rb', line 134

def tetradic_rectangle(angle: 60)
  [rotate_hue(angle), rotate_hue(180), rotate_hue(180 + angle)]
end

#tetradic_squareArray<RGB, HSL, OKLCH>

Returns three tetradic colors forming a square on the color wheel.

Square tetradic uses four colors evenly spaced (90° apart). This creates a rich, bold color scheme with equal visual weight.

Examples:

red = Unmagic::Color.parse("#FF0000")
red.tetradic_square
# => [#<RGB ...>, #<RGB ...>, #<RGB ...>]

Returns:

  • (Array<RGB, HSL, OKLCH>)

    Three colors at 90°, 180°, +270°



119
120
121
# File 'lib/unmagic/color/harmony.rb', line 119

def tetradic_square
  [rotate_hue(90), rotate_hue(180), rotate_hue(270)]
end

#tints(steps: 5, amount: 0.5) ⇒ Array<RGB, HSL, OKLCH>

Returns an array of progressively lighter colors (tints).

Tints are created by increasing lightness, simulating the effect of adding white to the original color.

Examples:

blue = Unmagic::Color.parse("#0000FF")
blue.tints(steps: 3)
# => [slightly lighter blue, lighter blue, lightest blue]

Parameters:

  • steps (Integer) (defaults to: 5)

    Number of tints to generate (default: 5)

  • amount (Float) (defaults to: 0.5)

    Total amount of lightening 0.0-1.0 (default: 0.5)

Returns:

  • (Array<RGB, HSL, OKLCH>)

    Progressively lighter colors

Raises:

  • (ArgumentError)


214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/unmagic/color/harmony.rb', line 214

def tints(steps: 5, amount: 0.5)
  raise ArgumentError, "steps must be at least 1" if steps < 1

  hsl = to_hsl
  step_amount = amount / steps.to_f

  (1..steps).map do |i|
    new_lightness = hsl.lightness.value + (100 - hsl.lightness.value) * (step_amount * i)
    result = HSL.new(
      hue: hsl.hue.value,
      saturation: hsl.saturation.value,
      lightness: new_lightness.clamp(0, 100),
      alpha: hsl.alpha.value,
    )
    convert_harmony_result(result)
  end
end

#tones(steps: 5, amount: 0.5) ⇒ Array<RGB, HSL, OKLCH>

Returns an array of progressively desaturated colors (tones).

Tones are created by reducing saturation, simulating the effect of adding gray to the original color.

Examples:

red = Unmagic::Color.parse("#FF0000")
red.tones(steps: 3)
# => [slightly muted red, more muted red, grayish red]

Parameters:

  • steps (Integer) (defaults to: 5)

    Number of tones to generate (default: 5)

  • amount (Float) (defaults to: 0.5)

    Total amount of desaturation 0.0-1.0 (default: 0.5)

Returns:

  • (Array<RGB, HSL, OKLCH>)

    Progressively less saturated colors

Raises:

  • (ArgumentError)


245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/unmagic/color/harmony.rb', line 245

def tones(steps: 5, amount: 0.5)
  raise ArgumentError, "steps must be at least 1" if steps < 1

  hsl = to_hsl
  step_amount = amount / steps.to_f

  (1..steps).map do |i|
    new_saturation = hsl.saturation.value * (1 - (step_amount * i))
    result = HSL.new(
      hue: hsl.hue.value,
      saturation: new_saturation.clamp(0, 100),
      lightness: hsl.lightness.value,
      alpha: hsl.alpha.value,
    )
    convert_harmony_result(result)
  end
end

#triadicArray<RGB, HSL, OKLCH>

Returns two triadic colors (evenly spaced 120° on the color wheel).

Triadic colors offer strong visual contrast while retaining harmony. They tend to be vibrant even when using pale or unsaturated versions.

Examples:

red = Unmagic::Color.parse("#FF0000")
red.triadic
# => [#<RGB ...>, #<RGB ...>] (green-ish, blue-ish)

Returns:

  • (Array<RGB, HSL, OKLCH>)

    Two colors at 120° and 240°



88
89
90
# File 'lib/unmagic/color/harmony.rb', line 88

def triadic
  [rotate_hue(120), rotate_hue(240)]
end