Class: GSpotMichael

Inherits:
Object
  • Object
show all
Defined in:
lib/g_spot_michael.rb

Defined Under Namespace

Classes: Swatch

Constant Summary collapse

TARGET_DARK_LUMA =
0.26
MAX_DARK_LUMA =
0.45
MIN_LIGHT_LUMA =
0.55
TARGET_LIGHT_LUMA =
0.74
MIN_NORMAL_LUMA =
0.3
TARGET_NORMAL_LUMA =
0.5
MAX_NORMAL_LUMA =
0.7
TARGET_MUTED_SATURATION =
0.3
MAX_MUTED_SATURATION =
0.4
TARGET_VIBRANT_SATURATION =
1
MIN_VIBRANT_SATURATION =
0.35
WEIGHT_SATURATION =
3
WEIGHT_LUMA =
6
WEIGHT_POPULATION =
1

Instance Method Summary collapse

Constructor Details

#initialize(file, colorCount = 64, quality = '5%') ⇒ GSpotMichael

Returns a new instance of GSpotMichael.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/g_spot_michael.rb', line 25

def initialize(file, colorCount=64, quality='5%')
  @HighestPopulation = 0

  # we're going to take the original file,
  # reduce the size, quantize the colors,
  # and then return an array of distinct
  # RGBs with their occurence counts(as pairs)
  file.rewind
  results = run_command "convert - -scale '#{quality}' +dither -colors #{colorCount} txt:", file.read.force_encoding("UTF-8")
  pixels  = results.split("\n").map{|x| (r = x.match(/srgb\((\d{1,3},\d{1,3},\d{1,3})\)/)) ? r[1] : nil}.compact.map{|p| p.split(",").map(&:to_i)}
  cmap    = {}
  pixels.each do |pixel|
    # If pixel is mostly opaque and not white
    if !(pixel[0] > 250 && pixel[1] > 250 && pixel[2] > 250)
      cmap[pixel] ||= 0
      cmap[pixel] += 1
    end
  end
  
  @swatches = cmap.map do |vbox|
    Swatch.new vbox[0], vbox[1]  # values, count
  end

  @maxPopulation     = find_max_population
  @HighestPopulation = @maxPopulation

  generate_variation_colors
  generate_empty_swatches
end

Instance Method Details

#create_comparison_value(saturation, targetSaturation, luma, targetLuma, population, maxPopulation) ⇒ Object



110
111
112
113
114
115
116
117
# File 'lib/g_spot_michael.rb', line 110

def create_comparison_value(saturation, targetSaturation, luma, targetLuma, population, maxPopulation)
  # pop = (maxPopulation != 0) ? (population / maxPopulation) : 0.0
  weighted_mean(
    invert_diff(saturation, targetSaturation), WEIGHT_SATURATION,
    invert_diff(luma, targetLuma), WEIGHT_LUMA,
    (population / maxPopulation), WEIGHT_POPULATION
  )
end

#find_color_variation(targetLuma, minLuma, maxLuma, targetSaturation, minSaturation, maxSaturation) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/g_spot_michael.rb', line 91

def find_color_variation (targetLuma, minLuma, maxLuma, targetSaturation, minSaturation, maxSaturation)
  max      = nil
  maxValue = 0
  
  @swatches.each do |swatch|
    sat  = swatch.hsl[1]
    luma = swatch.hsl[2]
    if !is_already_selected(swatch) && sat >= minSaturation && sat <= maxSaturation && luma >= minLuma && luma <= maxLuma
      value = create_comparison_value sat, targetSaturation, luma, targetLuma, swatch.population, @HighestPopulation
      if !max || value > maxValue
        max      = swatch
        maxValue = value
      end
    end
  end

  max
end

#find_max_populationObject



86
87
88
89
# File 'lib/g_spot_michael.rb', line 86

def find_max_population
  population = @swatches.map{|swatch| [0, swatch.population].max }.max
  population
end

#generate_empty_swatchesObject



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/g_spot_michael.rb', line 64

def generate_empty_swatches
  if !@VibrantSwatch
    # If we do not have a vibrant color...
    if @DarkVibrantSwatch
      # ...but we do have a dark vibrant, generate the value by modifying the luma
      hsl    = @DarkVibrantSwatch.hsl
      hsl[2] = TARGET_NORMAL_LUMA
      @VibrantSwatch = Swatch.new hslToRgb(hsl[0], hsl[1], hsl[2]), 0
    end
  end

  if !@DarkVibrantSwatch
    # If we do not have a vibrant color...
    if @VibrantSwatch
      # ...but we do have a dark vibrant, generate the value by modifying the luma
      hsl    = @VibrantSwatch.hsl
      hsl[2] = TARGET_DARK_LUMA
      @DarkVibrantSwatch = Swatch.new hslToRgb(hsl[0], hsl[1], hsl[2]), 0
    end
  end
end

#generate_variation_colorsObject



55
56
57
58
59
60
61
62
# File 'lib/g_spot_michael.rb', line 55

def generate_variation_colors
  @VibrantSwatch      = find_color_variation(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA, TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1)
  @LightVibrantSwatch = find_color_variation(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1, TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1)
  @DarkVibrantSwatch  = find_color_variation(TARGET_DARK_LUMA, 0, MAX_DARK_LUMA, TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1)
  @MutedSwatch        = find_color_variation(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA, TARGET_MUTED_SATURATION, 0, MAX_MUTED_SATURATION)
  @LightMutedSwatch   = find_color_variation(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1, TARGET_MUTED_SATURATION, 0, MAX_MUTED_SATURATION)
  @DarkMutedSwatch    = find_color_variation(TARGET_DARK_LUMA, 0, MAX_DARK_LUMA, TARGET_MUTED_SATURATION, 0, MAX_MUTED_SATURATION)
end

#hslToRgb(h, s, l) ⇒ Object



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/g_spot_michael.rb', line 157

def hslToRgb(h, s, l)
  r = nil
  g = nil
  b = nil

  hue2rgb = Proc.new { |p, q, t|
    if t < 0
      t += 1
    end
    if t > 1
      t -= 1
    end
    if t < 1 / 6
      return p + (q - p) * 6 * t
    end
    if t < 1 / 2
      return q
    end
    if t < 2 / 3
      return p + (q - p) * (2 / 3 - t) * 6
    end
    p
  }

  if s == 0
    r = g = b = l
    # achromatic
  else
    q = (l < 0.5) ? (l * (1 + s)) : (l + s - (l * s))
    p = 2 * l - q
    r = hue2rgb.call(p, q, h + 1 / 3)
    g = hue2rgb.call(p, q, h)
    b = hue2rgb.call(p, q, h - (1 / 3))
  end
  [
    r * 255,
    g * 255,
    b * 255
  ]
end

#invert_diff(value, targetValue) ⇒ Object



119
120
121
# File 'lib/g_spot_michael.rb', line 119

def invert_diff (value, targetValue)
  1 - (value - targetValue).abs
end

#is_already_selected(swatch) ⇒ Object



148
149
150
151
152
153
154
155
# File 'lib/g_spot_michael.rb', line 148

def is_already_selected(swatch)
  @VibrantSwatch == swatch || 
  @DarkVibrantSwatch == swatch ||
  @LightVibrantSwatch == swatch || 
  @MutedSwatch == swatch ||
  @DarkMutedSwatch == swatch || 
  @LightMutedSwatch == swatch
end

#run_command(command, input) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/g_spot_michael.rb', line 198

def run_command command, input
  stdin, stdout, stderr, wait_thr = Open3.popen3(command)
  pid = wait_thr.pid

  Timeout.timeout(10) do # cancel in 10 seconds
    stdin.write input
    stdin.close

    output_buffer = []
    error_buffer  = []

    while (output_chunk = stdout.gets) || (error_chunk = stderr.gets)
      output_buffer << output_chunk
      error_buffer  << error_chunk
    end

    output_buffer.compact!
    error_buffer.compact!

    output = output_buffer.any? ? output_buffer.join('') : nil
    error  = error_buffer.any?  ? error_buffer.join('')  : nil

    unless error
      raise StandardError.new("No output received.") if !output
      return output
    else
      raise StandardError.new(error)
    end
  end
rescue Timeout::Error, StandardError, Errno::EPIPE => e
  e
ensure
  begin
    Process.kill("KILL", pid) if pid
  rescue Errno::ESRCH
    # Process is already dead so do nothing.
  end
  stdin  = nil
  stdout = nil
  stderr = nil
  wait_thr.value if wait_thr # Process::Status object returned.
end

#swatchesObject



137
138
139
140
141
142
143
144
145
146
# File 'lib/g_spot_michael.rb', line 137

def swatches
  {
    Vibrant: @VibrantSwatch,
    Muted: @MutedSwatch,
    DarkVibrant: @DarkVibrantSwatch,
    DarkMuted: @DarkMutedSwatch,
    LightVibrant: @LightVibrantSwatch,
    LightMuted: @LightMutedSwatch
  }
end

#weighted_mean(*values) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/g_spot_michael.rb', line 123

def weighted_mean(*values)
  sum = 0
  sumWeight = 0
  i = 0
  while i < values.length
    value = values[i]
    weight = values[i + 1]
    sum += value * weight
    sumWeight += weight
    i += 2
  end
  sum / sumWeight
end