ラインモーションの練習
滑らかにラインを引く練習中。
滑らかにラインを引く練習中。
Progression4では仕様変更により今までのお作法が通用しない部分が結構あって、PRMLLoaderもその一つ。最近はPRMLからシーン構成を生成するので解決法をずっと探ってたんだけど、ようやく動作したのでメモ。同じとこで詰まってる人がいたら参考にしてみて下さい。
ポイントは以下の3つ。
1、PRML内で設定するシーンクラスをあらかじめ宣言しておく(25~26行目)
→ 宣言してないとエラーが出る。
2、親クラスを呼び出すときに第1、第2引数をnullにしておく(34行目)
→ 通常はsuper(“index”, IndexScene, new WebConfig());となるが、PRMLをつかってシーンを生成すると全部自動で設定してくれるため、ここで宣言したidとPRMLで自動宣言されるidが重複してしまいエラーが出る。
3、PRMLLoaderクラス内のmanagerを使う(45~56行目)
→ ここが一番の鬼門。PRMLLoaderを使ってシーン構造を生成した場合はPRMLLoader.managerに展開される。なので「外部同期機能」や「最初のシーンに移動」とかを下記のようにしてしまうと別管理のmanagerとして動作してエラーが出る。
manager.sync = true; manager.goto(manager.syncedSceneId);
この3つのポイントをまとめると、ドキュメントクラスは以下のようになる。
Index.as
package {
import jp.progression.casts.*;
import jp.progression.commands.display.*;
import jp.progression.commands.lists.*;
import jp.progression.commands.net.*;
import jp.progression.commands.tweens.*;
import jp.progression.commands.*;
import jp.progression.config.*;
import jp.progression.data.*;
import jp.progression.debug.*;
import jp.progression.events.*;
import jp.progression.scenes.*;
import flash.events.Event;
import flash.net.URLRequest;
import jp.progression.loader.PRMLLoader;
/**
* ...
* @author ...
*/
public class Index extends CastDocument {
//PRMLで使うシーンクラスを宣言
IndexScene;
MyScene;
/**
* 新しい Index インスタンスを作成します。
*/
public function Index() {
// 自動的に作成される Progression インスタンスの初期設定を行います。
// 生成されたインスタンスにアクセスする場合には manager プロパティを参照してください。
super( null, null, new WebConfig() );
}
/**
* SWF ファイルの読み込みが完了し、stage 及び loaderInfo にアクセス可能になった場合に送出されます。
*/
protected override function atReady():void {
var pl:PRMLLoader = new PRMLLoader(stage, new URLRequest("xml/prml.xml"));
pl.addEventListener(Event.COMPLETE, function(e:Event):void
{
pl.removeEventListener(Event.COMPLETE, arguments.callee);
// 開発者用に Progression の動作状況を出力します。
Debugger.addTarget(pl.manager);
// 外部同期機能を有効化します。
pl.manager.sync = true;
// 最初のシーンに移動します。
pl.manager.goto(pl.manager.syncedSceneId);
// prml.xmlの内容
trace("prml="+pl.manager.root.toXMLString());
});
}
}
}
prml.xml
ExternalInterfaceを使うとJavaScriptと連携できるんだけど、HTMLにJSを書くのはめんどくさいしJSファイルをサーバーにアップするのも手間なのでASだけでJSを動作させるテスト。Flashサイトで良く使いそうなJSをまとめてクラスにしてみた。staticメソッドにしてあるので、インポートした後はnewせずにクラス名から直接呼び出せる。とりあえず10種類用意したので、随時追加していこうかなー。
上記のデモのHTML見てもらえば分かるけど、このFlashから指示してるJSの記述は一切していない。なのでswfファイルだけサーバーにアップすればJSも動く。HTMLにJSのリンクタグとか仕込まなくてもいいのでちょっとだけ楽できるよと。ドキュメントクラスからの使い方はこんな感じ。
private function onClick(e:MouseEvent):void
{
var browser:String = JsUtils.getBrowser();
switch(e.currentTarget.name)
{
//-----[ポップアップを開く]
case "btn1":
var url:String = "http://www.google.co.jp/";
//-----[safariのみ_blankで対応]
if (browser != null && browser.indexOf("Safari") >= 0) navigateToURL(new URLRequest(url), "_blank");
else JsUtils.windowOpen(url, 400, 400, 0, 0, true, false, "yes");
break;
//-----[ウインドウを揺らす]
case "btn2":
JsUtils.windowShake();
break;
//-----[アラートを表示]
case "btn3":
JsUtils.alert(browser);
break;
//-----[現在のURLを取得]
case "btn4":
JsUtils.alert(JsUtils.getLocation());
break;
//-----[ウインドウの移動]
case "btn5":
JsUtils.windowMove(0, 0);
break;
//-----[ウインドウのリサイズ]
case "btn6":
JsUtils.windowResize(400, 400);
break;
//-----[クッキーを保存]
case "btn7":
JsUtils.setCookie("cookiename", "hogehoge", 7);
break;
//-----[クッキーを取得]
case "btn8":
JsUtils.alert(JsUtils.getCookie("cookiename"));
break;
//-----[タイトルの変更]
case "btn9":
JsUtils.setTitle("fugafuga");
break;
//-----[タイトルの取得]
case "btn10":
JsUtils.alert(JsUtils.getTitle());
break;
}
}
やってる事は大したこと無くて、ExternalInterface.call()の中で関数を文字列としてコールしてるだけ。Flash側からは文字列でも、JSに渡された瞬間に関数になって実行さるイメージなのかな。リファラーも取れるので、GoogleやYahooからどんなキーワード検索で飛んできたのかもFlash側に渡してやれる。LPOみたいにFlash内のレイアウトや配置をキーワードに合わせて動的に変更したりも出来るだろうから、色々使い道はあるかも。
JsUtilsクラスはこちらからどうぞ。
こないだ本屋で見た上野樹里が表紙の雑誌「Levi’s book ROCKS」がえらいかっこ良かったのでFlashで真似てみる。透明度0.9くらいの青いベタ塗りを写真の上に引いて、ブレンドモードを乗算にして写真の彩度とコントラストを少々弄るとそれっぽくなった。
うーん、何かに使えないかなー。
ティザーサイト等で良く使われる「オープンまであと何日」みたいなカウントダウンを作ってみる。Flashだけで作るとローカルタイムに依存するので、ユーザーに時計を進められるとネタばれすることも。たまに見かけるけど。上のデモはアクセスした日から常に1ヶ月先をカウントダウンします。残りゼロになっても、何も起きませんよ。
サーバーから時間を取得した後にFlash側で経過時間を足して、目的の時間から差分を取ることでどのPCから見てもカウントダウンの時刻を同期することができる。
カウンタークラスにしてみた。ドキュメントクラスでは下記のように使います。
package
{
import flash.display.Sprite;
import flash.text.TextField;
import info.five.net.Counter;
import info.five.events.CountEvent;
/**
* ...
* @author 5ive
*/
public class Main extends Sprite
{
public function Main():void
{
var d:Date = new Date();
var ctr:Counter = new Counter(d.fullYear, d.month + 2, d.date);
ctr.getServerTime("servertime.php");
ctr.addEventListener(Counter.CHANGE, onChange);
ctr.addEventListener(Counter.FINISH, onFinish);
}
private function onFinish(e:CountEvent):void
{
trace("finish!");
}
private function onChange(e:CountEvent):void
{
countText.text = e.day + "d " + e.hour + "h " + e.minute + "m " + e.second + "s " + e.milisecond + "ms";
}
}
}
new Counter()の引数でカウントさせたい未来の日付と時間を渡す。getServerTime()メソッドでPHPへのパスを渡す。後はCHANGEとFINISHイベントをリスナーに登録して時間が流れるのを待つだけ。
Counter.as
package info.five.net
{
import flash.display.Sprite;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLVariables;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.events.EventDispatcher;
import flash.utils.getTimer;
import info.five.events.CountEvent;
/**
* ...
* @author 5ive
*/
public class Counter extends EventDispatcher
{
public static const CHANGE:String = "change";
public static const FINISH:String = "finish";
private var _loader:URLLoader;
private var _serverTime:Number;
private var _swfTime:Number;
private var _futureDate:Date;
private var _sp:Sprite;
public function Counter(year:uint, month:uint, day:uint, hour:uint = 0, monuites:uint = 0, second:uint = 0, milisecond:uint = 0):void
{
_sp = new Sprite();
_futureDate = new Date(year, month - 1, day, hour, monuites, second, milisecond);
}
//------------------------------
// サーバーへの問い合わせ
//------------------------------
public function getServerTime(url:String):void
{
//-----[キャッシュ対策]
var date:Date = new Date();
url += "?t=" + date.getTime();
//-----[サーバーの時間取得]
_loader = new URLLoader();
_loader.addEventListener(Event.COMPLETE, onComplete);
_loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
_loader.load(new URLRequest(url));
}
//------------------------------
// IOErrorEvent
//------------------------------
private function onError(e:IOErrorEvent):void
{
trace("error=" + e.text);
}
//------------------------------
// サーバーの時間取得
//------------------------------
private function onComplete(e:Event):void
{
var urlVariables:URLVariables = new URLVariables(_loader.data);
_serverTime = Number(urlVariables.returnValue);
_swfTime = getTimer();
//-----[URLLoaderの削除]
_loader.removeEventListener(Event.COMPLETE, onComplete);
_loader.removeEventListener(IOErrorEvent.IO_ERROR, onError);
_loader.data = null;
_loader = null;
//-----[タイマーの開始]
if (_futureDate.getTime() < _serverTime)
{
trace("[error]:カウントする日付が過去を指定しています。");
}
else
{
if (_sp != null) _sp.addEventListener(Event.ENTER_FRAME, onTicks);
}
}
//------------------------------
// タイマーの更新
//------------------------------
private function onTicks(e:Event):void
{
var passage:Number = _serverTime + (getTimer() - _swfTime);
var diff:Number = _futureDate.getTime() - passage;
var day:* = Math.floor(diff / (24 * 60 * 60 * 1000));
var total:* = Math.floor(diff / (60 * 60 * 1000));
var hour:* = total - (day * 24);
var minute:* = Math.floor(diff / (60 * 1000)) - (total * 60);
var second:* = Math.ceil(diff / 1000) - ((minute * 60) + (total * 60 * 60)) - 1;
var milisecond:String = diff.toString().substr(diff.toString().length - 3, 3);
//-----[2桁で表示]
day = String(day + 100).substr(1, 2);
hour = String(hour + 100).substr(1, 2);
minute = String(minute + 100).substr(1, 2);
second = String(second + 100).substr(1, 2);
//-----[イベントクラスに値を格納]
var ce:CountEvent = new CountEvent(CountEvent.CHANGE);
if (diff > 100)
{
ce.day = day;
ce.hour = hour;
ce.minute = minute;
ce.second = second;
ce.milisecond = milisecond;
dispatchEvent(ce);
}
else
{
ce.day = "00";
ce.hour = "00";
ce.minute = "00";
ce.second = "00";
ce.milisecond = "000";
dispatchEvent(ce);
dispatchEvent(new CountEvent(CountEvent.FINISH));
kill();
}
}
//------------------------------
// データの破棄
//------------------------------
public function kill():void
{
_sp.removeEventListener(Event.ENTER_FRAME, onTicks);
_sp = null;
}
}
}
Counterクラスではサーバータイムを取得した瞬間にENTER_FRAMEを発動。そこからFlash内での経過ミリ秒を加算していき、目的の時間との差分を計算してCountEventクラスに各時間を格納させる。そのイベントをドキュメントクラスに返して時間を取得させる。
CountEvent.as
package info.five.events
{
import flash.events.Event;
public class CountEvent extends Event
{
public static const CHANGE:String = "change";
public static const FINISH:String = "finish";
public var day:String;
public var hour:String;
public var minute:String;
public var second:String;
public var milisecond:String;
//------------------------------
// コンストラクタ
//------------------------------
public function CountEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false):void
{
super(type, bubbles, cancelable);
}
//------------------------------
// クローン
//------------------------------
public override function clone():Event
{
return new CountEvent(type, bubbles, cancelable);
}
}
}
CountEventクラスには「day」、「hour」、「minute」、「second」、「milisecond」プロパティが用意されているので、ドキュメントクラス側で必要に応じて取得して表示させて下さい。
serverttime.php
<?php
$stamp = microtime();
$ary = split(" ", $stamp);
echo "returnValue=".(string)$ary[1].(string)(int)($ary[0] * 1000);
?>
最後にサーバーに置くPHP。Flash側のgetTimer()のミリ秒と、PHP側のmicrotime()のミリ秒で桁が違うので、桁合わせのためにちょっとだけごにょごにょしてます。microtime()でミリ秒を取得すると小数点付きで分割して計算されるので、一度文字列にしてFlash側に合わせるように変換かけてます。
Flashネタをエントリーするのは久々だなー。先日の福岡てら子で発表した内容ですが、WordPressからProgressionで使うPRMLというXMLを出力する方法。PRMLの詳しい説明は本家リファレンスを参照して下さい。これを使うことにより、WordPressからProgressionにダイレクトにシーンを生成出来るようになる。今回はWordPressのカテゴリーをシーンに見立てて出力しているので、親、子、孫、曾孫といった深い階層のシーン構造も簡単に作ることができる。下記のPHPをWPがインストールされているサーバーのテーマフォルダにアップします。次にWPのメニューからページを新規で追加して、タイトルに「prml」と入力し、右サイドバーの属性のテンプレートから「prml」を選択。この状態でページを公開すると、タイトルの下にパーマリンクが表示されるので、このURLにアクセスすると現在のカテゴリー構造を維持したままのPRMLが出力されるようになる。こんな感じ。
prml.php
<?php /*
Template Name: prml
*/ ?>
<?php
header('Content-Type: text/xml; charset='.get_option('blog_charset'), true);
//-----[PRMLのヘッダー生成]
$title = get_option('blogname');
$header .= '<?xml version="1.0" encoding="'.get_option('blog_charset').'"?'.'>';
$header .= '<prml version="2.0.0" type="text/prml">';
$header .= '<scene name="index" cls="myproject.IndexScene" title="'.$title.'">';
//-----[WPから全階層のカテゴリー取得]
$str = wp_list_categories('orderby=id&echo=0&hide_empty=0&use_desc_for_title=0&title_li=');
$str = strip_tags($str, '<li>');
//-----[PRML形式に変換]
$category = strip_tags($str, '');
$category = str_replace(array("\r\n","\r","\n"), '', $category);
$category = preg_replace('/\s+/', ' ', $category);
$category = ltrim($category);
$category = split(' ', $category);
for($i=0; $i<count($category); $i++)
{
$str = str_replace(">".$category[$i]."\n", $i.">", $str);
}
$str = preg_replace('/"(.*?)"/', '', $str);
$str = str_replace('li', 'scene', $str);
for($i=0; $i<count($category); $i++)
{
$str = str_replace('class='.$i, 'name="'.$category[$i].'" cls="myproject.'.ucfirst($category[$i]).'Scene" title="'.$title." | ".$category[$i].'"', $str);
}
//-----[PRMLとして出力]
echo $header.$str.'</scene></prml>';
?>
このPHPでやっていることはwp_list_categories()で取得したリストタグ付きカテゴリーを力技でPRML形式のXMLに変換してるだけ。ソースを見てもらえば苦笑いできると思います。w
ここではシーン構造のみをPRMLとして出力してるけど、Progressionの各ページ内にWPから画像を読み込んだり、テキストを流し込む場合は個別にXMLを作ったほうが管理しやすいと思う。その場合は以前エントリーしたこの記事が役立つと思います。
今回参考にさせて頂いたのはMotuLogさんのエントリー。ありがとうございます!またflabakaさんは自分とは違ったアプローチでWordPressやMovableTypeとProgressionを連携させる方法をエントリーされてます。
■参考サイト
・MotuLogさん Progression(3.1.52) 動的にシーンを作成する
・flabakaさん ProgressionとWordPressの連携
Progressionでサイトを作る時に、直リンク(ディープリンク)でFlash内のページに直接アクセスされると意図しない状態になることがよくある。トップページから順番に各ページを移動されるのはもちろん問題ないのだけど、いきなり途中のページにアクセスされるとシーンがまだ生成されていなかったり、nullエラーがでたり。作り方が悪いんだろうけども。
Progression側でユーザーが最初にアクセスしたシーンがどこなのかを調べる方法。
var prog:Progression = getProgressionById("index");
var sceneId:String = prog.firstSceneId.toString();
trace(sceneId);
トップページにアクセスされた場合は
/index
アバウトページにアクセスされた場合は
/index/about
みたいなURLの形で取得できるので、ユーザーがどのページから入ってきたのか調べられますよ。
ちなみにProgression側で今どのシーンにいるかを取得するにはcurrentプロパティを使う。
var prog:Progression = getProgressionById("index");
trace(prog.current);
トップページにいる場合は
[IndexScene sceneId="/index" id="index" name="index" group="null"]
アバウトページにいる場合は
[AboutScene sceneId="/index/about" id="about" name="about" group="null"]
と、こんな感じ。
よく見るインターフェイスで、マウスを動かすだけでスクロールする動きを実装してみた。すごくシンプルに考えてみるとモーション自体のスクリプトはこの3行でいける。
percent = scrollContainer.stage.mouseX / (scrollLength - 1); pos = (scrollLength - (scrollContainer.width + _space) - _space) * percent; scrollContainer.x = scrollContainer.x - ((scrollContainer.x - _space - pos) / _friction);
ステージの横幅に対してマウスのx座標が何%の位置にいるのかを割り出して計算させれば意外なほどあっさり実装できる。条件分岐もしなくていいし。クラスにしてみたけど、汎用的に使うにはもうちょいチューニングが必要かも。
ScrollOver.as
package
{
import flash.display.DisplayObjectContainer;
import flash.events.Event;
public class ScrollOver
{
private var scrollContainer:DisplayObjectContainer;
private var scrollLength:uint;
private var vertical:Boolean;
private var _space:uint = 0;
private var _friction:uint = 15;
public function set space(value:uint):void { _space = value; }
public function set friction(value:uint):void { _friction = value; }
//------------------------------
// コンストラクタ
//------------------------------
public function ScrollOver(mc:DisplayObjectContainer, length:uint, vertical:Boolean = false):void
{
scrollContainer = mc;
scrollLength = length;
vertical = vertical;
}
//------------------------------
// スクロールモーション
//------------------------------
private function onEnter(e:Event):void
{
var percent:Number;
var pos:Number;
if (vertical == false && scrollLength < scrollContainer.width)
{
percent = scrollContainer.stage.mouseX / (scrollLength - 1);
pos = (scrollLength - (scrollContainer.width + _space) - _space) * percent;
scrollContainer.x = scrollContainer.x - ((scrollContainer.x - _space - pos) / _friction);
}
else if(vertical == true && scrollLength < scrollContainer.height)
{
percent = scrollContainer.stage.mouseY / (scrollLength - 1);
pos = (scrollLength - (scrollContainer.height + _space) - _space) * percent;
scrollContainer.y = scrollContainer.y - ((scrollContainer.y - _space - pos) / _friction);
}
}
//------------------------------
// スタート
//------------------------------
public function start():void
{
scrollContainer.addEventListener(Event.ENTER_FRAME, onEnter);
}
//------------------------------
// ストップ
//------------------------------
public function stop():void
{
scrollContainer.removeEventListener(Event.ENTER_FRAME, onEnter);
}
//------------------------------
// ポジションリセット
//------------------------------
public function positionReset():void
{
scrollContainer.x = _space;
}
}
}
ドキュメントクラスはこんな感じ。インスタンス名がboxというmcをステージに配置しておく。boxの中身は写真を横にずらっと配置させる。縦スクロールに対応させるにはnew ScrollOver()の第3引数をtrueにする。frictionは摩擦具合。spaceはスクロールの両サイド間隔。start()メソッドでモーション開始。
Main.as
package
{
import flash.display.Sprite;
import flash.display.MovieClip;
import info.five.ui.ScrollOver;
public class Main extends Sprite
{
public function Main():void
{
//-----[横スクロールの場合]
var so:ScrollOver = new ScrollOver(box, stage.stageWidth);
//-----[縦スクロールの場合]
//var so:ScrollOver = new ScrollOver(box, stage.stageHeight, true);
so.friction = 20;
so.space = 20;
so.start();
}
}
}
技術的なエントリーは久しぶりだなー。CBCNETで連載されているtrick7のteraさんの記事が面白かったので、自分なりにやってみる。この表現って素敵ですよね。記事にもあるようにPhotoshopでの参考サイトがあるので、これに忠実にActionScript3で再現してみる。元画像はこちら。
まずは画像をムービークリップに内包してステージにインスタンス名を「photo」にして配置。Tweenerでコントラストと彩度と明度を調整する。次にこのmcをBitmapDataでキャプチャしてコピー。コピーにも色を同じように調整して、ブラーをかける。新たにに円形グラデーション用のmcを作ってscaleYを半分にして楕円型にする。マスクするmcとマスクされるmcの両方にcacheAsBitmapをtrueに設定。最後にマスクをかけて完成と。
package
{
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Graphics;
import flash.display.Sprite;
import flash.display.MovieClip;
import flash.display.GradientType;
import flash.geom.Matrix;
import caurina.transitions.Tweener;
import caurina.transitions.properties.ColorShortcuts;
import caurina.transitions.properties.FilterShortcuts;
public class Main extends Sprite
{
public function Main():void
{
ColorShortcuts.init();
FilterShortcuts.init();
setToyFilter();
}
private function setToyFilter():void
{
//-----[色調整 ボケなし]
Tweener.addTween(photo, { _contrast:0.6, _saturation:1.6, _brightness:0.2 } );
//-----[キャプチャしてコピー]
var bmd:BitmapData = new BitmapData(photo.width, photo.height);
bmd.draw(photo);
var bm:Bitmap = new Bitmap(bmd);
var sp:Sprite = new Sprite();
sp.addChild(bm);
addChild(sp);
sp.x = photo.x;
sp.y = photo.y;
swapChildren(photo, sp);
//-----[色調整 ボケあり]
Tweener.addTween(sp, { _contrast:0.6, _saturation:1.6, _brightness:0.2, _Blur_blurX:5, _Blur_blurY:5, _Blur_quality:2 } );
//-----[グラデーションマスクの設定]
var gmask:Sprite = new Sprite();
var radius:uint = photo.width / 1.5;
var scale:Number = 1.0 / 1638.4 * radius * 2;
var m:Matrix = new Matrix();
m.identity();
m.scale(scale , scale);
addChild(gmask);
gmask.x = photo.x + photo.width / 2;
gmask.y = photo.y + photo.height / 2;
gmask.scaleY = 0.5;
gmask.graphics.lineStyle();
gmask.graphics.beginGradientFill(
GradientType.RADIAL,
[0x000000, 0x818181, 0xFFFFFF],
[1.0, 0.4, 0],
[120, 200, 255],
m
);
gmask.graphics.drawCircle(0, 0 , radius);
gmask.cacheAsBitmap = true;
photo.cacheAsBitmap = true;
photo.mask = gmask;
}
}
}
参考サイトでは被写体をパスで切り抜いて輪郭を強調してるけどFlashではそこまで出来ないのでピンのゆるさはご愛敬。写真素材をそれっぽいの使えば、擬似的に似せることは出来そうだなー。
追記
northprintさんがwonderflでPixel Bender版を公開されてます。こちらも勉強になります。
daeファイルを読み込んでアニメーションをループさせるとこまでは出来たけど。これだと全フレームのアニメーションになってしまう。10から20フレーム目までとかを任意で動かしたいのに、下記のように自前で無理やり動かすと激重で固まってしまうし。FLVのseek()みたいな感じで弄れたら分かりやすいのにね。DAEクラスにはplay()とstop()しかないのかー。もうちょっと調べてみよう・・・。
override protected function onRenderTick(e:Event = null):void
{
if(_channels)
{
for each(var channel:AbstractChannel3D in _channels)
{
channel.updateToFrame(_currentFrame);
}
_currentFrame++;
_currentFrame = _currentFrame < _numFrames ? _currentFrame:0;
}
}
左右の矢印キーで回転します。