hotcocoa 0.5

Sign up to get free protection for your applications and to get access to all the features.
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