Permalink
Browse files

initial copying of hotcocoa into gem

  • Loading branch information...
1 parent 29b4911 commit c00c997d5ac2bdfae5265ba837e1c152bc6ff9be @richkilmer committed Nov 8, 2009
Showing with 7,001 additions and 0 deletions.
  1. +4 −0 History.txt
  2. +11 −0 Manifest.txt
  3. +5 −0 PostInstall.txt
  4. +48 −0 README.rdoc
  5. +26 −0 Rakefile
  6. +26 −0 lib/hotcocoa.rb
  7. +320 −0 lib/hotcocoa/application_builder.rb
  8. +143 −0 lib/hotcocoa/attributed_string.rb
  9. +7 −0 lib/hotcocoa/behaviors.rb
  10. +44 −0 lib/hotcocoa/data_sources/combo_box_data_source.rb
  11. +18 −0 lib/hotcocoa/data_sources/table_data_source.rb
  12. +85 −0 lib/hotcocoa/delegate_builder.rb
  13. +161 −0 lib/hotcocoa/graphics.rb
  14. +836 −0 lib/hotcocoa/graphics/canvas.rb
  15. +781 −0 lib/hotcocoa/graphics/color.rb
  16. +75 −0 lib/hotcocoa/graphics/elements/particle.rb
  17. +99 −0 lib/hotcocoa/graphics/elements/rope.rb
  18. +71 −0 lib/hotcocoa/graphics/elements/sandpainter.rb
  19. +63 −0 lib/hotcocoa/graphics/gradient.rb
  20. +488 −0 lib/hotcocoa/graphics/image.rb
  21. +325 −0 lib/hotcocoa/graphics/path.rb
  22. +71 −0 lib/hotcocoa/graphics/pdf.rb
  23. +14 −0 lib/hotcocoa/kernel_ext.rb
  24. +48 −0 lib/hotcocoa/kvo_accessors.rb
  25. +448 −0 lib/hotcocoa/layout_view.rb
  26. +227 −0 lib/hotcocoa/mapper.rb
  27. +40 −0 lib/hotcocoa/mapping_methods.rb
  28. +109 −0 lib/hotcocoa/mappings.rb
  29. +25 −0 lib/hotcocoa/mappings/alert.rb
  30. +112 −0 lib/hotcocoa/mappings/application.rb
  31. +87 −0 lib/hotcocoa/mappings/array_controller.rb
  32. +39 −0 lib/hotcocoa/mappings/box.rb
  33. +92 −0 lib/hotcocoa/mappings/button.rb
  34. +44 −0 lib/hotcocoa/mappings/collection_view.rb
  35. +28 −0 lib/hotcocoa/mappings/color.rb
  36. +21 −0 lib/hotcocoa/mappings/column.rb
  37. +24 −0 lib/hotcocoa/mappings/combo_box.rb
  38. +33 −0 lib/hotcocoa/mappings/control.rb
  39. +44 −0 lib/hotcocoa/mappings/font.rb
  40. +15 −0 lib/hotcocoa/mappings/gradient.rb
  41. +15 −0 lib/hotcocoa/mappings/image.rb
  42. +43 −0 lib/hotcocoa/mappings/image_view.rb
  43. +25 −0 lib/hotcocoa/mappings/label.rb
  44. +9 −0 lib/hotcocoa/mappings/layout_view.rb
  45. +71 −0 lib/hotcocoa/mappings/menu.rb
  46. +47 −0 lib/hotcocoa/mappings/menu_item.rb
  47. +13 −0 lib/hotcocoa/mappings/movie.rb
  48. +27 −0 lib/hotcocoa/mappings/movie_view.rb
  49. +17 −0 lib/hotcocoa/mappings/notification.rb
  50. +110 −0 lib/hotcocoa/mappings/popup.rb
  51. +68 −0 lib/hotcocoa/mappings/progress_indicator.rb
  52. +29 −0 lib/hotcocoa/mappings/scroll_view.rb
  53. +9 −0 lib/hotcocoa/mappings/search_field.rb
  54. +17 −0 lib/hotcocoa/mappings/secure_text_field.rb
  55. +97 −0 lib/hotcocoa/mappings/segmented_control.rb
  56. +25 −0 lib/hotcocoa/mappings/slider.rb
  57. +13 −0 lib/hotcocoa/mappings/sort_descriptor.rb
  58. +9 −0 lib/hotcocoa/mappings/sound.rb
  59. +25 −0 lib/hotcocoa/mappings/speech_synthesizer.rb
  60. +21 −0 lib/hotcocoa/mappings/split_view.rb
  61. +7 −0 lib/hotcocoa/mappings/status_bar.rb
  62. +9 −0 lib/hotcocoa/mappings/status_item.rb
  63. +110 −0 lib/hotcocoa/mappings/table_view.rb
  64. +41 −0 lib/hotcocoa/mappings/text_field.rb
  65. +13 −0 lib/hotcocoa/mappings/text_view.rb
  66. +25 −0 lib/hotcocoa/mappings/timer.rb
  67. +97 −0 lib/hotcocoa/mappings/toolbar.rb
  68. +36 −0 lib/hotcocoa/mappings/toolbar_item.rb
  69. +67 −0 lib/hotcocoa/mappings/view.rb
  70. +22 −0 lib/hotcocoa/mappings/web_view.rb
  71. +118 −0 lib/hotcocoa/mappings/window.rb
  72. +41 −0 lib/hotcocoa/mappings/xml_parser.rb
  73. +175 −0 lib/hotcocoa/mvc.rb
  74. +62 −0 lib/hotcocoa/notification_listener.rb
  75. +22 −0 lib/hotcocoa/object_ext.rb
  76. +45 −0 lib/hotcocoa/plist.rb
  77. +17 −0 lib/hotcocoa/standard_rake_tasks.rb
  78. +23 −0 lib/hotcocoa/template.rb
  79. +172 −0 lib/hotcocoa/virtual_file_system.rb
  80. +10 −0 script/console
  81. +14 −0 script/destroy
  82. +14 −0 script/generate
  83. +3 −0 test/test_helper.rb
  84. +11 −0 test/test_hotcocoa.rb
View
4 History.txt
@@ -0,0 +1,4 @@
+=== 0.0.1 2009-11-07
+
+* 1 major enhancement:
+ * Initial release
View
11 Manifest.txt
@@ -0,0 +1,11 @@
+History.txt
+Manifest.txt
+PostInstall.txt
+README.rdoc
+Rakefile
+lib/hotcocoa.rb
+script/console
+script/destroy
+script/generate
+test/test_helper.rb
+test/test_hotcocoa.rb
View
5 PostInstall.txt
@@ -0,0 +1,5 @@
+
+For more information on hotcocoa, see http://hotcocoa.rubyforge.org
+
+
+
View
48 README.rdoc
@@ -0,0 +1,48 @@
+= hotcocoa
+
+* http://github.com/rich_kilmer/hotcocoa
+
+== DESCRIPTION:
+
+FIX (describe your package)
+
+== FEATURES/PROBLEMS:
+
+* FIX (list of features or problems)
+
+== SYNOPSIS:
+
+ FIX (code sample of usage)
+
+== REQUIREMENTS:
+
+* FIX (list of requirements)
+
+== INSTALL:
+
+* FIX (sudo gem install, anything else)
+
+== LICENSE:
+
+(The MIT License)
+
+Copyright (c) 2009 FIXME full name
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
26 Rakefile
@@ -0,0 +1,26 @@
+require 'rubygems'
+gem 'hoe', '>= 2.1.0'
+require 'hoe'
+require 'fileutils'
+require './lib/hotcocoa'
+
+Hoe.plugin :newgem
+# Hoe.plugin :website
+# Hoe.plugin :cucumberfeatures
+
+# Generate all the Rake tasks
+# Run 'rake -T' to see list of generated tasks (from gem root directory)
+$hoe = Hoe.spec 'hotcocoa' do
+ self.developer 'Rich Kilmer', 'rich@infoether.com'
+ self.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
+ self.rubyforge_name = self.name # TODO this is default value
+ # self.extra_deps = [['activesupport','>= 2.0.2']]
+
+end
+
+require 'newgem/tasks'
+Dir['tasks/**/*.rake'].each { |t| load t }
+
+# TODO - want other tests/tasks run by default? Add them to the list
+# remove_task :default
+# task :default => [:spec, :features]
View
26 lib/hotcocoa.rb
@@ -0,0 +1,26 @@
+$:.unshift(File.dirname(__FILE__)) unless
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
+
+framework 'Cocoa'
+
+module Hotcocoa
+ VERSION = '0.0.1'
+ Views = {}
+end
+
+require 'hotcocoa/object_ext'
+require 'hotcocoa/kernel_ext'
+require 'hotcocoa/mappings'
+require 'hotcocoa/behaviors'
+require 'hotcocoa/mapping_methods'
+require 'hotcocoa/mapper'
+require 'hotcocoa/layout_view'
+require 'hotcocoa/delegate_builder'
+require 'hotcocoa/notification_listener'
+require 'hotcocoa/data_sources/table_data_source'
+require 'hotcocoa/data_sources/combo_box_data_source'
+require 'hotcocoa/plist'
+require 'hotcocoa/kvo_accessors'
+require 'hotcocoa/attributed_string'
+
+HotCocoa::Mappings.reload
View
320 lib/hotcocoa/application_builder.rb
@@ -0,0 +1,320 @@
+framework 'Foundation'
+
+require 'fileutils'
+
+module HotCocoa
+
+ class ApplicationBuilder
+
+ class Configuration
+
+ attr_reader :name, :version, :icon, :resources, :sources, :info_string, :load
+
+ def initialize(file)
+ require 'yaml'
+ yml = YAML.load(File.read(file))
+ @name = yml["name"]
+ @load = yml["load"]
+ @version = yml["version"] || "1.0"
+ @icon = yml["icon"]
+ @info_string = yml["info_string"]
+ @sources = yml["sources"] || []
+ @resources = yml["resources"] || []
+ @overwrite = yml["overwrite"] == true ? true : false
+ @secure = yml["secure"] == true ? true : false
+ end
+
+ def overwrite?
+ @overwrite
+ end
+
+ def secure?
+ @secure
+ end
+
+ def icon_exist?
+ @icon ? File.exist?(@icon) : false
+ end
+
+ end
+
+ ApplicationBundlePackage = "APPL????"
+
+ attr_accessor :name, :load_file, :sources, :overwrite, :icon, :version, :info_string, :secure, :resources, :deploy
+
+ def self.build(config, options={:deploy => false})
+ if !config.kind_of?(Configuration) || !$LOADED_FEATURES.detect {|f| f.include?("standard_rake_tasks")}
+ require 'rbconfig'
+ puts "Your Rakefile needs to be updated. Please copy the Rakefile from:"
+ puts File.expand_path(File.join(Config::CONFIG['datadir'], "hotcocoa_template", "Rakefile"))
+ exit
+ end
+ builder = new
+ builder.deploy = options[:deploy] == true ? true : false
+ builder.secure = config.secure?
+ builder.name = config.name
+ builder.load_file = config.load
+ builder.icon = config.icon if config.icon_exist?
+ builder.version = config.version
+ builder.info_string = config.info_string
+ builder.overwrite = config.overwrite?
+ config.sources.each do |source|
+ builder.add_source_path source
+ end
+ config.resources.each do |resource|
+ builder.add_resource_path resource
+ end
+ builder.build
+ end
+
+ # Used by the "Embed MacRuby" Xcode target.
+ def self.deploy(path)
+ raise "Given path `#{path}' does not exist" unless File.exist?(path)
+ raise "Given path `#{path}' does not look like an application bundle" unless File.extname(path) == '.app'
+ deployer = new
+ Dir.chdir(File.dirname(path)) do
+ deployer.name = File.basename(path, '.app')
+ deployer.deploy
+ end
+ end
+
+ def initialize
+ @sources = []
+ @resources = []
+ end
+
+ def build
+ check_for_bundle_root
+ build_bundle_structure
+ write_bundle_files
+ copy_sources
+ copy_resources
+ deploy if deploy?
+ copy_icon_file if icon
+ end
+
+ def deploy
+ copy_framework
+ end
+
+ def deploy?
+ @deploy
+ end
+
+ def overwrite?
+ @overwrite
+ end
+
+ def add_source_path(source_file_pattern)
+ Dir.glob(source_file_pattern).each do |source_file|
+ sources << source_file
+ end
+ end
+
+ def add_resource_path(resource_file_pattern)
+ Dir.glob(resource_file_pattern).each do |resource_file|
+ resources << resource_file
+ end
+ end
+
+ def secure?
+ secure
+ end
+
+ private
+
+ def check_for_bundle_root
+ if File.exist?(bundle_root) && overwrite?
+ `rm -rf #{bundle_root}`
+ end
+ end
+
+ def build_bundle_structure
+ Dir.mkdir(bundle_root) unless File.exist?(bundle_root)
+ Dir.mkdir(contents_root) unless File.exist?(contents_root)
+ Dir.mkdir(frameworks_root) unless File.exist?(frameworks_root)
+ Dir.mkdir(macos_root) unless File.exist?(macos_root)
+ Dir.mkdir(resources_root) unless File.exist?(resources_root)
+ end
+
+ def write_bundle_files
+ write_pkg_info_file
+ write_info_plist_file
+ build_executable unless File.exist?(File.join(macos_root, objective_c_executable_file))
+ write_ruby_main
+ end
+
+ def copy_framework
+ unless File.exist?(File.join(frameworks_root, 'MacRuby.framework'))
+ FileUtils.mkdir_p frameworks_root
+ FileUtils.cp_r macruby_framework_path, frameworks_root
+ end
+ `install_name_tool -change #{current_macruby_path}/usr/lib/libmacruby.dylib @executable_path/../Frameworks/MacRuby.framework/Versions/#{current_macruby_version}/usr/lib/libmacruby.dylib '#{macos_root}/#{objective_c_executable_file}'`
+ end
+
+ def copy_sources
+ if secure?
+ data = {}
+ data["/"+load_file] = File.open(load_file, "r") {|f| f.read}
+ sources.each do |source|
+ data["/"+source] = File.open(source, "r") {|f| f.read}
+ end
+ File.open(File.join(resources_root, "vfs.db"), "wb") do |db|
+ db.write Marshal.dump(data)
+ end
+ else
+ FileUtils.cp_r load_file, resources_root unless sources.include?(load_file)
+ sources.each do |source|
+ destination = File.join(resources_root, source)
+ FileUtils.mkdir_p(File.dirname(destination)) unless File.exist?(File.dirname(destination))
+ FileUtils.cp_r source, destination
+ end
+ end
+ end
+
+ def copy_resources
+ resources.each do |resource|
+ destination = File.join(resources_root, resource.split("/")[1..-1].join("/"))
+ FileUtils.mkdir_p(File.dirname(destination)) unless File.exist?(File.dirname(destination))
+ FileUtils.cp_r resource, destination
+ end
+ end
+
+ def copy_icon_file
+ FileUtils.cp(icon, icon_file) unless File.exist?(icon_file)
+ end
+
+ def write_pkg_info_file
+ File.open(pkg_info_file, "wb") {|f| f.write ApplicationBundlePackage}
+ end
+
+ def write_info_plist_file
+ File.open(info_plist_file, "w") do |f|
+ f.puts %{<?xml version="1.0" encoding="UTF-8"?>}
+ f.puts %{<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">}
+ f.puts %{<plist version="1.0">}
+ f.puts %{<dict>}
+ f.puts %{ <key>CFBundleDevelopmentRegion</key>}
+ f.puts %{ <string>English</string>}
+ f.puts %{ <key>CFBundleIconFile</key>} if icon
+ f.puts %{ <string>#{name}.icns</string>} if icon
+ f.puts %{ <key>CFBundleGetInfoString</key>} if info_string
+ f.puts %{ <string>#{info_string}</string>} if info_string
+ f.puts %{ <key>CFBundleExecutable</key>}
+ f.puts %{ <string>#{name.gsub(/ /, '')}</string>}
+ f.puts %{ <key>CFBundleIdentifier</key>}
+ f.puts %{ <string>com.yourcompany.#{name}</string>}
+ f.puts %{ <key>CFBundleInfoDictionaryVersion</key>}
+ f.puts %{ <string>6.0</string>}
+ f.puts %{ <key>CFBundleName</key>}
+ f.puts %{ <string>#{name}</string>}
+ f.puts %{ <key>CFBundlePackageType</key>}
+ f.puts %{ <string>APPL</string>}
+ f.puts %{ <key>CFBundleSignature</key>}
+ f.puts %{ <string>????</string>}
+ f.puts %{ <key>CFBundleVersion</key>}
+ f.puts %{ <string>#{version}</string>}
+ f.puts %{ <key>NSPrincipalClass</key>}
+ f.puts %{ <string>NSApplication</string>}
+ f.puts %{</dict>}
+ f.puts %{</plist>}
+ end
+ end
+
+ def build_executable
+ File.open(objective_c_source_file, "wb") do |f|
+ f.puts %{
+
+ #import <MacRuby/MacRuby.h>
+
+ int main(int argc, char *argv[])
+ {
+ return macruby_main("rb_main.rb", argc, argv);
+ }
+ }
+ end
+ archs = RUBY_ARCH.include?('ppc') ? '-arch ppc' : '-arch i386 -arch x86_64'
+ puts `cd "#{macos_root}" && gcc main.m -o #{objective_c_executable_file} #{archs} -framework MacRuby -framework Foundation -fobjc-gc-only`
+ File.unlink(objective_c_source_file)
+ end
+
+ def write_ruby_main
+ File.open(main_ruby_source_file, "wb") do |f|
+ if secure?
+ require 'hotcocoa/virtual_file_system'
+ f.puts VirtualFileSystem.code_to_load(load_file)
+ else
+ f.puts "$:.map! { |x| x.sub(/^\\/Library\\/Frameworks/, NSBundle.mainBundle.privateFrameworksPath) }" if deploy?
+ f.puts "$:.unshift NSBundle.mainBundle.resourcePath.fileSystemRepresentation"
+ f.puts "load '#{load_file}'"
+ end
+ end
+ end
+
+ def bundle_root
+ "#{name}.app"
+ end
+
+ def contents_root
+ File.join(bundle_root, "Contents")
+ end
+
+ def frameworks_root
+ File.join(contents_root, "Frameworks")
+ end
+
+ def macos_root
+ File.join(contents_root, "MacOS")
+ end
+
+ def resources_root
+ File.join(contents_root, "Resources")
+ end
+
+ def bridgesupport_root
+ File.join(resources_root, "BridgeSupport")
+ end
+
+ def info_plist_file
+ File.join(contents_root, "Info.plist")
+ end
+
+ def icon_file
+ File.join(resources_root, "#{name}.icns")
+ end
+
+ def pkg_info_file
+ File.join(contents_root, "PkgInfo")
+ end
+
+ def objective_c_executable_file
+ name.gsub(/ /, '')
+ end
+
+ def objective_c_source_file
+ File.join(macos_root, "main.m")
+ end
+
+ def main_ruby_source_file
+ File.join(resources_root, "rb_main.rb")
+ end
+
+ def current_macruby_version
+ NSFileManager.defaultManager.pathContentOfSymbolicLinkAtPath(File.join(macruby_versions_path, "Current"))
+ end
+
+ def current_macruby_path
+ File.join(macruby_versions_path, current_macruby_version)
+ end
+
+ def macruby_versions_path
+ File.join(macruby_framework_path, "Versions")
+ end
+
+ def macruby_framework_path
+ "/Library/Frameworks/MacRuby.framework"
+ end
+
+ end
+
+end
View
143 lib/hotcocoa/attributed_string.rb
@@ -0,0 +1,143 @@
+module HotCocoa
+ class NSRangedProxyAttributeHash
+ ATTRIBUTE_KEYS = { :font => NSFontAttributeName,
+ :paragraph_style => NSParagraphStyleAttributeName,
+ :color => NSForegroundColorAttributeName,
+ :underline_style => NSUnderlineStyleAttributeName,
+ :superscript => NSSuperscriptAttributeName,
+ :background_color => NSBackgroundColorAttributeName,
+ :attachment => NSAttachmentAttributeName,
+ :ligature => NSLigatureAttributeName,
+ :baseline_offset => NSBaselineOffsetAttributeName,
+ :kerning => NSKernAttributeName,
+ :link => NSLinkAttributeName,
+ :stroke_width => NSStrokeWidthAttributeName,
+ :stroke_color => NSStrokeColorAttributeName,
+ :underline_color => NSUnderlineColorAttributeName,
+ :strikethrough_style => NSStrikethroughStyleAttributeName,
+ :strikethrough_color => NSStrikethroughColorAttributeName,
+ :shadow => NSShadowAttributeName,
+ :obliqueness => NSObliquenessAttributeName,
+ :expansion_factor => NSExpansionAttributeName,
+ :cursor => NSCursorAttributeName,
+ :tool_tip => NSToolTipAttributeName,
+ :character_shape => NSCharacterShapeAttributeName,
+ :glyph_info => NSGlyphInfoAttributeName,
+ :marked_clause_segment => NSMarkedClauseSegmentAttributeName,
+ :spelling_state => NSSpellingStateAttributeName }
+
+
+ def initialize(proxy)
+ @proxy = proxy
+ end
+
+ def [](k)
+ k = attribute_for_key(k)
+ @proxy.string.attribute(k, atIndex:@proxy.range.first, effectiveRange:nil)
+ end
+
+ def []=(k,v)
+ k = attribute_for_key(k)
+ @proxy.string.removeAttribute(k, range:@proxy.range.to_NSRange(@proxy.string.length - 1))
+ @proxy.string.addAttribute(k, value:v, range:@proxy.range.to_NSRange(@proxy.string.length - 1))
+ end
+
+ def <<(attributes)
+ attributes.each_pair do |k, v|
+ self[k] = v
+ end
+ self
+ end
+ alias :merge :<<
+
+ def to_hash
+ @proxy.string.attributesAtIndex(@proxy.range.first, effectiveRange:nil).inject({}) do |h, pair|
+ h[key_for_attribute(pair.first)] = pair.last
+ h
+ end
+ end
+
+ def inspect
+ to_hash.inspect
+ end
+
+ private
+ def key_for_attribute(attribute)
+ (ATTRIBUTE_KEYS.select { |k,v| v == attribute }.first || [attribute]).first
+ end
+
+ def attribute_for_key(key)
+ ATTRIBUTE_KEYS[key] || key
+ end
+ end
+
+ class NSRangedProxyAttributedString
+ attr_reader :string, :range
+ def initialize(string, range)
+ @string = string
+ @range = range
+ end
+
+ def attributes
+ NSRangedProxyAttributeHash.new(self)
+ end
+ end
+end
+
+class String
+ def with_attributes(attributes = {})
+ attributed_string = NSMutableAttributedString.alloc.initWithString(self)
+ attributed_string.attributes << attributes
+ attributed_string
+ end
+end
+
+class Range
+ def to_NSRange(max = nil)
+ location = first
+ if last == -1 and max
+ length = max - first + 1
+ else
+ length = last - first + 1
+ end
+ NSRange.new(location, length)
+ end
+end
+
+class NSMutableAttributedString
+ def with_attributes(attributes = {})
+ string.with_attributes(attributes)
+ end
+
+ def <<(s)
+ case s
+ when String
+ mutableString.appendString s
+ else
+ appendAttributedString s
+ end
+ end
+
+ def +(s)
+ attributed_string = mutableCopy
+ attributed_string << s
+ attributed_string
+ end
+
+ def attributes
+ HotCocoa::NSRangedProxyAttributedString.new(self, 0..-1).attributes
+ end
+
+ def [](r)
+ HotCocoa::NSRangedProxyAttributedString.new(self, r)
+ end
+
+ def []=(r, s)
+ case s
+ when String
+ replaceCharactersInRange(r.to_NSRange(length - 1), :withString => s)
+ else
+ replaceCharactersInRange(r.to_NSRange(length - 1), :withAttributedString => s)
+ end
+ end
+end
View
7 lib/hotcocoa/behaviors.rb
@@ -0,0 +1,7 @@
+module HotCocoa
+ module Behaviors
+ def Behaviors.included(klass)
+ Mappings::Mapper.map_class(klass)
+ end
+ end
+end
View
44 lib/hotcocoa/data_sources/combo_box_data_source.rb
@@ -0,0 +1,44 @@
+module HotCocoa
+ class ComboBoxDataSource
+ attr_reader :data
+
+ def initialize(data)
+ @data = data
+ end
+
+ def comboBox(combo_box, completedString:string)
+ data.length.times do |index|
+ value = string_value_of_index(index)
+ return value if value.start_with?(string)
+ end
+ nil
+ end
+
+ def comboBox(combo_box, indexOfItemWithStringValue:string)
+ data.length.times do |index|
+ return index if string_value_of_index(index) == string
+ end
+ NSNotFound
+ end
+
+ def comboBox(combo_box, objectValueForItemAtIndex:index)
+ string_value_of_index(index)
+ end
+
+ def numberOfItemsInComboBox(combo_box)
+ data.length
+ end
+
+ private
+
+ def string_value_of_index(i)
+ item = data[i]
+ if item.kind_of?(Hash)
+ item.values.first
+ else
+ item.to_s
+ end
+ end
+
+ end
+end
View
18 lib/hotcocoa/data_sources/table_data_source.rb
@@ -0,0 +1,18 @@
+module HotCocoa
+ class TableDataSource
+ attr_reader :data
+
+ def initialize(data)
+ @data = data
+ end
+
+ def numberOfRowsInTableView(tableView)
+ data.length
+ end
+
+ def tableView(view, objectValueForTableColumn:column, row:i)
+ data[i][column.identifier.intern]
+ end
+
+ end
+end
View
85 lib/hotcocoa/delegate_builder.rb
@@ -0,0 +1,85 @@
+module HotCocoa
+
+ class DelegateBuilder
+
+ attr_reader :control, :delegate, :method_count, :required_methods
+
+ def initialize(control, required_methods)
+ @control = control
+ @required_methods = required_methods
+ @method_count = 0
+ @delegate = Object.new
+ end
+
+ def add_delegated_method(block, selector_name, *parameters)
+ clear_delegate if required_methods.empty?
+ increment_method_count
+ bind_block_to_delegate_instance_variable(block)
+ create_delegate_method(selector_name, parameters)
+ set_delegate if required_methods.empty?
+ end
+
+ def delegate_to(object, *method_names)
+ method_names.each do |method_name|
+ control.send(method_name, &object.method(method_name)) if object.respond_to?(method_name)
+ end
+ end
+
+ private
+
+ def increment_method_count
+ @method_count += 1
+ end
+
+ def bind_block_to_delegate_instance_variable(block)
+ delegate.instance_variable_set(block_instance_variable, block)
+ end
+
+ def create_delegate_method(selector_name, parameters)
+ required_methods.delete(selector_name)
+ eval %{
+ def delegate.#{parameterize_selector_name(selector_name)}
+ #{block_instance_variable}.call(#{parameter_values_for_mapping(selector_name, parameters)})
+ end
+ }
+ end
+
+ def clear_delegate
+ control.setDelegate(nil) if control.delegate
+ end
+
+ def set_delegate
+ control.setDelegate(delegate)
+ end
+
+ def block_instance_variable
+ "@block#{method_count}"
+ end
+
+ def parameterize_selector_name(selector_name)
+ return selector_name unless selector_name.include?(":")
+ params = selector_name.split(":")
+ result = "#{params.shift}(p1"
+ params.each_with_index do |param, i|
+ result << ", #{param}:p#{i+2}"
+ end
+ result + ")"
+ end
+
+ def parameter_values_for_mapping(selector_name, parameters)
+ return if parameters.empty?
+ result = []
+ selector_params = selector_name.split(":")
+ parameters.each do |parameter|
+ if (dot = parameter.index("."))
+ result << "p#{selector_params.index(parameter[0...dot])+1}#{parameter[dot..-1]}"
+ else
+ result << "p#{selector_params.index(parameter)+1}"
+ end
+ end
+ result.join(", ")
+ end
+
+ end
+
+end
View
161 lib/hotcocoa/graphics.rb
@@ -0,0 +1,161 @@
+# Ruby Cocoa Graphics is a graphics library providing a simple object-oriented
+# interface into the power of Mac OS X's Core Graphics and Core Image drawing libraries.
+# With a few lines of easy-to-read code, you can write scripts to draw simple or complex
+# shapes, lines, and patterns, process and filter images, create abstract art or visualize
+# scientific data, and much more.
+#
+# Inspiration for this project was derived from Processing and NodeBox. These excellent
+# graphics programming environments are more full-featured than RCG, but they are implemented
+# in Java and Python, respectively. RCG was created to offer similar functionality using
+# the Ruby programming language.
+#
+# Author:: James Reynolds (mailto:drtoast@drtoast.com)
+# Copyright:: Copyright (c) 2008 James Reynolds
+# License:: Distributes under the same terms as Ruby
+
+# More information about Quartz 2D is available on the Apple's website:
+# http://developer.apple.com/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_overview/dq_overview.html#//apple_ref/doc/uid/TP30001066-CH202-TPXREF101
+
+
+
+module HotCocoa;end # needed in case this is required without hotcocoa
+
+framework 'Cocoa'
+
+module HotCocoa::Graphics
+
+ # UTILITY FUNCTIONS (math/geometry)
+ TEST = 'OK'
+
+ # convert degrees to radians
+ def radians(deg)
+ deg * (Math::PI / 180.0)
+ end
+
+ # convert radians to degrees
+ def degrees(rad)
+ rad * (180 / Math::PI)
+ end
+
+ # return the angle of the line joining the two points
+ def angle(x0, y0, x1, y1)
+ degrees(Math.atan2(y1-y0, x1-x0))
+ end
+
+ # return the distance between two points
+ def distance(x0, y0, x1, y1)
+ Math.sqrt((x1-x0)**2 + (y1-y0)**2)
+ end
+
+ # return the coordinates of a new point at the given distance and angle from a starting point
+ def coordinates(x0, y0, distance, angle)
+ x1 = x0 + Math.cos(radians(angle)) * distance
+ y1 = y0 + Math.sin(radians(angle)) * distance
+ [x1,y1]
+ end
+
+ # return the lesser of a,b
+ def min(a, b)
+ a < b ? a : b
+ end
+
+ # return the greater of a,b
+ def max(a, b)
+ a > b ? a : b
+ end
+
+ # restrict the value to stay within the range
+ def inrange(value, min, max)
+ if value < min
+ min
+ elsif value > max
+ max
+ else
+ value
+ end
+ end
+
+ # return a random number within the range, or a float from 0 to the number
+ def random(left=nil, right=nil)
+ if right
+ rand * (right - left) + left
+ elsif left
+ rand * left
+ else
+ rand
+ end
+ end
+
+ def reflect(x0, y0, x1, y1, d=1.0, a=180)
+ d *= distance(x0, y0, x1, y1)
+ a += angle(x0, y0, x1, y1)
+ x, y = coordinates(x0, y0, d, a)
+ [x,y]
+ end
+
+ def choose(object)
+ case object
+ when Range
+ case object.first
+ when Float
+ rand * (object.last - object.first) + object.first
+ when Integer
+ rand(object.last - object.first + 1) + object.first
+ end
+ when Array
+ object.sample
+ else
+ object
+ end
+ end
+
+ # given an object's x,y coordinates and dimensions, return the distance
+ # needed to move in order to orient the object at the given location (:center, :bottomleft, etc)
+ def reorient(x, y, w, h, location)
+ case location
+ when :bottomleft
+ movex = -x
+ movey = -y
+ when :centerleft
+ movex = -x
+ movey = -y - h / 2
+ when :topleft
+ movex = -x
+ movey = -x - h
+ when :bottomright
+ movex = -x - w
+ movey = -y
+ when :centerright
+ movex = -x - w
+ movey = -y - h / 2
+ when :topright
+ movex = -x - w
+ movey = -y - h
+ when :bottomcenter
+ movex = -x - w / 2
+ movey = -y
+ when :center
+ movex = -x - w / 2
+ movey = -y - h / 2
+ when :topcenter
+ movex = -x - w / 2
+ movey = -y - h
+ else
+ raise "ERROR: image origin locator not recognized: #{location}"
+ end
+ #newx = oldx + movex
+ #newy = oldy + movey
+ [movex,movey]
+ end
+
+end
+
+require 'hotcocoa/graphics/canvas'
+require 'hotcocoa/graphics/color'
+require 'hotcocoa/graphics/gradient'
+require 'hotcocoa/graphics/image'
+require 'hotcocoa/graphics/path'
+require 'hotcocoa/graphics/pdf'
+require 'hotcocoa/graphics/elements/particle'
+require 'hotcocoa/graphics/elements/rope'
+require 'hotcocoa/graphics/elements/sandpainter'
View
836 lib/hotcocoa/graphics/canvas.rb
@@ -0,0 +1,836 @@
+# Ruby Cocoa Graphics is a graphics library providing a simple object-oriented
+# interface into the power of Mac OS X's Core Graphics and Core Image drawing libraries.
+# With a few lines of easy-to-read code, you can write scripts to draw simple or complex
+# shapes, lines, and patterns, process and filter images, create abstract art or visualize
+# scientific data, and much more.
+#
+# Inspiration for this project was derived from Processing and NodeBox. These excellent
+# graphics programming environments are more full-featured than RCG, but they are implemented
+# in Java and Python, respectively. RCG was created to offer similar functionality using
+# the Ruby programming language.
+#
+# Author:: James Reynolds (mailto:drtoast@drtoast.com)
+# Copyright:: Copyright (c) 2008 James Reynolds
+# License:: Distributes under the same terms as Ruby
+
+# In Quartz 2D, the canvas is often referred as the "page".
+# Overview of the underlying page concept available at:
+# http://developer.apple.com/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_overview/dq_overview.html#//apple_ref/doc/uid/TP30001066-CH202-TPXREF101
+
+
+module HotCocoa::Graphics
+
+ # drawing destination for writing a PDF, PNG, GIF, JPG, or TIF file
+ class Canvas
+
+ BlendModes = {
+ :normal => KCGBlendModeNormal,
+ :darken => KCGBlendModeDarken,
+ :multiply => KCGBlendModeMultiply,
+ :screen => KCGBlendModeScreen,
+ :overlay => KCGBlendModeOverlay,
+ :darken => KCGBlendModeDarken,
+ :lighten => KCGBlendModeLighten,
+ :colordodge => KCGBlendModeColorDodge,
+ :colorburn => KCGBlendModeColorBurn,
+ :softlight => KCGBlendModeSoftLight,
+ :hardlight => KCGBlendModeHardLight,
+ :difference => KCGBlendModeDifference,
+ :exclusion => KCGBlendModeExclusion,
+ :hue => KCGBlendModeHue,
+ :saturation => KCGBlendModeSaturation,
+ :color => KCGBlendModeColor,
+ :luminosity => KCGBlendModeLuminosity,
+ }
+ BlendModes.default(KCGBlendModeNormal)
+
+ DefaultOptions = {:quality => 0.8, :width => 400, :height => 400}
+
+ attr_accessor :width, :height
+
+ # We make the context available so developers can directly use underlying CG methods
+ # on objects created by this wrapper
+ attr_reader :ctx
+
+ class << self
+ def for_rendering(options={}, &block)
+ options[:type] = :render
+ Canvas.new(options, &block)
+ end
+
+ def for_pdf(options={}, &block)
+ options[:type] = :pdf
+ Canvas.new(options, &block)
+ end
+
+ def for_image(options={}, &block)
+ options[:type] = :image
+ Canvas.new(options, &block)
+ end
+
+ def for_context(options={}, &block)
+ options[:type] = :context
+ Canvas.new(options, &block)
+ end
+
+ def for_current_context(options={}, &block)
+ options[:type] = :context
+ options[:context] = NSGraphicsContext.currentContext.graphicsPort
+ Canvas.new(options, &block)
+ end
+
+ end
+
+ # create a new canvas with the given width, height, and output filename (pdf, png, jpg, gif, or tif)
+ def initialize(options={}, &block)
+ if options[:size]
+ options[:width] = options[:size][0]
+ options[:height] = options[:size][1]
+ end
+ options = DefaultOptions.merge(options)
+
+ @width = options[:width]
+ @height = options[:height]
+ @output = options[:filename] || 'test'
+ @stacksize = 0
+ @colorspace = CGColorSpaceCreateDeviceRGB() # => CGColorSpaceRef
+ @autoclosepath = false
+
+ case options[:type]
+ when :pdf
+ @filetype = :pdf
+ # CREATE A PDF DRAWING CONTEXT
+ # url = NSURL.fileURLWithPath(image)
+ url = CFURLCreateFromFileSystemRepresentation(nil, @output, @output.length, false)
+ pdfrect = CGRect.new(CGPoint.new(0, 0), CGSize.new(width, height)) # Landscape
+ #@ctx = CGPDFContextCreateWithURL(url, pdfrect, nil)
+ consumer = CGDataConsumerCreateWithURL(url);
+ pdfcontext = CGPDFContextCreate(consumer, pdfrect, nil);
+ CGPDFContextBeginPage(pdfcontext, nil)
+ @ctx = pdfcontext
+ when :image, :render
+ # CREATE A BITMAP DRAWING CONTEXT
+ @filetype = File.extname(@output).downcase[1..-1].intern if options[:type] == :image
+
+ @bits_per_component = 8
+ @colorspace = CGColorSpaceCreateDeviceRGB() # => CGColorSpaceRef
+ #alpha = KCGImageAlphaNoneSkipFirst # opaque background
+ alpha = KCGImageAlphaPremultipliedFirst # transparent background
+
+ # 8 integer bits/component; 32 bits/pixel; 3-component colorspace; kCGImageAlphaPremultipliedFirst; 57141 bytes/row.
+ bytes = @bits_per_component * 4 * @width.ceil
+ @ctx = CGBitmapContextCreate(nil, @width, @height, @bits_per_component, bytes, @colorspace, alpha) # => CGContextRef
+ when :context
+ @ctx = options[:context]
+ else
+ raise "ERROR: output file type #{ext} not recognized"
+ end
+
+ # antialiasing
+ CGContextSetAllowsAntialiasing(@ctx, true)
+
+ # set defaults
+ fill # set the default fill
+ nostroke # no stroke by default
+ strokewidth # set the default stroke width
+ font # set the default font
+ antialias # set the default antialias state
+ autoclosepath # set the autoclosepath default
+ quality(options[:quality]) # set the compression default
+ push # save the pristine default default graphics state (retrieved by calling "reset")
+ push # create a new graphics state for the user to mess up
+ if block_given?
+ case block.arity
+ when 0
+ send(:instance_eval, &block)
+ else
+ block.call(self)
+ end
+ end
+ end
+
+ # SET CANVAS GLOBAL PARAMETERS
+
+ # print drawing functions if verbose is true
+ def verbose(tf=true)
+ @verbose = tf
+ end
+
+ # set whether or not drawn paths should be antialiased (true/false)
+ def antialias(tf=true)
+ CGContextSetShouldAntialias(@ctx, tf)
+ end
+
+ # set the alpha value for subsequently drawn objects
+ def alpha(val=1.0)
+ CGContextSetAlpha(@ctx, val)
+ end
+
+ # set compression (0.0 = max, 1.0 = none)
+ def quality(factor=0.8)
+ @quality = factor
+ end
+
+ # set the current fill (given a Color object, or RGBA values)
+ def fill(r=0, g=0, b=0, a=1)
+ case r
+ when Color
+ g = r.g
+ b = r.b
+ a = r.a
+ r = r.r
+ end
+ CGContextSetRGBFillColor(@ctx, r, g, b, a) # RGBA
+ @fill = true
+ end
+
+ # remove current fill
+ def nofill
+ CGContextSetRGBFillColor(@ctx, 0.0, 0.0, 0.0, 0.0) # RGBA
+ @fill = nil
+ end
+
+ # SET CANVAS STROKE PARAMETERS
+
+ # set stroke color (given a Color object, or RGBA values)
+ def stroke(r=0, g=0, b=0, a=1.0)
+ case r
+ when Color
+ g = r.g
+ b = r.b
+ a = r.a
+ r = r.r
+ end
+ CGContextSetRGBStrokeColor(@ctx, r, g, b, a) # RGBA
+ @stroke = true
+ end
+
+ # set stroke width
+ def strokewidth(width=1)
+ CGContextSetLineWidth(@ctx, width.to_f)
+ end
+
+ # don't use a stroke for subsequent drawing operations
+ def nostroke
+ CGContextSetRGBStrokeColor(@ctx, 0, 0, 0, 0) # RGBA
+ @stroke = false
+ end
+
+ # set cap style to round, square, or butt
+ def linecap(style=:butt)
+ case style
+ when :round
+ cap = KCGLineCapRound
+ when :square
+ cap = KCGLineCapSquare
+ when :butt
+ cap = KCGLineCapButt
+ else
+ raise "ERROR: line cap style not recognized: #{style}"
+ end
+ CGContextSetLineCap(@ctx,cap)
+ end
+
+ # set line join style to round, miter, or bevel
+ def linejoin(style=:miter)
+ case style
+ when :round
+ join = KCGLineJoinRound
+ when :bevel
+ join = KCGLineJoinBevel
+ when :miter
+ join = KCGLineJoinMiter
+ else
+ raise "ERROR: line join style not recognized: #{style}"
+ end
+ CGContextSetLineJoin(@ctx,join)
+ end
+
+ # set lengths of dashes and spaces, and distance before starting dashes
+ def linedash(lengths=[10,2], phase=0.0)
+ count=lengths.size
+ CGContextSetLineDash(@ctx, phase, lengths, count)
+ end
+
+ # revert to solid lines
+ def nodash
+ CGContextSetLineDash(@ctx, 0.0, nil, 0)
+ end
+
+
+ # DRAWING SHAPES ON CANVAS
+
+ # draw a rectangle starting at x,y and having dimensions w,h
+ def rect(x=0, y=0, w=20, h=20, reg=@registration)
+ # center the rectangle
+ if (reg == :center)
+ x = x - w / 2
+ y = y - h / 2
+ end
+ CGContextAddRect(@ctx, NSMakeRect(x, y, w, h))
+ CGContextDrawPath(@ctx, KCGPathFillStroke)
+ end
+
+ # inscribe an oval starting at x,y inside a rectangle having dimensions w,h
+ def oval(x=0, y=0, w=20, h=20, reg=@registration)
+ # center the oval
+ if (reg == :center)
+ x = x - w / 2
+ y = y - w / 2
+ end
+ CGContextAddEllipseInRect(@ctx, NSMakeRect(x, y, w, h))
+ CGContextDrawPath(@ctx, KCGPathFillStroke) # apply fill and stroke
+ end
+
+ # draw a background color (given a Color object, or RGBA values)
+ def background(r=1, g=1, b=1, a=1.0)
+ case r
+ when Color
+ g = r.g
+ b = r.b
+ a = r.a
+ r = r.r
+ end
+ push
+ CGContextSetRGBFillColor(@ctx, r, g, b, a) # RGBA
+ rect(0,0,@width,@height)
+ pop
+ end
+
+ # draw a radial gradiant starting at sx,sy with radius er
+ # optional: specify ending at ex,ey and starting radius sr
+ def radial(gradient, sx=@width/2, sy=@height/2, er=@width/2, ex=sx, ey=sy, sr=0.0)
+ #options = KCGGradientDrawsBeforeStartLocation
+ #options = KCGGradientDrawsAfterEndLocation
+ CGContextDrawRadialGradient(@ctx, gradient.gradient, NSMakePoint(sx, sy), sr, NSMakePoint(ex, ey), er, gradient.pre + gradient.post)
+ end
+
+ # draw an axial(linear) gradient starting at sx,sy and ending at ex,ey
+ def gradient(gradient=Gradient.new, start_x=@width/2, start_y=0, end_x=@width/2, end_y=@height)
+ #options = KCGGradientDrawsBeforeStartLocation
+ #options = KCGGradientDrawsAfterEndLocation
+ CGContextDrawLinearGradient(@ctx, gradient.gradient, NSMakePoint(start_x, start_y), NSMakePoint(end_x, end_y), gradient.pre + gradient.post)
+ end
+
+ # draw a cartesian coordinate grid for reference
+ def cartesian(res=50, stroke=1.0, fsize=10)
+ # save previous state
+ new_state do
+ # set font and stroke
+ fontsize(fsize)
+ fill(Color.black)
+ stroke(Color.red)
+ strokewidth(stroke)
+ # draw vertical numbered grid lines
+ for x in (-width / res)..(width / res) do
+ line(x * res, -height, x * res, height)
+ text("#{x * res}", x * res, 0)
+ end
+ # draw horizontal numbered grid lines
+ for y in (-height / res)..(height / res) do
+ line(-width, y * res, width, y * res)
+ text("#{y * res}", 0, y * res)
+ end
+ # draw lines intersecting center of canvas
+ stroke(Color.black)
+ line(-width, -height, width, height)
+ line(width, -height, -width, height)
+ line(0, height, width, 0)
+ line(width / 2, 0, width / 2, height)
+ line(0, height / 2, width, height / 2)
+ # restore previous state
+ end
+ end
+
+
+ # DRAWING COMPLETE PATHS TO CANVAS
+
+ # draw a line starting at x1,y1 and ending at x2,y2
+ def line(x1, y1, x2, y2)
+ CGContextAddLines(@ctx, [NSPoint.new(x1, y1), NSPoint.new(x2, y2)], 2)
+ CGContextDrawPath(@ctx, KCGPathStroke) # apply stroke
+ endpath
+
+ end
+
+ # draw a series of lines connecting the given array of points
+ def lines(points)
+ CGContextAddLines(@ctx, points, points.size)
+ CGContextDrawPath(@ctx, KCGPathStroke) # apply stroke
+ endpath
+ end
+
+ # draw the arc of a circle with center point x,y, radius, start angle (0 deg = 12 o'clock) and end angle
+ def arc(x, y, radius, start_angle, end_angle)
+ start_angle = radians(90-start_angle)
+ end_angle = radians(90-end_angle)
+ clockwise = 1 # 1 = clockwise, 0 = counterclockwise
+ CGContextAddArc(@ctx, x, y, radius, start_angle, end_angle, clockwise)
+ CGContextDrawPath(@ctx, KCGPathStroke)
+ end
+
+ # draw a bezier curve from the current point, given the coordinates of two handle control points and an end point
+ def curve(cp1x, cp1y, cp2x, cp2y, x1, y1, x2, y2)
+ beginpath(x1, y1)
+ CGContextAddCurveToPoint(@ctx, cp1x, cp1y, cp2x, cp2y, x2, y2)
+ endpath
+ end
+
+ # draw a quadratic bezier curve from x1,y1 to x2,y2, given the coordinates of one control point
+ def qcurve(cpx, cpy, x1, y1, x2, y2)
+ beginpath(x1, y1)
+ CGContextAddQuadCurveToPoint(@ctx, cpx, cpy, x2, y2)
+ endpath
+ end
+
+ # draw the given Path object
+ def draw(object, *args)
+ case object
+ when Path
+ draw_path(object, *args)
+ when Image
+ draw_image(object, *args)
+ else
+ raise ArgumentError.new("first parameter must be a Path or Image object not a #{object.class}")
+ end
+ end
+
+ # CONSTRUCTING PATHS ON CANVAS
+
+ # if true, automatically close the path after it is ended
+ def autoclosepath(tf=false)
+ @autoclosepath = tf
+ end
+
+ def new_path(x, y, &block)
+ beginpath(x, y)
+ block.call
+ endpath
+ end
+
+ # begin drawing a path at x,y
+ def beginpath(x, y)
+ CGContextBeginPath(@ctx)
+ CGContextMoveToPoint(@ctx, x, y)
+ end
+
+ # end the current path and draw it
+ def endpath
+ CGContextClosePath(@ctx) if @autoclosepath
+ #mode = KCGPathStroke
+ mode = KCGPathFillStroke
+ CGContextDrawPath(@ctx, mode) # apply fill and stroke
+ end
+
+ # move the "pen" to x,y
+ def moveto(x, y)
+ CGContextMoveToPoint(@ctx, x, y)
+ end
+
+ # draw a line from the current point to x,y
+ def lineto(x, y)
+ CGContextAddLineToPoint(@ctx ,x, y)
+ end
+
+ # draw a bezier curve from the current point, given the coordinates of two handle control points and an end point
+ def curveto(cp1x, cp1y, cp2x, cp2y, x, y)
+ CGContextAddCurveToPoint(@ctx, cp1x, cp1y, cp2x, cp2y, x, y)
+ end
+
+ # draw a quadratic bezier curve from the current point, given the coordinates of one control point and an end point
+ def qcurveto(cpx, cpy, x, y)
+ CGContextAddQuadCurveToPoint(@ctx, cpx, cpy, x, y)
+ end
+
+ # draw an arc given the endpoints of two tangent lines and a radius
+ def arcto(x1, y1, x2, y2, radius)
+ CGContextAddArcToPoint(@ctx, x1, y1, x2, y2, radius)
+ end
+
+ # draw the path in a grid with rows, columns
+ def grid(path, rows=10, cols=10)
+ push
+ rows.times do |row|
+ tx = (row+1) * (self.height / rows) - (self.height / rows) / 2
+ cols.times do |col|
+ ty = (col+1) * (self.width / cols) - (self.width / cols) / 2
+ push
+ translate(tx, ty)
+ draw(path)
+ pop
+ end
+ end
+ pop
+ end
+
+
+ # TRANSFORMATIONS
+
+ # set registration mode to :center or :corner
+ def registration(mode=:center)
+ @registration = mode
+ end
+
+ # rotate by the specified degrees
+ def rotate(deg=0)
+ CGContextRotateCTM(@ctx, radians(-deg));
+ end
+
+ # translate drawing context by x,y
+ def translate(x, y)
+ CGContextTranslateCTM(@ctx, x, y);
+ end
+
+ # scale drawing context by x,y
+ def scale(x, y=x)
+ CGContextScaleCTM(@ctx, x, y)
+ end
+
+ def skew(x=0, y=0)
+ x = Math::PI * x / 180.0
+ y = Math::PI * y / 180.0
+ transform = CGAffineTransformMake(1.0, Math::tan(y), Math::tan(x), 1.0, 0.0, 0.0)
+ CGContextConcatCTM(@ctx, transform)
+ end
+
+
+ # STATE
+
+ def new_state(&block)
+ push
+ block.call
+ pop
+ end
+
+ # push the current drawing context onto the stack
+ def push
+ CGContextSaveGState(@ctx)
+ @stacksize = @stacksize + 1
+ end
+
+ # pop the previous drawing context off the stack
+ def pop
+ CGContextRestoreGState(@ctx)
+ @stacksize = @stacksize - 1
+ end
+
+ # restore the initial context
+ def reset
+ until (@stacksize <= 1)
+ pop # retrieve graphics states until we get to the default state
+ end
+ push # push the retrieved pristine default state back onto the stack
+ end
+
+
+ # EFFECTS
+
+ # apply a drop shadow with offset dx,dy, alpha, and blur
+ def shadow(dx=0.0, dy=0.0, a=2.0/3.0, blur=5)
+ color = CGColorCreate(@colorspace, [0.0, 0.0, 0.0, a])
+ CGContextSetShadowWithColor(@ctx, [dx, dy], blur, color)
+ end
+
+ # apply a glow with offset dx,dy, alpha, and blur
+ def glow(dx=0.0, dy=0.0, a=2.0/3.0, blur=5)
+ color = CGColorCreate(@colorspace, [1.0, 1.0, 0.0, a])
+ CGContextSetShadowWithColor(@ctx, [dx, dy], blur, color)
+ end
+
+ # stop using a shadow
+ def noshadow
+ CGContextSetShadowWithColor(@ctx, [0,0], 1, nil)
+ end
+
+ # set the canvas blend mode (:normal, :darken, :multiply, :screen, etc)
+ def blend(mode)
+ CGContextSetBlendMode(@ctx, BlendModes[mode])
+ end
+
+
+ # CLIPPING MASKS
+
+ # clip subsequent drawing operations within the given path
+ def beginclip(p, &block)
+ push
+ CGContextAddPath(@ctx, p.path)
+ CGContextClip(@ctx)
+ if block
+ block.call
+ endclip
+ end
+ end
+
+ # stop clipping drawing operations
+ def endclip
+ pop
+ end
+
+ # DRAW TEXT TO CANVAS
+
+ # NOTE: may want to switch to ATSUI for text handling
+ # http://developer.apple.com/documentation/Carbon/Reference/ATSUI_Reference/Reference/reference.html
+
+ # write the text at x,y using the current fill
+ def text(txt="A", x=0, y=0)
+ txt = txt.to_s unless txt.kind_of?(String)
+ if @registration == :center
+ width = textwidth(txt)
+ x = x - width / 2
+ y = y + @fsize / 2
+ end
+ CGContextShowTextAtPoint(@ctx, x, y, txt, txt.length)
+ end
+
+ # determine the width of the given text without drawing it
+ def textwidth(txt, width=nil)
+ push
+ start = CGContextGetTextPosition(@ctx)
+ CGContextSetTextDrawingMode(@ctx, KCGTextInvisible)
+ CGContextShowText(@ctx, txt, txt.length)
+ final = CGContextGetTextPosition(@ctx)
+ pop
+ final.x - start.x
+ end
+
+ # def textheight(txt)
+ # # need to use ATSUI
+ # end
+ #
+ # def textmetrics(txt)
+ # # need to use ATSUI
+ # end
+
+ # set font by name and optional size
+ def font(name="Helvetica", size=nil)
+ fontsize(size) if size
+ @fname = name
+ fontsize unless @fsize
+ CGContextSelectFont(@ctx, @fname, @fsize, KCGEncodingMacRoman)
+ end
+
+ # set font size in points
+ def fontsize(points=20)
+ @fsize = points
+ font unless @fname
+ #CGContextSetFontSize(@ctx,points)
+ CGContextSelectFont(@ctx, @fname, @fsize, KCGEncodingMacRoman)
+ end
+
+
+ # SAVING/EXPORTING
+
+ def nsimage
+ image = NSImage.alloc.init
+ image.addRepresentation(NSBitmapImageRep.alloc.initWithCGImage(cgimage))
+ image
+ end
+
+ # return a CGImage of the canvas for reprocessing (only works if using a bitmap context)
+ def cgimage
+ CGBitmapContextCreateImage(@ctx) # => CGImageRef (works with bitmap context only)
+ #cgimageref = CGImageCreate(@width, @height, @bits_per_component, nil,nil,@colorspace, nil, @provider,nil,true,KCGRenderingIntentDefault)
+ end
+
+ # return a CIImage of the canvas for reprocessing (only works if using a bitmap context)
+ def ciimage
+ cgimageref = self.cgimage
+ CIImage.imageWithCGImage(cgimageref) # CIConcreteImage (CIImage)
+ end
+
+ # begin a new PDF page
+ def newpage
+ if (@filetype == :pdf)
+ CGContextFlush(@ctx)
+ CGPDFContextEndPage(@ctx)
+ CGPDFContextBeginPage(@ctx, nil)
+ else
+ puts "WARNING: newpage only valid when using PDF output"
+ end
+ end
+
+ # save the image to a file
+ def save
+
+ properties = {}
+ # exif = {}
+ # KCGImagePropertyExifDictionary
+ # exif[KCGImagePropertyExifUserComment] = 'Image downloaded from www.sheetmusicplus.com'
+ # exif[KCGImagePropertyExifAuxOwnerName] = 'www.sheetmusicplus.com'
+ if @filetype == :pdf
+ CGPDFContextEndPage(@ctx)
+ CGContextFlush(@ctx)
+ return
+ elsif @filetype == :png
+ format = NSPNGFileType
+ elsif @filetype == :tif
+ format = NSTIFFFileType
+ properties[NSImageCompressionMethod] = NSTIFFCompressionLZW
+ #properties[NSImageCompressionMethod] = NSTIFFCompressionNone
+ elsif @filetype == :gif
+ format = NSGIFFileType
+ #properties[NSImageDitherTransparency] = 0 # 1 = dithered, 0 = not dithered
+ #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.
+ elsif @filetype == :jpg
+ format = NSJPEGFileType
+ properties[NSImageCompressionFactor] = @quality # (jpeg compression, 0.0 = max, 1.0 = none)
+ #properties[NSImageEXIFData] = exif
+ end
+ cgimageref = CGBitmapContextCreateImage(@ctx) # => CGImageRef
+ bitmaprep = NSBitmapImageRep.alloc.initWithCGImage(cgimageref) # => NSBitmapImageRep
+ blob = bitmaprep.representationUsingType(format, properties:properties) # => NSConcreteData
+ blob.writeToFile(@output, atomically:true)
+ true
+ end
+
+ # open the output file in its associated application
+ def open
+ system "open #{@output}"
+ end
+
+ # def save(dest)
+ ## http://developer.apple.com/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_data_mgr/chapter_11_section_3.html
+ # properties = {
+ #
+ # }
+ # cgimageref = CGBitmapContextCreateImage(@ctx) # => CGImageRef
+ # destination = CGImageDestinationCreateWithURL(NSURL.fileURLWithPath(dest)) # => CGImageDestinationRef
+ # CGImageDestinationSetProperties(destination,properties)
+ # CGImageDestinationAddImage(cgimageref)
+ # end
+
+ private
+
+ # DRAWING PATHS ON A CANVAS
+
+ def draw_path(p, tx=0, ty=0, iterations=1)
+ new_state do
+ iterations.times do |i|
+ if (i > 0)
+ # INCREMENT TRANSFORM:
+ # translate x, y
+ translate(choose(p.inc[:x]), choose(p.inc[:y]))
+ # choose a rotation factor from the range
+ rotate(choose(p.inc[:rotation]))
+ # choose a scaling factor from the range
+ sc = choose(p.inc[:scale])
+ sx = choose(p.inc[:scalex]) * sc
+ sy = p.inc[:scaley] ? choose(p.inc[:scaley]) * sc : sx * sc
+ scale(sx, sy)
+ end
+
+ new_state do
+ # PICK AND ADJUST FILL/STROKE COLORS:
+ [:fill,:stroke].each do |kind|
+ # PICK A COLOR
+ if (p.inc[kind]) then
+ # increment color from array
+ colorindex = i % p.inc[kind].size
+ c = p.inc[kind][colorindex].copy
+ else
+ c = p.rand[kind]
+ case c
+ when Array
+ c = choose(c).copy
+ when Color
+ c = c.copy
+ else
+ next
+ end
+ end
+
+ if (p.inc[:hue] or p.inc[:saturation] or p.inc[:brightness])
+ # ITERATE COLOR
+ if (p.inc[:hue])
+ newhue = (c.hue + choose(p.inc[:hue])) % 1
+ c.hue(newhue)
+ end
+ if (p.inc[:saturation])
+ newsat = (c.saturation + choose(p.inc[:saturation]))
+ c.saturation(newsat)
+ end
+ if (p.inc[:brightness])
+ newbright = (c.brightness + choose(p.inc[:brightness]))
+ c.brightness(newbright)
+ end
+ if (p.inc[:alpha])
+ newalpha = (c.a + choose(p.inc[:alpha]))
+ c.a(newalpha)
+ end
+ p.rand[kind] = c
+ else
+ # RANDOMIZE COLOR
+ c.hue(choose(p.rand[:hue])) if p.rand[:hue]
+ c.saturation(choose(p.rand[:saturation])) if p.rand[:saturation]
+ c.brightness(choose(p.rand[:brightness])) if p.rand[:brightness]
+ end
+
+ # APPLY COLOR
+ fill(c) if kind == :fill
+ stroke(c) if kind == :stroke
+ end
+ # choose a stroke width from the range
+ strokewidth(choose(p.rand[:strokewidth])) if p.rand[:strokewidth]
+ # choose an alpha level from the range
+ alpha(choose(p.rand[:alpha])) if p.rand[:alpha]
+
+ # RANDOMIZE TRANSFORM:
+ # translate x, y
+ translate(choose(p.rand[:x]), choose(p.rand[:y]))
+ # choose a rotation factor from the range
+ rotate(choose(p.rand[:rotation]))
+ # choose a scaling factor from the range
+ sc = choose(p.rand[:scale])
+ sx = choose(p.rand[:scalex]) * sc
+ sy = p.rand[:scaley] ? choose(p.rand[:scaley]) * sc : sx * sc
+ scale(sx,sy)
+
+ # DRAW
+ if (tx > 0 || ty > 0)
+ translate(tx, ty)
+ end
+
+ CGContextAddPath(@ctx, p.path) if p.class == Path
+ CGContextDrawPath(@ctx, KCGPathFillStroke) # apply fill and stroke
+
+ # if there's an image, draw it clipped by the path
+ if (p.image)
+ beginclip(p)
+ image(p.image)
+ endclip
+ end
+
+ end
+ end
+ end
+ end
+
+ # DRAWING IMAGES ON CANVAS
+
+ # draw the specified image at x,y with dimensions w,h.
+ # "img" may be a path to an image, or an Image instance
+ def draw_image(img, x=0, y=0, w=nil, h=nil, pagenum=1)
+ new_state do
+ if (img.kind_of?(Pdf))
+ w ||= img.width(pagenum)
+ h ||= img.height(pagenum)
+ if(@registration == :center)
+ x = x - w / 2
+ y = y - h / 2
+ end
+ img.draw(@ctx, x, y, w, h, pagenum)
+ elsif(img.kind_of?(String) || img.kind_of?(Image))
+ img = Image.new(img) if img.kind_of?(String)
+ w ||= img.width
+ h ||= img.height
+ img.draw(@ctx, x, y, w, h)
+ else
+ raise ArgumentError.new("canvas.image: not a recognized image type: #{img.class}")
+ end
+ end
+ end
+
+
+ end
+
+end
View
781 lib/hotcocoa/graphics/color.rb
@@ -0,0 +1,781 @@
+# Ruby Cocoa Graphics is a graphics library providing a simple object-oriented
+# interface into the power of Mac OS X's Core Graphics and Core Image drawing libraries.
+# With a few lines of easy-to-read code, you can write scripts to draw simple or complex
+# shapes, lines, and patterns, process and filter images, create abstract art or visualize
+# scientific data, and much more.
+#
+# Inspiration for this project was derived from Processing and NodeBox. These excellent
+# graphics programming environments are more full-featured than RCG, but they are implemented
+# in Java and Python, respectively. RCG was created to offer similar functionality using
+# the Ruby programming language.
+#
+# Author:: James Reynolds (mailto:drtoast@drtoast.com)
+# Copyright:: Copyright (c) 2008 James Reynolds
+# License:: Distributes under the same terms as Ruby
+
+module HotCocoa::Graphics
+
+ # define and manipulate colors in RGBA format
+ class Color
+
+ # License: GPL - includes ports of some code by Tom De Smedt, Frederik De Bleser
+
+ #attr_accessor :r,:g,:b,:a
+ attr_accessor :rgb
+ # def initialize(r=0.0,g=0.0,b=0.0,a=1.0)
+ # @c = CGColorCreate(@colorspace, [r,g,b,a])
+ # end
+
+ # create a new color with the given RGBA values
+ def initialize(r=0.0, g=0.0, b=1.0, a=1.0)
+ @nsColor = NSColor.colorWithDeviceRed r, green:g, blue:b, alpha:a
+ @rgb = @nsColor.colorUsingColorSpaceName NSDeviceRGBColorSpace
+ self
+ end
+
+ COLORNAMES = {
+ "lightpink" => [1.00, 0.71, 0.76],
+ "pink" => [1.00, 0.75, 0.80],
+ "crimson" => [0.86, 0.08, 0.24],
+ "lavenderblush" => [1.00, 0.94, 0.96],
+ "palevioletred" => [0.86, 0.44, 0.58],
+ "hotpink" => [1.00, 0.41, 0.71],
+ "deeppink" => [1.00, 0.08, 0.58],
+ "mediumvioletred" => [0.78, 0.08, 0.52],
+ "orchid" => [0.85, 0.44, 0.84],
+ "thistle" => [0.85, 0.75, 0.85],
+ "plum" => [0.87, 0.63, 0.87],
+ "violet" => [0.93, 0.51, 0.93],
+ "fuchsia" => [1.00, 0.00, 1.00],
+ "darkmagenta" => [0.55, 0.00, 0.55],
+ "purple" => [0.50, 0.00, 0.50],
+ "mediumorchid" => [0.73, 0.33, 0.83],
+ "darkviolet" => [0.58, 0.00, 0.83],
+ "darkorchid" => [0.60, 0.20, 0.80],
+ "indigo" => [0.29, 0.00, 0.51],
+ "blueviolet" => [0.54, 0.17, 0.89],
+ "mediumpurple" => [0.58, 0.44, 0.86],
+ "mediumslateblue" => [0.48, 0.41, 0.93],
+ "slateblue" => [0.42, 0.35, 0.80],
+ "darkslateblue" => [0.28, 0.24, 0.55],
+ "ghostwhite" => [0.97, 0.97, 1.00],
+ "lavender" => [0.90, 0.90, 0.98],
+ "blue" => [0.00, 0.00, 1.00],
+ "mediumblue" => [0.00, 0.00, 0.80],
+ "darkblue" => [0.00, 0.00, 0.55],
+ "navy" => [0.00, 0.00, 0.50],
+ "midnightblue" => [0.10, 0.10, 0.44],
+ "royalblue" => [0.25, 0.41, 0.88],
+ "cornflowerblue" => [0.39, 0.58, 0.93],
+ "lightsteelblue" => [0.69, 0.77, 0.87],
+ "lightslategray" => [0.47, 0.53, 0.60],
+ "slategray" => [0.44, 0.50, 0.56],
+ "dodgerblue" => [0.12, 0.56, 1.00],
+ "aliceblue" => [0.94, 0.97, 1.00],
+ "steelblue" => [0.27, 0.51, 0.71],
+ "lightskyblue" => [0.53, 0.81, 0.98],
+ "skyblue" => [0.53, 0.81, 0.92],
+ "deepskyblue" => [0.00, 0.75, 1.00],
+ "lightblue" => [0.68, 0.85, 0.90],
+ "powderblue" => [0.69, 0.88, 0.90],
+ "cadetblue" => [0.37, 0.62, 0.63],
+ "darkturquoise" => [0.00, 0.81, 0.82],
+ "azure" => [0.94, 1.00, 1.00],
+ "lightcyan" => [0.88, 1.00, 1.00],
+ "paleturquoise" => [0.69, 0.93, 0.93],
+ "aqua" => [0.00, 1.00, 1.00],
+ "darkcyan" => [0.00, 0.55, 0.55],
+ "teal" => [0.00, 0.50, 0.50],
+ "darkslategray" => [0.18, 0.31, 0.31],
+ "mediumturquoise" => [0.28, 0.82, 0.80],
+ "lightseagreen" => [0.13, 0.70, 0.67],
+ "turquoise" => [0.25, 0.88, 0.82],
+ "aquamarine" => [0.50, 1.00, 0.83],
+ "mediumaquamarine" => [0.40, 0.80, 0.67],
+ "mediumspringgreen" => [0.00, 0.98, 0.60],
+ "mintcream" => [0.96, 1.00, 0.98],
+ "springgreen" => [0.00, 1.00, 0.50],
+ "mediumseagreen" => [0.24, 0.70, 0.44],
+ "seagreen" => [0.18, 0.55, 0.34],
+ "honeydew" => [0.94, 1.00, 0.94],
+ "darkseagreen" => [0.56, 0.74, 0.56],
+ "palegreen" => [0.60, 0.98, 0.60],
+ "lightgreen" => [0.56, 0.93, 0.56],
+ "limegreen" => [0.20, 0.80, 0.20],
+ "lime" => [0.00, 1.00, 0.00],
+ "forestgreen" => [0.13, 0.55, 0.13],
+ "green" => [0.00, 0.50, 0.00],
+ "darkgreen" => [0.00, 0.39, 0.00],
+ "lawngreen" => [0.49, 0.99, 0.00],
+ "chartreuse" => [0.50, 1.00, 0.00],
+ "greenyellow" => [0.68, 1.00, 0.18],
+ "darkolivegreen" => [0.33, 0.42, 0.18],
+ "yellowgreen" => [0.60, 0.80, 0.20],
+ "olivedrab" => [0.42, 0.56, 0.14],
+ "ivory" => [1.00, 1.00, 0.94],
+ "beige" => [0.96, 0.96, 0.86],
+ "lightyellow" => [1.00, 1.00, 0.88],
+ "lightgoldenrodyellow" => [0.98, 0.98, 0.82],
+ "yellow" => [1.00, 1.00, 0.00],
+ "olive" => [0.50, 0.50, 0.00],
+ "darkkhaki" => [0.74, 0.72, 0.42],
+ "palegoldenrod" => [0.93, 0.91, 0.67],
+ "lemonchiffon" => [1.00, 0.98, 0.80],
+ "khaki" => [0.94, 0.90, 0.55],
+ "gold" => [1.00, 0.84, 0.00],
+ "cornsilk" => [1.00, 0.97, 0.86],
+ "goldenrod" => [0.85, 0.65, 0.13],
+ "darkgoldenrod" => [0.72, 0.53, 0.04],
+ "floralwhite" => [1.00, 0.98, 0.94],
+ "oldlace" => [0.99, 0.96, 0.90],
+ "wheat" => [0.96, 0.87, 0.07],
+ "orange" => [1.00, 0.65, 0.00],
+ "moccasin" => [1.00, 0.89, 0.71],
+ "papayawhip" => [1.00, 0.94, 0.84],
+ "blanchedalmond" => [1.00, 0.92, 0.80],
+ "navajowhite" => [1.00, 0.87, 0.68],
+ "antiquewhite" => [0.98, 0.92, 0.84],
+ "tan" => [0.82, 0.71, 0.55],
+ "burlywood" => [0.87, 0.72, 0.53],
+ "darkorange" => [1.00, 0.55, 0.00],
+ "bisque" => [1.00, 0.89, 0.77],
+ "linen" => [0.98, 0.94, 0.90],
+ "peru" => [0.80, 0.52, 0.25],
+ "peachpuff" => [1.00, 0.85, 0.73],
+ "sandybrown" => [0.96, 0.64, 0.38],
+ "chocolate" => [0.82, 0.41, 0.12],
+ "saddlebrown" => [0.55, 0.27, 0.07],
+ "seashell" => [1.00, 0.96, 0.93],
+ "sienna" => [0.63, 0.32, 0.18],
+ "lightsalmon" => [1.00, 0.63, 0.48],
+ "coral" => [1.00, 0.50, 0.31],
+ "orangered" => [1.00, 0.27, 0.00],
+ "darksalmon" => [0.91, 0.59, 0.48],
+ "tomato" => [1.00, 0.39, 0.28],
+ "salmon" => [0.98, 0.50, 0.45],
+ "mistyrose" => [1.00, 0.89, 0.88],
+ "lightcoral" => [0.94, 0.50, 0.50],
+ "snow" => [1.00, 0.98, 0.98],
+ "rosybrown" => [0.74, 0.56, 0.56],
+ "indianred" => [0.80, 0.36, 0.36],
+ "red" => [1.00, 0.00, 0.00],
+ "brown" => [0.65, 0.16, 0.16],
+ "firebrick" => [0.70, 0.13, 0.13],
+ "darkred" => [0.55, 0.00, 0.00],
+ "maroon" => [0.50, 0.00, 0.00],
+ "white" => [1.00, 1.00, 1.00],
+ "whitesmoke" => [0.96, 0.96, 0.96],
+ "gainsboro" => [0.86, 0.86, 0.86],
+ "lightgrey" => [0.83, 0.83, 0.83],
+ "silver" => [0.75, 0.75, 0.75],
+ "darkgray" => [0.66, 0.66, 0.66],
+ "gray" => [0.50, 0.50, 0.50],
+ "grey" => [0.50, 0.50, 0.50],
+ "dimgray" => [0.41, 0.41, 0.41],
+ "dimgrey" => [0.41, 0.41, 0.41],
+ "black" => [0.00, 0.00, 0.00],
+ "cyan" => [0.00, 0.68, 0.94],
+ #"transparent" => [0.00, 0.00, 0.00, 0.00],
+ "bark" => [0.25, 0.19, 0.13]
+ }
+
+ RYBWheel = [
+ [ 0, 0], [ 15, 8],
+ [ 30, 17], [ 45, 26],
+ [ 60, 34], [ 75, 41],
+ [ 90, 48], [105, 54],
+ [120, 60], [135, 81],
+ [150, 103], [165, 123],
+ [180, 138], [195, 155],
+ [210, 171], [225, 187],
+ [240, 204], [255, 219],
+ [270, 234], [285, 251],
+ [300, 267], [315, 282],
+ [330, 298], [345, 329],
+ [360, 0 ]
+ ]
+
+ COLORNAMES.each_key do |name|
+ (class << self; self; end).define_method(name) do
+ named(name)
+ end
+ end
+
+ # create a color with the specified name
+ def self.named(name)
+ if COLORNAMES[name]
+ r, g, b = COLORNAMES[name]
+ #puts "matched name #{name}"
+ color = Color.new(r, g, b, 1.0)
+ elsif name.match(/^(dark|deep|light|bright)?(.*?)(ish)?$/)
+ #puts "matched #{$1}-#{$2}-#{$3}"
+ value = $1
+ color_name = $2
+ ish = $3
+ analogval = value ? 0 : 0.1
+ r, g, b = COLORNAMES[color_name] || [0.0, 0.0, 0.0]
+ color = Color.new(r, g, b, 1.0)
+ color = c.analog(20, analogval) if ish
+ color.lighten(0.2) if value and value.match(/light|bright/)
+ color.darken(0.2) if value and value.match(/dark|deep/)
+ else
+ color = Color.black
+ end
+ color
+ end
+
+ # return the name of the nearest named color
+ def name
+ nearest, d = ["", 1.0]
+ red = r
+ green = g
+ blue = b
+ for hue in COLORNAMES.keys
+ rdiff = (red - COLORNAMES[hue][0]).abs
+ gdiff = (green - COLORNAMES[hue][1]).abs
+ bdiff = (blue - COLORNAMES[hue][2]).abs
+ totaldiff = rdiff + gdiff + bdiff
+ if (totaldiff < d)
+ nearest, d = [hue, totaldiff]
+ end
+ end
+ nearest
+ end
+
+ # return a copy of this color
+ def copy
+ Color.new(r, g, b, a)
+ end
+
+ # print the color's component values
+ def to_s
+ "color: #{name} (#{r} #{g} #{b} #{a})"
+ end
+
+ # sort the color by brightness in an array
+ def <=> othercolor
+ self.brightness <=> othercolor.brightness || self.hue <=> othercolor.hue
+ end
+
+ # set or retrieve the red component
+ def r(val=nil)
+ if val
+ r, g, b, a = get_rgb
+ set_rgb(val, g, b, a)
+ self
+ else
+ @rgb.redComponent
+ end
+ end
+
+ # set or retrieve the green component
+ def g(val=nil)
+ if val
+ r, g, b, a = get_rgb
+ set_rgb(r, val, b, a)
+ self
+ else
+ @rgb.greenComponent
+ end
+ end
+
+ # set or retrieve the blue component
+ def b(val=nil)
+ if val
+ r, g, b, a = get_rgb
+ set_rgb(r, g, val, a)
+ self
+ else
+ @rgb.blueComponent
+ end
+ end
+
+ # set or retrieve the alpha component
+ def a(val=nil)
+ if val
+ r, g, b, a = get_rgb
+ set_rgb(r, g, b, val)
+ self
+ else
+ @rgb.alphaComponent
+ end
+ end
+
+ # set or retrieve the hue
+ def hue(val=nil)
+ if val
+ h, s, b, a = get_hsb
+ set_hsb(val, s, b, a)
+ self
+ else
+ @rgb.hueComponent
+ end
+ end
+
+ # set or retrieve the saturation
+ def saturation(val=nil)
+ if val
+ h, s, b, a = get_hsb
+ set_hsb(h, val, b, a)
+ self
+ else
+ @rgb.saturationComponent
+ end
+ end
+
+ # set or retrieve the brightness
+ def brightness(val=nil)
+ if val
+ h, s, b, a = get_hsb
+ set_hsb(h, s, val, a)
+ self
+ else
+ @rgb.brightnessComponent
+ end
+ end
+
+ # decrease saturation by the specified amount
+ def desaturate(step=0.1)
+ saturation(saturation - step)
+ self
+ end
+
+ # increase the saturation by the specified amount
+ def saturate(step=0.1)
+ saturation(saturation + step)
+ self
+ end
+
+ # decrease the brightness by the specified amount
+ def darken(step=0.1)
+ brightness(brightness - step)
+ self
+ end
+
+ # increase the brightness by the specified amount
+ def lighten(step=0.1)
+ brightness(brightness + step)
+ self
+ end
+
+ # set the R,G,B,A values
+ def set(r, g, b, a=1.0)
+ set_rgb(r, g, b, a)
+ self
+ end
+
+ # adjust the Red, Green, Blue, Alpha values by the specified amounts
+ def adjust_rgb(r=0.0, g=0.0, b=0.0, a=0.0)
+ r0, g0, b0, a0 = get_rgb
+ set_rgb(r0+r, g0+g, b0+b, a0+a)
+ self
+ end
+
+ # return RGBA values
+ def get_rgb
+ #@rgb.getRed_green_blue_alpha_()
+ [@rgb.redComponent, @rgb.greenComponent, @rgb.blueComponent, @rgb.alphaComponent]
+ end
+
+ # set color using RGBA values
+ def set_rgb(r, g, b, a=1.0)
+ @rgb = NSColor.colorWithDeviceRed r, green:g, blue:b, alpha:a
+ self
+ end
+
+ # return HSBA values
+ def get_hsb
+ #@rgb.getHue_saturation_brightness_alpha_()
+ [@rgb.hueComponent, @rgb.saturationComponent, @rgb.brightnessComponent, @rgb.alphaComponent]
+ end
+
+ # set color using HSBA values
+ def set_hsb(h,s,b,a=1.0)
+ @rgb = NSColor.colorWithDeviceHue h, saturation:s, brightness:b, alpha:a
+ self
+ end
+
+ # adjust Hue, Saturation, Brightness, and Alpha by specified amounts
+ def adjust_hsb(h=0.0, s=0.0, b=0.0, a=0.0)
+ h0, s0, b0, a0 = get_hsb
+ set_hsb(h0+h, s0+s, b0+b, a0+a)
+ self
+ end
+
+ # alter the color by the specified random scaling factor
+ # def ish(angle=10.0,d=0.02)
+ # # r,g,b,a = get_rgb
+ # # r = vary(r, variance)
+ # # g = vary(g, variance)
+ # # b = vary(b, variance)
+ # # a = vary(a, variance)
+ # # set_rgb(r,g,b,a)
+ # analog(angle,d)
+ # self
+ # end
+
+ # create a random color
+ def random
+ set_rgb(rand, rand, rand, 1.0)
+ self
+ end
+
+ # rotate the color on the artistic RYB color wheel (0 to 360 degrees)
+ def rotate_ryb(angle=180)
+
+ # An artistic color wheel has slightly different opposites
+ # (e.g. purple-yellow instead of purple-lime).
+ # It is mathematically incorrect but generally assumed
+ # to provide better complementary colors.
+ #
+ # http://en.wikipedia.org/wiki/RYB_color_model
+
+ h = hue * 360
+ angle = angle % 360.0
+ a = 0
+
+ # Approximation of Itten's RYB color wheel.
+ # In HSB, colors hues range from 0-360.
+ # However, on the artistic color wheel these are not evenly distributed.
+ # The second tuple value contains the actual distribution.
+
+ # Given a hue, find out under what angle it is
+ # located on the artistic color wheel.
+ (RYBWheel.size-1).times do |i|
+ x0,y0 = RYBWheel[i]
+ x1,y1 = RYBWheel[i+1]
+ y1 += 360 if y1 < y0
+ if y0 <= h && h <= y1
+ a = 1.0 * x0 + (x1-x0) * (h-y0) / (y1-y0)
+ break
+ end
+ end
+
+ # And the user-given angle (e.g. complement).
+ a = (a+angle) % 360
+
+ # For the given angle, find out what hue is
+ # located there on the artistic color wheel.
+ (RYBWheel.size-1).times do |i|