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,836 @@
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
+ # In Quartz 2D, the canvas is often referred as the "page".
17
+ # Overview of the underlying page concept available at:
18
+ # http://developer.apple.com/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_overview/dq_overview.html#//apple_ref/doc/uid/TP30001066-CH202-TPXREF101
19
+
20
+
21
+ module HotCocoa::Graphics
22
+
23
+ # drawing destination for writing a PDF, PNG, GIF, JPG, or TIF file
24
+ class Canvas
25
+
26
+ BlendModes = {
27
+ :normal => KCGBlendModeNormal,
28
+ :darken => KCGBlendModeDarken,
29
+ :multiply => KCGBlendModeMultiply,
30
+ :screen => KCGBlendModeScreen,
31
+ :overlay => KCGBlendModeOverlay,
32
+ :darken => KCGBlendModeDarken,
33
+ :lighten => KCGBlendModeLighten,
34
+ :colordodge => KCGBlendModeColorDodge,
35
+ :colorburn => KCGBlendModeColorBurn,
36
+ :softlight => KCGBlendModeSoftLight,
37
+ :hardlight => KCGBlendModeHardLight,
38
+ :difference => KCGBlendModeDifference,
39
+ :exclusion => KCGBlendModeExclusion,
40
+ :hue => KCGBlendModeHue,
41
+ :saturation => KCGBlendModeSaturation,
42
+ :color => KCGBlendModeColor,
43
+ :luminosity => KCGBlendModeLuminosity,
44
+ }
45
+ BlendModes.default(KCGBlendModeNormal)
46
+
47
+ DefaultOptions = {:quality => 0.8, :width => 400, :height => 400}
48
+
49
+ attr_accessor :width, :height
50
+
51
+ # We make the context available so developers can directly use underlying CG methods
52
+ # on objects created by this wrapper
53
+ attr_reader :ctx
54
+
55
+ class << self
56
+ def for_rendering(options={}, &block)
57
+ options[:type] = :render
58
+ Canvas.new(options, &block)
59
+ end
60
+
61
+ def for_pdf(options={}, &block)
62
+ options[:type] = :pdf
63
+ Canvas.new(options, &block)
64
+ end
65
+
66
+ def for_image(options={}, &block)
67
+ options[:type] = :image
68
+ Canvas.new(options, &block)
69
+ end
70
+
71
+ def for_context(options={}, &block)
72
+ options[:type] = :context
73
+ Canvas.new(options, &block)
74
+ end
75
+
76
+ def for_current_context(options={}, &block)
77
+ options[:type] = :context
78
+ options[:context] = NSGraphicsContext.currentContext.graphicsPort
79
+ Canvas.new(options, &block)
80
+ end
81
+
82
+ end
83
+
84
+ # create a new canvas with the given width, height, and output filename (pdf, png, jpg, gif, or tif)
85
+ def initialize(options={}, &block)
86
+ if options[:size]
87
+ options[:width] = options[:size][0]
88
+ options[:height] = options[:size][1]
89
+ end
90
+ options = DefaultOptions.merge(options)
91
+
92
+ @width = options[:width]
93
+ @height = options[:height]
94
+ @output = options[:filename] || 'test'
95
+ @stacksize = 0
96
+ @colorspace = CGColorSpaceCreateDeviceRGB() # => CGColorSpaceRef
97
+ @autoclosepath = false
98
+
99
+ case options[:type]
100
+ when :pdf
101
+ @filetype = :pdf
102
+ # CREATE A PDF DRAWING CONTEXT
103
+ # url = NSURL.fileURLWithPath(image)
104
+ url = CFURLCreateFromFileSystemRepresentation(nil, @output, @output.length, false)
105
+ pdfrect = CGRect.new(CGPoint.new(0, 0), CGSize.new(width, height)) # Landscape
106
+ #@ctx = CGPDFContextCreateWithURL(url, pdfrect, nil)
107
+ consumer = CGDataConsumerCreateWithURL(url);
108
+ pdfcontext = CGPDFContextCreate(consumer, pdfrect, nil);
109
+ CGPDFContextBeginPage(pdfcontext, nil)
110
+ @ctx = pdfcontext
111
+ when :image, :render
112
+ # CREATE A BITMAP DRAWING CONTEXT
113
+ @filetype = File.extname(@output).downcase[1..-1].intern if options[:type] == :image
114
+
115
+ @bits_per_component = 8
116
+ @colorspace = CGColorSpaceCreateDeviceRGB() # => CGColorSpaceRef
117
+ #alpha = KCGImageAlphaNoneSkipFirst # opaque background
118
+ alpha = KCGImageAlphaPremultipliedFirst # transparent background
119
+
120
+ # 8 integer bits/component; 32 bits/pixel; 3-component colorspace; kCGImageAlphaPremultipliedFirst; 57141 bytes/row.
121
+ bytes = @bits_per_component * 4 * @width.ceil
122
+ @ctx = CGBitmapContextCreate(nil, @width, @height, @bits_per_component, bytes, @colorspace, alpha) # => CGContextRef
123
+ when :context
124
+ @ctx = options[:context]
125
+ else
126
+ raise "ERROR: output file type #{ext} not recognized"
127
+ end
128
+
129
+ # antialiasing
130
+ CGContextSetAllowsAntialiasing(@ctx, true)
131
+
132
+ # set defaults
133
+ fill # set the default fill
134
+ nostroke # no stroke by default
135
+ strokewidth # set the default stroke width
136
+ font # set the default font
137
+ antialias # set the default antialias state
138
+ autoclosepath # set the autoclosepath default
139
+ quality(options[:quality]) # set the compression default
140
+ push # save the pristine default default graphics state (retrieved by calling "reset")
141
+ push # create a new graphics state for the user to mess up
142
+ if block_given?
143
+ case block.arity
144
+ when 0
145
+ send(:instance_eval, &block)
146
+ else
147
+ block.call(self)
148
+ end
149
+ end
150
+ end
151
+
152
+ # SET CANVAS GLOBAL PARAMETERS
153
+
154
+ # print drawing functions if verbose is true
155
+ def verbose(tf=true)
156
+ @verbose = tf
157
+ end
158
+
159
+ # set whether or not drawn paths should be antialiased (true/false)
160
+ def antialias(tf=true)
161
+ CGContextSetShouldAntialias(@ctx, tf)
162
+ end
163
+
164
+ # set the alpha value for subsequently drawn objects
165
+ def alpha(val=1.0)
166
+ CGContextSetAlpha(@ctx, val)
167
+ end
168
+
169
+ # set compression (0.0 = max, 1.0 = none)
170
+ def quality(factor=0.8)
171
+ @quality = factor
172
+ end
173
+
174
+ # set the current fill (given a Color object, or RGBA values)
175
+ def fill(r=0, g=0, b=0, a=1)
176
+ case r
177
+ when Color
178
+ g = r.g
179
+ b = r.b
180
+ a = r.a
181
+ r = r.r
182
+ end
183
+ CGContextSetRGBFillColor(@ctx, r, g, b, a) # RGBA
184
+ @fill = true
185
+ end
186
+
187
+ # remove current fill
188
+ def nofill
189
+ CGContextSetRGBFillColor(@ctx, 0.0, 0.0, 0.0, 0.0) # RGBA
190
+ @fill = nil
191
+ end
192
+
193
+ # SET CANVAS STROKE PARAMETERS
194
+
195
+ # set stroke color (given a Color object, or RGBA values)
196
+ def stroke(r=0, g=0, b=0, a=1.0)
197
+ case r
198
+ when Color
199
+ g = r.g
200
+ b = r.b
201
+ a = r.a
202
+ r = r.r
203
+ end
204
+ CGContextSetRGBStrokeColor(@ctx, r, g, b, a) # RGBA
205
+ @stroke = true
206
+ end
207
+
208
+ # set stroke width
209
+ def strokewidth(width=1)
210
+ CGContextSetLineWidth(@ctx, width.to_f)
211
+ end
212
+
213
+ # don't use a stroke for subsequent drawing operations
214
+ def nostroke
215
+ CGContextSetRGBStrokeColor(@ctx, 0, 0, 0, 0) # RGBA
216
+ @stroke = false
217
+ end
218
+
219
+ # set cap style to round, square, or butt
220
+ def linecap(style=:butt)
221
+ case style
222
+ when :round
223
+ cap = KCGLineCapRound
224
+ when :square
225
+ cap = KCGLineCapSquare
226
+ when :butt
227
+ cap = KCGLineCapButt
228
+ else
229
+ raise "ERROR: line cap style not recognized: #{style}"
230
+ end
231
+ CGContextSetLineCap(@ctx,cap)
232
+ end
233
+
234
+ # set line join style to round, miter, or bevel
235
+ def linejoin(style=:miter)
236
+ case style
237
+ when :round
238
+ join = KCGLineJoinRound
239
+ when :bevel
240
+ join = KCGLineJoinBevel
241
+ when :miter
242
+ join = KCGLineJoinMiter
243
+ else
244
+ raise "ERROR: line join style not recognized: #{style}"
245
+ end
246
+ CGContextSetLineJoin(@ctx,join)
247
+ end
248
+
249
+ # set lengths of dashes and spaces, and distance before starting dashes
250
+ def linedash(lengths=[10,2], phase=0.0)
251
+ count=lengths.size
252
+ CGContextSetLineDash(@ctx, phase, lengths, count)
253
+ end
254
+
255
+ # revert to solid lines
256
+ def nodash
257
+ CGContextSetLineDash(@ctx, 0.0, nil, 0)
258
+ end
259
+
260
+
261
+ # DRAWING SHAPES ON CANVAS
262
+
263
+ # draw a rectangle starting at x,y and having dimensions w,h
264
+ def rect(x=0, y=0, w=20, h=20, reg=@registration)
265
+ # center the rectangle
266
+ if (reg == :center)
267
+ x = x - w / 2
268
+ y = y - h / 2
269
+ end
270
+ CGContextAddRect(@ctx, NSMakeRect(x, y, w, h))
271
+ CGContextDrawPath(@ctx, KCGPathFillStroke)
272
+ end
273
+
274
+ # inscribe an oval starting at x,y inside a rectangle having dimensions w,h
275
+ def oval(x=0, y=0, w=20, h=20, reg=@registration)
276
+ # center the oval
277
+ if (reg == :center)
278
+ x = x - w / 2
279
+ y = y - w / 2
280
+ end
281
+ CGContextAddEllipseInRect(@ctx, NSMakeRect(x, y, w, h))
282
+ CGContextDrawPath(@ctx, KCGPathFillStroke) # apply fill and stroke
283
+ end
284
+
285
+ # draw a background color (given a Color object, or RGBA values)
286
+ def background(r=1, g=1, b=1, a=1.0)
287
+ case r
288
+ when Color
289
+ g = r.g
290
+ b = r.b
291
+ a = r.a
292
+ r = r.r
293
+ end
294
+ push
295
+ CGContextSetRGBFillColor(@ctx, r, g, b, a) # RGBA
296
+ rect(0,0,@width,@height)
297
+ pop
298
+ end
299
+
300
+ # draw a radial gradiant starting at sx,sy with radius er
301
+ # optional: specify ending at ex,ey and starting radius sr
302
+ def radial(gradient, sx=@width/2, sy=@height/2, er=@width/2, ex=sx, ey=sy, sr=0.0)
303
+ #options = KCGGradientDrawsBeforeStartLocation
304
+ #options = KCGGradientDrawsAfterEndLocation
305
+ CGContextDrawRadialGradient(@ctx, gradient.gradient, NSMakePoint(sx, sy), sr, NSMakePoint(ex, ey), er, gradient.pre + gradient.post)
306
+ end
307
+
308
+ # draw an axial(linear) gradient starting at sx,sy and ending at ex,ey
309
+ def gradient(gradient=Gradient.new, start_x=@width/2, start_y=0, end_x=@width/2, end_y=@height)
310
+ #options = KCGGradientDrawsBeforeStartLocation
311
+ #options = KCGGradientDrawsAfterEndLocation
312
+ CGContextDrawLinearGradient(@ctx, gradient.gradient, NSMakePoint(start_x, start_y), NSMakePoint(end_x, end_y), gradient.pre + gradient.post)
313
+ end
314
+
315
+ # draw a cartesian coordinate grid for reference
316
+ def cartesian(res=50, stroke=1.0, fsize=10)
317
+ # save previous state
318
+ new_state do
319
+ # set font and stroke
320
+ fontsize(fsize)
321
+ fill(Color.black)
322
+ stroke(Color.red)
323
+ strokewidth(stroke)
324
+ # draw vertical numbered grid lines
325
+ for x in (-width / res)..(width / res) do
326
+ line(x * res, -height, x * res, height)
327
+ text("#{x * res}", x * res, 0)
328
+ end
329
+ # draw horizontal numbered grid lines
330
+ for y in (-height / res)..(height / res) do
331
+ line(-width, y * res, width, y * res)
332
+ text("#{y * res}", 0, y * res)
333
+ end
334
+ # draw lines intersecting center of canvas
335
+ stroke(Color.black)
336
+ line(-width, -height, width, height)
337
+ line(width, -height, -width, height)
338
+ line(0, height, width, 0)
339
+ line(width / 2, 0, width / 2, height)
340
+ line(0, height / 2, width, height / 2)
341
+ # restore previous state
342
+ end
343
+ end
344
+
345
+
346
+ # DRAWING COMPLETE PATHS TO CANVAS
347
+
348
+ # draw a line starting at x1,y1 and ending at x2,y2
349
+ def line(x1, y1, x2, y2)
350
+ CGContextAddLines(@ctx, [NSPoint.new(x1, y1), NSPoint.new(x2, y2)], 2)
351
+ CGContextDrawPath(@ctx, KCGPathStroke) # apply stroke
352
+ endpath
353
+
354
+ end
355
+
356
+ # draw a series of lines connecting the given array of points
357
+ def lines(points)
358
+ CGContextAddLines(@ctx, points, points.size)
359
+ CGContextDrawPath(@ctx, KCGPathStroke) # apply stroke
360
+ endpath
361
+ end
362
+
363
+ # draw the arc of a circle with center point x,y, radius, start angle (0 deg = 12 o'clock) and end angle
364
+ def arc(x, y, radius, start_angle, end_angle)
365
+ start_angle = radians(90-start_angle)
366
+ end_angle = radians(90-end_angle)
367
+ clockwise = 1 # 1 = clockwise, 0 = counterclockwise
368
+ CGContextAddArc(@ctx, x, y, radius, start_angle, end_angle, clockwise)
369
+ CGContextDrawPath(@ctx, KCGPathStroke)
370
+ end
371
+
372
+ # draw a bezier curve from the current point, given the coordinates of two handle control points and an end point
373
+ def curve(cp1x, cp1y, cp2x, cp2y, x1, y1, x2, y2)
374
+ beginpath(x1, y1)
375
+ CGContextAddCurveToPoint(@ctx, cp1x, cp1y, cp2x, cp2y, x2, y2)
376
+ endpath
377
+ end
378
+
379
+ # draw a quadratic bezier curve from x1,y1 to x2,y2, given the coordinates of one control point
380
+ def qcurve(cpx, cpy, x1, y1, x2, y2)
381
+ beginpath(x1, y1)
382
+ CGContextAddQuadCurveToPoint(@ctx, cpx, cpy, x2, y2)
383
+ endpath
384
+ end
385
+
386
+ # draw the given Path object
387
+ def draw(object, *args)
388
+ case object
389
+ when Path
390
+ draw_path(object, *args)
391
+ when Image
392
+ draw_image(object, *args)
393
+ else
394
+ raise ArgumentError.new("first parameter must be a Path or Image object not a #{object.class}")
395
+ end
396
+ end
397
+
398
+ # CONSTRUCTING PATHS ON CANVAS
399
+
400
+ # if true, automatically close the path after it is ended
401
+ def autoclosepath(tf=false)
402
+ @autoclosepath = tf
403
+ end
404
+
405
+ def new_path(x, y, &block)
406
+ beginpath(x, y)
407
+ block.call
408
+ endpath
409
+ end
410
+
411
+ # begin drawing a path at x,y
412
+ def beginpath(x, y)
413
+ CGContextBeginPath(@ctx)
414
+ CGContextMoveToPoint(@ctx, x, y)
415
+ end
416
+
417
+ # end the current path and draw it
418
+ def endpath
419
+ CGContextClosePath(@ctx) if @autoclosepath
420
+ #mode = KCGPathStroke
421
+ mode = KCGPathFillStroke
422
+ CGContextDrawPath(@ctx, mode) # apply fill and stroke
423
+ end
424
+
425
+ # move the "pen" to x,y
426
+ def moveto(x, y)
427
+ CGContextMoveToPoint(@ctx, x, y)
428
+ end
429
+
430
+ # draw a line from the current point to x,y
431
+ def lineto(x, y)
432
+ CGContextAddLineToPoint(@ctx ,x, y)
433
+ end
434
+
435
+ # draw a bezier curve from the current point, given the coordinates of two handle control points and an end point
436
+ def curveto(cp1x, cp1y, cp2x, cp2y, x, y)
437
+ CGContextAddCurveToPoint(@ctx, cp1x, cp1y, cp2x, cp2y, x, y)
438
+ end
439
+
440
+ # draw a quadratic bezier curve from the current point, given the coordinates of one control point and an end point
441
+ def qcurveto(cpx, cpy, x, y)
442
+ CGContextAddQuadCurveToPoint(@ctx, cpx, cpy, x, y)
443
+ end
444
+
445
+ # draw an arc given the endpoints of two tangent lines and a radius
446
+ def arcto(x1, y1, x2, y2, radius)
447
+ CGContextAddArcToPoint(@ctx, x1, y1, x2, y2, radius)
448
+ end
449
+
450
+ # draw the path in a grid with rows, columns
451
+ def grid(path, rows=10, cols=10)
452
+ push
453
+ rows.times do |row|
454
+ tx = (row+1) * (self.height / rows) - (self.height / rows) / 2
455
+ cols.times do |col|
456
+ ty = (col+1) * (self.width / cols) - (self.width / cols) / 2
457
+ push
458
+ translate(tx, ty)
459
+ draw(path)
460
+ pop
461
+ end
462
+ end
463
+ pop
464
+ end
465
+
466
+
467
+ # TRANSFORMATIONS
468
+
469
+ # set registration mode to :center or :corner
470
+ def registration(mode=:center)
471
+ @registration = mode
472
+ end
473
+
474
+ # rotate by the specified degrees
475
+ def rotate(deg=0)
476
+ CGContextRotateCTM(@ctx, radians(-deg));
477
+ end
478
+
479
+ # translate drawing context by x,y
480
+ def translate(x, y)
481
+ CGContextTranslateCTM(@ctx, x, y);
482
+ end
483
+
484
+ # scale drawing context by x,y
485
+ def scale(x, y=x)
486
+ CGContextScaleCTM(@ctx, x, y)
487
+ end
488
+
489
+ def skew(x=0, y=0)
490
+ x = Math::PI * x / 180.0
491
+ y = Math::PI * y / 180.0
492
+ transform = CGAffineTransformMake(1.0, Math::tan(y), Math::tan(x), 1.0, 0.0, 0.0)
493
+ CGContextConcatCTM(@ctx, transform)
494
+ end
495
+
496
+
497
+ # STATE
498
+
499
+ def new_state(&block)
500
+ push
501
+ block.call
502
+ pop
503
+ end
504
+
505
+ # push the current drawing context onto the stack
506
+ def push
507
+ CGContextSaveGState(@ctx)
508
+ @stacksize = @stacksize + 1
509
+ end
510
+
511
+ # pop the previous drawing context off the stack
512
+ def pop
513
+ CGContextRestoreGState(@ctx)
514
+ @stacksize = @stacksize - 1
515
+ end
516
+
517
+ # restore the initial context
518
+ def reset
519
+ until (@stacksize <= 1)
520
+ pop # retrieve graphics states until we get to the default state
521
+ end
522
+ push # push the retrieved pristine default state back onto the stack
523
+ end
524
+
525
+
526
+ # EFFECTS
527
+
528
+ # apply a drop shadow with offset dx,dy, alpha, and blur
529
+ def shadow(dx=0.0, dy=0.0, a=2.0/3.0, blur=5)
530
+ color = CGColorCreate(@colorspace, [0.0, 0.0, 0.0, a])
531
+ CGContextSetShadowWithColor(@ctx, [dx, dy], blur, color)
532
+ end
533
+
534
+ # apply a glow with offset dx,dy, alpha, and blur
535
+ def glow(dx=0.0, dy=0.0, a=2.0/3.0, blur=5)
536
+ color = CGColorCreate(@colorspace, [1.0, 1.0, 0.0, a])
537
+ CGContextSetShadowWithColor(@ctx, [dx, dy], blur, color)
538
+ end
539
+
540
+ # stop using a shadow
541
+ def noshadow
542
+ CGContextSetShadowWithColor(@ctx, [0,0], 1, nil)
543
+ end
544
+
545
+ # set the canvas blend mode (:normal, :darken, :multiply, :screen, etc)
546
+ def blend(mode)
547
+ CGContextSetBlendMode(@ctx, BlendModes[mode])
548
+ end
549
+
550
+
551
+ # CLIPPING MASKS
552
+
553
+ # clip subsequent drawing operations within the given path
554
+ def beginclip(p, &block)
555
+ push
556
+ CGContextAddPath(@ctx, p.path)
557
+ CGContextClip(@ctx)
558
+ if block
559
+ block.call
560
+ endclip
561
+ end
562
+ end
563
+
564
+ # stop clipping drawing operations
565
+ def endclip
566
+ pop
567
+ end
568
+
569
+ # DRAW TEXT TO CANVAS
570
+
571
+ # NOTE: may want to switch to ATSUI for text handling
572
+ # http://developer.apple.com/documentation/Carbon/Reference/ATSUI_Reference/Reference/reference.html
573
+
574
+ # write the text at x,y using the current fill
575
+ def text(txt="A", x=0, y=0)
576
+ txt = txt.to_s unless txt.kind_of?(String)
577
+ if @registration == :center
578
+ width = textwidth(txt)
579
+ x = x - width / 2
580
+ y = y + @fsize / 2
581
+ end
582
+ CGContextShowTextAtPoint(@ctx, x, y, txt, txt.length)
583
+ end
584
+
585
+ # determine the width of the given text without drawing it
586
+ def textwidth(txt, width=nil)
587
+ push
588
+ start = CGContextGetTextPosition(@ctx)
589
+ CGContextSetTextDrawingMode(@ctx, KCGTextInvisible)
590
+ CGContextShowText(@ctx, txt, txt.length)
591
+ final = CGContextGetTextPosition(@ctx)
592
+ pop
593
+ final.x - start.x
594
+ end
595
+
596
+ # def textheight(txt)
597
+ # # need to use ATSUI
598
+ # end
599
+ #
600
+ # def textmetrics(txt)
601
+ # # need to use ATSUI
602
+ # end
603
+
604
+ # set font by name and optional size
605
+ def font(name="Helvetica", size=nil)
606
+ fontsize(size) if size
607
+ @fname = name
608
+ fontsize unless @fsize
609
+ CGContextSelectFont(@ctx, @fname, @fsize, KCGEncodingMacRoman)
610
+ end
611
+
612
+ # set font size in points
613
+ def fontsize(points=20)
614
+ @fsize = points
615
+ font unless @fname
616
+ #CGContextSetFontSize(@ctx,points)
617
+ CGContextSelectFont(@ctx, @fname, @fsize, KCGEncodingMacRoman)
618
+ end
619
+
620
+
621
+ # SAVING/EXPORTING
622
+
623
+ def nsimage
624
+ image = NSImage.alloc.init
625
+ image.addRepresentation(NSBitmapImageRep.alloc.initWithCGImage(cgimage))
626
+ image
627
+ end
628
+
629
+ # return a CGImage of the canvas for reprocessing (only works if using a bitmap context)
630
+ def cgimage
631
+ CGBitmapContextCreateImage(@ctx) # => CGImageRef (works with bitmap context only)
632
+ #cgimageref = CGImageCreate(@width, @height, @bits_per_component, nil,nil,@colorspace, nil, @provider,nil,true,KCGRenderingIntentDefault)
633
+ end
634
+
635
+ # return a CIImage of the canvas for reprocessing (only works if using a bitmap context)
636
+ def ciimage
637
+ cgimageref = self.cgimage
638
+ CIImage.imageWithCGImage(cgimageref) # CIConcreteImage (CIImage)
639
+ end
640
+
641
+ # begin a new PDF page
642
+ def newpage
643
+ if (@filetype == :pdf)
644
+ CGContextFlush(@ctx)
645
+ CGPDFContextEndPage(@ctx)
646
+ CGPDFContextBeginPage(@ctx, nil)
647
+ else
648
+ puts "WARNING: newpage only valid when using PDF output"
649
+ end
650
+ end
651
+
652
+ # save the image to a file
653
+ def save
654
+
655
+ properties = {}
656
+ # exif = {}
657
+ # KCGImagePropertyExifDictionary
658
+ # exif[KCGImagePropertyExifUserComment] = 'Image downloaded from www.sheetmusicplus.com'
659
+ # exif[KCGImagePropertyExifAuxOwnerName] = 'www.sheetmusicplus.com'
660
+ if @filetype == :pdf
661
+ CGPDFContextEndPage(@ctx)
662
+ CGContextFlush(@ctx)
663
+ return
664
+ elsif @filetype == :png
665
+ format = NSPNGFileType
666
+ elsif @filetype == :tif
667
+ format = NSTIFFFileType
668
+ properties[NSImageCompressionMethod] = NSTIFFCompressionLZW
669
+ #properties[NSImageCompressionMethod] = NSTIFFCompressionNone
670
+ elsif @filetype == :gif
671
+ format = NSGIFFileType
672
+ #properties[NSImageDitherTransparency] = 0 # 1 = dithered, 0 = not dithered
673
+ #properties[NSImageRGBColorTable] = nil # For GIF input and output. It consists of a 768 byte NSData object that contains a packed RGB table with each component being 8 bits.
674
+ elsif @filetype == :jpg
675
+ format = NSJPEGFileType
676
+ properties[NSImageCompressionFactor] = @quality # (jpeg compression, 0.0 = max, 1.0 = none)
677
+ #properties[NSImageEXIFData] = exif
678
+ end
679
+ cgimageref = CGBitmapContextCreateImage(@ctx) # => CGImageRef
680
+ bitmaprep = NSBitmapImageRep.alloc.initWithCGImage(cgimageref) # => NSBitmapImageRep
681
+ blob = bitmaprep.representationUsingType(format, properties:properties) # => NSConcreteData
682
+ blob.writeToFile(@output, atomically:true)
683
+ true
684
+ end
685
+
686
+ # open the output file in its associated application
687
+ def open
688
+ system "open #{@output}"
689
+ end
690
+
691
+ # def save(dest)
692
+ ## http://developer.apple.com/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_data_mgr/chapter_11_section_3.html
693
+ # properties = {
694
+ #
695
+ # }
696
+ # cgimageref = CGBitmapContextCreateImage(@ctx) # => CGImageRef
697
+ # destination = CGImageDestinationCreateWithURL(NSURL.fileURLWithPath(dest)) # => CGImageDestinationRef
698
+ # CGImageDestinationSetProperties(destination,properties)
699
+ # CGImageDestinationAddImage(cgimageref)
700
+ # end
701
+
702
+ private
703
+
704
+ # DRAWING PATHS ON A CANVAS
705
+
706
+ def draw_path(p, tx=0, ty=0, iterations=1)
707
+ new_state do
708
+ iterations.times do |i|
709
+ if (i > 0)
710
+ # INCREMENT TRANSFORM:
711
+ # translate x, y
712
+ translate(choose(p.inc[:x]), choose(p.inc[:y]))
713
+ # choose a rotation factor from the range
714
+ rotate(choose(p.inc[:rotation]))
715
+ # choose a scaling factor from the range
716
+ sc = choose(p.inc[:scale])
717
+ sx = choose(p.inc[:scalex]) * sc
718
+ sy = p.inc[:scaley] ? choose(p.inc[:scaley]) * sc : sx * sc
719
+ scale(sx, sy)
720
+ end
721
+
722
+ new_state do
723
+ # PICK AND ADJUST FILL/STROKE COLORS:
724
+ [:fill,:stroke].each do |kind|
725
+ # PICK A COLOR
726
+ if (p.inc[kind]) then
727
+ # increment color from array
728
+ colorindex = i % p.inc[kind].size
729
+ c = p.inc[kind][colorindex].copy
730
+ else
731
+ c = p.rand[kind]
732
+ case c
733
+ when Array
734
+ c = choose(c).copy
735
+ when Color
736
+ c = c.copy
737
+ else
738
+ next
739
+ end
740
+ end
741
+
742
+ if (p.inc[:hue] or p.inc[:saturation] or p.inc[:brightness])
743
+ # ITERATE COLOR
744
+ if (p.inc[:hue])
745
+ newhue = (c.hue + choose(p.inc[:hue])) % 1
746
+ c.hue(newhue)
747
+ end
748
+ if (p.inc[:saturation])
749
+ newsat = (c.saturation + choose(p.inc[:saturation]))
750
+ c.saturation(newsat)
751
+ end
752
+ if (p.inc[:brightness])
753
+ newbright = (c.brightness + choose(p.inc[:brightness]))
754
+ c.brightness(newbright)
755
+ end
756
+ if (p.inc[:alpha])
757
+ newalpha = (c.a + choose(p.inc[:alpha]))
758
+ c.a(newalpha)
759
+ end
760
+ p.rand[kind] = c
761
+ else
762
+ # RANDOMIZE COLOR
763
+ c.hue(choose(p.rand[:hue])) if p.rand[:hue]
764
+ c.saturation(choose(p.rand[:saturation])) if p.rand[:saturation]
765
+ c.brightness(choose(p.rand[:brightness])) if p.rand[:brightness]
766
+ end
767
+
768
+ # APPLY COLOR
769
+ fill(c) if kind == :fill
770
+ stroke(c) if kind == :stroke
771
+ end
772
+ # choose a stroke width from the range
773
+ strokewidth(choose(p.rand[:strokewidth])) if p.rand[:strokewidth]
774
+ # choose an alpha level from the range
775
+ alpha(choose(p.rand[:alpha])) if p.rand[:alpha]
776
+
777
+ # RANDOMIZE TRANSFORM:
778
+ # translate x, y
779
+ translate(choose(p.rand[:x]), choose(p.rand[:y]))
780
+ # choose a rotation factor from the range
781
+ rotate(choose(p.rand[:rotation]))
782
+ # choose a scaling factor from the range
783
+ sc = choose(p.rand[:scale])
784
+ sx = choose(p.rand[:scalex]) * sc
785
+ sy = p.rand[:scaley] ? choose(p.rand[:scaley]) * sc : sx * sc
786
+ scale(sx,sy)
787
+
788
+ # DRAW
789
+ if (tx > 0 || ty > 0)
790
+ translate(tx, ty)
791
+ end
792
+
793
+ CGContextAddPath(@ctx, p.path) if p.class == Path
794
+ CGContextDrawPath(@ctx, KCGPathFillStroke) # apply fill and stroke
795
+
796
+ # if there's an image, draw it clipped by the path
797
+ if (p.image)
798
+ beginclip(p)
799
+ image(p.image)
800
+ endclip
801
+ end
802
+
803
+ end
804
+ end
805
+ end
806
+ end
807
+
808
+ # DRAWING IMAGES ON CANVAS
809
+
810
+ # draw the specified image at x,y with dimensions w,h.
811
+ # "img" may be a path to an image, or an Image instance
812
+ def draw_image(img, x=0, y=0, w=nil, h=nil, pagenum=1)
813
+ new_state do
814
+ if (img.kind_of?(Pdf))
815
+ w ||= img.width(pagenum)
816
+ h ||= img.height(pagenum)
817
+ if(@registration == :center)
818
+ x = x - w / 2
819
+ y = y - h / 2
820
+ end
821
+ img.draw(@ctx, x, y, w, h, pagenum)
822
+ elsif(img.kind_of?(String) || img.kind_of?(Image))
823
+ img = Image.new(img) if img.kind_of?(String)
824
+ w ||= img.width
825
+ h ||= img.height
826
+ img.draw(@ctx, x, y, w, h)
827
+ else
828
+ raise ArgumentError.new("canvas.image: not a recognized image type: #{img.class}")
829
+ end
830
+ end
831
+ end
832
+
833
+
834
+ end
835
+
836
+ end