Wonderful! WordPress

JavaScript で keyframes の登録と Web Animations API

Page No.2

さて、と。
ここからは自分的備忘録の本題となる。
事の発端は雪である。
クリスマスヴァージョンにおいてはページに雪降りのようなものを表示させているのだけれど、それをちょっとヴァージョンアップさせようと思ったのがきっかけ。
雪降りのようなものなら、今は CSSkeyframes ( アニメーション ) を使えば簡単にできてしまう。JavaScript はいらない。実際に鳥ページの方は、php + css で作ったもの。ただ、JavaScript だけでできるなら、ページに装備するのが簡単だし、ページ読み込み時の負荷も少なくてすむのだろうか?と思ったりしたのである。
で、このページで雪を振らせている JavaScript ということになる。
先に php + css でさっと作ってから同じことを JavaScript で再現させて、と移植させていたのだけれど、そんなことをしていると、雪の画像を一つではなくいくつか使うかとか、雪だけじゃつまらないから花火でも仕込んじゃおうか、とか、いつものことではあるけれど余分なことをあれこれと考えはじめてしまうのであって。
で、JavaScript version は、クリックすると花火が出るように仕込んである雪が3つある。php version は、はじめから雪のかわりに花火になっているものが3つあって、自動的に花火が爆発する。JavaScript を使わないとイベントでの処理は使えないからそうなってしまったのだけれど、何回か表示させたら花火の画像を入れ変えるなんてことにしてしまったので、結局、JavaScript のお世話にならざるを得ない、ということになるのである。
JavaScript version において、花火を仕込んである雪は、それとわかるように黄色くするために疑似要素の ::before を使うことにしたので、結局、先の style の挿入なんてのも使うことになった。

( function() {
	const version ='snowfall prototype v1.0',
		doc = document,
        winwidth = window.innerWidth,
		snowfalls = doc.querySelector( '.snowfalls' );

		let parent,
		// スクリーンの幅が 770 より大きい場合に限り、雪を表示
        gowidth = winwidth > 770 ? true : false;

	// class に snowfalls を設定してある要素が存在する場合、その要素を親として雪を表示する div を生成する
	// なければ body の最後に挿入していく
    if ( null !== snowfalls ) {
		parent = snowfalls;
		
		// snowfalls の class を持つ要素に data-width の価を設定してある場合、
		// その width の価よりも横幅の大きいスクリーンにおいて雪を表示する
        if ( 'undefined' !== typeof parent.dataset.width ) {
            const mediawid = parent.dataset.width;

            gowidth = winwidth > mediawid ? true : false;
        }
    } else {
        parent = doc.body;
    }

	if ( gowidth ) {
		let snows = [],
			anims = [],
			fireanims = [],
			fireupanims = [],
			dynamicStyles = null,
			nums = [],
			m = 30,// 表示する雪の数
			l,
			shuffuls = [];

		// 雪の数分の配列を作る、ダサかろうがなんだろうがぱっと見でわかるほうがいいにきまってる!
		// そしてたぶんこれが一番高速
		for ( let i = 0; i < m; i++ ) {
			nums[ i ] = i;
		}

		// 雪の数の配列(0~29)からランダムに並び変えた配列を作る
		while ( m > 0) {
			l = Math.floor( Math.random() * m );
			
			shuffuls.push( nums[ l ] );
			nums.splice( l, 1 );
			m = nums.length;
		}

		// ランダムに並べた配列から先頭の三個を取得(ランダムな数を三個作る)
		// この三個の番号の雪に花火を仕込む
		const fire = shuffuls.splice( 0, 3 );
		// 花火を disable にする場合
		// const fire = [];

		// 花火を仕込んだ雪には目印として黄色くするため
		// その要素には::beforeを設定するためにstyle を挿入する
		// と、花火自体には明るくするために背景を白くする
		function addStyle( cssstr ) {
			if ( ! dynamicStyles ) {
				dynamicStyles = doc.createElement( 'style' );
				doc.head.appendChild( dynamicStyles );
			}
			
			dynamicStyles.sheet.insertRule( cssstr, dynamicStyles.length);
		}
		
		addStyle(`
			.firework .snowcrystal::before{
				content:"";
				display:block;
				position:absolute;
				width:20px;
				height:20px;
				top:15px;
				left:15px;
				margin:0;
				padding:0;
				border-radius: 50%;
				background:yellow;
				opacity:0.3;
				z-index:-1;
			}
		`);

		addStyle(`
		.fireburn::before{
			content:"";
			display:block;
			position:absolute;
			width:100%;
			height:100%;
			top:0px;
			left:0px;
			opacity: 0.8;
			background-image:radial-gradient( circle, white, transparent, transparent );
		}
	`);

		// メインの雪のオブジェクト
		// 10個をひとまとめにして、ランダムに画面に散らばらせる
		// width:0px、height:0pxの要素で画面内の位置を設定し、それを親として雪の要素を動かす
		// その位置指定は、0から9の配列をランダムに並び替え、それに10をかけた価を%として left に設定
		// 二巡目、三巡目と重なることを少なくするため+-3%のゆらぎを設定
		// top も 0px から 10px ごと、40px までランダムに設定
        const Snowflakes = function() {};

        Snowflakes.prototype = {
			tarparent: 'undefined' === typeof parent ? document.body : parent,
            leftpos: [],
            yuragi: [ -3, 0, 3 ],
            dura: [ 5, 6, 7 ],
            intrand: function( val ) {
                    return Math.floor( Math.random() * 100 ) % val;
                },
			stylesc: {// 雪div 用スタイル
					position: 'absolute',
					width: '50px',
					height: '50px',
					top: '0px',
					left: '0px',
					opacity: '0',
					borderRadius: '50%',
					backgroundImage: 'url( ./theme/snow_crystals.png )',
					backgroundRepeat: 'no-repeat',
                },
            fireary: 'undefined' === typeof fire ? [ 9, 19, 28 ] : fire,
			crystal: [ '0', '-50px', '-100px' ],
			images: [ '', '1', '2', '3', '4', '5', '6', '7', '8', '9' ],
			fireimg: [ 0, 4, 6 ],
			stylesf: {// 花火 div style
				position: 'fixed',
				width: '400px',
				height: '400px',
				opacity: 0,
				borderRadius: '50%',
				backgroundRepeat: 'no-repeat',
				backgroundPosition: '0 0',
				transform: 'scale(0)',
				// backgroundSize: 'contain',
				zIndex: 50
			},
			animOptf: {// 花火アニメーションの設定
				duration: 4000,
				easing: 'ease',
				delay: 1700
			},
			animFire: [// 花火アニメーション keyframes
				{ opacity: 0, transform: 'scale(0)', backgroundPosition: '0 0' },
				{ opacity: 1, transform: 'scale(0)', backgroudPosition: '0 0', offset: 0.01 },
				{ opacity: 1, offset: 0.300 },
				{ opacity: 0, backgroundPosition: '0 0', offset: 0.301 },
				{ backgroundPosition: '0 -400px', offset: 0.302 },
				{ opacity: 0, offset: 0.303 },
				{ opacity: 1, offset: 0.304 },
				{ opacity: 1, transform: 'scale(1.5)', offset: 0.45 },
				{ opacity: 1, transform: 'scale(1.6)', offset: 0.65 },
				{ opacity: 0, transform: 'scale(1.7)', offset: 0.95 },
				{ opacity: 0, transform: 'scale(0)', backgroundPosition: '0 -400px', offset: 1.00 }
			],
			stylesb: {// 上昇花火玉スタイル
				position: 'fixed',
				width: '50px',
				height: '200px',
				opacity: '1',
				backgroundImage: 'url( https://strix.main.jp/wp-content/themes/ural/fireup.png )',
				backgroundRepeat: 'no-repeat',
				backgroundPosition: 'center',
				backgroundSize: 'contain',
				zIndex: 50
			},
			animOptb: {// 上昇花火玉アニメーション設定
				duration: 4500,
				easing: 'ease-out'
			},
            setSnow: function( id ) {

				// 10個ごとに left 位置設定用のランダムな配列を並び替える
                if ( 0 === id % 10 ) {
                    let nums = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ],
                        n = 10,
                        k;

                    this.leftpos.length = 0;
                    while ( n > 0) {
                        k = Math.floor( Math.random() * n );
                        
                        this.leftpos.push( nums[ k ] );
                        nums.splice( k, 1 );
                        n = nums.length;
					}
					if ( 0 === id ) {
						this.fireimg = this.leftpos.slice( 0, 3 );
					}
                }

                const j = id % 10,
					styles = {// 親div 用スタイル
					position: 'fixed',
                        width: '0px',
                        height: '0px',
                        top: ( this.intrand(5) * 10 ) + 'px',
                        left: this.leftpos[ j ] * 10 + 5 + this.yuragi[ this.intrand(3) ] + '%',
                        zIndex: 50
                    },
                    roopnum = Math.floor( id / 10 ),
                    delay = id * 0.5 + 3 + ( roopnum * 5 ),
                    duration = this.dura[ this.intrand(3) ] + ( roopnum * 0.5 ),
                    imagepos = '0 ' + this.crystal[ this.intrand(3) ];
                    fireflg = this.fireary.indexOf( id ),
                    classset = -1 === fireflg ? 'snowpos' : 'snowpos firework',
                    iteration = -1=== fireflg ? Infinity : '100',
					animOpt = {// animation option
                        duration: duration * 1000,
                        iterations: iteration,
                        delay: delay * 1000,
                        easing: 'linear'
					},
					animflg = 0 === id % 2 ? 1 : -1,
					snowFall = [// 雪アニメーション、右回転と左回転あり
						{ opacity: 0, top: '0px' },
						{ left: ( 5 * animflg ) + 'px' },
						{ opacity: 1, top: '100px', transform: 'rotate( ' + ( -15 * animflg ) + 'deg )' },
						{ left: ( -5 * animflg ) + 'px', transform: 'rotate( ' + ( 15 * animflg ) + 'deg )' },
						{ opacity: 0.3, top: '200px', transform: 'rotate( ' + ( 80 * animflg ) + 'deg )' },
						{ left: ( 5 * animflg ) + 'px', transform: 'rotate( ' + ( 160 * animflg ) + 'deg )' },
						{ opacity: 1, top: '300px', transform: 'rotate( ' + ( 240 * animflg ) + 'deg )'  },
						{ left: ( -5 * animflg ) + 'px' },
						{ opacity: 0.3, top: '500px' },
						{ left: '-5px', opacity: 1, top: '600px' },
						{ opacity: 0, top: '700px' }
					];
    
                this.snow = doc.createElement( 'div' );
                this.snow.setAttribute( 'class', classset );

                // 変数に設定してあるスタイルをループにより対象の要素に適用させる
                for ( const [ property, value ] of Object.entries( styles ) ) {
                    this.snow.style[ property ] = value;
                }
                
                this.crystal = doc.createElement( 'div' );
                this.crystal.setAttribute( 'class', 'snowcrystal' );
                this.crystal.setAttribute( 'data-id', id );

                const cstyle = Object.assign ( this.stylesc, {
                        backgroundPosition: imagepos
                    }
                );

                for ( const [ property, value ] of Object.entries( cstyle ) ) {
                    this.crystal.style[ property ] = value;
                }

                if ( -1 !== fireflg ) {
					this.crystal.setAttribute( 'data-fw', fireflg );
					
					// アロー関数を使うとイベントのブロック内において this がイベント発生の対象要素ではなく、オブジェクトを指すようになる
					// ES5 の書き方であれば、bind( this ) で可能
                    // this.crystal.onclick = ( e ) => {
					// this.crystal.addEventListener( 'click', function( e ) {
					this.crystal.onclick = function( e ) {

						const target = e.target, 
							fireid = target.dataset.fw,
							crystalid = target.dataset.id;
		
						if ( fireanims[ crystalid ] ) {

							fireupanims[ crystalid ].play();
							fireanims[ crystalid ].play();
						} else {
		
							// クリックされた雪の位置を得る
							const clientRect = this.getBoundingClientRect();

							// 花火を一度しか表示させない場合は元の雪を消す
							// anims[ crystalid ].cancel();
							// .finish() メソッドは infinite の animation には使えず、エラーとなる

							// 個々に違いをもたせるスタイル設定は、予め固定の部分を設定してあるprototype のデータに加える
							const fstyle = Object.assign( this.stylesf, {
									top: clientRect.top + 'px',
									left: clientRect.left + 'px',
									backgroundImage: 'url( .https://strix.main.jp/wp-content/themes/ural/firework' + this.images[ this.fireimg[ fireid ] ] + '.png )'
								}
							);

							// 花火
							const firework = doc.createElement( 'div' );
		
							firework.setAttribute( 'class', 'fireburn' );
							for ( const [ property, value ] of Object.entries( fstyle ) ) {
								firework.style[ property ] = value;
							}

							const winheight = window.innerHeight,
								animFup = [
									{ opacity: 1,  top: ( winheight + 200 ) + 'px', },
									{ opacity: 1, marginLeft: '-3px', height: '250px' },
									{ opacity: 0.7, marginLeft: '3px', height: '230px' },
									{ opacity: 1, marginLeft: '-3px', height: '210px' },
									{ opacity: 0.8, marginLeft: '3px', height: '190px' },
									{ opacity: 1, marginLeft: '-3px', height: '170px' },
									{ opacity: 0.7, marginLeft: '3px', height: '150px' },
									{ opacity: 1,  top: clientRect.top + 'px', offset: 0.5 },
									{ opacity: 0,  top: clientRect.top + 'px', offset: 0.6 },
									{ opacity: 0,  top: ( winheight + 200 ) + 'px', offset: 0.7 },
									{ opacity: 0,  top: ( winheight + 200 ) + 'px', offset: 1.0 }
								];

							const bstyle = Object.assign( this.stylesb, {
									left: clientRect.left + 'px'
								}
							);

							// 下から打ち上がってくる火の玉
							const fireup = doc.createElement( 'div' );
							fireup.setAttribute( 'class', 'fireup' );
							for ( const [ property, value ] of Object.entries( bstyle ) ) {
								fireup.style[ property ] = value;
							}
							
							this.tarparent.appendChild( fireup );

							this.tarparent.appendChild( firework );
							
							fireanims[ crystalid ] = firework.animate( this.animFire, this.animOptf );

							fireupanims[ crystalid ] = fireup.animate( animFup, this.animOptb );
						}
					// }// => アロー関数の場合の閉じ括弧
					// }.bind( this ) );// addEventListener
					}.bind( this );// onclick
				}
				
                this.snow.appendChild( this.crystal );
                this.tarparent.appendChild( this.snow );

				anims[ id ] = this.crystal.animate( snowFall, animOpt );
            },
        }
	
        for ( let i = 0; i < 30; i++ ) {
            snows[ i ] = new Snowflakes();
            snows[ i ].setSnow( i );
		}
	}
}());
JavaScript
CopyExpand

思ったよりも随分と長いものになってしまったなぁ。もっと簡単なもののはずだったのだけれど・・・。まぁ、雪だけなら・・・、ってことか。
これをやっていて、ふと、感じたことは、これに関しては FireFox よりも chrome のほうが、軽く動いているような気がするということ(2020年12月において)。雪画像は三種類あって、画像のロードに一度のリクエストということで、一つの画像に連結させていた。それを transform で rotate させているわけだけれど、ひょっとすると、その大きなもの(実際は全く大きくなく、一つづつよりは大きいということ)をいくつも回転させることが重くなる原因だろうか、と。で、ばらばらにしてやってみたところ、なんとなく軽くなったような気もするが、はたして・・・。

ということで、この元となった php + css 版の方。
こちらは footer.php の </body> タグの直前に入れてる。

<?php
    function show_snow() {
        $leftpos = range( 0, 9 );
        $yuragi = [ 0, 4, 7 ];
        $dura = [ 5, 6, 7 ];
        $crystal = [ 'a', 'b', 'c' ];
        // $fireid = array_rand( range( 0, 29 ), 3 );
        $fireid = [ 9, 19, 28 ];
        $firecount = 0;
        $fireimg = [];

        for ( $i = 0; $i < 30; ++$i ) {
            if ( 0 === $i % 10) {
                shuffle( $leftpos );
                if ( 0 === $i ) {
                    $fireimg = array_slice( $leftpos, 0, 3 );
                }
            }
            $j = $i % 10;
            $top = 10 * mt_rand( 0, 5 );
            $left = $leftpos[ $j ] * 10 + 5 + $yuragi[ mt_rand( 0, 2 ) ];
            $delay = $i * 0.5 + 3 + ( floor( $i / 10 ) * 5 );
            $aorb = 0 === $i % 2 ? 'a' : 'b';
            $duration = $dura[ mt_rand( 0, 2 ) ] + ( floor( $i / 10 ) * 0.5 );

            if ( in_array( $i, $fireid, true ) ) {
                $classfire = 'fb' . $crystal[ $firecount ];
                echo '<div class="firework" style="top:' . ( $top + $fireid[ $firecount ] * 5 ) . 'px;left:' . $left . '%;"><div class="fireburn ' . $classfire . '" style="background-image:url( https://strix.main.jp/wp-content/themes/ural/firework' . $fireimg[ $firecount ] . '.png );animation-delay:' . $delay . 's;animation-duration:' . ( $duration + 10 ) . 's;" data-img="' . implode( '-', $fireimg) . '"></div><div class="fireup"  style="animation-delay:' . $delay . 's;animation-duration:' . ( $duration + 10 ) . 's;"></div></div>';
                ++$firecount;
            } else {
                $classcrystal = 'sc' . $crystal[ mt_rand( 0, 2 ) ];
                $zindex = 0 !== $i %3 ? ' z-index: 10;' : '';
                echo '<div class="snowpos' . $aorb . '" style="top:' . $top . 'px;left:' . $left . '%;' . $zindex . '"><div class="snowcrystal ' . $classcrystal . '" style="animation-delay:' . $delay . 's;animation-duration:' . $duration . 's;"></div></div>';
            }
        }
    }

    show_snow();
?>
PHP
CopyExpand

そして css の方。

.snowposa,.snowposb,.firework{
	position:fixed;
	width:0px;
	height:0px;
	left:5%;
}

.snowposa .snowcrystal{
	animation-name:snowfalla;
	animation-iteration-count:infinite;
	animation-timing-function:linear;
}

.snowposb .snowcrystal{
	animation-name:snowfallb;
	animation-iteration-count:infinite;
	animation-timing-function:linear;
}

.snowcrystal{
	position:absolute;
	width:50px;
	height:50px;
	top:0px;
	left:0px;
	opacity:0;
    /*background-image:url( snow_crystals.png );
	background-repeat:no-repeat;*/
}

.sca{
	background:url( snow_crystal.png ) no-repeat center;
	/*background-position: 0 0;*/
}

.scb{
	background:url( snow_crystal2.png ) no-repeat center;
	/*background-position: 0 -50px;*/
}

.scc{
	background:url( snow_crystal3.png ) no-repeat center;
	/*background-position: 0 -100px;*/
}

@keyframes snowfalla{
	from { opacity: 0; top:0px; }
	10% { left: 5px;}
	20% { opacity: 1; top: 100px; transform: rotate( -15deg ); }
	30% { left: -5px; transform: rotate( 15deg ); }
	40% { opacity:0.5; top: 200px;transform: rotate( 80deg ); }
	50% { left: 5px; transform: rotate( 160deg );}
	60% { opacity: 1; top: 300px; transform: rotate( 240deg );  }
	70% { left: -5px;}
	80% { opacity:0.5; top: 500px; }
	90% { left: 5px; opacity: 1; top: 600px;}
	to { opacity:0; top: 700px; }
}

@keyframes snowfallb{
	from { opacity: 0; top:0px; }
	10% { left: -5px;}
	20% { opacity: 1; top: 100px; transform: rotate( 15deg ); }
	30% { left: 5px; transform: rotate( -15deg ); }
	40% { opacity:0.5; top: 200px; transform: rotate( -80deg ); }
	50% { left: -5px; transform: rotate( -160deg ); }
	60% { opacity: 1; top: 400px; transform: rotate( -240deg ); }
	70% { left: 5px;}
	80% { opacity:0.5; top: 500px; }
	90% { left: -5px; opacity: 1; top: 600px;}
	to { opacity:0; top: 700px; }
}

.firework{
	z-index:10;
}

.fireburn{
	position:absolute;
	width:400px;
	height:400px;
	top:0px;
	left:-200px;
	opacity:0 ;
	transform: scale(0);
	animation-name:firework;
	animation-iteration-count:infinite;
	animation-timing-function:linear;
}

.fba,.fbb,.fbc{
	background-repeat: no-repeat;
	background-position: 0 0;
}

.fireburn::before{
	content:"";
	display:block;
	position:absolute;
	width:100%;
	height:100%;
	top:0px;
	left:0px;
	opacity: 0.8;
	background-image:radial-gradient( circle, white, transparent, transparent );
}

@keyframes firework{
	from { opacity: 0; transform: scale(0); background-position: 0 0; }
	85% { opacity: 1; transform: scale(0); }
	88.1% { opacity: 1; background-position: 0 0;}
	88.2% { opacity: 0; }
	88.3% { background-position: 0 -400px; }
	88.7% { opacity: 1; }
	90% { opacity: 1; transform: scale(1.5); }
	95% { opacity: 1; transform: scale(1.6); }
	99% { opacity: 0; transform: scale(1.7); }
	to { opacity: 0; transform: scale(0); background-position: 0 -400px; }
}

.fireup{
	position: absolute;
	width: 50px;
	height: 200px;
	top: 90vh;
	left: 0px;
	opacity: 0;
	background-image: url( fireup.png );
	background-repeat: no-repeat;
	background-position: center;
	background-size: contain;
	z-index: 50;
	animation-name:fireup;
	animation-iteration-count:infinite;
	animation-timing-function:ease-out;
	animation-duration: 4.5s;
}

@keyframes fireup{
	from { opacity: 0;  top: 900px; }
	70% { opacity: 0; top: 900px;}
	72% { opacity:1; margin-left: -10px; height: 250px; }
	74% { opacity: 0.7; margin-left: -4px; height: 230px; }
	76% { opacity: 1; margin-left: -10px; height: 210px; }
	78% { opacity: 0.8;margin-left: -4px; height: 190px; }
	80% { opacity: 1; margin-left: -10px; height: 170px; }
	82% { opacity: 0.7; margin-left: -4px; height: 150px; }
	84% { opacity: 1;  top: 170px; left: -10px;}
	87% { opacity: 0;  top: 170px; }
	to { opacity: 0;  top: 900px; }
}
CSS
CopyExpand

あと、何回か花火を表示したら、その画像を違うものに入れ替える Javascript 。css で設定されている keyframes のなにかしらのイベントを Javascript の方で得られないものか、と思い探してみたらやはりというかちゃんと操作できるように用意されている。

let fireburncount = 0;

const fireburn = document.getElementsByClassName( 'fireburn' );

if ( fireburn[0] ) {
	// 現在表示されている花火のファイル情報は data-img としてタグに仕込んである 例:'2-5-8'
	const imgset = fireburn[0].dataset.img;

	// アニメーションのイベント
	// animationiteration は infinity のときに一度づつのアニメーションで得られるイベント
	// ちなみに animationend は infinity のときは使えない
	fireburn[ 0 ].addEventListener( 'animationiteration', function() {
		++fireburncount;
		// console.log( fireburncount );

		if ( 0 === fireburncount % 3 ) {
			const newimgset = randomSortArray( imgset )

			if ( newimgset ) {
				const imglen = newimgset.length;

				for ( let i = 0; i < imglen; i++ ) {
					fireburn[ i ].style.backgroundImage = 'url( https://strix.main.jp/wp-content/themes/ural/firework' + newimgset[ i ] + '.png )';
				}
			}
		}
	});
}

// 10枚ある画像の番号から現在表示されている番号を除いてランダムに3つ選ぶ関数
function randomSortArray ( except ) {
	let nums = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ],
		n,
		k,
		ret = [];

	// 引数のexcept は現在の画像情報で文字列で受け取った場合は配列にする
	if ( except ) {
		if ( ! Array.isArray( except ) ) {
			if ( -1 !== except.indexOf( '-' ) ) {
				except = except.split( '-' );
				// console.log( except );
			} else {
				return false;
			}
		}
		const exceptLen = except.length;

		// 現在の画像番号を配列から除く
		for ( let i = 0; i < 10; i++ ) {
			for ( let j = 0; j < exceptLen; j++ ) {
				if ( nums[ i ] === parseInt( except[ j ] ) ) {
					nums.splice( i, 1 );
				}
			}
		}
		// console.log( nums );
	}

	// 有効な数字による配列をランダムに並び変える
	n = nums.length;
	while ( n > 0) {
		k = Math.floor( Math.random() * n );

		ret.push( nums[ k ] );
		nums.splice( k, 1 );
		n = nums.length;
	}
	ret.splice( 3 );
	return ret;
} 
JavaScript
CopyExpand

とまぁ、こんな感じ。
そして雪は降る。

Sanbanse Funabashi

Top

スクロールさせるか画像をクリックすると元に戻ります。