script.aculo.usを読み解く

第6回 effects.js(後編)基礎エフェクトの組み合わせからなる15種類の複合エフェクト

この記事を読むのに必要な時間:およそ 23.5 分

その他のエフェクト

Effect.Morph

914~1003行目のEffect.Morphは,目標のCSSに向かって変化するエフェクトです。基本エフェクトの組み合わせではありません。バージョン1.7で導入されました。まだ文書化が進んでいない機能です。まずはmir.aculo.usで公開されているデモをご覧ください。このように使います。

new Effect.Morph('error_message',{
  style:'background:#f00; color:#fff;'+
    'border: 20px solid #f88; font-size:2em',
  duration:0.8
});

0914:Effect.Morph = Class.create(Effect.Base, {
0915:  initialize: function(element) {
0916:    this.element = $(element);
0917:    if (!this.element) throw(Effect._elementDoesNotExistError);
0918:    var options = Object.extend({
0919:      style: { }
0920:    }, arguments[1] || { });
0921:    
0922:    if (!Object.isString(options.style)) this.style = $H(options.style);
0923:    else {
0924:      if (options.style.include(':'))
0925:        this.style = options.style.parseStyle();
0926:      else {
0927:        this.element.addClassName(options.style);
0928:        this.style = $H(this.element.getStyles());
0929:        this.element.removeClassName(options.style);
0930:        var css = this.element.getStyles();
0931:        this.style = this.style.reject(function(style) {
0932:          return style.value == css[style.key];
0933:        });
0934:        options.afterFinishInternal = function(effect) {
0935:          effect.element.addClassName(effect.options.style);
0936:          effect.transforms.each(function(transform) {
0937:            effect.element.style[transform.style] = '';
0938:          });
0939:        }
0940:      }
0941:    }
0942:    this.start(options);
0943:  },
0944:  

915~944行目のinitializeは,初期化をする関数です。

918行目で,options.styleのデフォルトは{}です。何もしません。

options.styleは,オブジェクトでプロパティ名と値の組を渡す方法と,文字列で"background-color: '#ffff99', font-size: 15pt, opacity: 0.5"となどと渡す方法と,CSSクラス名を渡す方法が用意されています。

922行目で,オブジェクトの方法なら,ハッシュテーブルに変換するだけです。

924行目で,文字列の方法なら,':'を含んでいることで判断します。後述のparseStyleでパースし,同様のハッシュテーブルに変換します。

926行目で、CSSクラス名の方法なら、以下の処理をします。

927行目で、一時的にそのクラス名を要素に追加します。

928行目で、要素のCSSスタイルを取得して、ハッシュテーブルにします。

929行目で、そのクラス名を要素から削除して元に戻します。

930行目で、クラス名を追加していない状態の、要素のCSSスタイルを取得します。

931行目で、rejectメソッドを使って、クラス名を追加してもしなくても値が変わらなかったプロパティを、処理から外します。

934行目で、終了後のフックで、クラス名を実際に追加すると同時に、クラス名を追加することで変化があったプロパティをスタイルから外します

0945:  setup: function(){
0946:    function parseColor(color){
0947:      if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
0948:      color = color.parseColor();
0949:      return $R(0,2).map(function(i){
0950:        return parseInt( color.slice(i*2+1,i*2+3), 16 ) 
0951:      });
0952:    }
0953:    this.transforms = this.style.map(function(pair){
0954:      var property = pair[0], value = pair[1], unit = null;
0955:
0956:      if (value.parseColor('#zzzzzz') != '#zzzzzz') {
0957:        value = value.parseColor();
0958:        unit  = 'color';
0959:      } else if (property == 'opacity') {
0960:        value = parseFloat(value);
0961:        if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
0962:          this.element.setStyle({zoom: 1});
0963:      } else if (Element.CSS_LENGTH.test(value)) {
0964:          var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
0965:          value = parseFloat(components[1]);
0966:          unit = (components.length == 3) ? components[2] : null;
0967:      }
0968:
0969:      var originalValue = this.element.getStyle(property);
0970:      return { 
0971:        style: property.camelize(), 
0972:        originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), 
0973:        targetValue: unit=='color' ? parseColor(value) : value,
0974:        unit: unit
0975:      };
0976:    }.bind(this)).reject(function(transform){
0977:      return (
0978:        (transform.originalValue == transform.targetValue) ||
0979:        (
0980:          transform.unit != 'color' &&
0981:          (isNaN(transform.originalValue) || isNaN(transform.targetValue))
0982:        )
0983:      )
0984:    });
0985:  },

945~985行目のsetupは,エフェクトをかける前の下準備をする関数です。

946~952行目のparseColorは,引数に色を表現した文字列をとって,前回解説したString.prototype.parseColorを内部的に使ったうえで,その結果を3原色の配列[0~255,0~255,0~255]にして返す関数です。

947行目で,引数無しで呼ばれた場合と,引数が'rgba(0,0,0,0)'か'transparent'だった場合は,'#ffffff'(白)と解釈します。

948行目で,String.prototype.parseColorでパースして,'rgb(...'や'#xxx'や'#xxxxxx'といった指定を'#xxxxxx'に統一します。

949行目で,統一した'#xxxxxx'の16進数を,10進数に解釈して,3原色の配列[0~255,0~255,0~255]にして返します。これはEffect.Highlightにも似た処理がありました。以上でParsecolorの定義は終わりです。

953行目で,initializeで作った,CSSのプロパティ名と値のハッシュテーブルであるthis.styleに,map関数を使います。

954行目で,propertyにプロパティ名を,valueに値を,unit(日本語で"単位"です)にnullを,それぞれ代入します。

956~967行目で,3パターン(色関係のプロパティの場合,透明度の場合,長さ関係のプロパティの場合)の場合分けをします。

第1のパターンでは,値の形から,プロパティが色関係のものかどうか調べます。

956行目で,値が色を表現した文字列('rgb(...)'や'#xxx'や'#xxxxxx')かどうか確かめます。前回解説したとおり,String.prototype.parseColorには,パースに失敗したら引数を返す(この場合'#zzzzzz')機能があるので,このif文は,"valueをparseColorでパースして失敗しなかったら"という意味です。

957行目で,valueが色を表現した文字列('rgb(...)'や'#xxx'や'#xxxxxx')であったら,'#xxxxxx'の形に統一します。

958行目で,単位は'color',つまり色関係のプロパティということです。

第2のパターンでは,プロパティ名が'opacity'かを調べます。

959行目で,IEのCSSのバグを回避するために,CSSのzoomプロパティを1にします。

第3のパターンでは,値の形から,後述のCSS_LENGTHを使って,プロパティが長さ関係のものかどうか調べます。

964行目で,正規表現で,+3.0emや-10pxなどを,数字と単位にわけます。この正規表現の3つのエスケープは、実は必要ありません。

965行目で,valueには,数字の部分が入ります。

966行目で,unitには,単位の部分が入ります(正規表現が思惑どおり機能しなければ,nullが入ります)⁠

969行目で,originalValueに,エフェクトのプロパティの初期値として,現在の要素のCSSのpropertyの値を入れます。

970~975行目で,プロパティ名と,プロパティの初期値と,プロパティの目標値と,単位の情報を返します。ここまでがmap関数です。

976行目で,map関数の結果を走査して,余分なものを取り除きます。初期値と目標値に差がないものと,初期値か目標値が数値ではないもの(ただし色関係は除く)です。

以上の結果を,this.transformsに代入します。

0986:  update: function(position) {
0987:    var style = { }, transform, i = this.transforms.length;
0988:    while(i--)
0989:      style[(transform = this.transforms[i]).style] = 
0990:        transform.unit=='color' ? '#'+
0991:          (Math.round(transform.originalValue[0]+
0992:            (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
0993:          (Math.round(transform.originalValue[1]+
0994:            (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
0995:          (Math.round(transform.originalValue[2]+
0996:            (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
0997:        (transform.originalValue +
0998:          (transform.targetValue - transform.originalValue) * position).toFixed(3) + 
0999:            (transform.unit === null ? '' : transform.unit);
1000:    this.element.setStyle(style, true);
1001:  }
1002:});
1003:

986~1003行目のupdateは,実際にプロパティの値をpositionに応じて変える関数です。この関数は頻繁に呼ばれることになるので,whileを使うなど性能を意識して書かれています。

988行目で,setupで作ったthis.transformsを,whileループで走査します。

989行目で,unitが'color'なら色関係のプロパティなので,3原色の配列から現在の色を計算します。

997行目で,そうでなければ透明度か長さ関係のプロパティなので,現在の値を計算して,toFixed(3)で小数点第3位で数値を丸めます。単位があれば付け加えます。

1000行目で,以上の結果を要素のCSSに反映します。setStyleで2番めの引数にtrueを渡しているのは,Prototype.jsの公式APIドキュメントにはまだ記載されていませんが,内部での自動的なString.camelized()呼び出しを省くオプションです。無駄が減ります(参考 「prototype.jsを読み解く」第5回)⁠

著者プロフィール

源馬照明(げんまてるあき)

名古屋大学大学院多元数理科学研究科1年。学部生のときにSchemeの素晴らしさを知ったのをきっかけに,関数型言語の世界へ。JavaScriptに,ブラウザからすぐに試せる関数型言語としての魅力と将来性を感じている。

ブログ:Gemmaの日記

コメント

コメントの記入