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,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