├── README.md ├── examples ├── app_wrapper.rb ├── canvas_example.rb ├── color_sampler_example.rb ├── drawing_iterate_example.rb ├── image_blend_example.rb ├── image_colors_example.rb ├── image_effect_example.rb ├── image_size_manipulation_example.rb ├── images │ ├── 1984.jpg │ ├── italy.jpg │ └── v2.jpg ├── particle_example.rb ├── randomize_example.rb ├── rope_hair_example.rb ├── rope_rubbon_example.rb └── spirograph_example.rb ├── graphics.rb ├── lib ├── canvas.rb ├── color.rb ├── elements │ ├── particle.rb │ ├── rope.rb │ └── sandpainter.rb ├── gradient.rb ├── image.rb ├── path.rb └── pdf.rb └── specs ├── canvas_spec.rb ├── graphics_spec.rb └── tmp └── .ignore /README.md: -------------------------------------------------------------------------------- 1 | # MacRuby Graphics 2 | 3 | MacRuby Graphics is a graphics library providing a simple object-oriented 4 | interface into the power of Mac OS X's Core Graphics and Core Image drawing libraries. 5 | With a few lines of easy-to-read code, you can write scripts to draw simple or complex 6 | shapes, lines, and patterns, process and filter images, create abstract art or visualize 7 | scientific data, and much more. 8 | 9 | Inspiration for this project was derived from Processing and NodeBox. These excellent 10 | graphics programming environments are more full-featured than MRG, but they are implemented 11 | in Java and Python, respectively. MRG was created to offer similar functionality using 12 | the Ruby programming language. 13 | 14 | The original author of this library is James Reynolds, MacRuby Graphics was then called Ruby Cocoa Graphics 15 | and was packaged as part of Hotcocoa. I made the choice to extract it, add more examples and specs 16 | so MacRuby developers could use this library as an addon to their projects without needing HotCocoa. 17 | 18 | ## Examples 19 | 20 | You can see a list of examples in the examples folder, but here is a quick sample: 21 | 22 | class CustomView < NSView 23 | include MRGraphics 24 | 25 | def drawRect(rect) 26 | dimensions = [CGRectGetWidth(rect), CGRectGetHeight(rect)] 27 | Canvas.for_current_context(:size => dimensions) do |c| 28 | c.background(Color.black) 29 | white = Color.white 30 | c.fill(white) 31 | c.stroke(0.2) 32 | c.stroke_width(1) 33 | c.font("Zapfino") 34 | 35 | 80.times do 36 | c.font_size rand(170) 37 | c.fill(white.copy.darken(rand(0.8))) 38 | letters = %W{ g i a n a } 39 | c.text(letters[rand(letters.size)], 40 | rand(c.width), 41 | rand(c.height)) 42 | end 43 | end 44 | end 45 | 46 | end 47 | 48 | # wrapper class to keep the examples as clean/simple as possible 49 | app = AppWrapper.new 50 | # assign an instance of our custiom NSView to the window's content view 51 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 52 | # start the app 53 | app.start 54 | 55 | ![MacRuby Graphics Canvas example](http://img.skitch.com/20100712-1x4dswurhxcqexq5tpidj29axc.png) 56 | 57 | 58 | class CustomView < NSView 59 | include MRGraphics 60 | 61 | def drawRect(rect) 62 | dimensions = [CGRectGetWidth(rect), CGRectGetHeight(rect)] 63 | Canvas.for_current_context(:size => dimensions) do |c| 64 | c.background(Color.white) 65 | c.font('Skia') 66 | c.font_size(14) 67 | # set image width,height 68 | w, h = [95,95] 69 | # set initial drawing position 70 | x, y = [10, c.height - h - 10] 71 | # load and resize two images 72 | img_a = Image.new(File.join(HERE, 'images', 'v2.jpg')).resize(w,h) 73 | img_b = Image.new(File.join(HERE, 'images', 'italy.jpg')).resize(w,h) 74 | 75 | # add image B to image A using each blending mode, and draw to canvas 76 | [:normal,:multiply,:screen,:overlay,:darken,:lighten, 77 | :colordodge,:colorburn,:softlight,:hardlight,:difference,:exclusion, 78 | :hue,:saturation,:color,:luminosity,:maximum,:minimum,:add,:atop, 79 | :in,:out,:over].each do |blendmode| 80 | img_a.reset.resize(w,h) 81 | img_a.blend(img_b, blendmode) 82 | c.draw(img_a,x,y) 83 | c.text(blendmode, x, y-15) 84 | x += w + 5 85 | if (x > c.width - w + 5) 86 | x = 10 87 | y -= h + 25 88 | end 89 | end 90 | end 91 | end 92 | 93 | end 94 | 95 | # wrapper class to keep the examples as clean/simple as possible 96 | app = AppWrapper.new(415,730) 97 | # assign an instance of our custiom NSView to the window's content view 98 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 99 | # start the app 100 | app.start 101 | 102 | ![MacRuby Image blend modes](http://img.skitch.com/20100712-bedhi8i4ppuqetad263w3ehuna.png) 103 | 104 | ##More examples: 105 | _see the examples folder for the source code of each image's source code._ 106 | 107 | ![MacRuby Graphics Image color effects](http://img.skitch.com/20100712-jr4jfhbaw2x9nmhy7bscapgbd4.png) 108 | ![MacRuby Graphics Image Iterate](http://img.skitch.com/20100713-1132mmahgum65tpgj9d9mag939.png) 109 | ![MacRuby Graphics particles examples](http://img.skitch.com/20100713-gb3ps8psw3ppyedx1t1x426rwa.png) 110 | ![MacRuby Graphics ropes](http://img.skitch.com/20100713-mseyj6qjxp38jnm2xkxpw6ebq4.png) 111 | ![MacRuby Graphics effects](http://img.skitch.com/20100716-8ma9te4tc8th723hd4t5rmbbb8.png) 112 | ![MacRuby Graphics Image resizing](http://img.skitch.com/20100715-k8k8f1gd8rb9e1wfj4ush9i5bf.png) 113 | ![MacRuby Graphics randomize](http://img.skitch.com/20100715-tycucqsgsfiy7syef8i24sw9xj.png) 114 | ![MacRuby Graphics Spirograph](http://img.skitch.com/20100715-jh4nsrm193a2ttdmjjnh4g1x96.png) 115 | ![MacRuby Graphics color sampler](http://img.skitch.com/20100716-nth8dcm4ag12bcns1fgngt4ird.png) 116 | ![MacRuby Graphics Rubbons](http://img.skitch.com/20100715-18f5pwc96b2gdfcdag26sjujam.png) -------------------------------------------------------------------------------- /examples/app_wrapper.rb: -------------------------------------------------------------------------------- 1 | class AppWrapper 2 | 3 | attr_reader :app, :window, :frame 4 | 5 | class AppDelegate 6 | def applicationDidFinishLaunching(notification) 7 | end 8 | 9 | def windowWillClose(notification) 10 | puts "Bye!" 11 | exit 12 | end 13 | end 14 | 15 | def initialize(width=400, height=400) 16 | @app = NSApplication.sharedApplication 17 | @app.delegate = AppDelegate.new 18 | @frame = [0.0, 0.0, width,height] 19 | 20 | # window 21 | @window = NSWindow.alloc.initWithContentRect(frame, 22 | styleMask:NSTitledWindowMask|NSClosableWindowMask|NSMiniaturizableWindowMask, 23 | backing:NSBackingStoreBuffered, 24 | defer:false) 25 | @window.delegate = app.delegate 26 | end 27 | 28 | def start 29 | window.center 30 | window.display 31 | window.makeKeyAndOrderFront(nil) 32 | window.orderFrontRegardless 33 | app.run 34 | end 35 | 36 | end -------------------------------------------------------------------------------- /examples/canvas_example.rb: -------------------------------------------------------------------------------- 1 | framework 'Cocoa' 2 | here = File.expand_path(File.dirname(__FILE__)) 3 | require File.join(here, '..', 'graphics') 4 | require File.join(here, 'app_wrapper') 5 | 6 | class CustomView < NSView 7 | include MRGraphics 8 | 9 | def drawRect(rect) 10 | dimensions = [CGRectGetWidth(rect), CGRectGetHeight(rect)] 11 | Canvas.for_current_context(:size => dimensions) do |c| 12 | c.background(Color.black) 13 | white = Color.white 14 | c.fill(white) 15 | c.stroke(0.2) 16 | c.stroke_width(1) 17 | c.font("Zapfino") 18 | 19 | 80.times do 20 | c.font_size rand(170) 21 | c.fill(white.copy.darken(rand(0.8))) 22 | letters = %W{ g i a n a } 23 | c.text(letters[rand(letters.size)], 24 | rand(c.width), 25 | rand(c.height)) 26 | end 27 | end 28 | end 29 | 30 | end 31 | 32 | app = AppWrapper.new 33 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 34 | app.start -------------------------------------------------------------------------------- /examples/color_sampler_example.rb: -------------------------------------------------------------------------------- 1 | framework 'Cocoa' 2 | HERE = File.expand_path(File.dirname(__FILE__)) 3 | require File.join(HERE, '..', 'graphics') 4 | require File.join(HERE, 'app_wrapper') 5 | 6 | class CustomView < NSView 7 | include MRGraphics 8 | 9 | def flip_order(ord) 10 | ord == 'ltr' ? 'rtl' : 'ltr' 11 | end 12 | 13 | def calculate_position(x, y, order, rect, offset=20) 14 | @width ||= CGRectGetWidth(rect) 15 | if (x + offset > @width) || ((x - offset) == -20 && y != 0 && order == 'rtl') 16 | order = flip_order(order) 17 | y += offset 18 | new_row = true 19 | end 20 | unless x == 0 && new_row 21 | x = order == 'ltr' ? x + offset : x - offset 22 | end 23 | [x, y, order] 24 | end 25 | 26 | def drawRect(rect) 27 | dimensions = [CGRectGetWidth(rect), CGRectGetHeight(rect)] 28 | Canvas.for_current_context(:size => dimensions) do |c| 29 | c.background(Color.gray.lighten(0.4)) 30 | # load image and grab colors 31 | img = Image.new(File.join(HERE, 'images', '1984.jpg')).saturation(1.9) 32 | 33 | # drawing the photo 34 | img_x = 100 35 | # y = height - resized img height - margin 36 | img_y = dimensions.last - 200 - 20 37 | c.draw(img.resize(200, 200), img_x, img_y) 38 | 39 | # drawing the text 40 | c.text("Color sampling from jpg", 100, img_y - 30) 41 | 42 | colors = img.colors(500) 43 | sample = Path.new.rect 44 | x, y = -20, 0 45 | order = 'ltr' 46 | 47 | # sorting colors isn't quite easy 48 | # we are using spherical coordinates and multipass sorting 49 | theta_sorter = lambda{|cs, window| cs.each_slice(window).map{|s| s.sort_by {|c| c.spherical_coordinates.first} }.flatten} 50 | psi_sorter = lambda{|cs, window| cs.each_slice(window).map{|s| s.sort_by {|c| c.spherical_coordinates.last} }.flatten} 51 | # sort on psi 52 | first_sort = colors.sort_by{|c| c.spherical_coordinates.last } 53 | second_sort = theta_sorter.call(first_sort, 25) 54 | # third_sort = theta_sorter.call(first_sort, 15) #psi_sorter.call(first_sort, 35) 55 | second_sort.reverse.each do |color| 56 | # skip of the color is too white or too black 57 | next if color.white? || color.brightness < 0.2 58 | x, y, order = calculate_position(x, y, order, rect) 59 | c.push 60 | sample.fill(color) 61 | c.draw(sample, x, y) 62 | c.pop 63 | end 64 | 65 | end 66 | end 67 | 68 | end 69 | 70 | app = AppWrapper.new(400,520) 71 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 72 | app.start 73 | -------------------------------------------------------------------------------- /examples/drawing_iterate_example.rb: -------------------------------------------------------------------------------- 1 | framework 'Cocoa' 2 | HERE = File.expand_path(File.dirname(__FILE__)) 3 | require File.join(HERE, '..', 'graphics') 4 | require File.join(HERE, 'app_wrapper') 5 | 6 | class CustomView < NSView 7 | include MRGraphics 8 | 9 | def drawRect(rect) 10 | dimensions = [CGRectGetWidth(rect), CGRectGetHeight(rect)] 11 | Canvas.for_current_context(:size => dimensions) do |c| 12 | c.background(Color.white) 13 | 14 | # create a petal shape with base at (0,0), size 40×150, and bulge at 30px 15 | shape = Path.new 16 | shape.petal(0,0,40,150,30) 17 | # add a circle 18 | shape.oval(-10,20,20,20) 19 | # color it red 20 | shape.fill(Color.red) 21 | 22 | # increment shape parameters by the specified amount each iteration, 23 | # or by a random value selected from the specified range 24 | shape.increment(:rotation, 5.0) 25 | shape.increment(:scale_x, 0.99) 26 | shape.increment(:scale_y, 0.96) 27 | shape.increment(:x, 10.0) 28 | shape.increment(:y, 12.0) 29 | shape.increment(:hue,-0.02..0.02) 30 | shape.increment(:saturation, -0.1..0.1) 31 | shape.increment(:brightness, -0.1..0.1) 32 | shape.increment(:alpha, -0.1..0.1) 33 | 34 | # draw 200 petals on the canvas starting at location 50,200 35 | c.translate(50,220) 36 | c.draw(shape,0,0,200) 37 | end 38 | end 39 | 40 | end 41 | 42 | # wrapper class to keep the examples as clean/simple as possible 43 | app = AppWrapper.new(400,400) 44 | # assign an instance of our custiom NSView to the window's content view 45 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 46 | # start the app 47 | app.start -------------------------------------------------------------------------------- /examples/image_blend_example.rb: -------------------------------------------------------------------------------- 1 | framework 'Cocoa' 2 | HERE = File.expand_path(File.dirname(__FILE__)) 3 | require File.join(HERE, '..', 'graphics') 4 | require File.join(HERE, 'app_wrapper') 5 | 6 | 7 | class CustomView < NSView 8 | include MRGraphics 9 | 10 | def drawRect(rect) 11 | dimensions = [CGRectGetWidth(rect), CGRectGetHeight(rect)] 12 | Canvas.for_current_context(:size => dimensions) do |c| 13 | c.background(Color.white) 14 | c.font('Skia') 15 | c.font_size(14) 16 | # set image width,height 17 | w, h = [95,95] 18 | # set initial drawing position 19 | x, y = [10, c.height - h - 10] 20 | # load and resize two images 21 | img_a = Image.new(File.join(HERE, 'images', 'v2.jpg')).resize(w,h) 22 | img_b = Image.new(File.join(HERE, 'images', 'italy.jpg')).resize(w,h) 23 | 24 | # add image B to image A using each blending mode, and draw to canvas 25 | [:normal,:multiply,:screen,:overlay,:darken,:lighten, 26 | :colordodge,:colorburn,:softlight,:hardlight,:difference,:exclusion, 27 | :hue,:saturation,:color,:luminosity,:maximum,:minimum,:add,:atop, 28 | :in,:out,:over].each do |blendmode| 29 | img_a.reset.resize(w,h) 30 | img_a.blend(img_b, blendmode) 31 | c.draw(img_a,x,y) 32 | c.text(blendmode, x, y-15) 33 | x += w + 5 34 | if (x > c.width - w + 5) 35 | x = 10 36 | y -= h + 25 37 | end 38 | end 39 | end 40 | end 41 | 42 | end 43 | 44 | # wrapper class to keep the examples as clean/simple as possible 45 | app = AppWrapper.new(415,730) 46 | # assign an instance of our custiom NSView to the window's content view 47 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 48 | # start the app 49 | app.start -------------------------------------------------------------------------------- /examples/image_colors_example.rb: -------------------------------------------------------------------------------- 1 | framework 'Cocoa' 2 | HERE = File.expand_path(File.dirname(__FILE__)) 3 | require File.join(HERE, '..', 'graphics') 4 | require File.join(HERE, 'app_wrapper') 5 | 6 | 7 | class CustomView < NSView 8 | include MRGraphics 9 | 10 | def drawRect(rect) 11 | Canvas.for_current_context(:size => [CGRectGetWidth(rect), CGRectGetHeight(rect)]) do |c| 12 | c.background(Color.white) 13 | c.font('Skia') 14 | c.font_size(14) 15 | c.fill(Color.black) 16 | 17 | # load image 18 | img = Image.new(File.join(HERE, 'images', 'v2.jpg')) 19 | w,h = [100,100] 20 | x,y = [10,290] 21 | x_offset = 105 22 | y_offset = 130 23 | 24 | # original image, resized to fit within w,h: 25 | img.fit(w,h) 26 | c.draw(img,x,y) 27 | c.text("original",x,y-15) 28 | x += x_offset 29 | 30 | # HUE: rotate color wheel by degrees 31 | img.reset.fit(w,h) 32 | img.hue(45) 33 | c.draw(img,x,y) 34 | c.text("hue",x,y-15) 35 | x += x_offset 36 | 37 | # EXPOSURE: increase/decrease exposure by f-stops 38 | img.reset.fit(w,h) 39 | img.exposure(1.0) 40 | c.draw(img,x,y) 41 | c.text("exposure",x,y-15) 42 | x += x_offset 43 | 44 | # SATURATION: adjust saturation by multiplier 45 | img.reset.fit(w,h) 46 | img.saturation(2.0) 47 | c.draw(img,x,y) 48 | c.text("saturation",x,y-15) 49 | x += x_offset 50 | 51 | # (go to next row) 52 | x = 10 53 | y -= y_offset 54 | 55 | # CONTRAST: adjust contrast by multiplier 56 | img.reset.fit(w,h) 57 | img.contrast(2.0) 58 | c.draw(img,x,y) 59 | c.text("contrast",x,y-15) 60 | x += x_offset 61 | 62 | # BRIGHTNESS: adjust brightness 63 | img.reset.fit(w,h) 64 | img.brightness(0.5) 65 | c.draw(img,x,y) 66 | c.text("brightness",x,y-15) 67 | x += x_offset 68 | 69 | # MONOCHROME: convert to a monochrome image 70 | img.reset.fit(w,h) 71 | img.monochrome(Color.orange) 72 | c.draw(img,x,y) 73 | c.text("monochrome",x,y-15) 74 | x += x_offset 75 | 76 | # WHITEPOINT: remap the white point 77 | img.reset.fit(w,h) 78 | img.whitepoint(Color.white.ish) 79 | c.draw(img,x,y) 80 | c.text("white point",x,y-15) 81 | x += x_offset 82 | 83 | # (go to next row) 84 | x = 10 85 | y -= y_offset 86 | 87 | # CHAINING: apply multiple effects at once 88 | img.reset.fit(w,h) 89 | img.hue(60).saturation(2.0).contrast(2.5) 90 | c.draw(img,x,y) 91 | c.text("multi effects",x,y-15) 92 | x += x_offset 93 | 94 | # COLORS: sample random colors from the image and render as a gradient 95 | img.reset.fit(w,h) # reset the image and scale to fit within w,h 96 | colors = img.colors(10).sort! # select 10 random colors and sort by brightness 97 | # gradient 98 | gradient = Gradient.new(colors) # create a new gradient using the selected colors 99 | rect = Path.new.rect(x,y,img.width,img.height) # create a rectangle the size of the image 100 | c.begin_clip(rect) # begin clipping so the gradient will only fill the rectangle 101 | c.gradient(gradient,x,y,x+img.width,y+img.height) # draw the gradient between opposite corners of the rectangle 102 | c.end_clip # end clipping so we can draw on the rest of the canvas 103 | c.text("get colors",x,y-15) # add text label 104 | x += x_offset 105 | end 106 | end 107 | 108 | end 109 | 110 | # wrapper class to keep the examples as clean/simple as possible 111 | app = AppWrapper.new(410,400) 112 | # assign an instance of our custiom NSView to the window's content view 113 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 114 | # start the app 115 | app.start 116 | -------------------------------------------------------------------------------- /examples/image_effect_example.rb: -------------------------------------------------------------------------------- 1 | framework 'Cocoa' 2 | HERE = File.expand_path(File.dirname(__FILE__)) 3 | require File.join(HERE, '..', 'graphics') 4 | require File.join(HERE, 'app_wrapper') 5 | 6 | 7 | class CustomView < NSView 8 | include MRGraphics 9 | 10 | def drawRect(rect) 11 | Canvas.for_current_context(:size => [CGRectGetWidth(rect), CGRectGetHeight(rect)]) do |canvas| 12 | canvas.background(Color.white) 13 | canvas.font('Skia') 14 | canvas.font_size(14) 15 | canvas.fill(Color.black) 16 | 17 | # load image file 18 | img = Image.new(File.join(HERE, 'images', 'v2.jpg')) 19 | 20 | # set image width/height, starting position, and increment position 21 | w,h = [100,100] 22 | x,y = [10,290] 23 | x_offset = 105 24 | y_offset = 130 25 | 26 | # ORIGINAL image, resized to fit within w,h: 27 | img.fit(w,h) 28 | canvas.draw(img,x,y) 29 | canvas.text("original",x,y-15) 30 | x += x_offset 31 | 32 | # CRYSTALLIZE: apply a "crystallize" effect with the given radius 33 | img.reset.fit(w,h) 34 | img.crystallize(5.0) 35 | canvas.draw(img,x,y) 36 | canvas.text("crystallize",x,y-15) 37 | x += x_offset 38 | 39 | # BLOOM: apply a "bloom" effect with the given radius and intensity 40 | img.reset.fit(w,h) 41 | img.bloom(10, 1.0) 42 | canvas.draw(img,x,y) 43 | canvas.text("bloom",x,y-15) 44 | x += x_offset 45 | 46 | # EDGES: detect edges 47 | img.reset.fit(w,h) 48 | img.edges(10) 49 | canvas.draw(img,x,y) 50 | canvas.text("edges",x,y-15) 51 | x += x_offset 52 | 53 | # (go to next row) 54 | x = 10 55 | y -= y_offset 56 | 57 | # POSTERIZE: reduce image to the specified number of colors 58 | img.reset.fit(w,h) 59 | img.posterize(8) 60 | canvas.draw(img,x,y) 61 | canvas.text("posterize",x,y-15) 62 | x += x_offset 63 | 64 | # TWIRL: rotate around x,y with radius and angle 65 | img.reset.fit(w,h) 66 | img.twirl(35,50,40,90) 67 | canvas.draw(img,x,y) 68 | canvas.text("twirl",x,y-15) 69 | x += x_offset 70 | 71 | # EDGEWORK: simulate a woodcut print 72 | img.reset.fit(w,h) 73 | canvas.rect(x,y,img.width,img.height) # needs a black background 74 | img.edgework(0.5) 75 | canvas.draw(img,x,y) 76 | canvas.text("edgework",x,y-15) 77 | x += x_offset 78 | 79 | # DISPLACEMENT: use a second image as a displacement map 80 | img.reset.fit(w,h) 81 | img2 = Image.new(File.join(HERE, 'images', 'italy.jpg')).resize(img.width,img.height) 82 | img.displacement(img2, 30.0) 83 | canvas.draw(img,x,y) 84 | canvas.text("displacement",x,y-15) 85 | x += x_offset 86 | 87 | # (go to next row) 88 | x = 10 89 | y -= y_offset 90 | 91 | # DOTSCREEN: simulate a dot screen: center point, angle(0-360), width(1-50), and sharpness(0-1) 92 | img.reset.fit(w,h) 93 | img.dotscreen(0,0,45,5,0.7) 94 | canvas.draw(img,x,y) 95 | canvas.text("dotscreen",x,y-15) 96 | x += x_offset 97 | 98 | # SHARPEN: sharpen using the given radius and intensity 99 | img.reset.fit(w,h) 100 | img.sharpen(2.0,2.0) 101 | canvas.draw(img,x,y) 102 | canvas.text("sharpen",x,y-15) 103 | x += x_offset 104 | 105 | # BLUR: apply a gaussian blur with the given radius 106 | img.reset.fit(w,h) 107 | img.blur(3.0) 108 | canvas.draw(img,x,y) 109 | canvas.text("blur",x,y-15) 110 | x += x_offset 111 | 112 | # MOTION BLUR: apply a motion blur with the given radius and angle 113 | img.reset.fit(w,h) 114 | img.motionblur(10.0,90) 115 | canvas.draw(img,x,y) 116 | canvas.text("motion blur",x,y-15) 117 | x += x_offset 118 | end 119 | end 120 | 121 | end 122 | 123 | app = AppWrapper.new(410,400) 124 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 125 | app.start -------------------------------------------------------------------------------- /examples/image_size_manipulation_example.rb: -------------------------------------------------------------------------------- 1 | framework 'Cocoa' 2 | HERE = File.expand_path(File.dirname(__FILE__)) 3 | require File.join(HERE, '..', 'graphics') 4 | require File.join(HERE, 'app_wrapper') 5 | 6 | class CustomView < NSView 7 | include MRGraphics 8 | 9 | def drawRect(rect) 10 | dimensions = [CGRectGetWidth(rect), CGRectGetHeight(rect)] 11 | Canvas.for_current_context(:size => dimensions) do |canvas| 12 | canvas.background(Color.white) 13 | canvas.font('Skia') 14 | canvas.font_size(14) 15 | canvas.fill(Color.black) 16 | canvas.stroke(Color.red) 17 | 18 | # load an image 19 | img = Image.new(File.join(HERE, 'images', 'v2.jpg')) 20 | 21 | # SCALE (multiply both dimensions by a scaling factor) 22 | img.scale(0.2) 23 | canvas.draw(img, 10, 240) # draw the image at the specified coordinates 24 | canvas.text("scale to 20%", 10, 220) 25 | 26 | # FIT (scale to fit within the given dimensions, maintaining original aspect ratio) 27 | img.reset # first reset the image to its original size 28 | img.fit(100,100) 29 | canvas.fill(Color.white) 30 | canvas.rect(120,240,100,100) 31 | canvas.fill(Color.black) 32 | canvas.draw(img,143,240) 33 | canvas.text("fit into 100x100",130,220) 34 | 35 | # RESIZE (scale to fit exactly within the given dimensions) 36 | img.reset 37 | img.resize(100,100) 38 | canvas.draw(img,250,240) 39 | canvas.text("resize to 100x100",250,220) 40 | 41 | # CROP (to the largest square containing image data) 42 | img.reset 43 | img.scale(0.2).crop 44 | canvas.draw(img,10,100) 45 | canvas.text("crop max square",10,80) 46 | 47 | # CROP (within a rectangle starting at x,y with width,height) 48 | img.reset 49 | img.scale(0.3).crop(0,0,100,100) 50 | canvas.draw(img,130,100) 51 | canvas.text("crop to 100x100",130,80) 52 | 53 | # ROTATE 54 | img.origin(:center) 55 | img.rotate(45) 56 | canvas.draw(img,310,140) 57 | canvas.text("rotate 45 degrees",260,50) 58 | end 59 | end 60 | 61 | end 62 | 63 | app = AppWrapper.new(400,400) 64 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 65 | app.start -------------------------------------------------------------------------------- /examples/images/1984.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/macruby_graphics/cb7c119d455d647d511980b9d0e1a79c59921f48/examples/images/1984.jpg -------------------------------------------------------------------------------- /examples/images/italy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/macruby_graphics/cb7c119d455d647d511980b9d0e1a79c59921f48/examples/images/italy.jpg -------------------------------------------------------------------------------- /examples/images/v2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattetti/macruby_graphics/cb7c119d455d647d511980b9d0e1a79c59921f48/examples/images/v2.jpg -------------------------------------------------------------------------------- /examples/particle_example.rb: -------------------------------------------------------------------------------- 1 | framework 'Cocoa' 2 | HERE = File.expand_path(File.dirname(__FILE__)) 3 | require File.join(HERE, '..', 'graphics') 4 | require File.join(HERE, 'app_wrapper') 5 | 6 | class CustomView < NSView 7 | include MRGraphics 8 | 9 | def drawRect(rect) 10 | dimensions = [CGRectGetWidth(rect), CGRectGetHeight(rect)] 11 | Canvas.for_current_context(:size => dimensions) do |c| 12 | c.background(Color.black) 13 | 14 | # load images and grab colors 15 | img = Image.new(File.join(HERE, 'images', 'italy.jpg')).saturation(1.9) 16 | red_colors = img.colors(100) 17 | img = Image.new(File.join(HERE, 'images', 'v2.jpg')).saturation(1.9) 18 | blue_colors = img.colors(100) 19 | 20 | # create flower head shape 21 | head = Path.new.oval(0,0,10,10,:center) 22 | petals = 3 23 | petals.times do 24 | head.rotate(360/petals) 25 | head.oval(0,10,5,5,:center) 26 | head.oval(0,17,2,2,:center) 27 | end 28 | # randomize head attributes 29 | head.randomize(:fill, red_colors) 30 | head.randomize(:stroke, blue_colors) 31 | head.randomize(:scale, 0.2..2.0) 32 | head.randomize(:rotation, 0..360) 33 | 34 | # create particles 35 | numparticles = 200 36 | numframes = 200 37 | particles = [] 38 | numparticles.times do |i| 39 | # start particle at random point at bottom of canvas 40 | x = MRGraphics.random(c.width/2 - 50, c.width/2 + 50) 41 | p = Particle.new(x,0) 42 | p.velocity_x = MRGraphics.random(-0.5,0.5) # set initial x velocity 43 | p.velocity_y = MRGraphics.random(1.0,3.0) # set initial y velocity 44 | p.acceleration = 0.1 # set drag or acceleration 45 | particles[i] = p # add particle to array 46 | end 47 | 48 | # animate particles 49 | numframes.times do |frame| 50 | numparticles.times do |i| 51 | particles[i].move 52 | end 53 | end 54 | 55 | # draw particle trails and heads 56 | numparticles.times do |i| 57 | c.push 58 | # choose a stem color 59 | color = MRGraphics.choose(blue_colors).a(0.7).analog(20,0.7) 60 | c.stroke(color) 61 | c.stroke_width(MRGraphics.random(0.5,2.0)) 62 | 63 | # draw the particle 64 | particles[i].draw(c) 65 | 66 | # go to the last particle position and draw the flower head 67 | c.translate(particles[i].points[-1][0],particles[i].points[-1][1]) 68 | c.draw(head) 69 | c.pop 70 | end 71 | end 72 | end 73 | 74 | end 75 | 76 | app = AppWrapper.new(800,600) 77 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 78 | app.start -------------------------------------------------------------------------------- /examples/randomize_example.rb: -------------------------------------------------------------------------------- 1 | framework 'Cocoa' 2 | HERE = File.expand_path(File.dirname(__FILE__)) 3 | require File.join(HERE, '..', 'graphics') 4 | require File.join(HERE, 'app_wrapper') 5 | 6 | class CustomView < NSView 7 | include MRGraphics 8 | 9 | def drawRect(rect) 10 | dimensions = [CGRectGetWidth(rect), CGRectGetHeight(rect)] 11 | Canvas.for_current_context(:size => dimensions) do |canvas| 12 | canvas.background(Color.white) 13 | 14 | # create a flower shape 15 | shape = Path.new 16 | petals = 5 17 | petals.times do 18 | shape.petal(0, 0, 40, 100) # petal at x,y with width,height 19 | shape.rotate(360 / petals) # rotate by 1/5th 20 | end 21 | 22 | # randomize shape parameters 23 | shape.randomize(:fill, Color.blue.complementary) 24 | shape.randomize(:stroke, Color.blue.complementary) 25 | shape.randomize(:stroke_width, 1.0..10.0) 26 | shape.randomize(:rotation, 0..360) 27 | shape.randomize(:scale, 0.5..1.0) 28 | shape.randomize(:scale_x, 0.5..1.0) 29 | shape.randomize(:scale_y, 0.5..1.0) 30 | shape.randomize(:alpha, 0.5..1.0) 31 | # shape.randomize(:hue, 0.5..0.8) 32 | shape.randomize(:saturation, 0.0..1.0) 33 | shape.randomize(:brightness, 0.0..1.0) 34 | shape.randomize(:x, -100.0..100.0) 35 | shape.randomize(:y, -100.0..100.0) 36 | 37 | # draw 50 flowers starting at the center of the canvas 38 | canvas.translate(200, 200) 39 | canvas.draw(shape, 0, 0, 100) 40 | end 41 | end 42 | 43 | end 44 | 45 | app = AppWrapper.new(400,400) 46 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 47 | app.start -------------------------------------------------------------------------------- /examples/rope_hair_example.rb: -------------------------------------------------------------------------------- 1 | framework 'Cocoa' 2 | HERE = File.expand_path(File.dirname(__FILE__)) 3 | require File.join(HERE, '..', 'graphics') 4 | require File.join(HERE, 'app_wrapper') 5 | 6 | class CustomView < NSView 7 | include MRGraphics 8 | 9 | def drawRect(rect) 10 | dimensions = [CGRectGetWidth(rect), CGRectGetHeight(rect)] 11 | Canvas.for_current_context(:size => dimensions) do |canvas| 12 | # choose a random color and set the background to a darker variant 13 | clr = Color.random.a(0.5) 14 | canvas.background(clr.copy.darken(0.6)) 15 | 16 | # create a new rope with 200 fibers 17 | rope = Rope.new(canvas, :width => 100, :fibers => 200, :stroke_width => 0.4, :roundness => 3.0) 18 | 19 | # randomly rotate the canvas from its center 20 | canvas.translate(canvas.width/2, canvas.height/2) 21 | canvas.rotate(MRGraphics.random(0, 360)) 22 | canvas.translate(-canvas.width/2, -canvas.height/2) 23 | 24 | # draw 20 ropes 25 | ropes = 20 26 | ropes.times do 27 | canvas.stroke(clr.copy.analog(20, 0.8)) # rotate hue up to 20 deg left/right, vary brightness/saturation by up to 70% 28 | rope.x0 = -100 # start rope off bottom left of canvas 29 | rope.y0 = -100 30 | rope.x1 = canvas.width + 100 # end rope off top right of canvas 31 | rope.y1 = canvas.height + 100 32 | rope.hair # draw rope in organic ‚Äúhair‚Äù style 33 | end 34 | 35 | end 36 | end 37 | 38 | end 39 | 40 | app = AppWrapper.new(400,400) 41 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 42 | app.start -------------------------------------------------------------------------------- /examples/rope_rubbon_example.rb: -------------------------------------------------------------------------------- 1 | framework 'Cocoa' 2 | HERE = File.expand_path(File.dirname(__FILE__)) 3 | require File.join(HERE, '..', 'graphics') 4 | require File.join(HERE, 'app_wrapper') 5 | 6 | class CustomView < NSView 7 | include MRGraphics 8 | 9 | def drawRect(rect) 10 | dimensions = [CGRectGetWidth(rect), CGRectGetHeight(rect)] 11 | Canvas.for_current_context(:size => dimensions) do |canvas| 12 | # choose a random color and set the background to a darker variant 13 | clr = Color.random.a(0.5) 14 | canvas.background(clr.copy.darken(0.6)) 15 | 16 | # create a new rope with 200 fibers 17 | rope = Rope.new(canvas, :width => 500, :fibers => 200, :stroke_width => 1.0, :roundness => 1.5) 18 | 19 | # randomly rotate the canvas from its center 20 | canvas.translate(canvas.width/2, canvas.height/2) 21 | canvas.rotate(MRGraphics.random(0, 360)) 22 | canvas.translate(-canvas.width/2, -canvas.height/2) 23 | 24 | # draw 20 ropes 25 | ropes = 20 26 | ropes.times do |i| 27 | canvas.stroke(clr.copy.analog(10, 0.7)) # rotate hue up to 10 deg left/right, vary brightness/saturation by up to 70% 28 | rope.x0 = -100 # start rope off bottom left of canvas 29 | rope.y0 = -100 30 | rope.x1 = canvas.width + 200 # end rope off top right of canvas 31 | rope.y1 = canvas.height + 200 32 | rope.ribbon # draw rope 33 | end 34 | end 35 | end 36 | 37 | end 38 | 39 | app = AppWrapper.new(400,400) 40 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 41 | app.start -------------------------------------------------------------------------------- /examples/spirograph_example.rb: -------------------------------------------------------------------------------- 1 | framework 'Cocoa' 2 | HERE = File.expand_path(File.dirname(__FILE__)) 3 | require File.join(HERE, '..', 'graphics') 4 | require File.join(HERE, 'app_wrapper') 5 | 6 | class CustomView < NSView 7 | include MRGraphics 8 | 9 | def drawRect(rect) 10 | dimensions = [CGRectGetWidth(rect), CGRectGetHeight(rect)] 11 | Canvas.for_current_context(:size => dimensions) do |c| 12 | c.background(Color.beige) 13 | c.fill(Color.black) 14 | c.font('Book Antiqua') 15 | c.font_size(12) 16 | c.translate(200,200) 17 | 18 | # rotate, draw text, repeat 19 | 180.times do |frame| 20 | c.new_state do 21 | c.rotate((frame*2) + 120) 22 | c.translate(70,0) 23 | c.text('going...', 80, 0) 24 | c.rotate(frame * 6) 25 | c.text('Around and', 20, 0) 26 | end 27 | end 28 | end 29 | end 30 | 31 | end 32 | 33 | app = AppWrapper.new(400,400) 34 | app.window.contentView = CustomView.alloc.initWithFrame(app.frame) 35 | app.start -------------------------------------------------------------------------------- /graphics.rb: -------------------------------------------------------------------------------- 1 | # MacRuby 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 MRG, but they are implemented 9 | # in Java and Python, respectively. MRG was created to offer similar functionality using 10 | # the Ruby programming language. 11 | # 12 | # Author:: James Reynolds (mailto:drtoast@drtoast.com), Matt Aimonetti 13 | # Copyright:: Copyright (c) 2008 James Reynolds 14 | # License:: Distributes under the same terms as Ruby 15 | 16 | # More information about Quartz 2D is available on the Apple's website: 17 | # http://developer.apple.com/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_overview/dq_overview.html#//apple_ref/doc/uid/TP30001066-CH202-TPXREF101 18 | 19 | framework 'Cocoa' 20 | framework 'Quartz' 21 | framework 'CoreGraphics' 22 | 23 | module MRGraphics 24 | 25 | # UTILITY FUNCTIONS (math/geometry) 26 | TEST = 'OK' 27 | 28 | # convert degrees to radians 29 | def self.radians(deg) 30 | deg * (Math::PI / 180.0) 31 | end 32 | 33 | # convert radians to degrees 34 | def self.degrees(rad) 35 | rad * (180 / Math::PI) 36 | end 37 | 38 | # return the angle of the line joining the two points 39 | def self.angle(x0, y0, x1, y1) 40 | degrees(Math.atan2(y1-y0, x1-x0)) 41 | end 42 | 43 | # return the distance between two points 44 | def self.distance(x0, y0, x1, y1) 45 | Math.sqrt((x1-x0)**2 + (y1-y0)**2) 46 | end 47 | 48 | # return the coordinates of a new point at the given distance and angle from a starting point 49 | def self.coordinates(x0, y0, distance, angle) 50 | x1 = x0 + Math.cos(radians(angle)) * distance 51 | y1 = y0 + Math.sin(radians(angle)) * distance 52 | [x1,y1] 53 | end 54 | 55 | # return the lesser of a,b 56 | def self.min(a, b) 57 | a < b ? a : b 58 | end 59 | 60 | # return the greater of a,b 61 | def self.max(a, b) 62 | a > b ? a : b 63 | end 64 | 65 | # restrict the value to stay within the range 66 | def self.in_range(value, min, max) 67 | if value < min 68 | min 69 | elsif value > max 70 | max 71 | else 72 | value 73 | end 74 | end 75 | 76 | # return a random number within the range, or a float from 0 to the number 77 | def self.random(left=nil, right=nil) 78 | if right 79 | rand * (right - left) + left 80 | elsif left 81 | rand * left 82 | else 83 | rand 84 | end 85 | end 86 | 87 | def self.reflect(x0, y0, x1, y1, d=1.0, a=180) 88 | d *= distance(x0, y0, x1, y1) 89 | a += angle(x0, y0, x1, y1) 90 | x, y = coordinates(x0, y0, d, a) 91 | [x,y] 92 | end 93 | 94 | def self.choose(object) 95 | case object 96 | when Range 97 | case object.first 98 | when Float 99 | rand * (object.last - object.first) + object.first 100 | when Integer 101 | rand(object.last - object.first + 1) + object.first 102 | end 103 | when Array 104 | object.sample 105 | else 106 | object 107 | end 108 | end 109 | 110 | # given an object's x,y coordinates and dimensions, return the distance 111 | # needed to move in order to orient the object at the given location (:center, :bottom_left, etc) 112 | def self.reorient(x, y, w, h, location) 113 | case location 114 | when :bottom_left 115 | move_x = -x 116 | move_y = -y 117 | when :center_left 118 | move_x = -x 119 | move_y = -y - h / 2 120 | when :top_left 121 | move_x = -x 122 | move_y = -x - h 123 | when :bottom_right 124 | move_x = -x - w 125 | move_y = -y 126 | when :center_right 127 | move_x = -x - w 128 | move_y = -y - h / 2 129 | when :top_right 130 | move_x = -x - w 131 | move_y = -y - h 132 | when :bottom_center 133 | move_x = -x - w / 2 134 | move_y = -y 135 | when :center 136 | move_x = -x - w / 2 137 | move_y = -y - h / 2 138 | when :top_center 139 | move_x = -x - w / 2 140 | move_y = -y - h 141 | else 142 | raise "ERROR: image origin locator not recognized: #{location}" 143 | end 144 | [move_x,move_y] 145 | end 146 | 147 | end 148 | 149 | here = File.expand_path(File.dirname(__FILE__)) 150 | require File.join(here, 'lib', 'canvas') 151 | require File.join(here, 'lib', 'color') 152 | require File.join(here, 'lib', 'gradient') 153 | require File.join(here, 'lib', 'image') 154 | require File.join(here, 'lib', 'path') 155 | require File.join(here, 'lib', 'pdf') 156 | require File.join(here, 'lib', 'elements/particle') 157 | require File.join(here, 'lib', 'elements/rope') 158 | require File.join(here, 'lib', 'elements/sandpainter') -------------------------------------------------------------------------------- /lib/canvas.rb: -------------------------------------------------------------------------------- 1 | # MacRuby 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), Matt Aimonetti 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 MRGraphics 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 | class ParamsError < StandardError; end 85 | 86 | # create a new canvas with the given width, height, and output filename (pdf, png, jpg, gif, or tif) 87 | def initialize(options={}, &block) 88 | if options[:size] 89 | options[:width] = options[:size][0] 90 | options[:height] = options[:size][1] 91 | end 92 | options = DefaultOptions.merge(options) 93 | 94 | @width = options[:width] 95 | @height = options[:height] 96 | @output = options[:filename] || 'test' 97 | @stacksize = 0 98 | @colorspace = CGColorSpaceCreateDeviceRGB() # => CGColorSpaceRef 99 | @autoclose_path = false 100 | 101 | case options[:type] 102 | when :pdf 103 | @filetype = :pdf 104 | # CREATE A PDF DRAWING CONTEXT 105 | url = CFURLCreateFromFileSystemRepresentation(nil, @output, @output.length, false) 106 | pdfrect = CGRect.new(CGPoint.new(0, 0), CGSize.new(width, height)) # Landscape 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 | if options[:type] == :image 114 | extension = File.extname(@output).downcase[1..-1] 115 | @filetype = extension.to_sym unless extension.nil? 116 | end 117 | @bits_per_component = 8 118 | @colorspace = CGColorSpaceCreateDeviceRGB() # => CGColorSpaceRef 119 | #alpha = KCGImageAlphaNoneSkipFirst # opaque background 120 | alpha = KCGImageAlphaPremultipliedFirst # transparent background 121 | # 8 integer bits/component; 32 bits/pixel; 3-component colorspace; kCGImageAlphaPremultipliedFirst; 57141 bytes/row. 122 | bytes = @bits_per_component * 4 * @width.ceil 123 | @ctx = CGBitmapContextCreate(nil, @width, @height, @bits_per_component, bytes, @colorspace, alpha) # => CGContextRef 124 | when :context 125 | @ctx = options[:context] 126 | else 127 | raise ParamsError, "The output file type #{ext} was not recognized" 128 | end 129 | 130 | # antialiasing 131 | CGContextSetAllowsAntialiasing(@ctx, true) 132 | 133 | # set defaults 134 | fill # set the default fill 135 | no_stroke # no stroke by default 136 | stroke_width # set the default stroke width 137 | font # set the default font 138 | antialias # set the default antialias state 139 | autoclose_path! # set the autoclosepath default 140 | quality(options[:quality]) # set the compression default 141 | push # save the pristine default graphics state (retrieved by calling "reset") 142 | push # create a new graphics state for the user to mess up 143 | block.call(self) if block_given? 144 | end 145 | 146 | # SET CANVAS GLOBAL PARAMETERS 147 | 148 | # print drawing functions if verbose is true 149 | def verbose(tf=true) 150 | @verbose = tf 151 | end 152 | 153 | # set whether or not drawn paths should be antialiased (true/false) 154 | def antialias(tf=true) 155 | CGContextSetShouldAntialias(@ctx, tf) 156 | end 157 | 158 | # set the alpha value for subsequently drawn objects 159 | def alpha(val=1.0) 160 | CGContextSetAlpha(@ctx, val) 161 | end 162 | 163 | # set compression (0.0 = max, 1.0 = none) 164 | def quality(factor=0.8) 165 | @quality = factor 166 | end 167 | 168 | # set the current fill (given a Color object, or RGBA values) 169 | def fill(r=0, g=0, b=0, a=1) 170 | case r 171 | when Color 172 | g = r.g 173 | b = r.b 174 | a = r.a 175 | r = r.r 176 | end 177 | CGContextSetRGBFillColor(@ctx, r, g, b, a) # RGBA 178 | @fill = true 179 | end 180 | 181 | # remove current fill 182 | def no_fill 183 | CGContextSetRGBFillColor(@ctx, 0.0, 0.0, 0.0, 0.0) # RGBA 184 | @fill = nil 185 | end 186 | 187 | # SET CANVAS STROKE PARAMETERS 188 | 189 | # set stroke color (given a Color object, or RGBA values) 190 | def stroke(r=0, g=0, b=0, a=1.0) 191 | case r 192 | when Color 193 | g = r.g 194 | b = r.b 195 | a = r.a 196 | r = r.r 197 | end 198 | CGContextSetRGBStrokeColor(@ctx, r, g, b, a) # RGBA 199 | @stroke = true 200 | end 201 | 202 | # don't use a stroke for subsequent drawing operations 203 | def no_stroke 204 | CGContextSetRGBStrokeColor(@ctx, 0, 0, 0, 0) # RGBA 205 | @stroke = false 206 | end 207 | 208 | # set stroke width 209 | def stroke_width(width=1) 210 | CGContextSetLineWidth(@ctx, width.to_f) 211 | end 212 | 213 | # set cap style to round, square, or butt 214 | def line_cap(style=:butt) 215 | case style 216 | when :round 217 | cap = KCGLineCapRound 218 | when :square 219 | cap = KCGLineCapSquare 220 | when :butt 221 | cap = KCGLineCapButt 222 | else 223 | raise "ERROR: line cap style not recognized: #{style}" 224 | end 225 | CGContextSetLineCap(@ctx,cap) 226 | end 227 | 228 | # set line join style to round, miter, or bevel 229 | def line_join(style=:miter) 230 | case style 231 | when :round 232 | join = KCGLineJoinRound 233 | when :bevel 234 | join = KCGLineJoinBevel 235 | when :miter 236 | join = KCGLineJoinMiter 237 | else 238 | raise "ERROR: line join style not recognized: #{style}" 239 | end 240 | CGContextSetLineJoin(@ctx,join) 241 | end 242 | 243 | # set lengths of dashes and spaces, and distance before starting dashes 244 | def line_dash(lengths=[10,2], phase=0.0) 245 | count=lengths.size 246 | CGContextSetLineDash(@ctx, phase, lengths, count) 247 | end 248 | 249 | # revert to solid lines 250 | def no_dash 251 | CGContextSetLineDash(@ctx, 0.0, nil, 0) 252 | end 253 | 254 | # DRAWING SHAPES ON CANVAS 255 | 256 | # draw a rectangle starting at x,y and having dimensions w,h 257 | def rect(x=0, y=0, w=20, h=20, reg=@registration) 258 | # center the rectangle 259 | if (reg == :center) 260 | x = x - w / 2 261 | y = y - h / 2 262 | end 263 | CGContextAddRect(@ctx, NSMakeRect(x, y, w, h)) 264 | CGContextDrawPath(@ctx, KCGPathFillStroke) 265 | end 266 | 267 | # inscribe an oval starting at x,y inside a rectangle having dimensions w,h 268 | def oval(x=0, y=0, w=20, h=20, reg=@registration) 269 | # center the oval 270 | if (reg == :center) 271 | x = x - w / 2 272 | y = y - w / 2 273 | end 274 | CGContextAddEllipseInRect(@ctx, NSMakeRect(x, y, w, h)) 275 | CGContextDrawPath(@ctx, KCGPathFillStroke) # apply fill and stroke 276 | end 277 | 278 | # draw a background color (given a Color object, or RGBA values) 279 | def background(r=1, g=1, b=1, a=1.0) 280 | case r 281 | when Color 282 | g = r.g 283 | b = r.b 284 | a = r.a 285 | r = r.r 286 | end 287 | push 288 | CGContextSetRGBFillColor(@ctx, r, g, b, a) # RGBA 289 | rect(0,0,@width,@height) 290 | pop 291 | end 292 | 293 | # draw a radial gradiant starting at sx,sy with radius er 294 | # optional: specify ending at ex,ey and starting radius sr 295 | def radial(gradient, sx=@width/2, sy=@height/2, er=@width/2, ex=sx, ey=sy, sr=0.0) 296 | #options = KCGGradientDrawsBeforeStartLocation 297 | #options = KCGGradientDrawsAfterEndLocation 298 | CGContextDrawRadialGradient(@ctx, gradient.gradient, NSMakePoint(sx, sy), sr, NSMakePoint(ex, ey), er, gradient.pre + gradient.post) 299 | end 300 | 301 | # draw an axial(linear) gradient starting at sx,sy and ending at ex,ey 302 | def gradient(gradient=Gradient.new, start_x=@width/2, start_y=0, end_x=@width/2, end_y=@height) 303 | #options = KCGGradientDrawsBeforeStartLocation 304 | #options = KCGGradientDrawsAfterEndLocation 305 | CGContextDrawLinearGradient(@ctx, gradient.gradient, NSMakePoint(start_x, start_y), NSMakePoint(end_x, end_y), gradient.pre + gradient.post) 306 | end 307 | 308 | # draw a cartesian coordinate grid for reference 309 | def cartesian(res=50, stroke=1.0, fsize=10) 310 | # save previous state 311 | new_state do 312 | # set font and stroke 313 | font_size(fsize) 314 | fill(Color.black) 315 | stroke(Color.red) 316 | stroke_width(stroke) 317 | # draw vertical numbered grid lines 318 | (-width / res)..(width / res).each do |x| 319 | line(x * res, -height, x * res, height) 320 | text("#{x * res}", x * res, 0) 321 | end 322 | # draw horizontal numbered grid lines 323 | (-height / res)..(height / res).each do |y| 324 | line(-width, y * res, width, y * res) 325 | text("#{y * res}", 0, y * res) 326 | end 327 | # draw lines intersecting center of canvas 328 | stroke(Color.black) 329 | line(-width, -height, width, height) 330 | line(width, -height, -width, height) 331 | line(0, height, width, 0) 332 | line(width / 2, 0, width / 2, height) 333 | line(0, height / 2, width, height / 2) 334 | # restore previous state 335 | end 336 | end 337 | 338 | 339 | # DRAWING COMPLETE PATHS TO CANVAS 340 | 341 | # draw a line starting at x1,y1 and ending at x2,y2 342 | def line(x1, y1, x2, y2) 343 | CGContextAddLines(@ctx, [NSPoint.new(x1, y1), NSPoint.new(x2, y2)], 2) 344 | CGContextDrawPath(@ctx, KCGPathStroke) # apply stroke 345 | end_path 346 | end 347 | 348 | # draw a series of lines connecting the given array of points 349 | def lines(points) 350 | CGContextAddLines(@ctx, points, points.size) 351 | CGContextDrawPath(@ctx, KCGPathStroke) # apply stroke 352 | end_path 353 | end 354 | 355 | # draw the arc of a circle with center point x,y, radius, start angle (0 deg = 12 o'clock) and end angle 356 | def arc(x, y, radius, start_angle, end_angle) 357 | start_angle = MRGraphics.radians(90-start_angle) 358 | end_angle = MRGraphics.radians(90-end_angle) 359 | clockwise = 1 # 1 = clockwise, 0 = counterclockwise 360 | CGContextAddArc(@ctx, x, y, radius, start_angle, end_angle, clockwise) 361 | CGContextDrawPath(@ctx, KCGPathStroke) 362 | end 363 | 364 | # draw a bezier curve from the current point, given the coordinates of two handle control points and an end point 365 | def curve(cp1x, cp1y, cp2x, cp2y, x1, y1, x2, y2) 366 | begin_path(x1, y1) 367 | CGContextAddCurveToPoint(@ctx, cp1x, cp1y, cp2x, cp2y, x2, y2) 368 | end_path 369 | end 370 | 371 | # draw a quadratic bezier curve from x1,y1 to x2,y2, given the coordinates of one control point 372 | def qcurve(cpx, cpy, x1, y1, x2, y2) 373 | begin_path(x1, y1) 374 | CGContextAddQuadCurveToPoint(@ctx, cpx, cpy, x2, y2) 375 | end_path 376 | end 377 | 378 | # draw the given Path object 379 | def draw(object, *args) 380 | case object 381 | when Path 382 | draw_path(object, *args) 383 | when Image 384 | draw_image(object, *args) 385 | else 386 | raise ArgumentError, "first parameter must be a Path or Image object not a #{object.class}" 387 | end 388 | end 389 | 390 | # CONSTRUCTING PATHS ON CANVAS 391 | 392 | # automatically close the path after it is ended 393 | def autoclose_path! 394 | @autoclose_path = true 395 | end 396 | 397 | def autoclose_path=(bool) 398 | if bool == true 399 | autoclose_path! 400 | else 401 | @autoclose_path = false 402 | end 403 | end 404 | 405 | def new_path(x, y, &block) 406 | begin_path(x, y) 407 | block.call 408 | end_path 409 | end 410 | 411 | # begin drawing a path at x,y 412 | def begin_path(x, y) 413 | CGContextBeginPath(@ctx) 414 | CGContextMoveToPoint(@ctx, x, y) 415 | end 416 | 417 | # end the current path and draw it 418 | def end_path 419 | return if CGContextIsPathEmpty(@ctx) 420 | CGContextClosePath(@ctx) if @autoclose_path 421 | mode = KCGPathFillStroke 422 | CGContextDrawPath(@ctx, mode) # apply fill and stroke 423 | end 424 | 425 | # move the "pen" to x,y 426 | def move_to(x, y) 427 | CGContextMoveToPoint(@ctx, x, y) 428 | end 429 | 430 | # draw a line from the current point to x,y 431 | def line_to(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 curve_to(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 qcurve_to(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 arc_to(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, MRGraphics.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 | # retrieve graphics states until we get to the default state 520 | pop until (@stacksize <= 1) 521 | push # push the retrieved pristine default state back onto the stack 522 | end 523 | 524 | 525 | # EFFECTS 526 | 527 | # apply a drop shadow with offset dx,dy, alpha, and blur 528 | def shadow(dx=0.0, dy=0.0, a=2.0/3.0, blur=5) 529 | color = CGColorCreate(@colorspace, [0.0, 0.0, 0.0, a]) 530 | CGContextSetShadowWithColor(@ctx, [dx, dy], blur, color) 531 | end 532 | 533 | # stop using a shadow 534 | def no_shadow 535 | CGContextSetShadowWithColor(@ctx, [0,0], 1, nil) 536 | end 537 | 538 | # apply a glow with offset dx,dy, alpha, and blur 539 | def glow(dx=0.0, dy=0.0, a=2.0/3.0, blur=5) 540 | color = CGColorCreate(@colorspace, [1.0, 1.0, 0.0, a]) 541 | CGContextSetShadowWithColor(@ctx, [dx, dy], blur, color) 542 | end 543 | 544 | # set the canvas blend mode (:normal, :darken, :multiply, :screen, etc) 545 | def blend(mode) 546 | CGContextSetBlendMode(@ctx, BlendModes[mode]) 547 | end 548 | 549 | # CLIPPING MASKS 550 | 551 | # clip subsequent drawing operations within the given path 552 | def begin_clip(p, &block) 553 | push 554 | CGContextAddPath(@ctx, p.path) 555 | CGContextClip(@ctx) 556 | if block 557 | block.call 558 | end_clip 559 | end 560 | end 561 | 562 | # stop clipping drawing operations 563 | def end_clip 564 | pop 565 | end 566 | 567 | # DRAW TEXT TO CANVAS 568 | 569 | # write the text at x,y using the current fill 570 | def text(txt="", x=0, y=0) 571 | # not sure that's worth doing that here 572 | txt = txt.to_s 573 | if @registration == :center 574 | width = textwidth(txt) 575 | x = x - width / 2 576 | y = y + @fsize / 2 577 | end 578 | CGContextShowTextAtPoint(@ctx, x, y, txt, txt.length) 579 | end 580 | 581 | # determine the width of the given text without drawing it 582 | def text_width(txt, width=nil) 583 | push 584 | start = CGContextGetTextPosition(@ctx) 585 | CGContextSetTextDrawingMode(@ctx, KCGTextInvisible) 586 | CGContextShowText(@ctx, txt, txt.length) 587 | final = CGContextGetTextPosition(@ctx) 588 | pop 589 | final.x - start.x 590 | end 591 | 592 | # def text_height(txt) 593 | # # need to use ATSUI 594 | # end 595 | # 596 | # def text_metrics(txt) 597 | # # need to use ATSUI 598 | # end 599 | 600 | # set font by name and optional size 601 | def font(name="Helvetica", size=nil) 602 | font_size(size) if size 603 | @fname = name 604 | font_size unless @fsize 605 | CGContextSelectFont(@ctx, @fname, @fsize, KCGEncodingMacRoman) 606 | end 607 | 608 | # set font size in points 609 | def font_size(points=20) 610 | @fsize = points 611 | font unless @fname 612 | # encoding could have also been set as KCGEncodingFontSpecific 613 | CGContextSelectFont(@ctx, @fname, @fsize, KCGEncodingMacRoman) 614 | end 615 | 616 | 617 | # SAVING/EXPORTING 618 | 619 | def ns_image 620 | image = NSImage.alloc.init 621 | image.addRepresentation(NSBitmapImageRep.alloc.initWithCGImage(cgimage)) 622 | image 623 | end 624 | 625 | # return a CGImage of the canvas for reprocessing (only works if using a bitmap context) 626 | def cgimage 627 | CGBitmapContextCreateImage(@ctx) # => CGImageRef (works with bitmap context only) 628 | #cgimageref = CGImageCreate(@width, @height, @bits_per_component, nil,nil,@colorspace, nil, @provider,nil,true,KCGRenderingIntentDefault) 629 | end 630 | 631 | # return a CIImage of the canvas for reprocessing (only works if using a bitmap context) 632 | def ciimage 633 | cgimageref = self.cgimage 634 | CIImage.imageWithCGImage(cgimageref) # CIConcreteImage (CIImage) 635 | end 636 | 637 | # begin a new PDF page 638 | def new_page 639 | if (@filetype == :pdf) 640 | CGContextFlush(@ctx) 641 | CGPDFContextEndPage(@ctx) 642 | CGPDFContextBeginPage(@ctx, nil) 643 | else 644 | puts "WARNING: new_page only valid when using PDF output" 645 | end 646 | end 647 | 648 | # save the image to a file 649 | def save 650 | properties = {} 651 | # exif = {} 652 | # KCGImagePropertyExifDictionary 653 | # exif[KCGImagePropertyExifUserComment] = 'Image downloaded from www.sheetmusicplus.com' 654 | # exif[KCGImagePropertyExifAuxOwnerName] = 'www.sheetmusicplus.com' 655 | if @filetype == :pdf 656 | CGPDFContextEndPage(@ctx) 657 | CGContextFlush(@ctx) 658 | return 659 | elsif @filetype == :png 660 | format = NSPNGFileType 661 | elsif @filetype == :tif 662 | format = NSTIFFFileType 663 | properties[NSImageCompressionMethod] = NSTIFFCompressionLZW 664 | #properties[NSImageCompressionMethod] = NSTIFFCompressionNone 665 | elsif @filetype == :gif 666 | format = NSGIFFileType 667 | #properties[NSImageDitherTransparency] = 0 # 1 = dithered, 0 = not dithered 668 | #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. 669 | elsif @filetype == :jpg 670 | format = NSJPEGFileType 671 | properties[NSImageCompressionFactor] = @quality # (jpeg compression, 0.0 = max, 1.0 = none) 672 | #properties[NSImageEXIFData] = exif 673 | end 674 | cgimageref = CGBitmapContextCreateImage(@ctx) # => CGImageRef 675 | bitmaprep = NSBitmapImageRep.alloc.initWithCGImage(cgimageref) # => NSBitmapImageRep 676 | blob = bitmaprep.representationUsingType(format, properties:properties) # => NSConcreteData 677 | blob.writeToFile(@output, atomically:true) 678 | puts @output 679 | true 680 | end 681 | 682 | # open the output file in its associated application 683 | def open 684 | system "open #{@output}" 685 | end 686 | 687 | # def save(dest) 688 | ## http://developer.apple.com/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_data_mgr/chapter_11_section_3.html 689 | # properties = { 690 | # 691 | # } 692 | # cgimageref = CGBitmapContextCreateImage(@ctx) # => CGImageRef 693 | # destination = CGImageDestinationCreateWithURL(NSURL.fileURLWithPath(dest)) # => CGImageDestinationRef 694 | # CGImageDestinationSetProperties(destination,properties) 695 | # CGImageDestinationAddImage(cgimageref) 696 | # end 697 | 698 | private 699 | 700 | # DRAWING PATHS ON A CANVAS 701 | 702 | def draw_path(p, tx=0, ty=0, iterations=1) 703 | new_state do 704 | iterations.times do |i| 705 | if (i > 0) 706 | # INCREMENT TRANSFORM: 707 | # translate x, y 708 | translate(MRGraphics.choose(p.inc[:x]), MRGraphics.choose(p.inc[:y])) 709 | # choose a rotation factor from the range 710 | rotate(MRGraphics.choose(p.inc[:rotation])) 711 | # choose a scaling factor from the range 712 | sc = MRGraphics.choose(p.inc[:scale]) 713 | sx = MRGraphics.choose(p.inc[:scale_x]) * sc 714 | sy = p.inc[:scale_y] ? MRGraphics.choose(p.inc[:scale_y]) * sc : sx * sc 715 | scale(sx, sy) 716 | end 717 | 718 | new_state do 719 | # PICK AND ADJUST FILL/STROKE COLORS: 720 | [:fill,:stroke].each do |kind| 721 | # PICK A COLOR 722 | if (p.inc[kind]) 723 | # increment color from array 724 | colorindex = i % p.inc[kind].size 725 | c = p.inc[kind][colorindex].copy 726 | else 727 | c = p.rand[kind] 728 | case c 729 | when Array 730 | c = MRGraphics.choose(c).copy 731 | when Color 732 | c = c.copy 733 | else 734 | next 735 | end 736 | end 737 | 738 | if (p.inc[:hue] or p.inc[:saturation] or p.inc[:brightness]) 739 | # ITERATE COLOR 740 | if (p.inc[:hue]) 741 | newhue = (c.hue + MRGraphics.choose(p.inc[:hue])) % 1 742 | c.hue(newhue) 743 | end 744 | if (p.inc[:saturation]) 745 | newsat = (c.saturation + MRGraphics.choose(p.inc[:saturation])) 746 | c.saturation(newsat) 747 | end 748 | if (p.inc[:brightness]) 749 | newbright = (c.brightness + MRGraphics.choose(p.inc[:brightness])) 750 | c.brightness(newbright) 751 | end 752 | if (p.inc[:alpha]) 753 | newalpha = (c.a + MRGraphics.choose(p.inc[:alpha])) 754 | c.a(newalpha) 755 | end 756 | p.rand[kind] = c 757 | else 758 | # RANDOMIZE COLOR 759 | c.hue(MRGraphics.choose(p.rand[:hue])) if p.rand[:hue] 760 | c.saturation(MRGraphics.choose(p.rand[:saturation])) if p.rand[:saturation] 761 | c.brightness(MRGraphics.choose(p.rand[:brightness])) if p.rand[:brightness] 762 | end 763 | 764 | # APPLY COLOR 765 | fill(c) if kind == :fill 766 | stroke(c) if kind == :stroke 767 | end 768 | # choose a stroke width from the range 769 | stroke_width(MRGraphics.choose(p.rand[:stroke_width])) if p.rand[:stroke_width] 770 | # choose an alpha level from the range 771 | alpha(MRGraphics.choose(p.rand[:alpha])) if p.rand[:alpha] 772 | 773 | # RANDOMIZE TRANSFORM: 774 | # translate x, y 775 | translate(MRGraphics.choose(p.rand[:x]), MRGraphics.choose(p.rand[:y])) 776 | # choose a rotation factor from the range 777 | rotate(MRGraphics.choose(p.rand[:rotation])) 778 | # choose a scaling factor from the range 779 | sc = MRGraphics.choose(p.rand[:scale]) 780 | sx = MRGraphics.choose(p.rand[:scale_x]) * sc 781 | sy = p.rand[:scale_y] ? MRGraphics.choose(p.rand[:scale_y]) * sc : sx * sc 782 | scale(sx,sy) 783 | 784 | # DRAW 785 | if (tx > 0 || ty > 0) 786 | translate(tx, ty) 787 | end 788 | 789 | CGContextAddPath(@ctx, p.path) if p.class == Path 790 | CGContextDrawPath(@ctx, KCGPathFillStroke) # apply fill and stroke 791 | # if there's an image, draw it clipped by the path 792 | if (p.image) 793 | begin_clip(p) 794 | image(p.image) 795 | end_clip 796 | end 797 | end 798 | end 799 | end 800 | end 801 | 802 | # DRAWING IMAGES ON CANVAS 803 | 804 | # draw the specified image at x,y with dimensions w,h. 805 | # "img" may be a path to an image, or an Image instance 806 | def draw_image(img, x=0, y=0, w=nil, h=nil, pagenum=1) 807 | new_state do 808 | if (img.kind_of?(Pdf)) 809 | w ||= img.width(pagenum) 810 | h ||= img.height(pagenum) 811 | if(@registration == :center) 812 | x = x - w / 2 813 | y = y - h / 2 814 | end 815 | img.draw(@ctx, x, y, w, h, pagenum) 816 | elsif(img.kind_of?(String) || img.kind_of?(Image)) 817 | img = Image.new(img) if img.kind_of?(String) 818 | w ||= img.width 819 | h ||= img.height 820 | img.draw(@ctx, x, y, w, h) 821 | else 822 | raise ArgumentError, "canvas.image: not a recognized image type: #{img.class}" 823 | end 824 | end 825 | end 826 | 827 | end 828 | 829 | end 830 | -------------------------------------------------------------------------------- /lib/color.rb: -------------------------------------------------------------------------------- 1 | # MacRuby Graphics is a graphics library providing a simple object-oriented 2 | # interface into the power of Mac OS X's Core Graphics and Core Image drawing libraries. 3 | # With a few lines of easy-to-read code, you can write scripts to draw simple or complex 4 | # shapes, lines, and patterns, process and filter images, create abstract art or visualize 5 | # scientific data, and much more. 6 | # 7 | # Inspiration for this project was derived from Processing and NodeBox. These excellent 8 | # graphics programming environments are more full-featured than RCG, but they are implemented 9 | # in Java and Python, respectively. RCG was created to offer similar functionality using 10 | # the Ruby programming language. 11 | # 12 | # Author:: James Reynolds (mailto:drtoast@drtoast.com) 13 | # Copyright:: Copyright (c) 2008 James Reynolds 14 | # License:: Distributes under the same terms as Ruby 15 | 16 | module MRGraphics 17 | 18 | # define and manipulate colors in RGBA format 19 | class Color 20 | 21 | attr_accessor :rgb 22 | 23 | # create a new color with the given RGBA values 24 | def initialize(r=0.0, g=0.0, b=1.0, a=1.0) 25 | # color space gets automatically set to NSDeviceRGBColorSpace 26 | @rgb = NSColor.colorWithDeviceRed(r, green:g, blue:b, alpha:a) 27 | self 28 | end 29 | 30 | COLORNAMES = { 31 | "lightpink" => [1.00, 0.71, 0.76], 32 | "pink" => [1.00, 0.75, 0.80], 33 | "crimson" => [0.86, 0.08, 0.24], 34 | "lavenderblush" => [1.00, 0.94, 0.96], 35 | "palevioletred" => [0.86, 0.44, 0.58], 36 | "hotpink" => [1.00, 0.41, 0.71], 37 | "deeppink" => [1.00, 0.08, 0.58], 38 | "mediumvioletred" => [0.78, 0.08, 0.52], 39 | "orchid" => [0.85, 0.44, 0.84], 40 | "thistle" => [0.85, 0.75, 0.85], 41 | "plum" => [0.87, 0.63, 0.87], 42 | "violet" => [0.93, 0.51, 0.93], 43 | "fuchsia" => [1.00, 0.00, 1.00], 44 | "darkmagenta" => [0.55, 0.00, 0.55], 45 | "purple" => [0.50, 0.00, 0.50], 46 | "mediumorchid" => [0.73, 0.33, 0.83], 47 | "darkviolet" => [0.58, 0.00, 0.83], 48 | "darkorchid" => [0.60, 0.20, 0.80], 49 | "indigo" => [0.29, 0.00, 0.51], 50 | "blueviolet" => [0.54, 0.17, 0.89], 51 | "mediumpurple" => [0.58, 0.44, 0.86], 52 | "mediumslateblue" => [0.48, 0.41, 0.93], 53 | "slateblue" => [0.42, 0.35, 0.80], 54 | "darkslateblue" => [0.28, 0.24, 0.55], 55 | "ghostwhite" => [0.97, 0.97, 1.00], 56 | "lavender" => [0.90, 0.90, 0.98], 57 | "blue" => [0.00, 0.00, 1.00], 58 | "mediumblue" => [0.00, 0.00, 0.80], 59 | "darkblue" => [0.00, 0.00, 0.55], 60 | "navy" => [0.00, 0.00, 0.50], 61 | "midnightblue" => [0.10, 0.10, 0.44], 62 | "royalblue" => [0.25, 0.41, 0.88], 63 | "cornflowerblue" => [0.39, 0.58, 0.93], 64 | "lightsteelblue" => [0.69, 0.77, 0.87], 65 | "lightslategray" => [0.47, 0.53, 0.60], 66 | "slategray" => [0.44, 0.50, 0.56], 67 | "dodgerblue" => [0.12, 0.56, 1.00], 68 | "aliceblue" => [0.94, 0.97, 1.00], 69 | "steelblue" => [0.27, 0.51, 0.71], 70 | "lightskyblue" => [0.53, 0.81, 0.98], 71 | "skyblue" => [0.53, 0.81, 0.92], 72 | "deepskyblue" => [0.00, 0.75, 1.00], 73 | "lightblue" => [0.68, 0.85, 0.90], 74 | "powderblue" => [0.69, 0.88, 0.90], 75 | "cadetblue" => [0.37, 0.62, 0.63], 76 | "darkturquoise" => [0.00, 0.81, 0.82], 77 | "azure" => [0.94, 1.00, 1.00], 78 | "lightcyan" => [0.88, 1.00, 1.00], 79 | "paleturquoise" => [0.69, 0.93, 0.93], 80 | "aqua" => [0.00, 1.00, 1.00], 81 | "darkcyan" => [0.00, 0.55, 0.55], 82 | "teal" => [0.00, 0.50, 0.50], 83 | "darkslategray" => [0.18, 0.31, 0.31], 84 | "mediumturquoise" => [0.28, 0.82, 0.80], 85 | "lightseagreen" => [0.13, 0.70, 0.67], 86 | "turquoise" => [0.25, 0.88, 0.82], 87 | "aquamarine" => [0.50, 1.00, 0.83], 88 | "mediumaquamarine" => [0.40, 0.80, 0.67], 89 | "mediumspringgreen" => [0.00, 0.98, 0.60], 90 | "mintcream" => [0.96, 1.00, 0.98], 91 | "springgreen" => [0.00, 1.00, 0.50], 92 | "mediumseagreen" => [0.24, 0.70, 0.44], 93 | "seagreen" => [0.18, 0.55, 0.34], 94 | "honeydew" => [0.94, 1.00, 0.94], 95 | "darkseagreen" => [0.56, 0.74, 0.56], 96 | "palegreen" => [0.60, 0.98, 0.60], 97 | "lightgreen" => [0.56, 0.93, 0.56], 98 | "limegreen" => [0.20, 0.80, 0.20], 99 | "lime" => [0.00, 1.00, 0.00], 100 | "forestgreen" => [0.13, 0.55, 0.13], 101 | "green" => [0.00, 0.50, 0.00], 102 | "darkgreen" => [0.00, 0.39, 0.00], 103 | "lawngreen" => [0.49, 0.99, 0.00], 104 | "chartreuse" => [0.50, 1.00, 0.00], 105 | "greenyellow" => [0.68, 1.00, 0.18], 106 | "darkolivegreen" => [0.33, 0.42, 0.18], 107 | "yellowgreen" => [0.60, 0.80, 0.20], 108 | "olivedrab" => [0.42, 0.56, 0.14], 109 | "ivory" => [1.00, 1.00, 0.94], 110 | "beige" => [0.96, 0.96, 0.86], 111 | "lightyellow" => [1.00, 1.00, 0.88], 112 | "lightgoldenrodyellow" => [0.98, 0.98, 0.82], 113 | "yellow" => [1.00, 1.00, 0.00], 114 | "olive" => [0.50, 0.50, 0.00], 115 | "darkkhaki" => [0.74, 0.72, 0.42], 116 | "palegoldenrod" => [0.93, 0.91, 0.67], 117 | "lemonchiffon" => [1.00, 0.98, 0.80], 118 | "khaki" => [0.94, 0.90, 0.55], 119 | "gold" => [1.00, 0.84, 0.00], 120 | "cornsilk" => [1.00, 0.97, 0.86], 121 | "goldenrod" => [0.85, 0.65, 0.13], 122 | "darkgoldenrod" => [0.72, 0.53, 0.04], 123 | "floralwhite" => [1.00, 0.98, 0.94], 124 | "oldlace" => [0.99, 0.96, 0.90], 125 | "wheat" => [0.96, 0.87, 0.07], 126 | "orange" => [1.00, 0.65, 0.00], 127 | "moccasin" => [1.00, 0.89, 0.71], 128 | "papayawhip" => [1.00, 0.94, 0.84], 129 | "blanchedalmond" => [1.00, 0.92, 0.80], 130 | "navajowhite" => [1.00, 0.87, 0.68], 131 | "antiquewhite" => [0.98, 0.92, 0.84], 132 | "tan" => [0.82, 0.71, 0.55], 133 | "burlywood" => [0.87, 0.72, 0.53], 134 | "darkorange" => [1.00, 0.55, 0.00], 135 | "bisque" => [1.00, 0.89, 0.77], 136 | "linen" => [0.98, 0.94, 0.90], 137 | "peru" => [0.80, 0.52, 0.25], 138 | "peachpuff" => [1.00, 0.85, 0.73], 139 | "sandybrown" => [0.96, 0.64, 0.38], 140 | "chocolate" => [0.82, 0.41, 0.12], 141 | "saddlebrown" => [0.55, 0.27, 0.07], 142 | "seashell" => [1.00, 0.96, 0.93], 143 | "sienna" => [0.63, 0.32, 0.18], 144 | "lightsalmon" => [1.00, 0.63, 0.48], 145 | "coral" => [1.00, 0.50, 0.31], 146 | "orangered" => [1.00, 0.27, 0.00], 147 | "darksalmon" => [0.91, 0.59, 0.48], 148 | "tomato" => [1.00, 0.39, 0.28], 149 | "salmon" => [0.98, 0.50, 0.45], 150 | "mistyrose" => [1.00, 0.89, 0.88], 151 | "lightcoral" => [0.94, 0.50, 0.50], 152 | "snow" => [1.00, 0.98, 0.98], 153 | "rosybrown" => [0.74, 0.56, 0.56], 154 | "indianred" => [0.80, 0.36, 0.36], 155 | "red" => [1.00, 0.00, 0.00], 156 | "brown" => [0.65, 0.16, 0.16], 157 | "firebrick" => [0.70, 0.13, 0.13], 158 | "darkred" => [0.55, 0.00, 0.00], 159 | "maroon" => [0.50, 0.00, 0.00], 160 | "white" => [1.00, 1.00, 1.00], 161 | "whitesmoke" => [0.96, 0.96, 0.96], 162 | "gainsboro" => [0.86, 0.86, 0.86], 163 | "lightgrey" => [0.83, 0.83, 0.83], 164 | "silver" => [0.75, 0.75, 0.75], 165 | "darkgray" => [0.66, 0.66, 0.66], 166 | "gray" => [0.50, 0.50, 0.50], 167 | "grey" => [0.50, 0.50, 0.50], 168 | "dimgray" => [0.41, 0.41, 0.41], 169 | "dimgrey" => [0.41, 0.41, 0.41], 170 | "black" => [0.00, 0.00, 0.00], 171 | "cyan" => [0.00, 0.68, 0.94], 172 | #"transparent" => [0.00, 0.00, 0.00, 0.00], 173 | "bark" => [0.25, 0.19, 0.13] 174 | } 175 | 176 | RYBWheel = [ 177 | [ 0, 0], [ 15, 8], 178 | [ 30, 17], [ 45, 26], 179 | [ 60, 34], [ 75, 41], 180 | [ 90, 48], [105, 54], 181 | [120, 60], [135, 81], 182 | [150, 103], [165, 123], 183 | [180, 138], [195, 155], 184 | [210, 171], [225, 187], 185 | [240, 204], [255, 219], 186 | [270, 234], [285, 251], 187 | [300, 267], [315, 282], 188 | [330, 298], [345, 329], 189 | [360, 0 ] 190 | ] 191 | 192 | COLORNAMES.each_key do |name| 193 | metaclass = (class << self; self; end) 194 | metaclass.instance_eval do 195 | define_method(name) do 196 | named(name) 197 | end 198 | end 199 | end 200 | 201 | # create a color with the specified name 202 | def self.named(name) 203 | if COLORNAMES[name] 204 | r, g, b = COLORNAMES[name] 205 | #puts "matched name #{name}" 206 | color = Color.new(r, g, b, 1.0) 207 | elsif name.match(/^(dark|deep|light|bright)?(.*?)(ish)?$/) 208 | #puts "matched #{$1}-#{$2}-#{$3}" 209 | value = $1 210 | color_name = $2 211 | ish = $3 212 | analogval = value ? 0 : 0.1 213 | r, g, b = COLORNAMES[color_name] || [0.0, 0.0, 0.0] 214 | color = Color.new(r, g, b, 1.0) 215 | color = c.analog(20, analogval) if ish 216 | color.lighten(0.2) if value && value.match(/light|bright/) 217 | color.darken(0.2) if value && value.match(/dark|deep/) 218 | else 219 | color = Color.black 220 | end 221 | color 222 | end 223 | 224 | # return the name of the nearest named color 225 | def name 226 | nearest, d = ["", 1.0] 227 | red = r 228 | green = g 229 | blue = b 230 | COLORNAMES.keys.each do |hue| 231 | rdiff = (red - COLORNAMES[hue][0]).abs 232 | gdiff = (green - COLORNAMES[hue][1]).abs 233 | bdiff = (blue - COLORNAMES[hue][2]).abs 234 | totaldiff = rdiff + gdiff + bdiff 235 | nearest, d = [hue, totaldiff] if (totaldiff < d) 236 | end 237 | nearest 238 | end 239 | 240 | # return a copy of this color 241 | def copy 242 | Color.new(r, g, b, a) 243 | end 244 | 245 | # print the color's component values 246 | def to_s 247 | "color: #{name} (#{r} #{g} #{b} #{a})" 248 | end 249 | 250 | # sort the color by brightness in an array 251 | def <=> othercolor 252 | self.brightness <=> othercolor.brightness || self.hue <=> othercolor.hue 253 | end 254 | 255 | # set or retrieve the red component 256 | def r(val=nil) 257 | if val 258 | r, g, b, a = get_rgb 259 | set_rgb(val, g, b, a) 260 | self 261 | else 262 | @rgb.redComponent 263 | end 264 | end 265 | 266 | # set or retrieve the green component 267 | def g(val=nil) 268 | if val 269 | r, g, b, a = get_rgb 270 | set_rgb(r, val, b, a) 271 | self 272 | else 273 | @rgb.greenComponent 274 | end 275 | end 276 | 277 | # set or retrieve the blue component 278 | def b(val=nil) 279 | if val 280 | r, g, b, a = get_rgb 281 | set_rgb(r, g, val, a) 282 | self 283 | else 284 | @rgb.blueComponent 285 | end 286 | end 287 | 288 | # set or retrieve the alpha component 289 | def a(val=nil) 290 | if val 291 | r, g, b, a = get_rgb 292 | set_rgb(r, g, b, val) 293 | self 294 | else 295 | @rgb.alphaComponent 296 | end 297 | end 298 | 299 | # 0 to 255 300 | def full_r 301 | r * 255 302 | end 303 | 304 | def full_g 305 | g * 255 306 | end 307 | 308 | def full_b 309 | b * 255 310 | end 311 | 312 | # Y from YUV 313 | # http://en.wikipedia.org/wiki/YUV 314 | # http://softpixel.com/~cwright/programming/colorspace/yuv/ 315 | def y 316 | (0.299 * full_r) + (0.587 * full_g) + (0.114 * full_b) 317 | end 318 | 319 | # U from YUV 320 | def u 321 | (full_r * -0.168736) + (full_g * -0.331264) + (full_b * 0.500000) + 128 322 | end 323 | 324 | # V from YUV 325 | def v 326 | (full_r * 0.500000) + (full_g * -0.418688) + (full_b * -0.081312) + 128 327 | end 328 | 329 | def yuv 330 | [y,u,v] 331 | end 332 | 333 | # set or retrieve the hue 334 | def hue(val=nil) 335 | if val 336 | h, s, b, a = get_hsb 337 | set_hsb(val, s, b, a) 338 | self 339 | else 340 | @rgb.hueComponent 341 | end 342 | end 343 | 344 | # set or retrieve the saturation 345 | def saturation(val=nil) 346 | if val 347 | h, s, b, a = get_hsb 348 | set_hsb(h, val, b, a) 349 | self 350 | else 351 | @rgb.saturationComponent 352 | end 353 | end 354 | 355 | # set or retrieve the brightness 356 | def brightness(val=nil) 357 | if val 358 | h, s, b, a = get_hsb 359 | set_hsb(h, s, val, a) 360 | self 361 | else 362 | @rgb.brightnessComponent 363 | end 364 | end 365 | 366 | # decrease saturation by the specified amount 367 | def desaturate(step=0.1) 368 | saturation(saturation - step) 369 | self 370 | end 371 | 372 | # increase the saturation by the specified amount 373 | def saturate(step=0.1) 374 | saturation(saturation + step) 375 | self 376 | end 377 | 378 | # decrease the brightness by the specified amount 379 | def darken(step=0.1) 380 | brightness(brightness - step) 381 | self 382 | end 383 | 384 | # increase the brightness by the specified amount 385 | def lighten(step=0.1) 386 | brightness(brightness + step) 387 | self 388 | end 389 | 390 | def ish(val=0.1) 391 | self.analog(20, val) 392 | self 393 | end 394 | 395 | # set the R,G,B,A values 396 | def set(r, g, b, a=1.0) 397 | set_rgb(r, g, b, a) 398 | self 399 | end 400 | 401 | # adjust the Red, Green, Blue, Alpha values by the specified amounts 402 | def adjust_rgb(r=0.0, g=0.0, b=0.0, a=0.0) 403 | r0, g0, b0, a0 = get_rgb 404 | set_rgb(r0+r, g0+g, b0+b, a0+a) 405 | self 406 | end 407 | 408 | # return RGBA values 409 | def get_rgb 410 | #@rgb.getRed_green_blue_alpha_() 411 | [@rgb.redComponent, @rgb.greenComponent, @rgb.blueComponent, @rgb.alphaComponent] 412 | end 413 | 414 | # set color using RGBA values 415 | def set_rgb(r, g, b, a=1.0) 416 | @rgb = NSColor.colorWithDeviceRed r, green:g, blue:b, alpha:a 417 | self 418 | end 419 | 420 | # return HSBA values 421 | def get_hsb 422 | #@rgb.getHue_saturation_brightness_alpha_() 423 | [@rgb.hueComponent, @rgb.saturationComponent, @rgb.brightnessComponent, @rgb.alphaComponent] 424 | end 425 | 426 | # set color using HSBA values 427 | def set_hsb(h,s,b,a=1.0) 428 | @rgb = NSColor.colorWithDeviceHue h, saturation:s, brightness:b, alpha:a 429 | self 430 | end 431 | 432 | # adjust Hue, Saturation, Brightness, and Alpha by specified amounts 433 | def adjust_hsb(h=0.0, s=0.0, b=0.0, a=0.0) 434 | h0, s0, b0, a0 = get_hsb 435 | set_hsb(h0+h, s0+s, b0+b, a0+a) 436 | self 437 | end 438 | 439 | # alter the color by the specified random scaling factor 440 | # def ish(angle=10.0,d=0.02) 441 | # # r,g,b,a = get_rgb 442 | # # r = vary(r, variance) 443 | # # g = vary(g, variance) 444 | # # b = vary(b, variance) 445 | # # a = vary(a, variance) 446 | # # set_rgb(r,g,b,a) 447 | # analog(angle,d) 448 | # self 449 | # end 450 | 451 | # create a random color 452 | def random 453 | set_rgb(rand, rand, rand, 1.0) 454 | self 455 | end 456 | 457 | # rotate the color on the artistic RYB color wheel (0 to 360 degrees) 458 | def rotate_ryb(angle=180) 459 | 460 | # An artistic color wheel has slightly different opposites 461 | # (e.g. purple-yellow instead of purple-lime). 462 | # It is mathematically incorrect but generally assumed 463 | # to provide better complementary colors. 464 | # 465 | # http://en.wikipedia.org/wiki/RYB_color_model 466 | 467 | h = hue * 360 468 | angle = angle % 360.0 469 | a = 0 470 | 471 | # Approximation of Itten's RYB color wheel. 472 | # In HSB, colors hues range from 0-360. 473 | # However, on the artistic color wheel these are not evenly distributed. 474 | # The second tuple value contains the actual distribution. 475 | 476 | # Given a hue, find out under what angle it is 477 | # located on the artistic color wheel. 478 | (RYBWheel.size-1).times do |i| 479 | x0,y0 = RYBWheel[i] 480 | x1,y1 = RYBWheel[i+1] 481 | y1 += 360 if y1 < y0 482 | if y0 <= h && h <= y1 483 | a = 1.0 * x0 + (x1-x0) * (h-y0) / (y1-y0) 484 | break 485 | end 486 | end 487 | 488 | # And the user-given angle (e.g. complement). 489 | a = (a+angle) % 360 490 | 491 | # For the given angle, find out what hue is 492 | # located there on the artistic color wheel. 493 | (RYBWheel.size-1).times do |i| 494 | x0,y0 = RYBWheel[i] 495 | x1,y1 = RYBWheel[i+1] 496 | y1 += 360 if y1 < y0 497 | if x0 <= a && a <= x1 498 | h = 1.0 * y0 + (y1-y0) * (a-x0) / (x1-x0) 499 | break 500 | end 501 | end 502 | 503 | h = h % 360 504 | set_hsb(h/360, self.saturation, self.brightness, self.a) 505 | self 506 | end 507 | 508 | # rotate the color on the RGB color wheel (0 to 360 degrees) 509 | def rotate_rgb(angle=180) 510 | hue = (self.hue + 1.0 * angle / 360) % 1 511 | set_hsb(hue, self.saturation, self.brightness, @a) 512 | self 513 | end 514 | 515 | # return a similar color, varying the hue by angle (0-360) and brightness/saturation by d 516 | def analog(angle=20, d=0.5) 517 | c = self.copy 518 | c.rotate_ryb(angle * (rand*2-1)) 519 | c.lighten(d * (rand*2-1)) 520 | c.saturate(d * (rand*2-1)) 521 | end 522 | 523 | # randomly vary the color within a maximum hue range, saturation range, and brightness range 524 | def drift(maxhue=0.1,maxsat=0.3,maxbright=maxsat) 525 | # save original values the first time 526 | @original_hue ||= self.hue 527 | @original_saturation ||= self.saturation 528 | @original_brightness ||= self.brightness 529 | # get current values 530 | current_hue = self.hue 531 | current_saturation = self.saturation 532 | current_brightness = self.brightness 533 | # generate new values 534 | randhue = ((rand * maxhue) - maxhue/2.0) + current_hue 535 | randhue = MRGraphics.in_range(randhue, (@original_hue - maxhue/2.0),(@original_hue + maxhue/2.0)) 536 | randsat = (rand * maxsat) - maxsat/2.0 + current_saturation 537 | randsat = MRGraphics.in_range(randsat, @original_saturation - maxsat/2.0,@original_saturation + maxsat/2.0) 538 | randbright = (rand * maxbright) - maxbright/2.0 + current_brightness 539 | randbright = MRGraphics.in_range(randbright, @original_brightness - maxbright/2.0,@original_brightness + maxbright/2.0) 540 | # assign new values 541 | self.hue(randhue) 542 | self.saturation(randsat) 543 | self.brightness(randbright) 544 | self 545 | end 546 | 547 | # convert to the complementary color (the color at opposite on the artistic color wheel) 548 | def complement 549 | rotate_ryb(180) 550 | self 551 | end 552 | 553 | # blend with another color (doesn't work?) 554 | # def blend(color, pct=0.5) 555 | # blended = NSColor.blendedColorWithFraction_ofColor(pct,color.rgb) 556 | # @rgb = blended.colorUsingColorSpaceName(NSDeviceRGBColorSpace) 557 | # self 558 | # end 559 | 560 | # create a new RGBA color 561 | def self.rgb(r, g, b, a=1.0) 562 | Color.new(r,g,b,a) 563 | end 564 | 565 | # create a new HSBA color 566 | def self.hsb(h, s, b, a=1.0) 567 | Color.new.set_hsb(h,s,b,a) 568 | end 569 | 570 | # create a new gray color with the specified darkness 571 | def self.gray(pct=0.5) 572 | Color.new(pct,pct,pct,1.0) 573 | end 574 | 575 | # return a random color 576 | def self.random 577 | Color.new.random 578 | end 579 | 580 | # Returns a list of complementary colors. The complement is the color 180 degrees across the artistic RYB color wheel. 581 | # 1) ORIGINAL: the original color 582 | # 2) CONTRASTING: same hue as original but much darker or lighter 583 | # 3) SOFT SUPPORTING: same hue but lighter and less saturated than the original 584 | # 4) CONTRASTING COMPLEMENT: a much brighter or darker version of the complement hue 585 | # 5) COMPLEMENT: the hue 180 degrees opposite on the RYB color wheel with same brightness/saturation 586 | # 6) LIGHT SUPPORTING COMPLEMENT VARIANT: a lighter less saturated version of the complement hue 587 | def complementary 588 | colors = [] 589 | 590 | # A contrasting color: much darker or lighter than the original. 591 | colors.push(self) 592 | c = self.copy 593 | if self.brightness > 0.4 594 | c.brightness(0.1 + c.brightness*0.25) 595 | else 596 | c.brightness(1.0 - c.brightness*0.25) 597 | end 598 | colors.push(c) 599 | 600 | # A soft supporting color: lighter and less saturated. 601 | c = self.copy 602 | c.brightness(0.3 + c.brightness) 603 | c.saturation(0.1 + c.saturation*0.3) 604 | colors.push(c) 605 | 606 | # A contrasting complement: very dark or very light. 607 | c_comp = self.copy.complement 608 | c = c_comp.copy 609 | if c_comp.brightness > 0.3 610 | c.brightness(0.1 + c_comp.brightness*0.25) 611 | else 612 | c.brightness(1.0 - c.brightness*0.25) 613 | end 614 | colors.push(c) 615 | 616 | # The complement 617 | colors.push(c_comp) 618 | 619 | # and a light supporting variant. 620 | c = c_comp.copy 621 | c.brightness(0.3 + c.brightness) 622 | c.saturation(0.1 + c.saturation*0.25) 623 | colors.push(c) 624 | 625 | colors 626 | end 627 | 628 | # Returns a list with the split complement of the color. 629 | # The split complement are the two colors to the left and right 630 | # of the color's complement. 631 | def split_complementary(angle=30) 632 | colors = [] 633 | colors.push(self) 634 | comp = self.copy.complement 635 | colors.push(comp.copy.rotate_ryb(-angle).lighten(0.1)) 636 | colors.push(comp.copy.rotate_ryb(angle).lighten(0.1)) 637 | colors 638 | end 639 | 640 | # Returns the left half of the split complement. 641 | # A list is returned with the same darker and softer colors 642 | # as in the complementary list, but using the hue of the 643 | # left split complement instead of the complement itself. 644 | def left_complement(angle=-30) 645 | left = copy.complement.rotate_ryb(angle).lighten(0.1) 646 | colors = complementary 647 | colors[3].hue(left.hue) 648 | colors[4].hue(left.hue) 649 | colors[5].hue(left.hue) 650 | colors 651 | end 652 | 653 | # Returns the right half of the split complement. 654 | # A list is returned with the same darker and softer colors 655 | # as in the complementary list, but using the hue of the 656 | # right split complement instead of the complement itself. 657 | def right_complement(angle=30) 658 | right = copy.complement.rotate_ryb(angle).lighten(0.1) 659 | colors = complementary 660 | colors[3].hue(right.hue) 661 | colors[4].hue(right.hue) 662 | colors[5].hue(right.hue) 663 | colors 664 | end 665 | 666 | # Returns colors that are next to each other on the wheel. 667 | # These yield natural color schemes (like shades of water or sky). 668 | # The angle determines how far the colors are apart, 669 | # making it bigger will introduce more variation. 670 | # The contrast determines the darkness/lightness of 671 | # the analogue colors in respect to the given colors. 672 | def analogous(angle=10, contrast=0.25) 673 | contrast = MRGraphics.in_range(contrast, 0.0, 1.0) 674 | 675 | colors = [] 676 | colors << self 677 | 678 | [[1,2.2], [2,1], [-1,-0.5], [-2,1]].each do |i,j| 679 | c = copy.rotate_ryb(angle*i) 680 | t = 0.44-j*0.1 681 | if brightness - contrast*j < t 682 | c.brightness(t) 683 | else 684 | c.brightness(self.brightness - contrast*j) 685 | end 686 | c.saturation(c.saturation - 0.05) 687 | colors << c 688 | end 689 | colors 690 | end 691 | 692 | # Returns colors in the same hue with varying brightness/saturation. 693 | def monochrome 694 | 695 | colors = [self] 696 | 697 | c = copy 698 | c.brightness(_wrap(brightness, 0.5, 0.2, 0.3)) 699 | c.saturation(_wrap(saturation, 0.3, 0.1, 0.3)) 700 | colors.push(c) 701 | 702 | c = copy 703 | c.brightness(_wrap(brightness, 0.2, 0.2, 0.6)) 704 | colors.push(c) 705 | 706 | c = copy 707 | c.brightness(max(0.2, brightness+(1-brightness)*0.2)) 708 | c.saturation(_wrap(saturation, 0.3, 0.1, 0.3)) 709 | colors.push(c) 710 | 711 | c = self.copy 712 | c.brightness(_wrap(brightness, 0.5, 0.2, 0.3)) 713 | colors.push(c) 714 | 715 | colors 716 | end 717 | 718 | # Returns a triad of colors. 719 | # The triad is made up of this color and two other colors 720 | # that together make up an equilateral triangle on 721 | # the artistic color wheel. 722 | def triad(angle=120) 723 | colors = [self] 724 | colors.push(copy.rotate_ryb(angle).lighten(0.1)) 725 | colors.push(copy.rotate_ryb(-angle).lighten(0.1)) 726 | colors 727 | end 728 | 729 | # Returns a tetrad of colors. 730 | # The tetrad is made up of this color and three other colors 731 | # that together make up a cross on the artistic color wheel. 732 | def tetrad(angle=90) 733 | 734 | colors = [self] 735 | 736 | c = copy.rotate_ryb(angle) 737 | if brightness < 0.5 then 738 | c.brightness(c.brightness + 0.2) 739 | else 740 | c.brightness(c.brightness - 0.2) 741 | end 742 | colors.push(c) 743 | 744 | c = copy.rotate_ryb(angle*2) 745 | if brightness < 0.5 746 | c.brightness(c.brightness + 0.1) 747 | else 748 | c.brightness(c.brightness - 0.1) 749 | end 750 | 751 | colors.push(c) 752 | colors.push(copy.rotate_ryb(angle*3).lighten(0.1)) 753 | colors 754 | end 755 | 756 | # Roughly the complement and some far analogs. 757 | def compound(flip=false) 758 | d = (flip ? -1 : 1) 759 | 760 | colors = [self] 761 | 762 | c = copy.rotate_ryb(30*d) 763 | c.brightness(_wrap(brightness, 0.25, 0.6, 0.25)) 764 | colors.push(c) 765 | 766 | c = copy.rotate_ryb(30*d) 767 | c.saturation(_wrap(saturation, 0.4, 0.1, 0.4)) 768 | c.brightness(_wrap(brightness, 0.4, 0.2, 0.4)) 769 | colors.push(c) 770 | 771 | c = copy.rotate_ryb(160*d) 772 | c.saturation(_wrap(saturation, 0.25, 0.1, 0.25)) 773 | c.brightness(max(0.2, brightness)) 774 | colors.push(c) 775 | 776 | c = copy.rotate_ryb(150*d) 777 | c.saturation(_wrap(saturation, 0.1, 0.8, 0.1)) 778 | c.brightness(_wrap(brightness, 0.3, 0.6, 0.3)) 779 | colors.push(c) 780 | 781 | # c = copy.rotate_ryb(150*d) 782 | # c.saturation(_wrap(saturation, 0.1, 0.8, 0.1)) 783 | # c.brightness(_wrap(brightness, 0.4, 0.2, 0.4)) 784 | # #colors.push(c) 785 | 786 | colors 787 | end 788 | 789 | # Roughly the complement and some far analogs. 790 | def flipped_compound 791 | compound(true) 792 | end 793 | 794 | # Returns true if the color is white-ish 795 | def white? 796 | r, g, b, a = get_rgb 797 | if r > 0.96 && g > 0.96 && b > 0.96 798 | true 799 | else 800 | false 801 | end 802 | end 803 | 804 | # The spherical coordinates (θ, φ) of the color can be obtained from Cartesian coordinates (x, y, z) 805 | # using the YUV values, we are getting 2 spherical coordinates 806 | # r isn't calculated 807 | # By using theta and psi, one can sort colors 808 | def spherical_coordinates 809 | theta = Math.acos(y / (Math.sqrt(u*u + v*v + y*y))) 810 | psi = Math.atan2(v, u) 811 | [theta, psi] 812 | end 813 | 814 | private 815 | 816 | # vary a single color component by a multiplier 817 | def vary(original, variance) 818 | newvalue = original + (rand * variance * (rand > 0.5 ? 1 : -1)) 819 | newvalue = MRGraphics.in_range(newvalue,0.0,1.0) 820 | newvalue 821 | end 822 | 823 | # wrap within range 824 | def _wrap(x, min, threshold, plus) 825 | if x - min < threshold 826 | x + plus 827 | else 828 | x - min 829 | end 830 | end 831 | 832 | end 833 | 834 | end 835 | -------------------------------------------------------------------------------- /lib/elements/particle.rb: -------------------------------------------------------------------------------- 1 | # MacRuby Graphics is a graphics library providing a simple object-oriented 2 | # interface into the power of Mac OS X's Core Graphics and Core Image drawing libraries. 3 | # With a few lines of easy-to-read code, you can write scripts to draw simple or complex 4 | # shapes, lines, and patterns, process and filter images, create abstract art or visualize 5 | # scientific data, and much more. 6 | # 7 | # Inspiration for this project was derived from Processing and NodeBox. These excellent 8 | # graphics programming environments are more full-featured than RCG, but they are implemented 9 | # in Java and Python, respectively. RCG was created to offer similar functionality using 10 | # the Ruby programming language. 11 | # 12 | # Author:: James Reynolds (mailto:drtoast@drtoast.com) 13 | # Copyright:: Copyright (c) 2008 James Reynolds 14 | # License:: Distributes under the same terms as Ruby 15 | 16 | module MRGraphics 17 | 18 | # wandering particle with brownian motion 19 | class Particle 20 | 21 | attr_accessor :acceleration, :points, :stroke, :velocity_x, :velocity_y, :x, :y 22 | 23 | # initialize particle origin x,y coordinates (relative to the center) 24 | def initialize (x, y, velocity_x=0.0, velocity_y=2.0) 25 | @age = 0 26 | @acceleration = 0.5 27 | 28 | @x = x 29 | @y = y 30 | 31 | @previous_x = 0 32 | @previous_y = 0 33 | 34 | # initialize velocity 35 | @velocity_x=velocity_x 36 | @velocity_y=velocity_y 37 | 38 | # append the point to the array 39 | @points = [NSPoint.new(@x, @y)] 40 | @stroke = Color.white 41 | end 42 | 43 | # move to a new position using brownian motion 44 | def move 45 | # save old x,y position 46 | @previous_x=@x 47 | @previous_y=@y 48 | 49 | # move particle by velocity_x,velocity_y 50 | @x += @velocity_x 51 | @y += @velocity_y 52 | 53 | # randomly increase/decrease direction 54 | @velocity_x += MRGraphics.random(-1.0, 1.0) * @acceleration 55 | @velocity_y += MRGraphics.random(-1.0, 1.0) * @acceleration 56 | 57 | # draw a line from the old position to the new 58 | @points.push(NSPoint.new(@x, @y)) 59 | 60 | # grow old 61 | @age += 1 62 | if @age>200 63 | # die and be reborn 64 | end 65 | 66 | end 67 | 68 | def draw(canvas) 69 | canvas.no_fill 70 | canvas.lines(@points) 71 | end 72 | 73 | end 74 | end -------------------------------------------------------------------------------- /lib/elements/rope.rb: -------------------------------------------------------------------------------- 1 | # MacRuby Graphics is a graphics library providing a simple object-oriented 2 | # interface into the power of Mac OS X's Core Graphics and Core Image drawing libraries. 3 | # With a few lines of easy-to-read code, you can write scripts to draw simple or complex 4 | # shapes, lines, and patterns, process and filter images, create abstract art or visualize 5 | # scientific data, and much more. 6 | # 7 | # Inspiration for this project was derived from Processing and NodeBox. These excellent 8 | # graphics programming environments are more full-featured than RCG, but they are implemented 9 | # in Java and Python, respectively. RCG was created to offer similar functionality using 10 | # the Ruby programming language. 11 | # 12 | # Author:: James Reynolds (mailto:drtoast@drtoast.com) 13 | # Copyright:: Copyright (c) 2008 James Reynolds 14 | # License:: Distributes under the same terms as Ruby 15 | 16 | module MRGraphics 17 | 18 | class Rope 19 | 20 | attr_accessor :x0, :y0, :x1, :y1, :width, :fibers, :roundness, :stroke_width 21 | 22 | def initialize(canvas, options={}) 23 | @canvas = canvas 24 | @width = options[:width] || 200 25 | @fibers = options[:fibers] || 200 26 | @roundness = options[:roundness] || 1.0 27 | @stroke_width = options[:stroke_width] || 0.4 28 | end 29 | 30 | def hair(hair_x0=@x0, hair_y0=@y0, hair_x1=@x1, hair_y1=@y1, hair_width=@width, hair_fibers=@fibers) 31 | @canvas.push 32 | @canvas.stroke_width(@stroke_width) 33 | @canvas.autoclose_path = false 34 | @canvas.no_fill 35 | hair_x0 = MRGraphics.choose(hair_x0) 36 | hair_y0 = MRGraphics.choose(hair_y0) 37 | hair_x1 = MRGraphics.choose(hair_x1) 38 | hair_y1 = MRGraphics.choose(hair_y1) 39 | vx0 = MRGraphics.random(-@canvas.width / 2, @canvas.width / 2) * @roundness 40 | vy0 = MRGraphics.random(-@canvas.height / 2, @canvas.height / 2) * @roundness 41 | vx1 = MRGraphics.random(-@canvas.width / 2, @canvas.width / 2) * @roundness 42 | vy1 = MRGraphics.random(-@canvas.height / 2, @canvas.height / 2) * @roundness 43 | hair_fibers.times do |j| 44 | #x0,y0,x1,y1 = [@x0.choose,@y0.choose,@x1.choose,@y1.choose] 45 | @canvas.begin_path(hair_x0, hair_y0) 46 | @canvas.curve_to( 47 | hair_x0 + vx0 + rand(hair_width), hair_y0 + vy0 + rand(hair_width), # control point 1 48 | hair_x1 + vx1, hair_y1 + vy1, # control point 2 49 | hair_x1, hair_y1 # end point 50 | ) 51 | @canvas.end_path 52 | end 53 | @canvas.pop 54 | end 55 | 56 | def ribbon(ribbon_x0=@x0, ribbon_y0=@y0, ribbon_x1=@x1, ribbon_y1=@y1, ribbon_width=@width, ribbon_fibers=@fibers) 57 | @canvas.push 58 | @canvas.stroke_width(@stroke_width) 59 | @canvas.autoclose_path = false 60 | @canvas.no_fill 61 | black = Color.black 62 | white = Color.white 63 | ribbon_x0 = MRGraphics.choose(ribbon_x0) 64 | ribbon_y0 = MRGraphics.choose(ribbon_y0) 65 | ribbon_x1 = MRGraphics.choose(ribbon_x1) 66 | ribbon_y1 = MRGraphics.choose(ribbon_y1) 67 | vx0 = MRGraphics.random(-@canvas.width / 2, @canvas.width / 2) * @roundness 68 | vy0 = MRGraphics.random(-@canvas.height / 2, @canvas.height / 2) * @roundness 69 | vx1 = MRGraphics.random(-@canvas.width / 2, @canvas.width / 2) * @roundness 70 | vy1 = MRGraphics.random(-@canvas.height / 2, @canvas.height / 2) * @roundness 71 | xwidth = rand(ribbon_width) 72 | ywidth = rand(ribbon_width) 73 | ribbon_fibers.times do |j| 74 | xoffset = (j-1).to_f * xwidth / ribbon_fibers 75 | yoffset = (j-1).to_f * ywidth / ribbon_fibers 76 | cpx0 = ribbon_x0 + vx0 + xoffset 77 | cpy0 = ribbon_y0 + vy0 + yoffset 78 | cpx1 = ribbon_x1 + vx1 79 | cpy1 = ribbon_y1 + vy1 80 | # debug - show control points 81 | # @canvas.fill(black) 82 | # @canvas.oval(cpx0,cpy0,5,5,:center) 83 | # @canvas.fill(white) 84 | # @canvas.oval(cpx1,cpy1,5,5,:center) 85 | # @canvas.no_fill 86 | 87 | @canvas.begin_path(x0, y0) 88 | @canvas.curve_to( 89 | cpx0, cpy0, # control point 1 90 | cpx1, cpy1, # control point 2 91 | ribbon_x1, ribbon_y1 # end point 92 | ) 93 | @canvas.end_path 94 | end 95 | @canvas.pop 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/elements/sandpainter.rb: -------------------------------------------------------------------------------- 1 | # MacRuby Graphics is a graphics library providing a simple object-oriented 2 | # interface into the power of Mac OS X's Core Graphics and Core Image drawing libraries. 3 | # With a few lines of easy-to-read code, you can write scripts to draw simple or complex 4 | # shapes, lines, and patterns, process and filter images, create abstract art or visualize 5 | # scientific data, and much more. 6 | # 7 | # Inspiration for this project was derived from Processing and NodeBox. These excellent 8 | # graphics programming environments are more full-featured than RCG, but they are implemented 9 | # in Java and Python, respectively. RCG was created to offer similar functionality using 10 | # the Ruby programming language. 11 | # 12 | # Author:: James Reynolds (mailto:drtoast@drtoast.com) 13 | # Copyright:: Copyright (c) 2008 James Reynolds 14 | # License:: Distributes under the same terms as Ruby 15 | 16 | module MRGraphics 17 | 18 | # draw watercolor-like painted strokes (adapted from code by Jared Tarbell - complexification.net) 19 | class SandPainter 20 | 21 | attr_accessor :color, :grains, :grainsize, :maxalpha, :jitter, :huedrift, :saturationdrift, :brightnessdrift 22 | 23 | def initialize(canvas, color=Color.red) 24 | @canvas = canvas 25 | @color = color 26 | # set a random initial gain value 27 | @gain = random(0.01, 0.1) 28 | @grainsize = 6.0 29 | @grains = 64 30 | @maxalpha = 0.1 31 | @jitter = 0 32 | @huedrift = 0.2 33 | @saturationdrift = 0.3 34 | @brightnessdrift = 0.3 35 | end 36 | 37 | # render a line that fades out from ox,oy to x,y 38 | def render(ox, oy, x, y) 39 | @canvas.push 40 | # modulate gain 41 | @gain += random(-0.050, 0.050) 42 | @gain = MRGraphics.in_range(@gain, 0.0, 1.0) 43 | # calculate grains by distance 44 | #@grains = (sqrt((ox-x)*(ox-x)+(oy-y)*(oy-y))).to_i 45 | 46 | # ramp from 0 to .015 for g = 0 to 1 47 | w = @gain / (@grains-1) 48 | 49 | #raycolor = @color.analog 50 | #@color.drift(0.2,0.1,0.1) 51 | @color.drift(huedrift, saturationdrift, brightnessdrift) 52 | #for i in 0..@grains do ##RACK change to fixnum.times 53 | (@grains + 1).times do |i| 54 | # set alpha for this grain (ramp from maxalpha to 0.0 for i = 0 to 64) 55 | a = @maxalpha - (i / @grains.to_f) * @maxalpha 56 | fillcolor = @color.copy.a(a) 57 | @canvas.fill(fillcolor) 58 | #C.rect(ox+(x-ox)*sin(sin(i*w)),oy+(y-oy)*sin(sin(i*w)),1,1) 59 | scaler = sin(sin(i * w)) # ramp sinusoidally from 0 to 1 60 | x1 = ox + (x - ox) * scaler + random(-@jitter, @jitter) 61 | y1 = oy + (y - oy) * scaler + random(-@jitter, @jitter) 62 | #puts "#{scaler} #{w} #{i} #{a} => #{x1},#{y1} #{scaler}" 63 | @canvas.oval(x1, y1, @grainsize, @grainsize, :center) 64 | #C.oval(x,y,@grainsize,@grainsize,:center) 65 | #C.oval(ox,oy,@grainsize,@grainsize,:center) 66 | end 67 | @canvas.pop 68 | 69 | end 70 | end 71 | end -------------------------------------------------------------------------------- /lib/gradient.rb: -------------------------------------------------------------------------------- 1 | # MacRuby Graphics is a graphics library providing a simple object-oriented 2 | # interface into the power of Mac OS X's Core Graphics and Core Image drawing libraries. 3 | # With a few lines of easy-to-read code, you can write scripts to draw simple or complex 4 | # shapes, lines, and patterns, process and filter images, create abstract art or visualize 5 | # scientific data, and much more. 6 | # 7 | # Inspiration for this project was derived from Processing and NodeBox. These excellent 8 | # graphics programming environments are more full-featured than RCG, but they are implemented 9 | # in Java and Python, respectively. RCG was created to offer similar functionality using 10 | # the Ruby programming language. 11 | # 12 | # Author:: James Reynolds (mailto:drtoast@drtoast.com) 13 | # Copyright:: Copyright (c) 2008 James Reynolds 14 | # License:: Distributes under the same terms as Ruby 15 | 16 | module MRGraphics 17 | 18 | # draw a smooth gradient between any number of key colors 19 | class Gradient 20 | 21 | attr_reader :gradient, :drawpre, :drawpost 22 | 23 | # create a new gradient from black to white 24 | def initialize(*colors) 25 | @colorspace = CGColorSpaceCreateDeviceRGB() 26 | colors = colors[0] if colors[0].class == Array 27 | set(colors) 28 | pre(true) 29 | post(true) 30 | self 31 | end 32 | 33 | # create a gradient that evenly distributes the given colors 34 | def set(colors) 35 | colors ||= [Color.black, Color.white] 36 | cgcolors = [] 37 | locations = [] 38 | increment = 1.0 / (colors.size - 1).to_f 39 | i = 0 40 | colors.each do |c| 41 | cgcolor = CGColorCreate(@colorspace, [c.r, c.g, c.b, c.a]) 42 | cgcolors.push(cgcolor) 43 | location = i * increment 44 | locations.push(location) 45 | i = i + 1 46 | end 47 | @gradient = CGGradientCreateWithColors(@colorspace, cgcolors, locations) 48 | end 49 | 50 | # extend gradient before start location? (true/false) 51 | def pre(tf=nil) 52 | @drawpre = (tf ? KCGGradientDrawsBeforeStartLocation : 0) unless tf.nil? 53 | @drawpre 54 | end 55 | 56 | # extend gradient after end location? (true/false) 57 | def post(tf=nil) 58 | @drawpost = (tf ? KCGGradientDrawsAfterEndLocation : 0) unless tf.nil? 59 | @drawpost 60 | end 61 | 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/image.rb: -------------------------------------------------------------------------------- 1 | # MacRuby Graphics is a graphics library providing a simple object-oriented 2 | # interface into the power of Mac OS X's Core Graphics and Core Image drawing libraries. 3 | # With a few lines of easy-to-read code, you can write scripts to draw simple or complex 4 | # shapes, lines, and patterns, process and filter images, create abstract art or visualize 5 | # scientific data, and much more. 6 | # 7 | # Inspiration for this project was derived from Processing and NodeBox. These excellent 8 | # graphics programming environments are more full-featured than RCG, but they are implemented 9 | # in Java and Python, respectively. RCG was created to offer similar functionality using 10 | # the Ruby programming language. 11 | # 12 | # Author:: James Reynolds (mailto:drtoast@drtoast.com) 13 | # Copyright:: Copyright (c) 2008 James Reynolds 14 | # License:: Distributes under the same terms as Ruby 15 | 16 | module MRGraphics 17 | 18 | # load a raw image file for use on a canvas 19 | class Image 20 | 21 | BlendModes = { 22 | :normal => 'CISourceOverCompositing', 23 | :multiply => 'CIMultiplyBlendMode', 24 | :screen => 'CIScreenBlendMode', 25 | :overlay => 'CIOverlayBlendMode', 26 | :darken => 'CIDarkenBlendMode', 27 | :lighten => 'CILightenBlendMode', 28 | :colordodge => 'CIColorDodgeBlendMode', 29 | :colorburn => 'CIColorBurnBlendMode', 30 | :softlight => 'CISoftLightBlendMode', 31 | :hardlight => 'CIHardLightBlendMode', 32 | :difference => 'CIDifferenceBlendMode', 33 | :exclusion => 'CIExclusionBlendMode', 34 | :hue => 'CIHueBlendMode', 35 | :saturation => 'CISaturationBlendMode', 36 | :color => 'CIColorBlendMode', 37 | :luminosity => 'CILuminosityBlendMode', 38 | # following modes not available in CGContext: 39 | :maximum => 'CIMaximumCompositing', 40 | :minimum => 'CIMinimumCompositing', 41 | :add => 'CIAdditionCompositing', 42 | :atop => 'CISourceAtopCompositing', 43 | :in => 'CISourceInCompositing', 44 | :out => 'CISourceOutCompositing', 45 | :over => 'CISourceOverCompositing' 46 | } 47 | BlendModes.default('CISourceOverCompositing') 48 | 49 | attr_reader :cgimage 50 | 51 | # load the image from the given path 52 | def initialize(img, verbose=false) 53 | self.verbose(verbose) 54 | case img 55 | when String 56 | # if it's the path to an image file, load as a CGImage 57 | @path = img 58 | File.exists?(@path) or raise "ERROR: file not found: #{@path}" 59 | image_source = CGImageSourceCreateWithURL(NSURL.fileURLWithPath(@path), nil) 60 | @cgimage = CGImageSourceCreateImageAtIndex(image_source, 0, nil) 61 | @ciimage = CIImage.imageWithCGImage(@cgimage) 62 | when Canvas 63 | puts "Image.new with canvas" if @verbose 64 | @path = 'canvas' 65 | @cgimage = img.cgimage 66 | else 67 | raise "ERROR: image type not recognized: #{img.class}" 68 | end 69 | # save the original 70 | @original = @ciimage.copy 71 | puts "Image.new from [#{@path}] at [#{x},#{y}] with #{width}x#{height}" if @verbose 72 | self 73 | end 74 | 75 | # reload the bitmap image 76 | def reset 77 | @ciimage = CIImage.imageWithCGImage(@cgimage) 78 | self 79 | end 80 | 81 | # set registration mode to :center or :corner 82 | def registration(mode=:center) 83 | @registration = mode 84 | end 85 | 86 | # print the parameters of the path 87 | def to_s 88 | "image.to_s: #{@path} at [#{x},#{y}] with #{width}x#{height}" 89 | end 90 | 91 | # print drawing functions if verbose is true 92 | def verbose(tf=true) 93 | @verbose = tf 94 | end 95 | 96 | # return the width of the image 97 | def width 98 | @ciimage ? @ciimage.extent.size.width : CGImageGetWidth(@cgimage) 99 | end 100 | 101 | # return the height of the image 102 | def height 103 | @ciimage ? @ciimage.extent.size.height : CGImageGetHeight(@cgimage) 104 | end 105 | 106 | # return the x coordinate of the image's origin 107 | def x 108 | @ciimage ? @ciimage.extent.origin.x : 0 109 | end 110 | 111 | # return the y coordinate of the image's origin 112 | def y 113 | @ciimage ? @ciimage.extent.origin.y : 0 114 | end 115 | 116 | # def translate(x,y) 117 | # matrix = CGAffineTransformMakeTranslation(x,y) 118 | # @ciimage = @ciimage.imageByApplyingTransform(matrix) 119 | # @ciimage.extent 120 | # self 121 | # end 122 | 123 | # RESIZING/MOVING 124 | 125 | # scale the image by multiplying the width by a multiplier, optionally scaling height using aspect ratio 126 | def scale(multiplier, aspect=1.0) 127 | puts "image.scale: #{multiplier},#{aspect}" if @verbose 128 | filter 'CILanczosScaleTransform', :inputScale => multiplier.to_f, :inputAspectRatio => aspect.to_f 129 | self 130 | end 131 | 132 | # scale image to fit within a box of w,h using CIAffineTransform (sharper) 133 | def fit2(w, h) 134 | width_multiplier = w.to_f / width 135 | height_multiplier = h.to_f / height 136 | multiplier = width_multiplier < height_multiplier ? width_multiplier : height_multiplier 137 | puts "image.fit2: #{multiplier}" if @verbose 138 | transform = NSAffineTransform.transform 139 | transform.scaleBy(multiplier) 140 | filter 'CIAffineTransform', :inputTransform => transform 141 | self 142 | end 143 | 144 | # scale image to fit within a box of w,h using CILanczosScaleTransform 145 | def fit(w, h) 146 | # http://gigliwood.com/weblog/Cocoa/Core_Image,_part_2.html 147 | old_w = self.width.to_f 148 | old_h = self.height.to_f 149 | old_x = self.x 150 | old_y = self.y 151 | 152 | # choose a scaling factor 153 | width_multiplier = w.to_f / old_w 154 | height_multiplier = h.to_f / old_h 155 | multiplier = width_multiplier < height_multiplier ? width_multiplier : height_multiplier 156 | 157 | # crop result to integer pixel dimensions 158 | new_width = (self.width * multiplier).truncate 159 | new_height = (self.height * multiplier).truncate 160 | 161 | puts "image.fit: old size #{old_w}x#{old_h}, max target #{w}x#{h}, multiplier #{multiplier}, new size #{new_width}x#{new_height}" if @verbose 162 | clamp 163 | scale(multiplier) 164 | crop(old_x, old_y, new_width, new_height) 165 | #origin(:bottomleft) 166 | self 167 | end 168 | 169 | # resize the image to have new dimensions w,h 170 | def resize(w, h) 171 | oldw = width 172 | oldh = height 173 | puts "image.resize #{oldw}x#{oldh} => #{w}x#{h}" if @verbose 174 | width_ratio = w.to_f / oldw.to_f 175 | height_ratio = h.to_f / oldh.to_f 176 | aspect = width_ratio / height_ratio # (works when stretching tall, gives aspect = 0.65) 177 | scale(height_ratio,aspect) 178 | origin(:bottom_left) 179 | self 180 | end 181 | 182 | # crop the image to a rectangle from x1,y2 with width x height 183 | def crop(x=nil,y=nil,w=nil,h=nil) 184 | 185 | # crop to largest square if no parameters were given 186 | unless x 187 | if (self.width > self.height) 188 | side = self.height 189 | x = (self.width - side) / 2 190 | y = 0 191 | else 192 | side = self.width 193 | y = (self.height - side) / 2 194 | x = 0 195 | end 196 | w = h = side 197 | end 198 | 199 | puts "image.crop [#{x},#{y}] with #{w},#{h}" if @verbose 200 | #vector = CIVector.vectorWithX_Y_Z_W(x.to_f,y.to_f,w.to_f,h.to_f) 201 | vector = CIVector.vectorWithX x.to_f, Y:y.to_f, Z:w.to_f, W:h.to_f 202 | filter('CICrop', :inputRectangle => vector) 203 | origin(:bottom_left) 204 | self 205 | end 206 | 207 | # apply an affine transformation using matrix parameters a,b,c,d,tx,ty 208 | def transform(a, b, c, d, tx, ty) 209 | puts "image.transform #{a},#{b},#{c},#{d},#{tx},#{ty}" if @verbose 210 | transform = CGAffineTransformMake(a, b, c, d, tx, ty) # FIXME: needs to be NSAffineTransform? 211 | filter 'CIAffineTransform', :inputTransform => transform 212 | self 213 | end 214 | 215 | # translate image by tx,ty 216 | def translate(tx, ty) 217 | puts "image.translate #{tx},#{ty}" if @verbose 218 | #transform = CGAffineTransformMakeTranslation(tx,ty); 219 | transform = NSAffineTransform.transform 220 | transform.translateXBy tx, yBy:ty 221 | filter 'CIAffineTransform', :inputTransform => transform 222 | self 223 | end 224 | 225 | # rotate image by degrees 226 | def rotate(deg) 227 | puts "image.rotate #{deg}" if @verbose 228 | #transform = CGAffineTransformMakeRotation(MRGraphics.radians(deg)); 229 | transform = NSAffineTransform.transform 230 | transform.rotateByDegrees(-deg) 231 | filter 'CIAffineTransform', :inputTransform => transform 232 | self 233 | end 234 | 235 | # set the origin to the specified location (:center, :bottom_left, etc) 236 | def origin(location=:bottom_left) 237 | movex, movey = MRGraphics.reorient(x, y, width, height, location) 238 | translate(movex, movey) 239 | end 240 | 241 | 242 | # FILTERS 243 | 244 | # apply a crystallizing effect with pixel radius 1-100 245 | def crystallize(radius=20.0) 246 | filter 'CICrystallize', :inputRadius => radius 247 | self 248 | end 249 | 250 | # apply a gaussian blur with pixel radius 1-100 251 | def blur(radius=10.0) 252 | filter 'CIGaussianBlur', :inputRadius => MRGraphics.in_range(radius, 1.0, 100.0) 253 | self 254 | end 255 | 256 | # sharpen the image given a radius (0-100) and intensity factor 257 | def sharpen(radius=2.50, intensity=0.50) 258 | filter 'CIUnsharpMask', :inputRadius => radius, :inputIntensity => intensity 259 | self 260 | end 261 | 262 | # apply a gaussian blur with pixel radius 1-100 263 | def motionblur(radius=10.0, angle=90.0) 264 | oldx, oldy, oldw, oldh = [x, y, width, height] 265 | clamp 266 | filter 'CIMotionBlur', :inputRadius => radius, :inputAngle => MRGraphics.radians(angle) 267 | crop(oldx, oldy, oldw, oldh) 268 | self 269 | end 270 | 271 | # rotate pixels around x,y with radius and angle 272 | def twirl(x=0, y=0, radius=300, angle=90.0) 273 | filter 'CITwirlDistortion', :inputCenter => CIVector.vectorWithX(x, Y:y), :inputRadius => radius, :inputAngle => MRGraphics.radians(angle) 274 | self 275 | end 276 | 277 | # apply a bloom effect 278 | def bloom(radius=10, intensity=1.0) 279 | filter 'CIBloom', :inputRadius => MRGraphics.in_range(radius, 0, 100), :inputIntensity => MRGraphics.in_range(intensity, 0.0, 1.0) 280 | self 281 | end 282 | 283 | # adjust the hue of the image by rotating the color wheel from 0 to 360 degrees 284 | def hue(angle=180) 285 | filter 'CIHueAdjust', :inputAngle => MRGraphics.radians(angle) 286 | self 287 | end 288 | 289 | # remap colors so they fall within shades of a single color 290 | def monochrome(color=Color.gray) 291 | filter 'CIColorMonochrome', :inputColor => CIColor.colorWithRed(color.r, green:color.g, blue:color.b, alpha:color.a) 292 | self 293 | end 294 | 295 | # adjust the reference white point for an image and maps all colors in the source using the new reference 296 | def whitepoint(color=Color.white.ish) 297 | filter 'CIWhitePointAdjust', :inputColor => CIColor.colorWithRed(color.r, green:color.g, blue:color.b, alpha:color.a) 298 | self 299 | end 300 | 301 | # reduce colors with a banding effect 302 | def posterize(levels=6.0) 303 | filter 'CIColorPosterize', :inputLevels => MRGraphics.in_range(levels, 1.0, 300.0) 304 | self 305 | end 306 | 307 | # detect edges 308 | def edges(intensity=1.0) 309 | filter 'CIEdges', :inputIntensity => MRGraphics.in_range(intensity, 0.0,10.0) 310 | self 311 | end 312 | 313 | # apply woodblock-like effect 314 | def edgework(radius=1.0) 315 | filter 'CIEdgeWork', :inputRadius => MRGraphics.in_range(radius, 0.0,20.0) 316 | self 317 | end 318 | 319 | # adjust exposure by f-stop 320 | def exposure(ev=0.5) 321 | filter 'CIExposureAdjust', :inputEV => MRGraphics.in_range(ev, -10.0, 10.0) 322 | self 323 | end 324 | 325 | # adjust saturation 326 | def saturation(value=1.5) 327 | filter 'CIColorControls', :inputSaturation => value, :inputBrightness => 0.0, :inputContrast => 1.0 328 | self 329 | end 330 | 331 | # adjust brightness (-1 to 1) 332 | def brightness(value=1.1) 333 | filter 'CIColorControls', :inputSaturation => 1.0, :inputBrightness => MRGraphics.in_range(value, -1.0, 1.0), :inputContrast => 1.0 334 | self 335 | end 336 | 337 | # adjust contrast (0 to 4) 338 | def contrast(value=1.5) 339 | #value = MRGraphics.in_range(value,0.25,100.0) 340 | filter 'CIColorControls', :inputSaturation => 1.0, :inputBrightness => 0.0, :inputContrast => value 341 | self 342 | end 343 | 344 | # fill with a gradient from color0 to color1 from [x0,y0] to [x1,y1] 345 | def gradient(color0, color1, x0 = x / 2, y0 = y, x1 = x / 2, y1 = height) 346 | filter 'CILinearGradient', 347 | :inputColor0 => color0.rgb, 348 | :inputColor1 => color1.rgb, 349 | :inputPoint0 => CIVector.vectorWithX(x0, Y:y0), 350 | :inputPoint1 => CIVector.vectorWithX(x1, Y:y1) 351 | self 352 | end 353 | 354 | # use the gray values of the input image as a displacement map (doesn't work with PNG?) 355 | def displacement(image, scale=50.0) 356 | filter 'CIDisplacementDistortion', :inputDisplacementImage => image.ciimage, :inputScale => MRGraphics.in_range(scale, 0.0, 200.0) 357 | self 358 | end 359 | 360 | # simulate a halftone screen given a center point, angle(0-360), width(1-50), and sharpness(0-1) 361 | def dotscreen(dx=0, dy=0, angle=0, width=6, sharpness=0.7) 362 | filter 'CIDotScreen', 363 | :inputCenter => CIVector.vectorWithX(dx.to_f, Y:dy.to_f), 364 | :inputAngle => MRGraphics.max(0, MRGraphics.min(angle, 360)), 365 | :inputWidth => MRGraphics.max(1, MRGraphics.min(width, 50)), 366 | :inputSharpness => MRGraphics.max(0, MRGraphics.min(sharpness, 1)) 367 | self 368 | end 369 | 370 | # extend pixels at edges to infinity for nicer sharpen/blur effects 371 | def clamp 372 | puts "image.clamp" if @verbose 373 | filter 'CIAffineClamp', :inputTransform => NSAffineTransform.transform 374 | self 375 | end 376 | 377 | # blend with the given image using mode (:add, etc) 378 | def blend(image, mode) 379 | case image 380 | when String 381 | ciimage_background = CIImage.imageWithContentsOfURL(NSURL.fileURLWithPath(image)) 382 | when Image 383 | ciimage_background = image.ciimage 384 | else 385 | raise "ERROR: Image: type not recognized" 386 | end 387 | filter BlendModes[mode], :inputBackgroundImage => ciimage_background 388 | self 389 | end 390 | 391 | # draw this image to the specified context 392 | def draw(ctx,x=0,y=0,w=width,h=height) 393 | ciimage 394 | # imgx = x + self.x 395 | # imyy = y + self.y 396 | resize(w, h) if w != self.width || h != self.height 397 | 398 | # add the ciimage's own origin coordinates to the target point 399 | x = x + self.x 400 | y = y + self.y 401 | 402 | puts "image.draw #{x},#{y} #{w}x#{h}" if @verbose 403 | cicontext = CIContext.contextWithCGContext ctx, options:nil 404 | #cicontext.drawImage_atPoint_fromRect(ciimage, [x,y], CGRectMake(self.x,self.y,w,h)) 405 | cicontext.drawImage ciimage, atPoint:CGPointMake(x,y), fromRect:CGRectMake(self.x,self.y,w,h) 406 | end 407 | 408 | # return the CIImage for this Image object 409 | def ciimage 410 | @ciimage ||= CIImage.imageWithCGImage(@cgimage) 411 | end 412 | 413 | # return an array of n colors chosen randomly from the source image. 414 | # if type = :grid, choose average colors from each square in a grid with n squares 415 | def colors(n=32, type=:random) 416 | ciimage 417 | colors = [] 418 | if (type == :grid) then 419 | filtername = 'CIAreaAverage' 420 | f = CIFilter.filterWithName(filtername) 421 | f.setDefaults 422 | f.setValue_forKey(@ciimage, 'inputImage') 423 | 424 | extents = [] 425 | 426 | rows = Math::sqrt(n).truncate 427 | cols = rows 428 | w = self.width 429 | h = self.height 430 | block_width = w / cols 431 | block_height = h / rows 432 | rows.times do |row| 433 | cols.times do |col| 434 | x = col * block_width 435 | y = row * block_height 436 | extents.push([x, y, block_width, block_height]) 437 | end 438 | end 439 | extents.each do |extent| 440 | x, y, w, h = extent 441 | extent = CIVector.vectorWithX x.to_f, Y:y.to_f, Z:w.to_f, W:h.to_f 442 | f.setValue_forKey(extent, 'inputExtent') 443 | ciimage = f.valueForKey('outputImage') # CIImageRef 444 | nsimg = NSBitmapImageRep.alloc.initWithCIImage(ciimage) 445 | nscolor = nsimg.colorAtX 0, y:0 # NSColor 446 | #r,g,b,a = nscolor.getRed_green_blue_alpha_() 447 | r,b,b,a = [nscolor.redComponent,nscolor.greenComponent,nscolor.blueComponent,nscolor.alphaComponent] 448 | colors.push(Color.new(r,g,b,1.0)) 449 | end 450 | elsif (type == :random) 451 | nsimg = NSBitmapImageRep.alloc.initWithCIImage(@ciimage) 452 | n.times do |i| 453 | x = rand(self.width) 454 | y = rand(self.height) 455 | nscolor = nsimg.colorAtX x, y:y # NSColor 456 | #r,g,b,a = nscolor.getRed_green_blue_alpha_() 457 | r,g,b,a = [nscolor.redComponent,nscolor.greenComponent,nscolor.blueComponent,nscolor.alphaComponent] 458 | colors.push(Color.new(r,g,b,1.0)) 459 | end 460 | end 461 | colors 462 | end 463 | 464 | private 465 | 466 | # apply the named CoreImage filter using a hash of parameters 467 | def filter(filtername, parameters) 468 | ciimage 469 | f = CIFilter.filterWithName(filtername) 470 | f.setDefaults 471 | f.setValue @ciimage, forKey:'inputImage' 472 | parameters.each do |key,value| 473 | f.setValue value, forKey:key 474 | end 475 | puts "image.filter #{filtername}" if @verbose 476 | @ciimage = f.valueForKey('outputImage') # CIImageRef 477 | self 478 | end 479 | 480 | end 481 | 482 | end 483 | -------------------------------------------------------------------------------- /lib/path.rb: -------------------------------------------------------------------------------- 1 | # MacRuby Graphics is a graphics library providing a simple object-oriented 2 | # interface into the power of Mac OS X's Core Graphics and Core Image drawing libraries. 3 | # With a few lines of easy-to-read code, you can write scripts to draw simple or complex 4 | # shapes, lines, and patterns, process and filter images, create abstract art or visualize 5 | # scientific data, and much more. 6 | # 7 | # Inspiration for this project was derived from Processing and NodeBox. These excellent 8 | # graphics programming environments are more full-featured than RCG, but they are implemented 9 | # in Java and Python, respectively. RCG was created to offer similar functionality using 10 | # the Ruby programming language. 11 | # 12 | # Author:: James Reynolds (mailto:drtoast@drtoast.com) 13 | # Copyright:: Copyright (c) 2008 James Reynolds 14 | # License:: Distributes under the same terms as Ruby 15 | 16 | module MRGraphics 17 | 18 | # Make a reusable path. Draw it using canvas.draw(path) 19 | class Path 20 | 21 | attr_accessor :path, :rand, :inc, :fill, :stroke, :scale, :stroke_width, :x, :y, :image 22 | 23 | # create a new path, starting at optional x,y 24 | def initialize(x=0, y=0, &block) 25 | @path = CGPathCreateMutable() 26 | @transform = CGAffineTransformMakeTranslation(0,0) 27 | move_to(x, y) 28 | 29 | # set randomized rendering parameters 30 | @rand = {} 31 | randomize(:x, 0.0) 32 | randomize(:y, 0.0) 33 | randomize(:scale, 1.0) 34 | randomize(:scale_x, 1.0) 35 | randomize(:scale_y, 1.0) 36 | randomize(:rotation, 0.0) 37 | randomize(:stroke_width, 1.0) 38 | 39 | # set incremental rendering parameters 40 | @inc = {} 41 | increment(:rotation, 0.0) 42 | increment(:x, 0.0) 43 | increment(:y, 0.0) 44 | increment(:scale, 1.0) 45 | increment(:scale_x, 1.0) 46 | increment(:scale_y, 1.0) 47 | 48 | @stroke_width = 1.0 49 | @x = 0.0 50 | @y = 0.0 51 | 52 | block.call(self) if block 53 | self 54 | end 55 | 56 | def fill(colors=nil) 57 | if colors 58 | rand[:fill] = colors 59 | else 60 | @fill 61 | end 62 | end 63 | 64 | # randomize the specified parameter within the specified range 65 | def randomize(parameter, value) 66 | rand[parameter] = value 67 | end 68 | 69 | # increment the specified parameter within the specified range 70 | def increment(parameter, value) 71 | inc[parameter] = value 72 | end 73 | 74 | # return a mutable clone of this path 75 | def clone 76 | new_path = self.dup 77 | new_path.path = CGPathCreateMutableCopy(@path) 78 | new_path 79 | end 80 | 81 | 82 | # SET PARAMETERS 83 | 84 | # set registration mode to :center or :corner 85 | def registration(mode=:center) 86 | @registration = mode 87 | end 88 | 89 | # print drawing operations if verbose is true 90 | def verbose(bool=true) 91 | @verbose = bool 92 | end 93 | 94 | # draw without stroke 95 | def no_stroke 96 | @stroke = nil 97 | end 98 | 99 | # GET PATH INFO 100 | 101 | # print origin and dimensions 102 | def to_s 103 | "path.to_s: bounding box at [#{origin_x},#{origin_y}] with #{width}x#{height}, current point [#{currentpoint[0]},#{currentpoint[1]}]" 104 | end 105 | 106 | # return the x coordinate of the path's origin 107 | def origin_x 108 | CGPathGetBoundingBox(@path).origin.x 109 | end 110 | 111 | # return the y coordinate of the path's origin 112 | def origin_y 113 | CGPathGetBoundingBox(@path).origin.y 114 | end 115 | 116 | # return the width of the path's bounding box 117 | def width 118 | CGPathGetBoundingBox(@path).size.width 119 | end 120 | 121 | # return the height of the path's bounding box 122 | def height 123 | CGPathGetBoundingBox(@path).size.height 124 | end 125 | 126 | # return the current point 127 | def current_point 128 | CGPathGetCurrentPoint(@path) 129 | end 130 | 131 | # true if the path contains the current point # doesn't work? 132 | def contains(x,y) 133 | eorule = true 134 | CGPathContainsPoint(@path, @transform, CGPointMake(x, y), eorule) 135 | end 136 | 137 | 138 | # ADD SHAPES TO PATH 139 | 140 | # add another path to this path 141 | def add_path(p) 142 | CGPathadd_path(@path, @transform, p.path) 143 | end 144 | 145 | # add a rectangle starting at [x,y] with dimensions w x h 146 | def rect(x=0, y=0, w=20, h=20, reg=@registration) 147 | if reg == :center 148 | x = x - w / 2 149 | y = y - h / 2 150 | end 151 | puts "path.rect at [#{x},#{y}] with #{w}x#{h}" if @verbose 152 | CGPathAddRect(@path, @transform, CGRectMake(x,y,w,h)) 153 | self 154 | end 155 | 156 | # draw a rounded rectangle using quadratic curved corners (FIXME) 157 | def round_rect(x=0, y=0, width=20, height=20, roundness=0, reg=@registration) 158 | if roundness == 0 159 | p.rect(x, y, width, height, reg) 160 | else 161 | if reg == :center 162 | x = x - self.width / 2 163 | y = y - self.height / 2 164 | end 165 | curve = MRGraphics.min(width * roundness, height * roundness) 166 | p = Path.new 167 | p.move_to(x, y+curve) 168 | p.curve_to(x, y, x, y, x+curve, y) 169 | p.line_to(x+width-curve, y) 170 | p.curve_to(x+width, y, x+width, y, x+width, y+curve) 171 | p.line_to(x+width, y+height-curve) 172 | p.curve_to(x+width, y+height, x+width, y+height, x+width-curve, y+height) 173 | p.line_to(x+curve, y+height) 174 | p.curve_to(x, y+height, x, y+height, x, y+height-curve) 175 | p.end_path 176 | end 177 | add_path(p) 178 | self 179 | end 180 | 181 | # create an oval starting at x,y with dimensions w x h, optionally registered at :center 182 | def oval(x=0, y=0, w=20, h=20, reg=@registration) 183 | if (reg == :center) 184 | x = x - w / 2 185 | y = y - h / 2 186 | end 187 | puts "path.oval at [#{x},#{y}] with #{w}x#{h}" if @verbose 188 | CGPathAddEllipseInRect(@path, @transform, CGRectMake(x, y, w, h)) 189 | self 190 | end 191 | 192 | # draw a circle with center at x,y with width and (optional) height 193 | # def circle(x,y,w,h=w) 194 | # oval(x - w/2, y - h/2, w, h) 195 | # end 196 | 197 | # ADD LINES TO PATH 198 | 199 | # draw a line from x1,x2 to x2,y2 200 | def line(x1, y1, x2, y2) 201 | CGPathAddLines(@path, @transform, [NSMakePoint(x1, y1), NSMakePoint(x2, y2)]) 202 | self 203 | end 204 | 205 | # draw the arc of a circle with center point x,y, radius, start angle (0 deg = 12 o'clock) and end angle 206 | def arc(x, y, radius, start_angle, end_angle) 207 | start_angle = MRGraphics.radians(90 - start_angle) 208 | end_angle = MRGraphics.radians(90 - end_angle) 209 | clockwise = 1 # 1 = clockwise, 0 = counterclockwise 210 | CGPathAddArc(@path, @transform, x, y, radius, start_angle, end_angle, clockwise) 211 | self 212 | end 213 | 214 | # draw lines connecting the array of points 215 | def lines(points) 216 | CGPathAddLines(@path, @transform, points) 217 | self 218 | end 219 | 220 | 221 | # CONSTRUCT PATHS IN PATH OBJECT 222 | 223 | # move the "pen" to x,y 224 | def move_to(x, y) 225 | CGPathMoveToPoint(@path, @transform,x,y) 226 | self 227 | end 228 | 229 | # draw a line from the current point to x,y 230 | def line_to(x,y) 231 | CGPathAddLineToPoint(@path, @transform, x, y) 232 | self 233 | end 234 | 235 | # draw a bezier curve from the current point, given the coordinates of two handle control points and an end point 236 | def curve_to(cp1x, cp1y, cp2x, cp2y, x, y) 237 | CGPathAddCurveToPoint(@path, @transform, cp1x, cp1y, cp2x, cp2y, x, y) 238 | self 239 | end 240 | 241 | # draw a quadratic curve given a single control point and an end point 242 | def qcurve_to(cpx, cpy, x, y) 243 | CGPathAddQuadCurveToPoint(@path, @transform, cpx, cpy, x, y) 244 | self 245 | end 246 | 247 | # draw an arc given the endpoints of two tangent lines and a radius 248 | def arc_to(x1, y1, x2, y2, radius) 249 | CGPathAddArcToPoint(@path, @transform, x1, y1, x2, y2, radius) 250 | self 251 | end 252 | 253 | # end the current path 254 | def end_path 255 | CGPathCloseSubpath(@path) 256 | end 257 | 258 | 259 | # TRANSFORMATIONS 260 | 261 | # specify rotation for subsequent operations 262 | def rotate(deg) 263 | puts "path.rotate #{deg}" if @verbose 264 | @transform = CGAffineTransformRotate(@transform, MRGraphics.radians(deg)) 265 | end 266 | 267 | # scale by horizontal/vertical scaling factors sx,sy for subsequent drawing operations 268 | def scale(sx=nil, sy=nil) 269 | if sx == nil && sy == nil 270 | @scale 271 | else 272 | sy = sx unless sy 273 | puts "path.scale #{sx}x#{sy}" if @verbose 274 | @transform = CGAffineTransformScale(@transform, sx, sy) 275 | end 276 | end 277 | 278 | # specify translation by x,y for subsequent drawing operations 279 | def translate(x,y) 280 | puts "path.translate #{x}x#{y}" if @verbose 281 | @transform = CGAffineTransformTranslate(@transform, x, y) 282 | end 283 | 284 | 285 | # BUILD PRIMITIVES 286 | 287 | # draw a petal starting at x,y with w x h and center bulge height using quadratic curves 288 | def petal(x=0, y=0, w=10, h=50, bulge=h/2) 289 | move_to(x,y) 290 | qcurve_to(x - w, y + bulge, x, y + h) 291 | qcurve_to(x + w, y + bulge, x, y) 292 | end_path 293 | self 294 | end 295 | 296 | # duplicate and rotate the Path object the specified number of times 297 | def kaleidoscope(path,qty) 298 | deg = 360 / qty 299 | qty.times do 300 | add_path(path) 301 | rotate(deg) 302 | end 303 | end 304 | 305 | # duplicate and rotate the Path object the specified number of times 306 | #path, rotation, scale, translation, iterations 307 | def spiral(path=nil, rotation=20, scale_x=0.95, scale_y=0.95, tx=10, ty=10, iterations=30) 308 | iterations.times do 309 | add_path(path) 310 | rotate(rotation) 311 | scale(scale_x, scale_y) 312 | translate(tx, ty) 313 | end 314 | end 315 | 316 | end 317 | end 318 | -------------------------------------------------------------------------------- /lib/pdf.rb: -------------------------------------------------------------------------------- 1 | # MacRuby Graphics is a graphics library providing a simple object-oriented 2 | # interface into the power of Mac OS X's Core Graphics and Core Image drawing libraries. 3 | # With a few lines of easy-to-read code, you can write scripts to draw simple or complex 4 | # shapes, lines, and patterns, process and filter images, create abstract art or visualize 5 | # scientific data, and much more. 6 | # 7 | # Inspiration for this project was derived from Processing and NodeBox. These excellent 8 | # graphics programming environments are more full-featured than RCG, but they are implemented 9 | # in Java and Python, respectively. RCG was created to offer similar functionality using 10 | # the Ruby programming language. 11 | # 12 | # Author:: James Reynolds (mailto:drtoast@drtoast.com) 13 | # Copyright:: Copyright (c) 2008 James Reynolds 14 | # License:: Distributes under the same terms as Ruby 15 | 16 | module MRGraphics 17 | 18 | # parse a PDF file to determine pages, width, height 19 | class Pdf 20 | 21 | attr_reader :pages, :pdf 22 | 23 | # create a new Pdf object given the original pathname, and password if needed 24 | def initialize(original, password = nil) 25 | # http://developer.apple.com/documentation/GraphicsImaging/Reference/CGPDFDocument/Reference/reference.html 26 | # http://developer.apple.com/documentation/GraphicsImaging/Reference/CGPDFPage/Reference/reference.html 27 | @pdf = CGPDFDocumentCreateWithURL(NSURL.fileURLWithPath(original)) # => CGPDFDocumentRef 28 | result = CGPDFDocumentUnlockWithPassword(@pdf, password) if password # unlock if necessary 29 | @pages = CGPDFDocumentGetNumberOfPages(@pdf) # => 4 30 | puts "pdf.new #{original} (#{@pages} pages)" if @verbose 31 | self 32 | end 33 | 34 | # print drawing functions to console if verbose is true 35 | def verbose(tf) 36 | @verbose = tf 37 | end 38 | 39 | # get the width of the specified pagenum 40 | def width(pagenum=1) 41 | cgpdfpage = page(pagenum) 42 | mediabox = CGPDFPageGetBoxRect(cgpdfpage, KCGPDFMediaBox) # => CGRect 43 | width = mediabox.size.width # CGRectGetWidth(mediabox) 44 | width 45 | end 46 | 47 | # get the height of the specified pagenum 48 | def height(pagenum=1) 49 | cgpdfpage = page(pagenum) 50 | mediabox = CGPDFPageGetBoxRect(cgpdfpage, KCGPDFMediaBox) # => CGRect 51 | height = mediabox.size.height # CGRectGetHeight(mediabox) 52 | height 53 | end 54 | 55 | # draw pagenum of the pdf document into a rectangle at x,y with dimensions w,h of drawing context ctx 56 | def draw(ctx, x=0, y=0, w=width(pagenum), h=height(pagenum), pagenum=1) 57 | rect = CGRectMake(x,y,w,h) 58 | puts "pdf.draw page #{pagenum} at [#{x},#{y}] with #{w}x#{h}" if @verbose 59 | CGContextDrawPDFDocument(ctx, rect, @pdf, pagenum) 60 | true 61 | end 62 | 63 | private 64 | 65 | # return a CGPDFPageRef for this pagenum 66 | def page(pagenum) 67 | CGPDFDocumentGetPage(@pdf, pagenum) # => CGPDFPageRef 68 | end 69 | 70 | end 71 | end -------------------------------------------------------------------------------- /specs/canvas_spec.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/spec' 2 | require File.join(File.expand_path(File.dirname(__FILE__)), '..', 'graphics') 3 | MiniTest::Unit.autorun 4 | include MRGraphics 5 | 6 | describe "MRGraphics Canvas" do 7 | before do 8 | @destination = File.expand_path("#{File.dirname(__FILE__)}/tmp/test.png") 9 | @canvas = Canvas.for_image(:filename => @destination) do |c| 10 | c.background(Color.black) 11 | c.fill(Color.white) 12 | c.text('this is a test') 13 | end 14 | end 15 | 16 | after do 17 | File.delete(@destination) if File.exist?(@destination) 18 | end 19 | 20 | it "should have a width" do 21 | @canvas.width 22 | end 23 | 24 | it "should have a height" do 25 | @canvas.height 26 | end 27 | 28 | it "should save to a file" do 29 | File.delete(@destination) if File.exist?(@destination) 30 | @canvas.save 31 | File.exist?(@destination).must_equal(true) 32 | end 33 | 34 | end -------------------------------------------------------------------------------- /specs/graphics_spec.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/spec' 2 | require File.join(File.expand_path(File.dirname(__FILE__)), '..', 'graphics') 3 | MiniTest::Unit.autorun 4 | 5 | describe 'MRGraphics module methods' do 6 | 7 | it "should convert degrees to radians" do 8 | MRGraphics.radians(30).must_be_close_to(0.523598776) 9 | MRGraphics.radians(90).must_be_close_to(1.57079633) 10 | MRGraphics.radians(360).must_be_close_to(6.28318531) 11 | end 12 | 13 | it "should convert radians to degrees" do 14 | MRGraphics.degrees(0.523598776).must_be_close_to(30) 15 | MRGraphics.degrees(1.57079633).must_be_close_to(90) 16 | MRGraphics.degrees(6.28318531).must_be_close_to(360) 17 | end 18 | 19 | it "should calculate the angle of the line joining two points" do 20 | MRGraphics.angle(0,0, 10,0).must_be_close_to(0) 21 | MRGraphics.angle(0,0, 10,10).must_be_close_to(45) 22 | MRGraphics.angle(0,0, 0,10).must_be_close_to(90) 23 | end 24 | 25 | it "should calculate the distance between 2 points" do 26 | MRGraphics.distance(0,0, 10,0).must_be_close_to(10) 27 | MRGraphics.distance(0,0, 10,10).must_be_close_to(14.14, 0.1) 28 | end 29 | 30 | it "should calculate the coordinate of a new point" do 31 | point = MRGraphics.coordinates(0, 0, 10, 45) 32 | point.first.must_be_close_to(7.07, 0.1) 33 | point.last.must_be_close_to(7.07, 0.1) 34 | end 35 | 36 | it "should let you randomly choose an integer from a range" do 37 | from_range = MRGraphics.choose(0..100) 38 | from_range.must_be_instance_of(Fixnum) 39 | (0..100).must_include(from_range) 40 | randoms = [] 41 | 50.times{randoms << MRGraphics.choose(0..1000) } 42 | randoms.uniq.size.must_be_close_to(randoms.size, 5) 43 | end 44 | 45 | it "should let you randomly choose a float from a range" do 46 | from_range = MRGraphics.choose(0.0..10.0) 47 | from_range.must_be_instance_of(Float) 48 | (0.0..10.0).must_include(from_range) 49 | randoms = [] 50 | 50.times{randoms << MRGraphics.choose(0.0..100.0) } 51 | randoms.uniq.size.must_be_close_to(randoms.size, 5) 52 | end 53 | 54 | it "should let you randomly choose an item from an array" do 55 | from_range = MRGraphics.choose([1,2,3,4,5]) 56 | from_range.must_be_instance_of(Fixnum) 57 | (1..5).must_include(from_range) 58 | end 59 | 60 | it "should return the point to reorient an item" do 61 | new_location = MRGraphics.reorient(0, 0, 100, 100, :center) 62 | new_location.must_be_instance_of(Array) 63 | new_location.first.must_be_same_as(-50) 64 | new_location.last.must_be_same_as(-50) 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /specs/tmp/.ignore: -------------------------------------------------------------------------------- 1 | *.png --------------------------------------------------------------------------------