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