hotcocoa 0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. data/History.txt +4 -0
  2. data/bin/hotcocoa +31 -0
  3. data/lib/hotcocoa/application_builder.rb +320 -0
  4. data/lib/hotcocoa/attributed_string.rb +143 -0
  5. data/lib/hotcocoa/behaviors.rb +7 -0
  6. data/lib/hotcocoa/data_sources/combo_box_data_source.rb +44 -0
  7. data/lib/hotcocoa/data_sources/table_data_source.rb +18 -0
  8. data/lib/hotcocoa/delegate_builder.rb +85 -0
  9. data/lib/hotcocoa/graphics/canvas.rb +836 -0
  10. data/lib/hotcocoa/graphics/color.rb +781 -0
  11. data/lib/hotcocoa/graphics/elements/particle.rb +75 -0
  12. data/lib/hotcocoa/graphics/elements/rope.rb +99 -0
  13. data/lib/hotcocoa/graphics/elements/sandpainter.rb +71 -0
  14. data/lib/hotcocoa/graphics/gradient.rb +63 -0
  15. data/lib/hotcocoa/graphics/image.rb +488 -0
  16. data/lib/hotcocoa/graphics/path.rb +325 -0
  17. data/lib/hotcocoa/graphics/pdf.rb +71 -0
  18. data/lib/hotcocoa/graphics.rb +161 -0
  19. data/lib/hotcocoa/kernel_ext.rb +14 -0
  20. data/lib/hotcocoa/kvo_accessors.rb +48 -0
  21. data/lib/hotcocoa/layout_view.rb +448 -0
  22. data/lib/hotcocoa/mapper.rb +227 -0
  23. data/lib/hotcocoa/mapping_methods.rb +40 -0
  24. data/lib/hotcocoa/mappings/alert.rb +25 -0
  25. data/lib/hotcocoa/mappings/application.rb +112 -0
  26. data/lib/hotcocoa/mappings/array_controller.rb +87 -0
  27. data/lib/hotcocoa/mappings/box.rb +39 -0
  28. data/lib/hotcocoa/mappings/button.rb +92 -0
  29. data/lib/hotcocoa/mappings/collection_view.rb +44 -0
  30. data/lib/hotcocoa/mappings/color.rb +28 -0
  31. data/lib/hotcocoa/mappings/column.rb +21 -0
  32. data/lib/hotcocoa/mappings/combo_box.rb +24 -0
  33. data/lib/hotcocoa/mappings/control.rb +33 -0
  34. data/lib/hotcocoa/mappings/font.rb +44 -0
  35. data/lib/hotcocoa/mappings/gradient.rb +15 -0
  36. data/lib/hotcocoa/mappings/image.rb +15 -0
  37. data/lib/hotcocoa/mappings/image_view.rb +43 -0
  38. data/lib/hotcocoa/mappings/label.rb +25 -0
  39. data/lib/hotcocoa/mappings/layout_view.rb +9 -0
  40. data/lib/hotcocoa/mappings/menu.rb +71 -0
  41. data/lib/hotcocoa/mappings/menu_item.rb +47 -0
  42. data/lib/hotcocoa/mappings/movie.rb +13 -0
  43. data/lib/hotcocoa/mappings/movie_view.rb +27 -0
  44. data/lib/hotcocoa/mappings/notification.rb +17 -0
  45. data/lib/hotcocoa/mappings/popup.rb +110 -0
  46. data/lib/hotcocoa/mappings/progress_indicator.rb +68 -0
  47. data/lib/hotcocoa/mappings/scroll_view.rb +29 -0
  48. data/lib/hotcocoa/mappings/search_field.rb +9 -0
  49. data/lib/hotcocoa/mappings/secure_text_field.rb +17 -0
  50. data/lib/hotcocoa/mappings/segmented_control.rb +97 -0
  51. data/lib/hotcocoa/mappings/slider.rb +25 -0
  52. data/lib/hotcocoa/mappings/sort_descriptor.rb +13 -0
  53. data/lib/hotcocoa/mappings/sound.rb +9 -0
  54. data/lib/hotcocoa/mappings/speech_synthesizer.rb +25 -0
  55. data/lib/hotcocoa/mappings/split_view.rb +21 -0
  56. data/lib/hotcocoa/mappings/status_bar.rb +7 -0
  57. data/lib/hotcocoa/mappings/status_item.rb +9 -0
  58. data/lib/hotcocoa/mappings/table_view.rb +110 -0
  59. data/lib/hotcocoa/mappings/text_field.rb +41 -0
  60. data/lib/hotcocoa/mappings/text_view.rb +13 -0
  61. data/lib/hotcocoa/mappings/timer.rb +25 -0
  62. data/lib/hotcocoa/mappings/toolbar.rb +97 -0
  63. data/lib/hotcocoa/mappings/toolbar_item.rb +36 -0
  64. data/lib/hotcocoa/mappings/view.rb +67 -0
  65. data/lib/hotcocoa/mappings/web_view.rb +22 -0
  66. data/lib/hotcocoa/mappings/window.rb +118 -0
  67. data/lib/hotcocoa/mappings/xml_parser.rb +41 -0
  68. data/lib/hotcocoa/mappings.rb +109 -0
  69. data/lib/hotcocoa/mvc.rb +175 -0
  70. data/lib/hotcocoa/notification_listener.rb +62 -0
  71. data/lib/hotcocoa/object_ext.rb +22 -0
  72. data/lib/hotcocoa/plist.rb +45 -0
  73. data/lib/hotcocoa/standard_rake_tasks.rb +17 -0
  74. data/lib/hotcocoa/template.rb +27 -0
  75. data/lib/hotcocoa/virtual_file_system.rb +172 -0
  76. data/lib/hotcocoa.rb +26 -0
  77. data/template/Rakefile +5 -0
  78. data/template/config/build.yml +8 -0
  79. data/template/lib/application.rb +45 -0
  80. data/template/lib/menu.rb +32 -0
  81. data/template/resources/HotCocoa.icns +0 -0
  82. data/test/test_helper.rb +3 -0
  83. data/test/test_hotcocoa.rb +11 -0
  84. metadata +137 -0
@@ -0,0 +1,781 @@
1
+ # Ruby Cocoa Graphics is a graphics library providing a simple object-oriented
2
+ # interface into the power of Mac OS X's Core Graphics and Core Image drawing libraries.
3
+ # With a few lines of easy-to-read code, you can write scripts to draw simple or complex
4
+ # shapes, lines, and patterns, process and filter images, create abstract art or visualize
5
+ # scientific data, and much more.
6
+ #
7
+ # Inspiration for this project was derived from Processing and NodeBox. These excellent
8
+ # graphics programming environments are more full-featured than RCG, but they are implemented
9
+ # in Java and Python, respectively. RCG was created to offer similar functionality using
10
+ # the Ruby programming language.
11
+ #
12
+ # Author:: James Reynolds (mailto:drtoast@drtoast.com)
13
+ # Copyright:: Copyright (c) 2008 James Reynolds
14
+ # License:: Distributes under the same terms as Ruby
15
+
16
+ module HotCocoa::Graphics
17
+
18
+ # define and manipulate colors in RGBA format
19
+ class Color
20
+
21
+ # License: GPL - includes ports of some code by Tom De Smedt, Frederik De Bleser
22
+
23
+ #attr_accessor :r,:g,:b,:a
24
+ attr_accessor :rgb
25
+ # def initialize(r=0.0,g=0.0,b=0.0,a=1.0)
26
+ # @c = CGColorCreate(@colorspace, [r,g,b,a])
27
+ # end
28
+
29
+ # create a new color with the given RGBA values
30
+ def initialize(r=0.0, g=0.0, b=1.0, a=1.0)
31
+ @nsColor = NSColor.colorWithDeviceRed r, green:g, blue:b, alpha:a
32
+ @rgb = @nsColor.colorUsingColorSpaceName NSDeviceRGBColorSpace
33
+ self
34
+ end
35
+
36
+ COLORNAMES = {
37
+ "lightpink" => [1.00, 0.71, 0.76],
38
+ "pink" => [1.00, 0.75, 0.80],
39
+ "crimson" => [0.86, 0.08, 0.24],
40
+ "lavenderblush" => [1.00, 0.94, 0.96],
41
+ "palevioletred" => [0.86, 0.44, 0.58],
42
+ "hotpink" => [1.00, 0.41, 0.71],
43
+ "deeppink" => [1.00, 0.08, 0.58],
44
+ "mediumvioletred" => [0.78, 0.08, 0.52],
45
+ "orchid" => [0.85, 0.44, 0.84],
46
+ "thistle" => [0.85, 0.75, 0.85],
47
+ "plum" => [0.87, 0.63, 0.87],
48
+ "violet" => [0.93, 0.51, 0.93],
49
+ "fuchsia" => [1.00, 0.00, 1.00],
50
+ "darkmagenta" => [0.55, 0.00, 0.55],
51
+ "purple" => [0.50, 0.00, 0.50],
52
+ "mediumorchid" => [0.73, 0.33, 0.83],
53
+ "darkviolet" => [0.58, 0.00, 0.83],
54
+ "darkorchid" => [0.60, 0.20, 0.80],
55
+ "indigo" => [0.29, 0.00, 0.51],
56
+ "blueviolet" => [0.54, 0.17, 0.89],
57
+ "mediumpurple" => [0.58, 0.44, 0.86],
58
+ "mediumslateblue" => [0.48, 0.41, 0.93],
59
+ "slateblue" => [0.42, 0.35, 0.80],
60
+ "darkslateblue" => [0.28, 0.24, 0.55],
61
+ "ghostwhite" => [0.97, 0.97, 1.00],
62
+ "lavender" => [0.90, 0.90, 0.98],
63
+ "blue" => [0.00, 0.00, 1.00],
64
+ "mediumblue" => [0.00, 0.00, 0.80],
65
+ "darkblue" => [0.00, 0.00, 0.55],
66
+ "navy" => [0.00, 0.00, 0.50],
67
+ "midnightblue" => [0.10, 0.10, 0.44],
68
+ "royalblue" => [0.25, 0.41, 0.88],
69
+ "cornflowerblue" => [0.39, 0.58, 0.93],
70
+ "lightsteelblue" => [0.69, 0.77, 0.87],
71
+ "lightslategray" => [0.47, 0.53, 0.60],
72
+ "slategray" => [0.44, 0.50, 0.56],
73
+ "dodgerblue" => [0.12, 0.56, 1.00],
74
+ "aliceblue" => [0.94, 0.97, 1.00],
75
+ "steelblue" => [0.27, 0.51, 0.71],
76
+ "lightskyblue" => [0.53, 0.81, 0.98],
77
+ "skyblue" => [0.53, 0.81, 0.92],
78
+ "deepskyblue" => [0.00, 0.75, 1.00],
79
+ "lightblue" => [0.68, 0.85, 0.90],
80
+ "powderblue" => [0.69, 0.88, 0.90],
81
+ "cadetblue" => [0.37, 0.62, 0.63],
82
+ "darkturquoise" => [0.00, 0.81, 0.82],
83
+ "azure" => [0.94, 1.00, 1.00],
84
+ "lightcyan" => [0.88, 1.00, 1.00],
85
+ "paleturquoise" => [0.69, 0.93, 0.93],
86
+ "aqua" => [0.00, 1.00, 1.00],
87
+ "darkcyan" => [0.00, 0.55, 0.55],
88
+ "teal" => [0.00, 0.50, 0.50],
89
+ "darkslategray" => [0.18, 0.31, 0.31],
90
+ "mediumturquoise" => [0.28, 0.82, 0.80],
91
+ "lightseagreen" => [0.13, 0.70, 0.67],
92
+ "turquoise" => [0.25, 0.88, 0.82],
93
+ "aquamarine" => [0.50, 1.00, 0.83],
94
+ "mediumaquamarine" => [0.40, 0.80, 0.67],
95
+ "mediumspringgreen" => [0.00, 0.98, 0.60],
96
+ "mintcream" => [0.96, 1.00, 0.98],
97
+ "springgreen" => [0.00, 1.00, 0.50],
98
+ "mediumseagreen" => [0.24, 0.70, 0.44],
99
+ "seagreen" => [0.18, 0.55, 0.34],
100
+ "honeydew" => [0.94, 1.00, 0.94],
101
+ "darkseagreen" => [0.56, 0.74, 0.56],
102
+ "palegreen" => [0.60, 0.98, 0.60],
103
+ "lightgreen" => [0.56, 0.93, 0.56],
104
+ "limegreen" => [0.20, 0.80, 0.20],
105
+ "lime" => [0.00, 1.00, 0.00],
106
+ "forestgreen" => [0.13, 0.55, 0.13],
107
+ "green" => [0.00, 0.50, 0.00],
108
+ "darkgreen" => [0.00, 0.39, 0.00],
109
+ "lawngreen" => [0.49, 0.99, 0.00],
110
+ "chartreuse" => [0.50, 1.00, 0.00],
111
+ "greenyellow" => [0.68, 1.00, 0.18],
112
+ "darkolivegreen" => [0.33, 0.42, 0.18],
113
+ "yellowgreen" => [0.60, 0.80, 0.20],
114
+ "olivedrab" => [0.42, 0.56, 0.14],
115
+ "ivory" => [1.00, 1.00, 0.94],
116
+ "beige" => [0.96, 0.96, 0.86],
117
+ "lightyellow" => [1.00, 1.00, 0.88],
118
+ "lightgoldenrodyellow" => [0.98, 0.98, 0.82],
119
+ "yellow" => [1.00, 1.00, 0.00],
120
+ "olive" => [0.50, 0.50, 0.00],
121
+ "darkkhaki" => [0.74, 0.72, 0.42],
122
+ "palegoldenrod" => [0.93, 0.91, 0.67],
123
+ "lemonchiffon" => [1.00, 0.98, 0.80],
124
+ "khaki" => [0.94, 0.90, 0.55],
125
+ "gold" => [1.00, 0.84, 0.00],
126
+ "cornsilk" => [1.00, 0.97, 0.86],
127
+ "goldenrod" => [0.85, 0.65, 0.13],
128
+ "darkgoldenrod" => [0.72, 0.53, 0.04],
129
+ "floralwhite" => [1.00, 0.98, 0.94],
130
+ "oldlace" => [0.99, 0.96, 0.90],
131
+ "wheat" => [0.96, 0.87, 0.07],
132
+ "orange" => [1.00, 0.65, 0.00],
133
+ "moccasin" => [1.00, 0.89, 0.71],
134
+ "papayawhip" => [1.00, 0.94, 0.84],
135
+ "blanchedalmond" => [1.00, 0.92, 0.80],
136
+ "navajowhite" => [1.00, 0.87, 0.68],
137
+ "antiquewhite" => [0.98, 0.92, 0.84],
138
+ "tan" => [0.82, 0.71, 0.55],
139
+ "burlywood" => [0.87, 0.72, 0.53],
140
+ "darkorange" => [1.00, 0.55, 0.00],
141
+ "bisque" => [1.00, 0.89, 0.77],
142
+ "linen" => [0.98, 0.94, 0.90],
143
+ "peru" => [0.80, 0.52, 0.25],
144
+ "peachpuff" => [1.00, 0.85, 0.73],
145
+ "sandybrown" => [0.96, 0.64, 0.38],
146
+ "chocolate" => [0.82, 0.41, 0.12],
147
+ "saddlebrown" => [0.55, 0.27, 0.07],
148
+ "seashell" => [1.00, 0.96, 0.93],
149
+ "sienna" => [0.63, 0.32, 0.18],
150
+ "lightsalmon" => [1.00, 0.63, 0.48],
151
+ "coral" => [1.00, 0.50, 0.31],
152
+ "orangered" => [1.00, 0.27, 0.00],
153
+ "darksalmon" => [0.91, 0.59, 0.48],
154
+ "tomato" => [1.00, 0.39, 0.28],
155
+ "salmon" => [0.98, 0.50, 0.45],
156
+ "mistyrose" => [1.00, 0.89, 0.88],
157
+ "lightcoral" => [0.94, 0.50, 0.50],
158
+ "snow" => [1.00, 0.98, 0.98],
159
+ "rosybrown" => [0.74, 0.56, 0.56],
160
+ "indianred" => [0.80, 0.36, 0.36],
161
+ "red" => [1.00, 0.00, 0.00],
162
+ "brown" => [0.65, 0.16, 0.16],
163
+ "firebrick" => [0.70, 0.13, 0.13],
164
+ "darkred" => [0.55, 0.00, 0.00],
165
+ "maroon" => [0.50, 0.00, 0.00],
166
+ "white" => [1.00, 1.00, 1.00],
167
+ "whitesmoke" => [0.96, 0.96, 0.96],
168
+ "gainsboro" => [0.86, 0.86, 0.86],
169
+ "lightgrey" => [0.83, 0.83, 0.83],
170
+ "silver" => [0.75, 0.75, 0.75],
171
+ "darkgray" => [0.66, 0.66, 0.66],
172
+ "gray" => [0.50, 0.50, 0.50],
173
+ "grey" => [0.50, 0.50, 0.50],
174
+ "dimgray" => [0.41, 0.41, 0.41],
175
+ "dimgrey" => [0.41, 0.41, 0.41],
176
+ "black" => [0.00, 0.00, 0.00],
177
+ "cyan" => [0.00, 0.68, 0.94],
178
+ #"transparent" => [0.00, 0.00, 0.00, 0.00],
179
+ "bark" => [0.25, 0.19, 0.13]
180
+ }
181
+
182
+ RYBWheel = [
183
+ [ 0, 0], [ 15, 8],
184
+ [ 30, 17], [ 45, 26],
185
+ [ 60, 34], [ 75, 41],
186
+ [ 90, 48], [105, 54],
187
+ [120, 60], [135, 81],
188
+ [150, 103], [165, 123],
189
+ [180, 138], [195, 155],
190
+ [210, 171], [225, 187],
191
+ [240, 204], [255, 219],
192
+ [270, 234], [285, 251],
193
+ [300, 267], [315, 282],
194
+ [330, 298], [345, 329],
195
+ [360, 0 ]
196
+ ]
197
+
198
+ COLORNAMES.each_key do |name|
199
+ (class << self; self; end).define_method(name) do
200
+ named(name)
201
+ end
202
+ end
203
+
204
+ # create a color with the specified name
205
+ def self.named(name)
206
+ if COLORNAMES[name]
207
+ r, g, b = COLORNAMES[name]
208
+ #puts "matched name #{name}"
209
+ color = Color.new(r, g, b, 1.0)
210
+ elsif name.match(/^(dark|deep|light|bright)?(.*?)(ish)?$/)
211
+ #puts "matched #{$1}-#{$2}-#{$3}"
212
+ value = $1
213
+ color_name = $2
214
+ ish = $3
215
+ analogval = value ? 0 : 0.1
216
+ r, g, b = COLORNAMES[color_name] || [0.0, 0.0, 0.0]
217
+ color = Color.new(r, g, b, 1.0)
218
+ color = c.analog(20, analogval) if ish
219
+ color.lighten(0.2) if value and value.match(/light|bright/)
220
+ color.darken(0.2) if value and value.match(/dark|deep/)
221
+ else
222
+ color = Color.black
223
+ end
224
+ color
225
+ end
226
+
227
+ # return the name of the nearest named color
228
+ def name
229
+ nearest, d = ["", 1.0]
230
+ red = r
231
+ green = g
232
+ blue = b
233
+ for hue in COLORNAMES.keys
234
+ rdiff = (red - COLORNAMES[hue][0]).abs
235
+ gdiff = (green - COLORNAMES[hue][1]).abs
236
+ bdiff = (blue - COLORNAMES[hue][2]).abs
237
+ totaldiff = rdiff + gdiff + bdiff
238
+ if (totaldiff < d)
239
+ nearest, d = [hue, totaldiff]
240
+ end
241
+ end
242
+ nearest
243
+ end
244
+
245
+ # return a copy of this color
246
+ def copy
247
+ Color.new(r, g, b, a)
248
+ end
249
+
250
+ # print the color's component values
251
+ def to_s
252
+ "color: #{name} (#{r} #{g} #{b} #{a})"
253
+ end
254
+
255
+ # sort the color by brightness in an array
256
+ def <=> othercolor
257
+ self.brightness <=> othercolor.brightness || self.hue <=> othercolor.hue
258
+ end
259
+
260
+ # set or retrieve the red component
261
+ def r(val=nil)
262
+ if val
263
+ r, g, b, a = get_rgb
264
+ set_rgb(val, g, b, a)
265
+ self
266
+ else
267
+ @rgb.redComponent
268
+ end
269
+ end
270
+
271
+ # set or retrieve the green component
272
+ def g(val=nil)
273
+ if val
274
+ r, g, b, a = get_rgb
275
+ set_rgb(r, val, b, a)
276
+ self
277
+ else
278
+ @rgb.greenComponent
279
+ end
280
+ end
281
+
282
+ # set or retrieve the blue component
283
+ def b(val=nil)
284
+ if val
285
+ r, g, b, a = get_rgb
286
+ set_rgb(r, g, val, a)
287
+ self
288
+ else
289
+ @rgb.blueComponent
290
+ end
291
+ end
292
+
293
+ # set or retrieve the alpha component
294
+ def a(val=nil)
295
+ if val
296
+ r, g, b, a = get_rgb
297
+ set_rgb(r, g, b, val)
298
+ self
299
+ else
300
+ @rgb.alphaComponent
301
+ end
302
+ end
303
+
304
+ # set or retrieve the hue
305
+ def hue(val=nil)
306
+ if val
307
+ h, s, b, a = get_hsb
308
+ set_hsb(val, s, b, a)
309
+ self
310
+ else
311
+ @rgb.hueComponent
312
+ end
313
+ end
314
+
315
+ # set or retrieve the saturation
316
+ def saturation(val=nil)
317
+ if val
318
+ h, s, b, a = get_hsb
319
+ set_hsb(h, val, b, a)
320
+ self
321
+ else
322
+ @rgb.saturationComponent
323
+ end
324
+ end
325
+
326
+ # set or retrieve the brightness
327
+ def brightness(val=nil)
328
+ if val
329
+ h, s, b, a = get_hsb
330
+ set_hsb(h, s, val, a)
331
+ self
332
+ else
333
+ @rgb.brightnessComponent
334
+ end
335
+ end
336
+
337
+ # decrease saturation by the specified amount
338
+ def desaturate(step=0.1)
339
+ saturation(saturation - step)
340
+ self
341
+ end
342
+
343
+ # increase the saturation by the specified amount
344
+ def saturate(step=0.1)
345
+ saturation(saturation + step)
346
+ self
347
+ end
348
+
349
+ # decrease the brightness by the specified amount
350
+ def darken(step=0.1)
351
+ brightness(brightness - step)
352
+ self
353
+ end
354
+
355
+ # increase the brightness by the specified amount
356
+ def lighten(step=0.1)
357
+ brightness(brightness + step)
358
+ self
359
+ end
360
+
361
+ # set the R,G,B,A values
362
+ def set(r, g, b, a=1.0)
363
+ set_rgb(r, g, b, a)
364
+ self
365
+ end
366
+
367
+ # adjust the Red, Green, Blue, Alpha values by the specified amounts
368
+ def adjust_rgb(r=0.0, g=0.0, b=0.0, a=0.0)
369
+ r0, g0, b0, a0 = get_rgb
370
+ set_rgb(r0+r, g0+g, b0+b, a0+a)
371
+ self
372
+ end
373
+
374
+ # return RGBA values
375
+ def get_rgb
376
+ #@rgb.getRed_green_blue_alpha_()
377
+ [@rgb.redComponent, @rgb.greenComponent, @rgb.blueComponent, @rgb.alphaComponent]
378
+ end
379
+
380
+ # set color using RGBA values
381
+ def set_rgb(r, g, b, a=1.0)
382
+ @rgb = NSColor.colorWithDeviceRed r, green:g, blue:b, alpha:a
383
+ self
384
+ end
385
+
386
+ # return HSBA values
387
+ def get_hsb
388
+ #@rgb.getHue_saturation_brightness_alpha_()
389
+ [@rgb.hueComponent, @rgb.saturationComponent, @rgb.brightnessComponent, @rgb.alphaComponent]
390
+ end
391
+
392
+ # set color using HSBA values
393
+ def set_hsb(h,s,b,a=1.0)
394
+ @rgb = NSColor.colorWithDeviceHue h, saturation:s, brightness:b, alpha:a
395
+ self
396
+ end
397
+
398
+ # adjust Hue, Saturation, Brightness, and Alpha by specified amounts
399
+ def adjust_hsb(h=0.0, s=0.0, b=0.0, a=0.0)
400
+ h0, s0, b0, a0 = get_hsb
401
+ set_hsb(h0+h, s0+s, b0+b, a0+a)
402
+ self
403
+ end
404
+
405
+ # alter the color by the specified random scaling factor
406
+ # def ish(angle=10.0,d=0.02)
407
+ # # r,g,b,a = get_rgb
408
+ # # r = vary(r, variance)
409
+ # # g = vary(g, variance)
410
+ # # b = vary(b, variance)
411
+ # # a = vary(a, variance)
412
+ # # set_rgb(r,g,b,a)
413
+ # analog(angle,d)
414
+ # self
415
+ # end
416
+
417
+ # create a random color
418
+ def random
419
+ set_rgb(rand, rand, rand, 1.0)
420
+ self
421
+ end
422
+
423
+ # rotate the color on the artistic RYB color wheel (0 to 360 degrees)
424
+ def rotate_ryb(angle=180)
425
+
426
+ # An artistic color wheel has slightly different opposites
427
+ # (e.g. purple-yellow instead of purple-lime).
428
+ # It is mathematically incorrect but generally assumed
429
+ # to provide better complementary colors.
430
+ #
431
+ # http://en.wikipedia.org/wiki/RYB_color_model
432
+
433
+ h = hue * 360
434
+ angle = angle % 360.0
435
+ a = 0
436
+
437
+ # Approximation of Itten's RYB color wheel.
438
+ # In HSB, colors hues range from 0-360.
439
+ # However, on the artistic color wheel these are not evenly distributed.
440
+ # The second tuple value contains the actual distribution.
441
+
442
+ # Given a hue, find out under what angle it is
443
+ # located on the artistic color wheel.
444
+ (RYBWheel.size-1).times do |i|
445
+ x0,y0 = RYBWheel[i]
446
+ x1,y1 = RYBWheel[i+1]
447
+ y1 += 360 if y1 < y0
448
+ if y0 <= h && h <= y1
449
+ a = 1.0 * x0 + (x1-x0) * (h-y0) / (y1-y0)
450
+ break
451
+ end
452
+ end
453
+
454
+ # And the user-given angle (e.g. complement).
455
+ a = (a+angle) % 360
456
+
457
+ # For the given angle, find out what hue is
458
+ # located there on the artistic color wheel.
459
+ (RYBWheel.size-1).times do |i|
460
+ x0,y0 = RYBWheel[i]
461
+ x1,y1 = RYBWheel[i+1]
462
+ y1 += 360 if y1 < y0
463
+ if x0 <= a && a <= x1
464
+ h = 1.0 * y0 + (y1-y0) * (a-x0) / (x1-x0)
465
+ break
466
+ end
467
+ end
468
+
469
+ h = h % 360
470
+ set_hsb(h/360, self.saturation, self.brightness, self.a)
471
+ self
472
+ end
473
+
474
+ # rotate the color on the RGB color wheel (0 to 360 degrees)
475
+ def rotate_rgb(angle=180)
476
+ hue = (self.hue + 1.0 * angle / 360) % 1
477
+ set_hsb(hue, self.saturation, self.brightness, @a)
478
+ self
479
+ end
480
+
481
+ # return a similar color, varying the hue by angle (0-360) and brightness/saturation by d
482
+ def analog(angle=20, d=0.5)
483
+ c = self.copy
484
+ c.rotate_ryb(angle * (rand*2-1))
485
+ c.lighten(d * (rand*2-1))
486
+ c.saturate(d * (rand*2-1))
487
+ end
488
+
489
+ # randomly vary the color within a maximum hue range, saturation range, and brightness range
490
+ def drift(maxhue=0.1,maxsat=0.3,maxbright=maxsat)
491
+ # save original values the first time
492
+ @original_hue ||= self.hue
493
+ @original_saturation ||= self.saturation
494
+ @original_brightness ||= self.brightness
495
+ # get current values
496
+ current_hue = self.hue
497
+ current_saturation = self.saturation
498
+ current_brightness = self.brightness
499
+ # generate new values
500
+ randhue = ((rand * maxhue) - maxhue/2.0) + current_hue
501
+ randhue = inrange(randhue, (@original_hue - maxhue/2.0),(@original_hue + maxhue/2.0))
502
+ randsat = (rand * maxsat) - maxsat/2.0 + current_saturation
503
+ randsat = inrange(randsat, @original_saturation - maxsat/2.0,@original_saturation + maxsat/2.0)
504
+ randbright = (rand * maxbright) - maxbright/2.0 + current_brightness
505
+ randbright = inrange(randbright, @original_brightness - maxbright/2.0,@original_brightness + maxbright/2.0)
506
+ # assign new values
507
+ self.hue(randhue)
508
+ self.saturation(randsat)
509
+ self.brightness(randbright)
510
+ self
511
+ end
512
+
513
+ # convert to the complementary color (the color at opposite on the artistic color wheel)
514
+ def complement
515
+ rotate_ryb(180)
516
+ self
517
+ end
518
+
519
+ # blend with another color (doesn't work?)
520
+ # def blend(color, pct=0.5)
521
+ # blended = NSColor.blendedColorWithFraction_ofColor(pct,color.rgb)
522
+ # @rgb = blended.colorUsingColorSpaceName(NSDeviceRGBColorSpace)
523
+ # self
524
+ # end
525
+
526
+ # create a new RGBA color
527
+ def self.rgb(r, g, b, a=1.0)
528
+ Color.new(r,g,b,a)
529
+ end
530
+
531
+ # create a new HSBA color
532
+ def self.hsb(h, s, b, a=1.0)
533
+ Color.new.set_hsb(h,s,b,a)
534
+ end
535
+
536
+ # create a new gray color with the specified darkness
537
+ def self.gray(pct=0.5)
538
+ Color.new(pct,pct,pct,1.0)
539
+ end
540
+
541
+ # return a random color
542
+ def self.random
543
+ Color.new.random
544
+ end
545
+
546
+ # Returns a list of complementary colors. The complement is the color 180 degrees across the artistic RYB color wheel.
547
+ # 1) ORIGINAL: the original color
548
+ # 2) CONTRASTING: same hue as original but much darker or lighter
549
+ # 3) SOFT SUPPORTING: same hue but lighter and less saturated than the original
550
+ # 4) CONTRASTING COMPLEMENT: a much brighter or darker version of the complement hue
551
+ # 5) COMPLEMENT: the hue 180 degrees opposite on the RYB color wheel with same brightness/saturation
552
+ # 6) LIGHT SUPPORTING COMPLEMENT VARIANT: a lighter less saturated version of the complement hue
553
+ def complementary
554
+ colors = []
555
+
556
+ # A contrasting color: much darker or lighter than the original.
557
+ colors.push(self)
558
+ c = self.copy
559
+ if self.brightness > 0.4
560
+ c.brightness(0.1 + c.brightness*0.25)
561
+ else
562
+ c.brightness(1.0 - c.brightness*0.25)
563
+ end
564
+ colors.push(c)
565
+
566
+ # A soft supporting color: lighter and less saturated.
567
+ c = self.copy
568
+ c.brightness(0.3 + c.brightness)
569
+ c.saturation(0.1 + c.saturation*0.3)
570
+ colors.push(c)
571
+
572
+ # A contrasting complement: very dark or very light.
573
+ c_comp = self.copy.complement
574
+ c = c_comp.copy
575
+ if c_comp.brightness > 0.3
576
+ c.brightness(0.1 + c_comp.brightness*0.25)
577
+ else
578
+ c.brightness(1.0 - c.brightness*0.25)
579
+ end
580
+ colors.push(c)
581
+
582
+ # The complement
583
+ colors.push(c_comp)
584
+
585
+ # and a light supporting variant.
586
+ c = c_comp.copy
587
+ c.brightness(0.3 + c.brightness)
588
+ c.saturation(0.1 + c.saturation*0.25)
589
+ colors.push(c)
590
+
591
+ colors
592
+ end
593
+
594
+ # Returns a list with the split complement of the color.
595
+ # The split complement are the two colors to the left and right
596
+ # of the color's complement.
597
+ def split_complementary(angle=30)
598
+ colors = []
599
+ colors.push(self)
600
+ comp = self.copy.complement
601
+ colors.push(comp.copy.rotate_ryb(-angle).lighten(0.1))
602
+ colors.push(comp.copy.rotate_ryb(angle).lighten(0.1))
603
+ colors
604
+ end
605
+
606
+ # Returns the left half of the split complement.
607
+ # A list is returned with the same darker and softer colors
608
+ # as in the complementary list, but using the hue of the
609
+ # left split complement instead of the complement itself.
610
+ def left_complement(angle=-30)
611
+ left = copy.complement.rotate_ryb(angle).lighten(0.1)
612
+ colors = complementary
613
+ colors[3].hue(left.hue)
614
+ colors[4].hue(left.hue)
615
+ colors[5].hue(left.hue)
616
+ colors
617
+ end
618
+
619
+ # Returns the right half of the split complement.
620
+ # A list is returned with the same darker and softer colors
621
+ # as in the complementary list, but using the hue of the
622
+ # right split complement instead of the complement itself.
623
+ def right_complement(angle=30)
624
+ right = copy.complement.rotate_ryb(angle).lighten(0.1)
625
+ colors = complementary
626
+ colors[3].hue(right.hue)
627
+ colors[4].hue(right.hue)
628
+ colors[5].hue(right.hue)
629
+ colors
630
+ end
631
+
632
+ # Returns colors that are next to each other on the wheel.
633
+ # These yield natural color schemes (like shades of water or sky).
634
+ # The angle determines how far the colors are apart,
635
+ # making it bigger will introduce more variation.
636
+ # The contrast determines the darkness/lightness of
637
+ # the analogue colors in respect to the given colors.
638
+ def analogous(angle=10, contrast=0.25)
639
+ contrast = inrange(contrast, 0.0, 1.0)
640
+
641
+ colors = []
642
+ colors.push(self)
643
+
644
+ for i, j in [[1,2.2], [2,1], [-1,-0.5], [-2,1]] do
645
+ c = copy.rotate_ryb(angle*i)
646
+ t = 0.44-j*0.1
647
+ if brightness - contrast*j < t then
648
+ c.brightness(t)
649
+ else
650
+ c.brightness(self.brightness - contrast*j)
651
+ end
652
+ c.saturation(c.saturation - 0.05)
653
+ colors.push(c)
654
+ end
655
+ colors
656
+ end
657
+
658
+ # Returns colors in the same hue with varying brightness/saturation.
659
+ def monochrome
660
+
661
+ colors = [self]
662
+
663
+ c = copy
664
+ c.brightness(_wrap(brightness, 0.5, 0.2, 0.3))
665
+ c.saturation(_wrap(saturation, 0.3, 0.1, 0.3))
666
+ colors.push(c)
667
+
668
+ c = copy
669
+ c.brightness(_wrap(brightness, 0.2, 0.2, 0.6))
670
+ colors.push(c)
671
+
672
+ c = copy
673
+ c.brightness(max(0.2, brightness+(1-brightness)*0.2))
674
+ c.saturation(_wrap(saturation, 0.3, 0.1, 0.3))
675
+ colors.push(c)
676
+
677
+ c = self.copy()
678
+ c.brightness(_wrap(brightness, 0.5, 0.2, 0.3))
679
+ colors.push(c)
680
+
681
+ colors
682
+ end
683
+
684
+ # Returns a triad of colors.
685
+ # The triad is made up of this color and two other colors
686
+ # that together make up an equilateral triangle on
687
+ # the artistic color wheel.
688
+ def triad(angle=120)
689
+ colors = [self]
690
+ colors.push(copy.rotate_ryb(angle).lighten(0.1))
691
+ colors.push(copy.rotate_ryb(-angle).lighten(0.1))
692
+ colors
693
+ end
694
+
695
+ # Returns a tetrad of colors.
696
+ # The tetrad is made up of this color and three other colors
697
+ # that together make up a cross on the artistic color wheel.
698
+ def tetrad(angle=90)
699
+
700
+ colors = [self]
701
+
702
+ c = copy.rotate_ryb(angle)
703
+ if brightness < 0.5 then
704
+ c.brightness(c.brightness + 0.2)
705
+ else
706
+ c.brightness(c.brightness - 0.2)
707
+ end
708
+ colors.push(c)
709
+
710
+ c = copy.rotate_ryb(angle*2)
711
+ if brightness < 0.5
712
+ c.brightness(c.brightness + 0.1)
713
+ else
714
+ c.brightness(c.brightness - 0.1)
715
+ end
716
+
717
+ colors.push(c)
718
+ colors.push(copy.rotate_ryb(angle*3).lighten(0.1))
719
+ colors
720
+ end
721
+
722
+ # Roughly the complement and some far analogs.
723
+ def compound(flip=false)
724
+
725
+ d = (flip ? -1 : 1)
726
+
727
+ colors = [self]
728
+
729
+ c = copy.rotate_ryb(30*d)
730
+ c.brightness(_wrap(brightness, 0.25, 0.6, 0.25))
731
+ colors.push(c)
732
+
733
+ c = copy.rotate_ryb(30*d)
734
+ c.saturation(_wrap(saturation, 0.4, 0.1, 0.4))
735
+ c.brightness(_wrap(brightness, 0.4, 0.2, 0.4))
736
+ colors.push(c)
737
+
738
+ c = copy.rotate_ryb(160*d)
739
+ c.saturation(_wrap(saturation, 0.25, 0.1, 0.25))
740
+ c.brightness(max(0.2, brightness))
741
+ colors.push(c)
742
+
743
+ c = copy.rotate_ryb(150*d)
744
+ c.saturation(_wrap(saturation, 0.1, 0.8, 0.1))
745
+ c.brightness(_wrap(brightness, 0.3, 0.6, 0.3))
746
+ colors.push(c)
747
+
748
+ c = copy.rotate_ryb(150*d)
749
+ c.saturation(_wrap(saturation, 0.1, 0.8, 0.1))
750
+ c.brightness(_wrap(brightness, 0.4, 0.2, 0.4))
751
+ #colors.push(c)
752
+
753
+ colors
754
+ end
755
+
756
+ # Roughly the complement and some far analogs.
757
+ def flipped_compound
758
+ compound(true)
759
+ end
760
+
761
+ private
762
+
763
+ # vary a single color component by a multiplier
764
+ def vary(original, variance)
765
+ newvalue = original + (rand * variance * (rand > 0.5 ? 1 : -1))
766
+ newvalue = inrange(newvalue,0.0,1.0)
767
+ newvalue
768
+ end
769
+
770
+ # wrap within range
771
+ def _wrap(x, min, threshold, plus)
772
+ if x - min < threshold
773
+ x + plus
774
+ else
775
+ x - min
776
+ end
777
+ end
778
+
779
+ end
780
+
781
+ end