#!ruby -Ks # -*- mode: ruby; encoding: sjis -*- # Last updated: <2014/05/29 08:56:24 +0900> require "nokogiri" require "dxruby" require "find" require 'benchmark' # swfから変換したxmlを読み込んでDXRubyで再生するクラス # # @note swf から xml への変換は、swfmill を利用。 # @note DXRuby、Nokogiri が必要。 # @note License : Public domain # # @example # require "playxmlmove" # m = PlayXmlMovie.new("hoge.xml") # m.init # Window.loop do # break unless m.play # end # class PlayXmlMovie DBG = false TMDBG = false BMDBG = false # 論理ピクセル値 TWIP = 20.0 # ---------------------------------------- # 色変更をするためのShader # class SpriteShader < Shader hlsl = <; }; float4 PS(float2 input : TEXCOORD0) : COLOR0 { float4 output; output = tex2D( Samp0, input ); output.r = (output.r * g_mul[0]) + g_ofs[0]; output.g = (output.g * g_mul[1]) + g_ofs[1]; output.b = (output.b * g_mul[2]) + g_ofs[2]; output.a = (output.a * g_mul[3]) + g_ofs[3]; return output; } technique { pass { PixelShader = compile ps_2_0 PS(); } } EOS @@core = Shader::Core.new(hlsl, {:g_mul => :float, :g_ofs => :float}) def initialize super(@@core, "TShader") self.g_mul = [1.0, 1.0, 1.0, 1.0] self.g_ofs = [0.0, 0.0, 0.0, 0.0] @col = [1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0] class << self protected :g_mul, :g_mul=, :g_ofs, :g_ofs= end end def set_color(a) v = 256.0 self.g_mul = [ a[0] / v, a[1] / v, a[2] / v, a[3] / v] v = 255.0 self.g_ofs = [ a[4] / v, a[5] / v, a[6] / v, a[7] / v] @col = a end end # ---------------------------------------- # MATRIX情報格納用クラス # class TMatrix attr_accessor :trans_x, :trans_y attr_accessor :scale_x, :scale_y attr_accessor :skew_x, :skew_y attr_accessor :color def initialize(trans_x = 0, trans_y = 0, scale_x = 1, scale_y = 1, skew_x = 0, skew_y = 0, mul_r = 256, mul_g = 256, mul_b = 256, mul_a = 256, ofs_r = 0, ofs_g = 0, ofs_b = 0, ofs_a = 0) self.trans_x, self.trans_y = trans_x,trans_y self.scale_x, self.scale_y = scale_x, scale_y self.skew_x, self.skew_y = skew_x, skew_y self.color = [ mul_r, mul_g, mul_b, mul_a, ofs_r, ofs_g, ofs_b, ofs_a] end # # 属性値を更新 # # @param [Hash] v 属性と値のハッシュ # def set_value(v) self.trans_x = v["transX"].to_f if v.has_key?("transX") self.trans_y = v["transY"].to_f if v.has_key?("transY") self.scale_x = v["scaleX"].to_f if v.has_key?("scaleX") self.scale_y = v["scaleY"].to_f if v.has_key?("scaleY") self.skew_x = (v.has_key?("skewX"))? v["skewX"].to_f : 0 self.skew_y = (v.has_key?("skewY"))? v["skewY"].to_f : 0 [ "factorRed", "factorGreen", "factorBlue", "factorAlpha", "offsetRed", "offsetGreen", "offsetBlue", "offsetAlpha" ].each_with_index do |s, i| self.color[i] = v[s].to_f if v.has_key?(s) end end # # 座標変換 # # @param [Number] x x座標 # @param [Number] y y座標 # @return [Array] 座標変換後のx,y座標 # def trans(x, y) nx = (x * self.scale_x) + (y * self.skew_y) + self.trans_x ny = (x * self.skew_x) + (y * self.scale_y) + self.trans_y return nx, ny end # # 色変換パラメータを更新 # # @param [Array] c 色変更情報の配列 # @return [Array] 変更後の色変更情報配列 # def trans_color(c) nc = Array.new(8) for i in 0..3 do nc[i] = c[i] * self.color[i] / 256.0 nc[i] = 512 if nc[i] > 512 nc[i] = -512 if nc[i] < -512 end for i in 4..7 do nc[i] = c[i] + self.color[i] nc[i] = 255 if nc[i] > 255 nc[i] = -256 if nc[i] < -256 end return nc end # # 色変換が必要かどうかを返す # # @param [Array] c 色変換情報配列 # @return [Boolean] trueなら色変換が必要。falseなら色変換は不要。 # def TMatrix.trans_color?(c) [ [0, 256.0], [1, 256.0], [2, 256.0], [4, 0.0], [5, 0.0], [6, 0.0] ].each do |n| return true if c[n[0]] != n[1] end return false end # # 色変換情報を格納するハッシュを初期化する # # @param [Hash] v 色変換情報格納用ハッシュ # @return [Hash] 初期化したハッシュを返す # def TMatrix.init_color(v) v.merge!({ "factorRed" => 256, "factorGreen" => 256, "factorBlue" => 256, "factorAlpha" => 256, "offsetRed" => 0, "offsetGreen" => 0, "offsetBlue" => 0, "offsetAlpha" => 0 }) return v end end # ---------------------------------------- # 画像を格納するキャラobjクラス # class CharaObjImage attr_accessor :id, :src, :image def initialize(id, fname) self.id = id self.src = fname self.image = Image.load(fname) # puts "id[#{id}] Load Image ok #{fname}" end end # ---------------------------------------- # シェイプを格納するクラス # class CharaObjShape attr_accessor :id, :imgs def initialize(id, imgs) self.id = id self.imgs = imgs end end # ---------------------------------------- # スプライト(MovieClip)を格納するクラス # class CharaObjSprite attr_accessor :id attr_accessor :start_node def initialize(id, start_node) self.id = id self.start_node = start_node end end # ---------------------------------------- # フォント名を格納するクラス # class CharaObjFont attr_accessor :id, :fontname def initialize(id, fontname) self.id = id self.fontname = fontname puts "id[#{id}] font [#{fontname}]" if PlayXmlMovie::DBG end end # ---------------------------------------- # テキストを格納するクラス # class CharaObjText attr_accessor :id, :font, :text, :size, :fontname, :col def initialize(id, text, size, fontname, col = [0, 0, 0, 255]) self.id = id self.text = text self.size = size self.fontname = fontname self.font = Font.new(self.size, fontname) self.col = col if PlayXmlMovie::DBG puts "id[#{id}] DefineEditText \"#{text}\",#{size},\"#{fontname}\"" end end end # ---------------------------------------- # 描画用クラス # class MySprite attr_accessor :objs attr_accessor :cobj attr_accessor :pobj attr_accessor :z attr_accessor :mat attr_accessor :sprs attr_accessor :rt attr_accessor :shader attr_accessor :enable_loop # コンストラクタ # # @param [Hash] objs キャラobj情報が入ったハッシュ # @param [Number] cid 発生させたいキャラobjのID # @param [Object] pobj 親スプライト。nilなら親は居ない # @param [Number] depth 描画深度 # @param [Hash] v MATRIX情報+半透明度を格納したハッシュ # def initialize(objs, cid, pobj, depth, v) self.objs = objs self.cobj = self.objs[cid] self.pobj = pobj self.z = depth self.mat = TMatrix.new self.sprs = Hash.new self.rt = nil self.shader = nil update(depth, v) end # 描画に必要な値を設定 # # @param [depth] depth 描画深度 # @param [Hash] v MATRIX情報と半透明度情報を格納したハッシュ # def update(depth, v) self.z = depth self.mat.set_value(v) end # RenderTargetを解放 # def dispose if self.sprs != nil self.sprs.keys {|k| self.sprs[k].dispose } end if self.rt != nil self.rt.dispose unless self.rt.disposed? end end # 拡大縮小、半透明、回転描画 # # @param [Object] img Imageクラス # @param [Object] im_mat TMatrixクラス # def draw_image(img, img_mat) pobj = self.pobj # 矩形の4点を配列にする w = img.width h = img.height p = [0, 0, w, 0, w, h, 0, h] c = Marshal.load(Marshal.dump(self.mat.color)) # Shapeに割り当てられたMATRIXで変形 4.times do |i| p[i * 2], p[i * 2 + 1] = img_mat.trans(p[i * 2], p[i * 2 + 1]) end # 自身のMATRIXで変形 4.times do |i| p[i * 2], p[i * 2 + 1] = self.mat.trans(p[i * 2], p[i * 2 + 1]) end # 親スプライトのMATRIXで変形 while pobj != nil 4.times do |i| p[i * 2], p[i * 2 + 1] = pobj.mat.trans(p[i * 2], p[i * 2 + 1]) end c = pobj.mat.trans_color(c) pobj = pobj.pobj end # 論理ピクセル値から画面座標に変換 8.times {|i| p[i] /= PlayXmlMovie::TWIP } if TMatrix.trans_color?(c) # 色変換が必要 # 色変更用のShaderはアルファチャンネル付き画像描画がおかしいので、 # 色変更が必要な時だけ使うことにする。 self.rt = RenderTarget.new(Window.width, Window.height) if self.rt == nil self.shader = SpriteShader.new if self.shader == nil self.rt.drawMorph(p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], img, :dividex => 3, :dividey => 3) self.rt.update self.shader.set_color(c) Window.drawEx(0, 0, self.rt, :shader => self.shader) else # 色変換は不要 a = (255 * c[3] / 256.0).to_i if a != 255 # 半透明描画 Window.drawMorph(p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], img, :dividex => 3, :dividey => 3, :alpha => a, :blend => :alpha) else # 不透明描画 Window.drawMorph(p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], img, :dividex => 3, :dividey => 3) end end end # フォントを描画 # # @param [String] text 描画文字列 # @param [Object] font Fontオブジェクト # @param [Array] c RGBA配列 # def draw_font(text, font, c) x = self.mat.trans_x / PlayXmlMovie::TWIP y = self.mat.trans_y / PlayXmlMovie::TWIP Window.drawFontEx(x, y, text, font, :color => [c[0], c[1], c[2]], :alpha => c[3], :blend => :alpha, :aa => true) end # 再生描画処理 # # @param [Hash] objs キャラobj情報が入ったハッシュ # @return [Boolean] trueなら再生中, falseなら再生終了 # def play return true end end # ---------------------------------------- # 画像描画obj # class ImageObj < MySprite # コンストラクタ # # @param [Hash] objs キャラobj情報が入ったハッシュ # @param [Number] cid 発生させたいキャラobjのID # @param [Object] pobj 親スプライト。nilなら親は居ない # @param [Number] depth 描画深度 # @param [Hash] v MATRIX情報+半透明度を格納したハッシュ # def initialize(objs, cid, pobj, depth, v) super(objs, cid, pobj, depth, v) end # 再生描画処理 # # @return [Boolean] trueなら再生中, falseなら再生終了 # def play draw_image(self.cobj.image, TMatrix.new) return true end end # ---------------------------------------- # シェイプ描画obj # class ShapeObj < MySprite # コンストラクタ # # @param [Hash] objs キャラobj情報が入ったハッシュ # @param [Number] cid 発生させたいキャラobjのID # @param [Object] pobj 親スプライト。nilなら親は居ない # @param [Number] depth 描画深度 # @param [Hash] v MATRIX情報+半透明度を格納したハッシュ # def initialize(objs, cid, pobj, depth, v) super(objs, cid, pobj, depth, v) end # 再生描画処理 # # @return [Boolean] trueなら再生中, falseなら再生終了 # def play self.cobj.imgs.each do |v| draw_image(v[0].image, v[1]) end return true end end # ---------------------------------------- # スプライト描画obj # class SpriteObj < MySprite attr_accessor :node attr_accessor :now_frame attr_accessor :enable_loop # コンストラクタ # # @param [Hash] objs キャラobj情報が入ったハッシュ # @param [Number] cid 発生させたいキャラobjのID # @param [Object] cobj 発生させたいキャラobj # @param [Object] pobj 親スプライト。nilなら親は居ない # @param [Number] depth 描画深度 # @param [Hash] v MATRIX情報+半透明度を格納したハッシュ # @param [Boolean] looped trueならループ再生 # def initialize(objs, cid, pobj, depth, v, looped = true) super(objs, cid, pobj, depth, v) self.node = cobj.start_node self.now_frame = 0 self.enable_loop = looped end # 再生描画処理 # # @return [Boolean] trueなら再生中, falseなら再生終了 # def play pobj = self endfg = false until endfg case self.node.name when "PlaceObject2" # シェイプやスプライトを配置、もしくは移動 nn = self.node rn = nn["replace"].to_i depth = nn["depth"].to_i id = nn["objectID"] id = id.to_i if id != nil # 属性を取り出す v = {} nn.children.each do |n| n.children.each do |m| if m.name == "Transform" # 変形情報 m.keys.each do |k| a = m.attribute(k) v[a.name] = a.value.to_f end elsif m.name == "ColorTransform2" # 色変換情報 v = TMatrix.init_color(v) m.keys.each do |k| a = m.attribute(k) v[a.name] = a.value.to_f end end end end if id != nil abort "Error: Not found ChrObj[#{id}]" unless self.objs.has_key?(id) case self.objs[id] when PlayXmlMovie::CharaObjImage self.sprs[depth] = ImageObj.new(self.objs, id, pobj, depth, v) when PlayXmlMovie::CharaObjShape self.sprs[depth] = ShapeObj.new(self.objs, id, pobj, depth, v) when PlayXmlMovie::CharaObjSprite self.sprs[depth] = SpriteObj.new(self.objs, id, pobj, depth, v) when PlayXmlMovie::CharaObjText self.sprs[depth] = TextObj.new(self.objs, id, pobj, depth, v) end elsif rn == 1 self.sprs[depth].update(depth, v) if self.sprs.has_key?(depth) end if TMDBG s = "PLACE, replace=#{rn}, depth=#{depth}, id=#{id}, {" v.each_key {|k| s += "#{k} => #{v[k]}, "} s += "}" puts s end when "RemoveObject2" # シェイプやスプライトを削除 depth = node["depth"].to_i if self.sprs.has_key?(depth) self.sprs[depth].dispose self.sprs.delete(depth) end puts "REMOVE, depth=#{depth}" if TMDBG when "ShowFrame" # 現在フレームを描画 self.now_frame += 1 puts "ShowFrame #{now_frame}" if TMDBG endfg = true when "End" # タグが無くなった endfg = true puts "End" if TMDBG break else # puts node_name end # 次のノードを読む self.node = self.node.next end # ウインドウに描画 key_list = self.sprs.keys.sort # depthでソート key_list.each {|k| self.sprs[k].play } return true if self.node.name != "End" # 再生終了 sprs = Hash.new if self.enable_loop self.node = self.cobj.start_node self.now_frame = 0 end return false end end # ---------------------------------------- # テキスト描画obj # class TextObj < MySprite # コンストラクタ # # @param [Hash] objs キャラobj情報が入ったハッシュ # @param [Number] cid 発生させたいキャラobjのID # @param [Object] pobj 親スプライト。nilなら親は居ない # @param [Number] depth 描画深度 # @param [Hash] v MATRIX情報+半透明度を格納したハッシュ # def initialize(objs, cid, pobj, depth, v) super(objs, cid, pobj, depth, v) end # 再生描画処理 # # @return [Boolean] trueなら再生中, falseなら再生終了 # def play o = self.cobj draw_font(o.text, o.font, o.col) return true end end # ---------------------------------------- # コンストラクタ # # @param [String] infile xmlファイルのパス # def initialize(infile) @infile = infile abort "Error : Not found #{infile}" unless File.exist?(infile) if BMDBG Benchmark.bm(15) do |x| x.report("xml parse:") { @doc = Nokogiri::XML(File.open(infile)) {|config| config.noblanks} } x.report("get fps") { @framerate, @frames = get_framerate(@doc) @wdw_w, @wdw_h = get_widnow_size(@doc) @bgcol = get_bgcolor(@doc) } @objs = Hash.new x.report("load image:") { load_image(@doc, infile) } x.report("load font:") { load_font(@doc) } x.report("load shape:") { load_shape(@doc) } x.report("load sprite:") { load_sprite(@doc) n = @doc.at("//swf/Header/tags/PlaceObject2") @objs[0] = CharaObjSprite.new(0, n) } end else # xmlを解析 @doc = Nokogiri::XML(File.open(infile)) {|config| config.noblanks} @framerate, @frames = get_framerate(@doc) @wdw_w, @wdw_h = get_widnow_size(@doc) @bgcol = get_bgcolor(@doc) # 画像、シェイプ、スプライトを登録 @objs = Hash.new load_image(@doc, infile) load_font(@doc) load_shape(@doc) load_sprite(@doc) # rootスプライトを登録 puts "\nid[0] DefineSprite" if DBG n = @doc.at("//swf/Header/tags/PlaceObject2") @objs[0] = CharaObjSprite.new(0, n) end end # # フレームレート、フレーム数を取得 # # @param [Object] doc xmlノード情報 # @return [Array] フレームレート, フレーム数を配列で返す # def get_framerate(doc) n = doc.at("//Header") fpsv = n["framerate"].to_i frms = n["frames"].to_i puts "framerate=#{fpsv} , frames=#{frms}" if DBG return fpsv, frms end # # 画面サイズを取得 # # @param [Object] doc xmlノード情報 # @return [Array] 画面横幅, 画面縦幅を配列で返す # def get_widnow_size(doc) n = doc.at("//Header/size/Rectangle") # x = n["left"].to_i / TWIP # y = n["top"].to_i / TWIP w = n["right"].to_i / TWIP h = n["bottom"].to_i / TWIP puts "Window size : #{w}, #{h}" if DBG return w, h end # # 背景色を取得 # # @param [Object] doc xmlノード情報 # @return [Array] 背景色red, green, blueを配列で返す # def get_bgcolor(doc) n = doc.at("//SetBackgroundColor/color/Color") c = [] ["red", "green", "blue"].each {|k| c.push(n[k].to_i)} return c end # # 画像をロード # # @param [Object] doc xmlノード情報 # @param [String] infile xmlファイルパス # def load_image(doc, infile) imglist = get_img_file_list(infile) # 画像ファイルパスの一覧を取得 # 検索するタグ名を列挙 taglst = "//DefineBitsJPEG2|//DefineBitsLossless2|//DefineBitsLossless" doc.xpath(taglst).each do |n| id = n["objectID"].to_i n.content = "" unless imglist.has_key?(id) # IDと一致する画像ファイルパスが存在しない puts "Not found image file : id = #{id}" next end # IDと一致する画像ファイルパスが存在する fn = imglist[id] n["src"] = fn @objs[id] = CharaObjImage.new(id, fn) puts "id[#{id}] Load Image : #{fn}" if DBG end end # # xmlファイルパスを元にして、全画像ファイルのパス一覧を取得 # # @param [String] xmlファイルパス # @return [Hash] 画像ファイルパスを格納したハッシュ # def get_img_file_list(infile) bsn = File.basename(infile, ".xml") # ファイルの拡張子を取り除いた文字列を取得 dir = File::dirname(infile) # ファイルのフォルダパスを取得 imglist = {} # サブフォルダ以下も探索して、拡張子が画像ファイル形式なら記録していく Find.find(dir) do |f| next unless FileTest.file?(f) imglist[$1.to_i] = f if /#{bsn}\[(\d+)\]\.(png|jpg|bmp|gif)/ =~ f end return imglist end # # フォント情報やテキスト情報を読み込み # # @param [Object] doc xmlノード情報 # def load_font(doc) lst = Hash.new lst.merge!( { "_ゴシック" => "MS UI Gothic", "_明朝" => "MS P明朝", "_等幅" => "MS ゴシック", "_sans" => "Arial", "_serif" => "Times New Roman", "_typewriter" => "Courier" }) # 使用している全フォント名を取得 doc.xpath("//DefineFont2|//DefineFont3").each do |n| id = n["objectID"].to_i fnt = n["name"].encode("Windows-31J") # xmlはutf-8なのでsjisに変換 if lst.has_key?(fnt) fnt = lst[fnt] @objs[id] = CharaObjFont.new(id, fnt) else @objs[id] = CharaObjFont.new(id, fnt) end end # 使用している静止テキスト情報を取得 doc.xpath("//DefineEditText").each do |n| id = n["objectID"].to_i fid = n["fontRef"].to_i # フォント種類ID abort "Error: Not found font [#{fid}]" unless @objs.has_key?(fid) size = n["fontHeight"].to_f / PlayXmlMovie::TWIP # Flash CS5で作成したswfは、initialText に html が入っている。 # htmlタグを除去して描画文字列を取得する nm = Nokogiri::HTML(n["initialText"]) text = nm.content.encode("Windows-31J") col = [0, 0, 0, 255] c = n.at(".//color/Color") if c != nil col[0] = c["red"].to_i col[1] = c["green"].to_i col[2] = c["blue"].to_i col[3] = c["alpha"].to_i end fontname = @objs[fid].fontname @objs[id] = CharaObjText.new(id, text, size, fontname, col) end end # # 画像を内包するShapeを生成 # # @param [Object] doc xmlノード情報 # def load_shape(doc) doc.xpath("//DefineShape|//DefineShape2").each do |n| id = n["objectID"].to_i sn = n.xpath(".//bounds/Rectangle")[0] # x = sn["left"].to_i / TWIP # y = sn["top"].to_i / TWIP w = sn["right"].to_i / TWIP h = sn["bottom"].to_i / TWIP imgs = [] bn = n.xpath(".//styles/StyleList/fillStyles") bn.xpath(".//ClippedBitmap|.//ClippedBitmap2").each do |m| img_id = m["objectID"].to_i if img_id == 65535 # objectID が 65535 の時がある。正体不明 puts "id[#{id}] Unknown Image [#{img_id}]" if DBG next end unless @objs.has_key?(img_id) # 画像が存在しない abort "Error : id[#{id}] Not found Image [#{img_id}] #{w} x #{h}" end # 画像が存在するので、シェイプに割り当てる # MATRXI情報も反映させる puts "id[#{id}] Shape = bitmap[#{img_id}]" if DBG dic = {} mn = m.at(".//matrix/Transform") mn.keys.each do |k| atr = mn.attribute(k) dic[atr.name] = atr.value.to_f end v = TMatrix.new v.set_value(dic) imgs.push([@objs[img_id], v]) end @objs[id] = CharaObjShape.new(id, imgs) end end # # スプライトを登録 # def load_sprite(doc) doc.xpath("//DefineSprite").each do |n| id = n["objectID"].to_i frames = n["frames"].to_i puts "\nid[#{id}] DefineSprite, frames=#{frames}" if DBG node = n.at(".//PlaceObject2") @objs[id] = CharaObjSprite.new(id, node) end end # 再生開始のための初期化処理 # # @note fpsや背景色が変更されるので、再生が終わったら本来のfps・背景色に戻すこと # # @param [Boolean] init_size ウインドウサイズをxmlに従って変更するかどうか # @param [Boolean] init_fps フレームレートをxmlに従って変更するかどうか # @param [Boolean] init_bgcol 背景色をxmlに従って変更するかどうか # def init(init_size = false, init_fps = true, init_bgcol = true) @now_frame = 0 @root_sprite = SpriteObj.new(@objs, 0, nil, 0, {}) Window.resize(@wdw_w, @wdw_h) if init_size Window.fps = @framerate if init_fps Window.bgcolor = [@bgcol[0], @bgcol[1], @bgcol[2]] if init_bgcol end # 再生描画処理 # # @note メインループから毎フレーム呼ぶこと # # @return [Boolean] trueなら再生中, falseなら再生終了 # def play return @root_sprite.play end # # 多少整理したxmlを標準出力にダンプする # def dump puts @doc.to_xml end end if $0 == __FILE__ # ---------------------------------------- # 使い方の例 m = PlayXmlMovie.new("swfdata/movie0.xml") # m = PlayXmlMovie.new("swfdata/movie1.xml") # m = PlayXmlMovie.new("swfdata/movie2.xml") # m = PlayXmlMovie.new("swfdata/movie3.xml") # m = PlayXmlMovie.new("swfdata/movie4.xml") # 再生開始のための初期化 # fpsや背景色が変更されるので、 # 再生が終わったら本来のfps・背景色に戻すこと m.init # メインループ Window.loop do break if Input.keyPush?(K_ESCAPE) # ESCキーで終了 # 1フレーム分描画 unless m.play m.init # 再生が終了したら、再度頭から再生 end end end