# Time-based cutoff module CFDG class Model attr_accessor :startshape, :rules, :background def initialize @rules = {} @background = [0,0,1] end def to_s s = '' s = "startshape #{@startshape}\n" if @startshape s += @rules.values.flatten.map{|r|r.to_s}.join("\n") end def add_rule rule @rules[rule.name] ||= [] @rules[rule.name] << rule @startshape ||= rule.name end end class Rule attr_accessor :name, :weight, :shapes def initialize name @name = name @weight = 1.0 @shapes = [] end def to_s s = "rule #{name}#{' '+weight if weight != 1.0} {" s += "\n" if shapes s += shapes.map{|s|' '+s.to_s + "\n"}.join return s + "}" end end ATTRIBUTE_NAMES = %w{x y rotate flip size sx sy skew hue sat brightness alpha} unless self.const_defined?(:ATTRIBUTE_NAMES) class Shape attr_accessor :name, :attributes def initialize name @name = name @attributes = [] end def to_s "#{name} [#{@attributes.map{|n,v|n+' '+v.join(' ')}.join(' ')}]" end def set_attribute name, value name = {'s' => 'size', 'r' => 'rotate', 'h' => 'hue', 'b' => 'brightness'}[name] || name arity = 1 if %w{size skew}.include?(name) value *= 2 if value.length == 1 arity = 2 end raise "wrong arity: #{name} takes #{arity} got #{value.length}" if value.length != arity @attributes << [name, value] end def sort_attributes @attributes = @attributes.sort_by {|pair| ATTRIBUTE_NAMES.index pair[0]} end end class CompilationError < StandardError def initialize msg, tokens=nil super msg end end class Lexer def initialize source @source = source end def each lineno = 0 @source.split(/\n|\r/).each_with_index do |line, i| lineno = i+1 callcc do |next_line| line.split(/\s+/).each do |word| begin while word.any? next_line.call if word =~ /^(?:--|\/\/)/ case word when /^-?\d+(\.\d*)?|-?\.\d+/ word = $' yield Float, $&.to_f, lineno when /^[\[\]\{\}\|]/ word = $' yield '<>', $&, lineno when /^\w+/ word = $' yield String, $&, lineno else raise "Uknown word: #{word.inspect}" end end rescue CompilationError raise "#{$!.message} at #{word} on line #{@lineno}" end end end end yield '<>', nil, lineno end end class Parser attr_accessor :model def parse source, model @model = model if model @model ||= Model.new @tokens = [] Lexer.new(source).each do |type, token, lineno| @tokens << [type, token, lineno] end start model end def compilation_error message raise "cfdg:#{@tokens.first[2]}: #{message} at '#{@tokens.first[1]}'" end def peek_type; @tokens.first[0]; end def peek_token; @tokens.first[1]; end def next_token; @tokens.shift[1]; end def expect_type type compilation_error "expected #{type}" unless peek_type == type next_token end def expect_string compilation_error "expected a string" unless peek_type == String next_token end def expect_number expect_type Float end # Returns the token def expect_token *list compilation_error "expected #{list.inspect}, got #{peek_token}" unless list.include? peek_token next_token end def start if peek_token == 'startshape' next_token model.startshape = expect_string end if peek_token == 'background' next_token expect_token '{' while peek_token != '}' name = expect_string name = {'h' => 'hue', 'b' => 'brightness'}[name] || name index = %w{hue sat brightness}.index(name) raise "unknown background attribute: #{name}" unless index model.background[index] = expect_number end next_token end rule while peek_token == 'rule' expect_type '<>' end def rule expect_token 'rule' rule = Rule.new expect_string model.add_rule rule if peek_type == Float rule.weight = next_token end expect_token '{' while peek_token != '}' rule.shapes << shape end expect_token '}' end def shape shape = Shape.new expect_string case expect_token('{', '[') when '{' final = '}' sort = true when '[' final = ']' sort = false end while peek_token != final name = expect_string value = [] while peek_type == Float value << next_token end shape.set_attribute name, value end expect_token final shape.sort_attributes if sort shape end end class Graphics include CFDG attr_accessor :transform def initialize @transform = Transform.new [[100,0,0],[0,-100,0]] @s = [] @rvg = [] @background = [0,0,0] @xmin = @ymin = @ymin = @ymax = nil end def content @s.join(' ') end # returns file def self.write output, options={}, &block size = options[:size] || 200 background = options[:background] require 'tempfile' src = 'drawing.mvg' dst = output || 'drawing.png' File.delete(dst) rescue nil Tempfile.open(src) do |f| src = f.path f << options[:content] if options[:context] yield f if block end bg = "##{background.map{|v|format "%02x",(v*255).to_i}.join('')}" if background result = `convert -size #{size}x#{size} -background #{bg} mvg:#{src} #{dst}` p ['result', result] and return if result.chomp.any? return dst end # returns file def write output, options={} size = options[:size] || 200 @xmin, @xmax, @ymin, @ymax = 0,1,0,1 unless @xmin side = [@xmax-@xmin, @ymax-@ymin,0.001].max cx, cy = (@xmin+@xmax)/2, (@ymin+@ymax)/2 scale = size.to_f/side self.class.write(output, :size => size, :background => @background) do |f| f << "scale #{scale} #{scale} " f << "translate #{-@xmin} #{-@ymin} " f << content end end def update_bounds points xs = (points.map{|x,y|x}+[@xmin,@xmax]).compact ys = (points.map{|x,y|y}+[@ymin,@ymax]).compact @xmin = xs.min; @xmax = xs.max @ymin = ys.min; @ymax = ys.max end def polygon points, transform points = transform.transformPoints points update_bounds points @s << "polygon #{points.map{|p|p.join(',')}.join(' ')}" #@rvg << [:polygon, points] end def circle center, radius, transform affine = transform.matrix[0].zip(transform.matrix[1]).flatten @s << 'push graphic-context' << "affine #{affine.join(',')}" << "circle #{center.join(',')} #{center[0]},#{center[1]-radius}" << "pop graphic-context" #@rvg << [:circle, center, radius, affine] cx, cy = center r = radius points = [[cx-r,cy-r],[cx-r,cy+r],[cx+r,cy+r],[cx+r,cy-r]] points = transform.transformPoints points update_bounds points end def hsva= hsva argb = [hsva[3]]+hsv2rgb(*hsva[0..2]) return if @argb == argb @argb = argb @s << "fill ##{argb.map{|v|format "%02x",(v*255).to_i}.join('')}" #@rvg << [:rgb=, rgb] end def background= hsv @background = hsv2rgb *hsv end end def hsv2rgb h, s, v h, s, v = h.to_f, s.to_f, v.to_f h = ((h % 360) + 360) % 360 s = [[s,0].max,1].min v = [[v,0].max,1].min return v, v, v if s == 0 # gray h = h / 60.0 # sector 0 to 5 i = h.to_i f = h - i p = v * (1 - s) q = v * (1 - s * f) t = v * (1 - s * (1 - f)) return [[v,t,p],[q,v,p],[p,v,t],[p,q,v],[t,p,v],[v,p,q]][i % 6] end class Transform attr_accessor :matrix class Premultiplier def initialize transform @target = transform end def method_missing message, *args pre = Transform.new pre = pre.send(message, *args) || pre result = pre * @target if message.to_s[-1] == ?! @target.matrix = result.matrix nil else result end end end def initialize matrix=nil, &block @matrix = matrix || [[1,0,0],[0,1,0],[0,0,1]] @matrix << [0,0,1] if @matrix.length == 2 yield @matrix if block end def pre @pre ||= Premultiplier.new self end def cloned clone = Transform.new clone.matrix = @matrix.map {|c| c.clone} clone end def determinant m = @matrix m[0][0]*m[1][1] - m[0][1]*m[1][0] end def * b ma = self.matrix mb = b.matrix Transform.new do |m| for i in 0..2 do for j in 0..2 do m[i][j] = (0..2).map{|k| mb[i][k]*ma[k][j]}.sum end end end end def transformPoints points mx = @matrix[0] my = @matrix[1] points.map do |x, y| [ x*mx[0]+y*mx[1]+mx[2], x*my[0]+y*my[1]+my[2]] end end def scale sx, sy xform = Transform.new do |m| m[0][0] = sx m[1][1] = sy end self * xform end def translate dx, dy xform = Transform.new do |m| m[0][2] = dx m[1][2] = dy end self * xform end def rotate r xform = Transform.new do |m| cos = Math::cos r m[0][0] = m[1][1] = cos m[0][1] = -(m[1][0] = Math::sin r) end self * xform end def method_missing message, *args if message.to_s[-1] == ?! @matrix = send(message.to_s[0...-1], *args).matrix return end super end end class Context attr_accessor :transform, :color, :trace, :summary def initialize graphics, model @graphics = graphics || Graphics.new @model = model @transform = @graphics.transform @size_cutoff = @transform.determinant.abs * 0.01 @color = [0,0,0,1] @queue = [] end def cloned clone = self.clone clone.transform = @transform.cloned clone.color = @color.clone clone end def invoke name return if @transform.determinant.abs < @size_cutoff puts "Invoking #{name}" if @trace @queue << [name, self] end def flush duration=1000 stopTime = Time.now + duration/1000.0 while Time.now < stopTime and @queue.any? @model.invoke *@queue.shift end end def draw_polygon name, points puts "polygon #{points.inspect}" and return if @summary @graphics.hsva = color @graphics.polygon points, transform end def draw_circle center, radius puts "circle #{center.inspect} #{radius}" and return if @summary @graphics.hsva = color @graphics.circle center, radius, transform end def rotate r; @transform.pre.rotate! r*Math::PI/180; end def size x, y; @transform.pre.scale! x, y; end def x dx; @transform.pre.translate! dx, 0; end def y dy; @transform.pre.translate! 0, dy; end def flip r r *= Math::PI/180 @transform.pre.rotate!(-r) @transform.pre.scale!(1, -1) @transform.pre.rotate!(r) end def skew x, y skew = Transform.new skew[0][1] = Math::tan(x*Math.PI/180) skew[1][0] = Math::tan(y*Math.PI/180) @transform.pre.multiply! sskew end def hue h; @color[0] += h; end def sat s; @color[1] = s; end def brightness b; @color[2] += (1-@color[2])*b; end def alpha a; @color[3] += @color[3]*a; end def background= hsv; @graphics.background = hsv; end end module ::Enumerable def sum inject(0){|a,b|a+b} end end class Model def draw context context.background = background invoke startshape, context end def invoke name, context rule = choose name rule.draw context end def choose name rules = @rules[name] raise "No rule named #{name}" unless rules sum = rules.map{|r|r.weight}.sum n = sum*rand rules.each do |r| return r if (n -= r.weight) <= 0 end raise "implementation error" end end class Rule def draw context raise "context is #{context.inspect}" unless Context === context shapes.each do |shape| shape.draw context end end end Sqrt3 = Math::sqrt(3) unless self.const_defined?(:Sqrt3) class Shape def draw context context = context.cloned attributes.each do |name, value| context.send name, *value end message = name.downcase.intern return send(message, context) if respond_to?(message) context.invoke name end def square context context.draw_polygon 'square', [[-0.5,-0.5],[-0.5,0.5],[0.5,0.5],[0.5,-0.5]] end def circle context context.draw_circle [0,0], 0.5 end def triangle context y = -0.5/Sqrt3 context.draw_polygon 'triangle', [[-0.5, y], [0.5, y], [0, y+Sqrt3/2]] end end def self.draw string, options={:mode => :file} mode = options[:mode] || :draw output = options[:output] ||= 'test.png' timeout = options[:timeout] || 10000 srand options[:srand] || 0 g = Graphics.new m = Model.new c = Context.new(g, m) c.trace = mode == :trace c.summary = mode == :summary Parser.new.parse(string, m).draw c puts m and return if mode == :parse puts g.content and return if mode == :print c.flush timeout g.write(output, options) `open #{output}` and return if mode == :view end end def draw string=nil, options=:view string ||= 'test.cfdg' string = File.open(string).read if File.exists? string options = {:mode => options} if Symbol === options CFDG::draw string, options end def parse str='test.cfdg' draw str, :parse end #draw "rule R {CIRCLE {r 0 sat 1 b 1} }" #draw "rule R {SQUARE {r 45 sat 1 b 1} R {s .8 h 10 r 10 x 0.1} }" #draw "rule R {CIRCLE {sat 1 b 1} R {s .90 r 10 h 10 x .1}}" #draw File.open('../../miles.cfdg').read, :width => 300 #draw "rule R {CIRCLE {sat 1 b 1} R {h 10 s .9} } rule R {SQUARE {sat 1 b 1} R {h 10 s .9} }" def repl file=nil file ||= 'test.cfdg' while true begin draw File.open(file).read, :view rescue puts $! puts $!.backtrace end t0 = File.mtime file st = File.mtime 'cfdg.rb' sleep 0.25 while t0 == File.mtime(file) && st == File.mtime('cfdg.rb') return if st != File.mtime('cfdg.rb') end end