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.
- data/History.txt +4 -0
- data/bin/hotcocoa +31 -0
- data/lib/hotcocoa/application_builder.rb +320 -0
- data/lib/hotcocoa/attributed_string.rb +143 -0
- data/lib/hotcocoa/behaviors.rb +7 -0
- data/lib/hotcocoa/data_sources/combo_box_data_source.rb +44 -0
- data/lib/hotcocoa/data_sources/table_data_source.rb +18 -0
- data/lib/hotcocoa/delegate_builder.rb +85 -0
- data/lib/hotcocoa/graphics/canvas.rb +836 -0
- data/lib/hotcocoa/graphics/color.rb +781 -0
- data/lib/hotcocoa/graphics/elements/particle.rb +75 -0
- data/lib/hotcocoa/graphics/elements/rope.rb +99 -0
- data/lib/hotcocoa/graphics/elements/sandpainter.rb +71 -0
- data/lib/hotcocoa/graphics/gradient.rb +63 -0
- data/lib/hotcocoa/graphics/image.rb +488 -0
- data/lib/hotcocoa/graphics/path.rb +325 -0
- data/lib/hotcocoa/graphics/pdf.rb +71 -0
- data/lib/hotcocoa/graphics.rb +161 -0
- data/lib/hotcocoa/kernel_ext.rb +14 -0
- data/lib/hotcocoa/kvo_accessors.rb +48 -0
- data/lib/hotcocoa/layout_view.rb +448 -0
- data/lib/hotcocoa/mapper.rb +227 -0
- data/lib/hotcocoa/mapping_methods.rb +40 -0
- data/lib/hotcocoa/mappings/alert.rb +25 -0
- data/lib/hotcocoa/mappings/application.rb +112 -0
- data/lib/hotcocoa/mappings/array_controller.rb +87 -0
- data/lib/hotcocoa/mappings/box.rb +39 -0
- data/lib/hotcocoa/mappings/button.rb +92 -0
- data/lib/hotcocoa/mappings/collection_view.rb +44 -0
- data/lib/hotcocoa/mappings/color.rb +28 -0
- data/lib/hotcocoa/mappings/column.rb +21 -0
- data/lib/hotcocoa/mappings/combo_box.rb +24 -0
- data/lib/hotcocoa/mappings/control.rb +33 -0
- data/lib/hotcocoa/mappings/font.rb +44 -0
- data/lib/hotcocoa/mappings/gradient.rb +15 -0
- data/lib/hotcocoa/mappings/image.rb +15 -0
- data/lib/hotcocoa/mappings/image_view.rb +43 -0
- data/lib/hotcocoa/mappings/label.rb +25 -0
- data/lib/hotcocoa/mappings/layout_view.rb +9 -0
- data/lib/hotcocoa/mappings/menu.rb +71 -0
- data/lib/hotcocoa/mappings/menu_item.rb +47 -0
- data/lib/hotcocoa/mappings/movie.rb +13 -0
- data/lib/hotcocoa/mappings/movie_view.rb +27 -0
- data/lib/hotcocoa/mappings/notification.rb +17 -0
- data/lib/hotcocoa/mappings/popup.rb +110 -0
- data/lib/hotcocoa/mappings/progress_indicator.rb +68 -0
- data/lib/hotcocoa/mappings/scroll_view.rb +29 -0
- data/lib/hotcocoa/mappings/search_field.rb +9 -0
- data/lib/hotcocoa/mappings/secure_text_field.rb +17 -0
- data/lib/hotcocoa/mappings/segmented_control.rb +97 -0
- data/lib/hotcocoa/mappings/slider.rb +25 -0
- data/lib/hotcocoa/mappings/sort_descriptor.rb +13 -0
- data/lib/hotcocoa/mappings/sound.rb +9 -0
- data/lib/hotcocoa/mappings/speech_synthesizer.rb +25 -0
- data/lib/hotcocoa/mappings/split_view.rb +21 -0
- data/lib/hotcocoa/mappings/status_bar.rb +7 -0
- data/lib/hotcocoa/mappings/status_item.rb +9 -0
- data/lib/hotcocoa/mappings/table_view.rb +110 -0
- data/lib/hotcocoa/mappings/text_field.rb +41 -0
- data/lib/hotcocoa/mappings/text_view.rb +13 -0
- data/lib/hotcocoa/mappings/timer.rb +25 -0
- data/lib/hotcocoa/mappings/toolbar.rb +97 -0
- data/lib/hotcocoa/mappings/toolbar_item.rb +36 -0
- data/lib/hotcocoa/mappings/view.rb +67 -0
- data/lib/hotcocoa/mappings/web_view.rb +22 -0
- data/lib/hotcocoa/mappings/window.rb +118 -0
- data/lib/hotcocoa/mappings/xml_parser.rb +41 -0
- data/lib/hotcocoa/mappings.rb +109 -0
- data/lib/hotcocoa/mvc.rb +175 -0
- data/lib/hotcocoa/notification_listener.rb +62 -0
- data/lib/hotcocoa/object_ext.rb +22 -0
- data/lib/hotcocoa/plist.rb +45 -0
- data/lib/hotcocoa/standard_rake_tasks.rb +17 -0
- data/lib/hotcocoa/template.rb +27 -0
- data/lib/hotcocoa/virtual_file_system.rb +172 -0
- data/lib/hotcocoa.rb +26 -0
- data/template/Rakefile +5 -0
- data/template/config/build.yml +8 -0
- data/template/lib/application.rb +45 -0
- data/template/lib/menu.rb +32 -0
- data/template/resources/HotCocoa.icns +0 -0
- data/test/test_helper.rb +3 -0
- data/test/test_hotcocoa.rb +11 -0
- 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
|