残像のテスト
残像のあるエフェクトをつけるには、enterFreameで以下をやればいいみたい。
- 残像をつけたいオブジェクトをBitmapDataに描画
- BitmapDataに、色を薄くするようなColorTransformをつける
これをやれば、色が段々消えていくような残像のある動きが出せる。
BlendModeのテスト
wonderflのコードをブログに張れることが分かったので試してみた。
クリックするとBlendModeが変わるデモをちょっと書いてみた。
JRubyでFilthyRichClients その6
Filthy Rich Clientsの第五章はパフォーマンスに関する話題です。
アプリの速度向上のために気をつけることが書かれています。
クリッピング
与えられたクリッピングの中だけで描画を行うようにする。
# クリッピングを使わないコード def paintComponent(g) g.setColor(Color::BLACK) g.fillRect(0, 0, getWidth, getHeight) end # クリッピングを使ったコード def paintComponent(g) g.setColor(Color::BLACK) clip = g.getClipBounds g.fillRect(clip.x, clip.y, clip.width, clip.height) end
でも、クリッピングを簡単に使えないときは、無理に使う必要は無いみたい。
中間画像
画像に何度も同じ加工をしなければいけない時は、最初に画像を加工したら、加工した画像を取っておく。2回目以降は、取っておいた画像をdrawImageでコピーして使う。毎回画像を加工するより、コピーするほうが速い。
互換画像
実行している環境に最適化された画像を内部でもっておけば速い。画像を互換画像に変換して、互換画像のほうを使うようにする。
画像を互換画像に変換するmoduleをJRubyで書いてみた。
require 'java' module MakeCompatibleImage import 'java.awt.GraphicsEnvironment' module_function def get_configuration GraphicsEnvironment.getLocalGraphicsEnvironment.getDefaultScreenDevice.getDefaultConfiguration end # 与えられた幅、高さ、透明度を持つ互換画像を作成する。 # transparencyは、BufferedImage#getTransparencyで取得するか、以下の定数を指定できる。 # java.awt.Transparency::BITMASK # java.awt.Transparency::OPAQUE # java.awt.Transparency::TRANSLUCENT # http://java.sun.com/j2se/1.5.0/ja/docs/ja/api/java/awt/Transparency.html def create_compatible_image(width, height, transparency) get_configuration.createCompatibleImage(width, height, transparency) end # 与えられた画像が、互換画像であるか調べる。 def compatible_image?(image) image.getColorModel.equals(get_configuration.getColorModel) end # ソース画像をコピーした、新しい互換画像を作成する。 # 既にソース画像が互換の場合は、ソース画像を返す。 def to_compatible_image(image) return image if compatible_image?(image) compatible_image = create_compatible_image(image.getWidth, image.getHeight, image.getTransparency) g = compatible_image.getGraphics g.drawImage(image, 0, 0, nil) g.dispose compatible_image end # 引数で指定された画像へのパスから、互換画像を作成する。 # パスはURLかファイル名を指定する。 # ex: # MakeCompatibleImage.load_compatible_image("http://sample.com/foo.png") # MakeCompatibleImage.load_compatible_image("foo.png") def load_compatible_image(path) klass = fetch_resource_class(path) image = javax.imageio.ImageIO.read(klass.new(path)) to_compatible_image(image) end def fetch_resource_class(path) return java.net.URL if path =~ %r!^http://! java.io.File end end if __FILE__ == $0 include_class 'java.awt.image.BufferedImage' p MakeCompatibleImage.get_configuration img = BufferedImage.new(100, 50, BufferedImage::TYPE_INT_ARGB) p MakeCompatibleImage.create_compatible_image(img.getWidth, img.getHeight, img.getTransparency) [BufferedImage::TYPE_INT_ARGB, BufferedImage::TYPE_INT_RGB].each do |type| image = BufferedImage.new(100, 50, type) print "compatible_image?:" p MakeCompatibleImage.compatible_image?(image) p image p MakeCompatibleImage.to_compatible_image(image) p "" end end
書いたんだけど、JavaのImageIOで、URLオブジェクトからimageを作ろうとすると例外が飛ぶ。
Fileだと上手くいくんだけど。。。↓のように単純化してもエラーになるんだけど、なんでだろ。。
image = javax.imageio.ImageIO.read java.net.URL.new("http://www.google.co.jp/images/srpr/nav_logo13.png")
javax.imageio.ImageIO:-1:in `createImageInputStream': javax.imageio.IIOException: Can't create cache file! (NativeException) from javax.imageio.ImageIO:-1:in `read'
JRubyでFilthyRichClients その5
第4章はスケーリングに関する品質とパフォーマンスの話題です。
Java2Dでは画像のスケーリングに使えるオプションがたくさんあり、オプションによって品質とパフォーマンスが異なっている。
Filthy Rich Clientsでは以下のスケーリングの品質を比較している。
(この順番は品質の低い順番から並んでいるとのこと。)
- NEAREST_NEIGHBOR
- BILINEAR
- BICUBIC
- AREA_AVERAGING
本によると、AREA_AVERAGINGは、品質は良いけどパフォーマンスがその他のアルゴリズムより
かなり悪く、あまり推奨されていないようだ。(どうか使わないでと書いてあった)
代わりに、AREA_AVERAGINGのように品質が良くて、かつパフォーマンスも良いProgressive Bilinearという方法が紹介されていた。
Progressive Bilinearというのは以下のようなもの。
- BILINEAR方式は、50%より小さく縮小処理を行った場合に品質が悪くなることが分かっている。
- 50%より小さい縮小処理を行うときは、50%の縮小を繰り返して目的のサイズに縮小する。
これらのスケーリングの品質とパフォーマンスを、比較するためのコードがJavaで書かれていたので、JRubyで書き直して検証してみた。
NEAREST_NEIGHBOR、BILINEAR、BICUBIC、AREA_AVERAGINGを使うには、Graphicsインスタンスのメソッドにオプションを渡せばよい。
Progressive Bilinearは、BILINEARを反復する処理を記述する必要があるので、moduleにまとめた。(ImageUtil#get_scaled_instance)
require 'java' include_class %w( JFrame JComponent SwingUtilities ).map{|s| 'javax.swing.' + s} include_class %w( Color RenderingHints BorderLayout Image Transparency ).map{|s| 'java.awt.' + s} include_class 'javax.imageio.ImageIO' include_class 'java.awt.image.BufferedImage' class ScaleTest < JComponent FULL_SIZE = 190 PADDING = 5 QUAD_SIZE = FULL_SIZE / 2 SCALE_FACTOR = 0.17 def initialize super() @original_image = BufferedImage.new(FULL_SIZE, FULL_SIZE, BufferedImage::TYPE_INT_RGB); @original_image_painted = false end def paintComponent(g) paint_original_image() unless @original_image_painted x = 5 y = 20 # nearest neighbor benchmark(g, x, y, "NEAREST") do draw_image(g, y) end # bilinear y += FULL_SIZE + PADDING g.setRenderingHint(RenderingHints::KEY_INTERPOLATION, RenderingHints::VALUE_INTERPOLATION_BILINEAR) benchmark(g, x, y, "BILINEAR") do draw_image(g, y) end # bicubic y += FULL_SIZE + PADDING g.setRenderingHint(RenderingHints::KEY_INTERPOLATION, RenderingHints::VALUE_INTERPOLATION_BICUBIC) benchmark(g, x, y, "BICUBIC") do draw_image(g, y) end # getScaledInstance y += FULL_SIZE + PADDING g.setRenderingHint(RenderingHints::KEY_INTERPOLATION, RenderingHints::VALUE_INTERPOLATION_NEAREST_NEIGHBOR) benchmark(g, x, y, "getScaled") do draw_image_getScaledInstance(g, y) end # Progressive Bilinear y += FULL_SIZE + PADDING benchmark(g, x, y, "Progressive") do draw_image_progressive_bilinear(g, y) end # Draw image sizes yield_draw_parametor do |x, scaled_size| g.drawString("#{scaled_size} x #{scaled_size}", x + [0, scaled_size/2 - 20].max, 15) end end private def paint_original_image g = @original_image.getGraphics() # clear background g.setColor(Color::BLACK) g.fillRect(0, 0, FULL_SIZE, FULL_SIZE) draw_rgb_rect(g, 0, 0, QUAD_SIZE) draw_picture(g, QUAD_SIZE, 0, QUAD_SIZE) draw_smile(g, 0, QUAD_SIZE, QUAD_SIZE) draw_grid(g, QUAD_SIZE, QUAD_SIZE, QUAD_SIZE) g.dispose() @original_image_painted = true end def draw_rgb_rect(g, x, y, width) colors = [Color::RED, Color::GREEN, Color::BLUE] 0.step(width, colors.size) do |i| colors.each_with_index do |color, j| g.setColor(color) g.drawLine(x+i+j, y, x+i+j, y+width) end end end def draw_picture(g, x, y, width) picture = ImageIO.read(java.io.File.new("lib/images/BBGrayscale.png")) # Center picture in quadrant area x_diff = width - picture.getWidth() y_diff = width - picture.getHeight() g.drawImage(picture, x + x_diff/2, y + y_diff/2, nil); end def draw_smile(g, x, y, width) g.setColor(Color::WHITE) g.fillRect(x, y, width, width) g.setColor(Color::BLACK) g.drawOval(x + 2, y + 2, width-4, width-4) g.drawArc(x + 20, y + 20, width - 40, width -40, 190, 160) eye_size = 7 eye_pos = 30 - (eye_size / 2) g.fillOval(x + eye_pos, y + eye_pos, eye_size, eye_size) g.fillOval(x + width - eye_pos - eye_size, y + eye_pos, eye_size, eye_size) end def draw_grid(g, x, y, width) g.setColor(Color::WHITE) g.fillRect(x, y, width, width) g.setColor(Color::BLACK) 1.step(width, 4) do |i| g.drawLine(x, y+i, x+width, y+i) g.drawLine(x+i, y, x+i, y+width) end end def yield_draw_parametor x = 100 delta = SCALE_FACTOR * FULL_SIZE FULL_SIZE.step(1, -delta) do |scaled_size| yield(x, scaled_size) x += scaled_size + 20 end end def draw_image(g, y) yield_draw_parametor do |x, scaled_size| g.drawImage(@original_image, x, y + (FULL_SIZE-scaled_size)/2, scaled_size, scaled_size, nil) end end def draw_image_getScaledInstance(g, y) yield_draw_parametor do |x, scaled_size| scaled_image = @original_image.getScaledInstance(scaled_size, scaled_size, Image::SCALE_AREA_AVERAGING) g.drawImage(scaled_image, x, y + (FULL_SIZE - scaled_size)/2, nil) end end def draw_image_progressive_bilinear(g, y) yield_draw_parametor do |x, scaled_size| scaled_image = ImageUtil.get_scaled_instance(@original_image, scaled_size, scaled_size) g.drawImage(scaled_image, x, y + (FULL_SIZE - scaled_size)/2, nil) end end def benchmark(g, x, y, type) start_time = java.lang.System.nanoTime() yield draw_result_time(g, x, y + FULL_SIZE/2, type, java.lang.System.nanoTime(), start_time) end def draw_result_time(g, x, y, type, end_time, start_time) g.drawString(type + " ", x, y) g.drawString(((end_time - start_time) / 1000000).to_s + " ms", x, y + 15); end end module ImageUtil module_function # progressive bilinear def get_scaled_instance(img, target_width, target_height, hint=RenderingHints::VALUE_INTERPOLATION_BILINEAR) type = (img.getTransparency() == Transparency::OPAQUE) ? BufferedImage::TYPE_INT_RGB : BufferedImage::TYPE_INT_ARGB ret = img w, h = img.getWidth(), img.getHeight() scratch_image = BufferedImage.new(w, h, type) g = scratch_image.createGraphics() prev_w, prev_h = ret.getWidth(), ret.getHeight() while(w != target_width || h != target_height) if w > target_width w /= 2 w = target_width if w < target_width end if h > target_height h /= 2 h = target_height if h < target_height end g.setRenderingHint(RenderingHints::KEY_INTERPOLATION, hint) g.drawImage(ret, 0, 0, w, h, 0, 0, prev_w, prev_h, nil) prev_w, prev_h = w, h ret = scratch_image end g.dispose() if (target_width != ret.getWidth() || target_height != ret.getHeight()) scratch_image = BufferedImage.new(target_width, target_height, type) g = scratch_image.createGraphics() g.drawImage(ret, 0, 0, nil) g.dispose() ret = scratch_image end ret end end SwingUtilities.invokeLater do f = JFrame.new f.add(ScaleTest.new) f.setSize(900, 50 + (5 * ScaleTest::FULL_SIZE) + (6 * ScaleTest::PADDING)) f.setDefaultCloseOperation(JFrame::EXIT_ON_CLOSE) f.setVisible(true) end
品質
縮小率が小さくなると、品質は、AREA_AVERAGINGとProgressive Bilinearが
他のアルゴリズムより良いと思われる。
パフォーマンス
アルゴリズム | 時間(ms) |
---|---|
NEAREST NEIGHBOR | 2 |
BILINEAR | 1 |
BICUBIC | 97 |
AREA_AVERAGING | 55 |
Progressive Bilinear | 10 |
AREA_AVERAGINGが一番遅いと思っていたが、BICUBICの方が遅い。
品質の良かったProgressive Bilinearがパフォーマンスもなかなか。
この結果を踏まえると、普段はNEAREST NEIGHBORを使って、品質を高めたいときはProgressive Bilinearを考えるのが良いのかも。
JRubyでFilthyRichClients その4
色々と自分でShapeを作れれば、Grapics2Dのfillやdrawを使って、これらのShapeを描画することができる。
Filthy Rich Clientsでは、
- Area、Elipse2Dを使ったドーナツ型
- GeneratePathを使った星型
などが紹介されている。このカスタムShapeをJRubyで書いてみた。
include Java include_class %w( JFrame JComponent SwingUtilities ).map{|s| 'javax.swing.' + s} include_class %w( Color GradientPaint RadialGradientPaint RenderingHints ).map{|s| 'java.awt.' + s} include_class %w( MouseAdapter ).map{|s| 'java.awt.event.' + s} include_class %w( Area Ellipse2D Point2D GeneralPath ).map{|s| 'java.awt.geom.' + s} class Canvas < JComponent def initialize() super() addMouseListener(ClickReceiver.new) @shapes = [] @colors = [] @get_star = true end def paintComponent(g) draw_background(g) g.setRenderingHint(RenderingHints::KEY_ANTIALIASING, RenderingHints::VALUE_ANTIALIAS_ON) draw_shape(g) end def create_shape(e) inner_size = 1 + rand(25) outer_size = inner_size + 10 + rand(15) if @get_star branch_count = (rand(8) + 5).to_i @shapes << generate_star(e.getX, e.getY, inner_size, outer_size, branch_count) else @shapes << generate_donut(e.getX-outer_size/2, e.getY-outer_size/2, inner_size, outer_size) end 2.times do @colors << Color.new(rand(0xFFFFFF)) end @get_star = !@get_star repaint() end private def draw_background(g) background = GradientPaint.new(0, 0, Color::GRAY.darker(), 0, getHeight(), Color::GRAY.brighter()) g.setPaint(background) g.fillRect(0, 0, getWidth(), 4*getHeight()/5) background = GradientPaint.new(0, 4*getHeight()/5, Color::BLACK, 0, getHeight(), Color::GRAY.darker()); g.setPaint(background) g.fillRect(0, 4*getHeight()/5, getWidth(), getHeight()/5) end def draw_shape(g) @shapes.each_with_index do |shape, i| rect = shape.getBounds() center = Point2D::Float.new(rect.x + rect.width/2.0, rect.y + rect.height/2.0) radius = rect.width/2.0 dist = [0.1, 0.9].to_java(:float) colors = [@colors[i*2], @colors[i*2+1]].to_java('java.awt.Color') g.setPaint(RadialGradientPaint.new(center, radius, dist, colors)) g.fill(shape) end end def generate_donut(x, y, inner_radius, outer_radius) a1 = Area.new(Ellipse2D::Double.new(x, y, outer_radius, outer_radius)) inner_offset = (outer_radius - inner_radius)/2.0 a2 = Area.new(Ellipse2D::Double.new(x + inner_offset, y + inner_offset, inner_radius, inner_radius)) a1.subtract(a2) a1 end def generate_star(center_x, center_y, inner_radius, outer_radius, branch_count) path = GeneralPath.new path.moveTo(center_x + outer_radius, center_y) outer_angle_increment = 2.0 * Math::PI / branch_count outer_angle = 0.0 inner_angle = outer_angle_increment / 2.0 branch_count.times do x = Math.cos(outer_angle) * outer_radius + center_x y = Math.sin(outer_angle) * outer_radius + center_y path.lineTo(x, y) x = Math.cos(inner_angle) * inner_radius + center_x y = Math.sin(inner_angle) * inner_radius + center_y path.lineTo(x, y) outer_angle += outer_angle_increment inner_angle += outer_angle_increment end path.closePath() path end end class ClickReceiver < MouseAdapter def mouseClicked(e) c = e.getSource c.create_shape(e) end end SwingUtilities.invokeLater do f = JFrame.new f.add(Canvas.new) f.setSize(500, 500) f.setDefaultCloseOperation(JFrame::EXIT_ON_CLOSE) f.setLocationRelativeTo(nil) f.setVisible(true) end
erbのyield
railsのviewの処理のように、erbのソースにyieldを使ってhtmlを埋めこむようなテクニックがあるけど、nanocのソースを読んでいたら、同様の処理を行うようなコードがあった。erbとyieldの使い方をメモしておこう。
erbのyieldは以下のように使える。
>> require 'erb' => true >> class Context >> def get_binding >> binding >> end >> end => nil >> layout =<<EOS <html> <body> <%= yield %> </body> </html> EOS => "<html>\n<body>\n <%= yield %>\n</body>\n</html>\n" >> content = "content" => "content" >> e = ERB.new(layout) => #<ERB:0x18f9b75 @filename=nil, @src="_erbout = ''; _erbout.concat \"<html>\\n<body>\\n \"\n\n; _erbout.concat(( yield ).to_s); _erbout.concat \"\\n</body>\\n</html>\\n\"\n\n\n; _erbout", @safe_level=nil> >> e.result(Context.new.get_binding{ content }) => "<html>\n<body>\n content\n</body>\n</html>\n"
ERB#resultにブロックを付けたbindingを渡せばよい。
ブロックが無いと、以下のように例外が出る。
>> e.result LocalJumpError: yield called out of block from /opt/env/ruby/jruby-1.5.0/lib/ruby/1.8/irb/ruby-token.rb:46 Maybe IRB bug!! >> e.result(binding) LocalJumpError: yield called out of block from /opt/env/ruby/jruby-1.5.0/lib/ruby/1.8/irb/ruby-token.rb:102:in `irb_binding' Maybe IRB bug!! >> e.result(Context.new.get_binding) LocalJumpError: yield called out of block from /opt/env/ruby/jruby-1.5.0/lib/ruby/1.8/irb/ruby-token.rb:102:in `get_binding' Maybe IRB bug!!
nanocのソースを読む(DirectedGraph)
nanocのDirectedGraphクラスのソースを読んだときのメモ。
このクラスが表現していることを理解するには、"有向グラフ"を検索してグラフの形を見ておくとわかりやすいかも。
irbでDirectedGraphインスタンスを作って、ソースのコメントに載っているサンプルの動作を確認してみる。
>> require 'pp' >> require 'nanoc3' ?> g = Nanoc3::DirectedGraph.new(%w( a b c d )) >> pp g #<Nanoc3::DirectedGraph:0xfc0 @from_graph={}, @predecessors={}, @successors={}, @to_graph={}, @vertice_indexes={"a"=>0, "b"=>1, "c"=>2, "d"=>3}, @vertices=["a", "b", "c", "d"]>
add_edge
add_edge('a', 'b')は、有向グラフで言えばaからbに線を引くようなもの。
>> g.add_edge('a', 'b') => {} >> pp g #<Nanoc3::DirectedGraph:0xfc0 @from_graph={"a"=>#<Set: {"b"}>}, @predecessors={}, @successors={}, @to_graph={"b"=>#<Set: {"a"}>}, @vertice_indexes={"a"=>0, "b"=>1, "c"=>2, "d"=>3}, @vertices=["a", "b", "c", "d"]> => nil >> g.add_edge('b', 'c') => {} >> pp g #<Nanoc3::DirectedGraph:0xfc0 @from_graph={"a"=>#<Set: {"b"}>, "b"=>#<Set: {"c"}>}, @predecessors={}, @successors={}, @to_graph={"b"=>#<Set: {"a"}>, "c"=>#<Set: {"b"}>}, @vertice_indexes={"a"=>0, "b"=>1, "c"=>2, "d"=>3}, @vertices=["a", "b", "c", "d"]> => nil >> g.add_edge('c', 'd') => {} >> pp g #<Nanoc3::DirectedGraph:0xfc0 @from_graph={"a"=>#<Set: {"b"}>, "b"=>#<Set: {"c"}>, "c"=>#<Set: {"d"}>}, @predecessors={}, @successors={}, @to_graph={"b"=>#<Set: {"a"}>, "c"=>#<Set: {"b"}>, "d"=>#<Set: {"c"}>}, @vertice_indexes={"a"=>0, "b"=>1, "c"=>2, "d"=>3}, @vertices=["a", "b", "c", "d"]> => nil
direct_predecessors_of
direct_predecessors_of('d')は、dへ直接線を引いている要素の集合を返す。
いまの場合、add_edge('c', 'd')でcからdへ線を引いたので、cが返る。
predecessors_of('d')は、dへたどりつける線が存在する要素の集合を返す。
たとえば、add_edge('a', 'b')、add_edge('b', 'c')、add_edge('c', 'd')としていたので、
a→b→c→dで、aはpredecessors_of('d')で返される要素の1つとなっている。
これと対応するような、direct_successors_of、successors_ofというメソッドもある。
>> g.direct_predecessors_of('d').sort => ["c"] >> g.predecessors_of('d').sort => ["a", "b", "c"]
remove_edge
remove_edge("a", "b")は、add_edge("a", "b")を取り消す。aからbへの線を消すことに値する。
>> g.remove_edge('a', 'b') => {} >> pp g #<Nanoc3::DirectedGraph:0xfc0 @from_graph={"a"=>#<Set: {}>, "b"=>#<Set: {"c"}>, "c"=>#<Set: {"d"}>}, @predecessors={}, @successors={}, @to_graph={"b"=>#<Set: {}>, "c"=>#<Set: {"b"}>, "d"=>#<Set: {"c"}>}, @vertice_indexes={"a"=>0, "b"=>1, "c"=>2, "d"=>3}, @vertices=["a", "b", "c", "d"]> => nil
これぐらい概要を書いておけば、あとで思い出すときもすぐ思い出せるかな。