Category Archives: Javascript

WebWorkerで爆速プリロード

HTMLで画像とか動画とかのリソースをページが表示される前にプリロードするパターンあるじゃないですか、あれでちょっと思いついたのがあるんでメモしときます。

以前やったPOOL inc.のトップページ、例のごとく最初にビデオをある程度プリロードしてから再生してるんですが、本数が40本程あって全部の読み込みを待つのさすがにダルいよねってことで2, 3本読み込んだら再生をはじめて、残りのビデオは裏で読みってやってみたんですが、この読み込みの処理がなかなかヘビーでして、再生してるビデオがコマ落ちしてまともに再生されないんです。

どうしよう、どうしようということで読み込み処理をビデオの再生とは別のスレッドにしてみたんです。HTML5のAPI、Web Workerで。すると狙いどおりビデオがコマ落ちすることなくバックグラウンドで読み込みができるようになりました。40本同時に並列でダウンロードしても処理落ちせず爆速です!(Workerが使えない環境の場合は同一スレッドで、しかもビデオの再生を邪魔しないように全部のビデオを読み込むのを待つようにしてるので段違いに遅い)

大量の画像をプリロードしてからはじまるサイトとかでもWorkerを使って分散すればずいぶん高速化できるんじゃないでしょうか。

ソースはこんな感じです。簡略化してるけど。main.coffeeから呼び出したPreload.coffeeまでが最初のスレッドで、Preload.coffeeがload.coffeeを別スレッドで実行してつつ読み込みが終わるのを待ってるという流れ。

※コード内のdefineはrequire.jsを使ってる想定

関連:
POOL inc. | blog.bouze.me

Require.jsを試してみた

Require.jsはActionScriptのimportのようにjsファイル同士の依存関係を崩さないよう非同期で読み込みつつ、多重読み込みなども防止してくれるライブラリ。
Require.jsを使えば細かくjsファイルを分割してもBackboneやjQuery等のライブラリを依存関係を保ちつつ読み込んでみる。

最小限のコードで挙動を確認してみる。(以降すべてCoffeeScript
これを実行するとMyModelのmessageに定義された「Hello, World!」が出力される。
Backboneに必要なUnderscoreとjQueryはBackboneより先に読み込まれている。

require([
	"libs/jquery-1.10.1.min"
	"libs/underscore.min"
	"libs/backbone.min"
],
($, _, Backbone) ->
	MyModel = Backbone.Model.extend({
		message: "Hello, World!"
	})

	MyView = Backbone.View.extend({
		initialize: (options) ->
			console.log @model.message
	})

	view = new MyView({
		model: new MyModel()
	})
	@
)

今度はRequire.jsの機能のdefineを使ってモジュールを作ってみる。
こちらも同じく「Hello, World!」が出力される。

define("myModule",
[
	"libs/jquery-1.10.1.min"
	"libs/underscore.min"
	"libs/backbone.min"
],
($, _, Backbone) ->
	MyModel = Backbone.Model.extend({
		message: "Hello, World!"
	})

	MyView = Backbone.View.extend({
		initialize: (options) ->
			console.log @model.message
	})

	return {
		modelClass: MyModel
		viewClass: MyView
	}
)

require([
	"myModule"
],
(module) ->
	model = new module.modelClass()
	view = new module.viewClass({
		"model": model
	})
	@
)

require.configのshimでライブラリの依存関係をあらかじめ設定してみる。
実行結果は同様。

require.config(
	shim:
		"libs/backbone.min":
			deps: [
				"libs/jquery-1.10.1.min"
				"libs/underscore.min"
			]
			exports:
				"Backbone"
)

define("myModule",
[
	"libs/backbone.min"
],
(Backbone) ->
	MyModel = Backbone.Model.extend({
		message: "Hello, World!"
	})

	MyView = Backbone.View.extend({
		initialize: (options) ->
			console.log @model.message
	})

	return {
		modelClass: MyModel
		viewClass: MyView
	}
)

require([
	"myModule"
],
(module) ->
	model = new module.modelClass()
	view = new module.viewClass({
		"model": model
	})
	@
)

今度はrequire.configのpathsを使ってライブラリのパスをあらかじめ定義しておく。
こちらも実行結果は同様。

require.config(
	paths:
		"jquery": "libs/jquery-1.10.1.min"
		"underscore": "libs/underscore.min"
		"backbone": "libs/backbone.min"
)

define("myModule",
[
	"jquery"
	"underscore"
	"backbone"
],
() ->
	MyModel = Backbone.Model.extend({
		message: "Hello, World!"
	})

	MyView = Backbone.View.extend({
		initialize: (options) ->
			console.log @model.message
	})

	return {
		modelClass: MyModel
		viewClass: MyView
	}
)

require([
	"myModule"
],
(module) ->
	model = new module.modelClass()
	view = new module.viewClass({
		"model": model
	})
	@
)

今度はshimにもpathsで定義したパスを使ってみる。

require.config(
	paths:
		"jquery": "libs/jquery-1.10.1.min"
		"underscore": "libs/underscore.min"
		"backbone": "libs/backbone.min"

	shim:
		"backbone":
			deps: [
				"jquery"
				"underscore"
			]
			exports:
				"Backbone"
)

define("myModule",
[
	"backbone"
],
() ->
	MyModel = Backbone.Model.extend({
		message: "Hello, World!"
	})

	MyView = Backbone.View.extend({
		initialize: (options) ->
			console.log @model.message
	})

	return {
		modelClass: MyModel
		viewClass: MyView
	}
)

require([
	"myModule"
],
(module) ->
	model = new module.modelClass()
	view = new module.viewClass({
		"model": model
	})
	@
)

require.configでの設定は一度きりなのでこれを無視すると、メインのコードはだいぶスッキリした。

参考:
RequireJS moduleについて – 文殊堂
java-ja.js #2 RequireJS実践編 – 文殊堂
[JS] JavaScriptをモジュール分割して開発できるRequireJSに入門 – YoheiM .NET

Grunt導入してみた

MacにはCodeKitっていうCoffeeScriptとかSassとかLessとかをコンパイルしてくれる便利なツールがあって最近までお世話になってた。でも自分はWindowsユーザーで、これだけのためにMacを起動するのも面倒くさいので、CodeKitでやってたことをWindowsでもできるようにとGruntをはじめてみた。

Gruntそのものはコンパイラではなく、いろんなタスクを実行してくれるだけのツールなんだけど、プラグインを追加することでCoffeeScriptのコンパイルや、Sass/Lessのコンパイル、Uglifyを使ってjsのminifyなどができるようになる。

これを使ってCodeKitでやってたことを再現してみた。

1. インストール

まずGruntを実行するのに必要なnode.jsをインストール。同時にnpm(node.js用のパッケージ管理ツール)もインストールされる。

環境変数PATHに「C:\Users\{ユーザー名}\AppData\Roaming\npm;C:\Program Files\nodejs\」を追加しておく。

gruntをインストール

npm install -g grunt-cli

※「npm install -g grunt」ではない

環境変数PATHに「grunt」を追加しておく。

参考:
Windows 環境変数 Path の設定方法 – コンピュータ/ソフトウェア関連Tips
はじめに | Grunt 日本語リファレンス | js STUDIO

2. プロジェクトの準備

プロジェクトのルートフォルダにGruntfile.js(もしくはGruntfile.coffee)と、package.jsonというファイルを作る。
Gruntfile.js(Gruntfile.coffee)はGruntに実行させるタスクを定義しておくファイル。package.jsonはプロジェクトに必要なGruntのプラグインのリスト。

3. package.json

package.jsonはこのように記述して、devDependenciesに必要なプラグイン(パッケージ)の名前とバージョンを羅列しておく。

{
  "name": "{プロジェクト名}",
  "version": "{パッケージのバージョン}",
  "author": "{作者}",
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-livereload": ">=0.0.0",
    "grunt-contrib-watch": ">=0.0.0",
    "grunt-contrib-coffee": ">=0.0.0",
    "grunt-contrib-uglify": ">=0.0.0",
    "grunt-contrib-jshint": ">=0.0.0",
    "grunt-contrib-jst": ">=0.0.0",
    "grunt-contrib-sass": ">=0.0.0",
    "grunt-contrib-compass": ">=0.0.0",
    "grunt-contrib-cssmin": ">=0.0.0"
  }
}

package.jsonを用意した状態で「npm install」をコマンドプロンプト上で実行すると、devDependenciesに書かれたプラグインを順番にプロジェクトフォルダ直下のnode_modulesフォルダにダウンロードしてきてくれる。

npm install

と書かれた.batファイルを作っておくと良い。

参考:
npm-json(1) — Specifics of npm’s package.json handling
npm と package.json でパッケージ管理 | FIRN.JP
npmとpackage.json使い方 – Kokudoriing

4. Gruntfile.js

下記のようなラッパー関数を用意し、その中にタスクを書いていく。Gruntfile.jsの内容はGruntを通してnode.js上で実行されるので、js(もしくはCoffee Script)で記述していく。

module.exports = function(grunt)
{
  //ここにGruntに実行させたいタスクを書いていく
};

coffeeファイルが入ったフォルダを監視してcoffeeファイルが更新されたらCoffeeScriptをコンパイルして、uglifyでminifyするGruntfile.coffee。

module.exports = (grunt) ->

    # load tasks
    grunt.loadNpmTasks "grunt-contrib-coffee"
    grunt.loadNpmTasks "grunt-contrib-uglify"
    grunt.loadNpmTasks "grunt-contrib-watch"

    grunt.initConfig(

        coffee:
            files:
                expand: true
                cwd: "assets/coffee/"
                src: ["**/**/*.coffee"]
                dest: "assets/js/"
                ext: ".js"

        uglify:
            files:
                expand: true
                cwd: "assets/js/"
                src: ["*.js"]
                dest: "assets/js/"
                ext: ".min.js"
            options: ""

        watch:
            coffee:
                files: ["assets/coffee/**/*.coffee"]
                tasks: [
                    "coffee"
                    "uglify"
                ]
	)

    grunt.registerTask "default", ["watch"]

sassファイルが入ったフォルダを監視してsassファイルが更新されたらSassをコンパイルして、sprite画像も生成する。

module.exports = (grunt) ->

    # load tasks
    grunt.loadNpmTasks "grunt-contrib-compass"
    grunt.loadNpmTasks "grunt-contrib-cssmin"
    grunt.loadNpmTasks "grunt-contrib-watch"

    grunt.initConfig(

        compass:
            dist:
                options:
                    environment: "development"
                    outputStyle: "compressed"
                    debugInfo: true
                    relativeAssets: true
                    basePath: "assets"
                    sassDir: "sass"
                    cssDir: "css"
                    generatedImagesDir: "images"

        cssmin:
            files:
                expand: true
                cwd: "assets/css"
                src: ["*.css", "!*.min.css"]
                dest: "assets/css/"
                ext: ".min.css"

        watch:
            sass:
                files: ["assets/sass/**/*.sass"]
                tasks: [
                    "compass"
                    "cssmin"
                ]
	)

    grunt.registerTask "default", ["watch"]

これらをまとめつつ、JavaScriptテンプレートのコンパイル、htmlの更新監視にコンパイル後にLiveReloadを使ってブラウザをリロードするタスクを加えるとこうなる。

module.exports = (grunt) ->

    # load tasks
    grunt.loadNpmTasks "grunt-contrib-compass"
    grunt.loadNpmTasks "grunt-contrib-cssmin"
    grunt.loadNpmTasks "grunt-contrib-coffee"
    grunt.loadNpmTasks "grunt-contrib-uglify"
    grunt.loadNpmTasks "grunt-contrib-jst"
    grunt.loadNpmTasks "grunt-contrib-watch"

    grunt.initConfig(
        compass:
            dist:
                options:
                    environment: "development"
                    outputStyle: "compressed"
                    debugInfo: true
                    relativeAssets: true
                    basePath: "assets"
                    sassDir: "sass"
                    cssDir: "css"
                    generatedImagesDir: "images"
        cssmin:
            files:
                expand: true
                cwd: "assets/css"
                src: ["*.css", "!*.min.css"]
                dest: "assets/css/"
                ext: ".min.css"
        coffee:
            files:
                expand: true
                cwd: "assets/coffee/"
                src: ["**/**/*.coffee"]
                dest: "assets/js/"
                ext: ".js"
            options:
                sourceMap: true
        jst:
            compile:
                options:
                    processName: (filename) ->
                        return filename.match(/assets\/jst\/(.*).js$/)[1]
                files:
                    "assets/js/templates.js": ["assets/jst/**/**/*.js"]
        uglify:
            files:
                expand: true
                cwd: "assets/js/"
                src: ["*.js"]
                dest: "assets/js/"
                ext: ".min.js"
            options: ""
        watch:
            html:
                files: ["**/*.html"]
                options:
                    livereload: true
            coffee:
                files: ["assets/coffee/**/*.coffee"]
                tasks: [
                    "start"
                    "coffee"
                    "uglify"
                ]
                options:
                    livereload: true
            jst:
                files: ["assets/jst/**/*.js"]
                tasks: [
                    "jst"
                    "uglify"
                ]
                options:
                    livereload: true
            sass:
                files: ["assets/sass/**/*.sass"]
                tasks: [
                    "start"
                    "compass"
                    "cssmin"
                ]
                options:
                    livereload: true
    )

    # register tasks
    grunt.registerTask "start", "start task", ->
        grunt.log.writeln "--------------------------------------------------------"
        grunt.log.writeln "    start task"
        grunt.log.writeln "--------------------------------------------------------"

    grunt.registerTask "default", ["watch"]

それぞれのタスクの設定はそれぞれのプラグインのページを参照のこと。

参考:
grunt-contrib-coffee
grunt-contrib-jst
grunt-contrib-uglify
grunt-contrib-compass
grunt-contrib-cssmin
grunt-contrib-watch

ExternalInterface.callが重い

Flashから毎フレームExternalInterface.callでJSの関数を呼び出したりしている時に、そのJSの関数内でなにかしらのObjectをreturnしてるとその処理に手間がかかるのか、Flashのフレームレートがすごい落ちる。

CoffeeScriptで関数の最後に@を入れてるとこの現象が起こりやすいので、ExternalInterface.callで直接呼び出される関数の最後はreturn falseして回避しておく。

さらにそのJSの関数内でDOMの操作みたいなちょっと重めの処理(width, height変えたり、left, top変えたり)をしてたりしても同様の現象が起こる。この場合はJSの関数内でやっている処理をsetTimeout(Function, 0)でくるんで非同期で処理してあげると回避できた。

なんだろこれ?ExternalInterface.callで呼び出した処理が終わるのをFlashが待っちゃうからかな?

追記1:
こういうことらしい

追記2:
CoffeeScriptで行末returnしないようにするには「return」だけ書いとけばいいっぽい。

Backbone.js (+ Underscore.js) 備忘録

定期的に記憶が揮発するのでメモ。

ActionScript 3 SDK for Facebook Platform API ハマリどころメモ

ActionScript 3 SDK for Facebook Platform APIを使う時に、attributes.nameを指定せずattributes.idだけを指定してswfobject.embedSWFすると、SDK内のExternalInterface.objectIDを参照している箇所でnullが返ってjsの方でエラーが出るブラウザがある。

このままだとjsの処理が止まってFB.loginとかFB.apiでコールバックが呼ばれなくなってしまうので、attributes.idと同じ名前をattributes.nameで指定してExternalInterface.objectIDを参照できるようにする。

var flashvars = {
};
 
var params = {
    base:'.',
    menu:'false',
    wmode: 'window',
    allowFullScreen:'true',
    allowscriptaccess:'always'
};
 
var attributes = {
    id:"hoge", //←これと
    name:"hoge" //←これを指定する
};
 
swfobject.embedSWF('main.swf', 'flashcontent', '100%', '100%', '10.0.0', '', flashvars, params, attributes);

FlashからOAuth用のポップアップ開くメモ

ポップアップしたウインドウ(子)とFlashがのっかってる元のウインドウ(親)はLocalConnection経由でやりとりする。

流れとしては、

元FlashでOAuthするボタンを押す
 ↓
子ウインドウがポップアップする
 ↓
TwitterのOAuthページが表示される。
 ↓
OAuth完了する
 ↓
元Flashに完了した旨が通知される。
 ↓
ポップアップウインドウが閉じる
 ↓
Twitterに各種情報をとりに行く

という感じ。

ここで問題になるのがLocalConnectionをする時に同じConnect IDを使い回すと、同じページを開いた場合にconnect対象が複数になってしまい接続が失敗する。

これを回避するためにconnect用のIDにタイムスタンプを使う。

var connectID:String = String((new Date()).getTime());

Flashから子ウインドウを開く時に、

ExternalInterface.call('window.open("' + url + '", "' + connectID + '")');

もしくは、

navigateToURL(new URLRequest(url) , connectID);

のようにwindow.nameとして生成したIDを子ウインドウに渡す。

子ウインドウはtwitterのOAuth画面が開き、
リダイレクトでもどってきたページでwindow.nameを参照し、
flashvars経由でsend用のswf(通知側)にこのIDを渡す。
send用のswfはこのIDを使ってsendしOAuthが終了したことを親ウインドウに知らせる。

connectが完了したら子ウインドウは自分でwindow.closeして閉じる。

さらに問題になるのがブラウザごとのポップアップブロックの条件。
IEはnavigateToURLがブロックされ、
MacのSafariはポップアップしたページに

タグがあるとwindow.closeできなくなるので、
ExternalInterfaceからwindow.openを呼びウインドウを開く。

ExternalInterface.call('window.open("' + url + '", "' + connectID + '")');

それ以外のブラウザはnavigateToURLで開く。

さらにさらに、Firefox4でwindow.nameを指定してウインドウを開くと、
window.closeした時に他のタブ(親ページ)も巻き込んで閉じてしまうので、
connectIDの頭に”_blank”をつけて開く。

var connectID:String = "_blank" + connectID;

こんな感じ。

卒展やります

3/5からうちの学科の卒展やります。
僕は質量ない系のものを作ってしまったのでたぶん出品しないです。

多摩美術大学グラフィックデザイン学科卒業制作展2011
日時:
平成23年3月5日 [土] 午前10時 ― 午後7時
平成23年3月6日 [日] 午前10時 ― 午後5時
入場料:無料
会場:
ガーデンホール/ザ・ガーデンルーム
東京都目黒区三田1-13-2 恵比寿ガーデンプレイス内
公式サイト:http://tamagraph2011.com/
facebookページ:http://www.facebook.com/taugd11

で、去年の年末に卒制の片手間にその卒展のプレサイト作りました。今インターネットで話題のHTML5です!
ただまぁわりとリソース使い回してサクッと作ったんであっさりした仕上がりですが…。

本サイトがっつり作るよりソーシャルメディアで力んだ方が色々と波及しやすい昨今のインターネット事情を汲んで、チラシ兼ソーシャルサービスのハブっぽい作りにしてます。(=あっさりした仕上がり)

技術的にはjQueryで画像グルングルンやってるだけでなんら変わったことはやってないです。
しいてあげれば、デカイ画像(スクリーンサイズ規模)の上(z-index的な意味)で24bitの透過pngを動かす(座標的に)と下に敷いてるデカイ画像との描画計算で負荷がかかってfpsが2ぐらいになっちゃう現象に12/24から12/25あたりにハマりました。
これはデカイ画像をいったんdisplay:noneしておいて低解像度の画像に差し替えて上に載ってる画像を動かしたあとにdisplay:blockに戻すFlashでいうstage.quality的なtipsで回避しました。

html+js的な案件をいくつかやってみて、cssである程度レイアウトをコントロールできればFlashもhtmlもあんまり変わらないなぁという印象です。
個人的にはFlashはIDEでアニメーション作れるので楽にリッチにできるのが優れてる気がしないでもないですが。(htmlもgifアニでアニメーション作れよと言われればそうなんですが、簡単にタイムラインをコントロールできないのでちょっと気が重いです。)
あとhtml+jsはブラウザの互換とらないといけないのがヘヴィーです。(作業時間の40%近くはクロスブラウザ対応だったり。)

というわけでソーシャルネットワークがおもしろかったです!

swfのtraceをブラウザのコンソールに出力

firebugやchrome、safariのjavascriptコンソールにswfからtrace感覚で出力したい→できた。

import flash.external.ExternalInterface;

function log(...args:*):void
{
    ExternalInterface.call("function(str){ if (typeof window.console == 'object'){ console.log(str) } }", args);
}

当然ながらhtmlに貼り付けたswfからのみ対応。あとconsoleの有無を判定してから出力してるのでIEでもエラーは出ないはず。trace結果も出ないけど。

shaking typography with bookmarklet

リンクをクリック!もしくはリンクをブックマークバーまでドラッグして好きなページでクリック!

SHAKE IT