まだ三次元

窓の外から 「あ、あの雲、面白いね」 と声がして、窓を開けてみたらこれ。 確かに、ちょっと面白い。
そんな雲を暫く眺めていて、ふと思いついた。 css の3次元はカバーフローに使えるんじゃないかと。 いつの間にか iTunes から消えて無くなった、あの機能に。
ということで作ってみる。
まずはそれっぽく並べてみる。 確かこんな感じだったはず。
html
<div class="stage smpl1">
<div class="covers">
<div class="cover cv00">00</div>
<div class="cover cv01">01</div>
<div class="cover cv02">02</div>
<div class="cover cv03">03</div>
<div class="cover cv04">04</div>
<div class="cover cv05">05</div>
<div class="cover cv06">06</div>
<div class="cover cv07">07</div>
<div class="cover cv08">08</div>
<div class="cover cv09">09</div>
<div class="cover cv10">10</div>
</div>
</div>
css
.stage {
width: auto;
height: 300px;
overflow: hidden;
position: relative;
color: white;
background-color: black;
}
.stage .covers {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
transform-style: preserve-3d;
perspective: 400px;
}
.stage .covers .cover {
width: 200px;
height: 200px;
overflow: hidden;
position: absolute;
left: calc( 50% - 100px );
top: calc( 50% - 100px );
display: grid;
align-items: center;
justify-content: center;
}
.stage .covers .cv00 { transform: translateZ( -20px ) translateX( -270px ) rotateY( 70deg ); }
.stage .covers .cv01 { transform: translateZ( -20px ) translateX( -240px ) rotateY( 70deg ); }
.stage .covers .cv02 { transform: translateZ( -20px ) translateX( -210px ) rotateY( 70deg ); }
.stage .covers .cv03 { transform: translateZ( -20px ) translateX( -180px ) rotateY( 70deg ); }
.stage .covers .cv04 { transform: translateZ( -20px ) translateX( -150px ) rotateY( 70deg ); }
.stage .covers .cv05 { transform: translateZ( 100px ) translateX( 0px ) rotateY( 0deg ); }
.stage .covers .cv06 { transform: translateZ( -20px ) translateX( 150px ) rotateY( -70deg ); }
.stage .covers .cv07 { transform: translateZ( -20px ) translateX( 180px ) rotateY( -70deg ); }
.stage .covers .cv08 { transform: translateZ( -20px ) translateX( 210px ) rotateY( -70deg ); }
.stage .covers .cv09 { transform: translateZ( -20px ) translateX( 240px ) rotateY( -70deg ); }
.stage .covers .cv10 { transform: translateZ( -20px ) translateX( 270px ) rotateY( -70deg ); }
.smpl1 .cover {
border: 1px solid #ffffff;
}
とりあえず、中央に1枚、左右それぞれ5枚、計11枚分の表示領域を並べてみた。 格好良い。
実際の artwork が11枚なんてことはないだろう。 俺の手元には1000枚近くあるし。 なので11枚分じゃ全然足りないのだが、ここではそれっぽい見た目と動きを再現するだけなので良しとする。
で、その artwork だが、 iTunes の画像は著作権的に駄目そうなので、代わりに最近撮った写真をはめ込んでみる。











記憶の中のカバーフローに枠線は無かったので、枠線を白から透明に変えている。
ワイヤーフレームにあったSF的な格好良さが、写真をはめ込むことでかなり薄れてしまった。 というか無くなった。 それはきっと写真の所為なのだが、ここでは気にしない。 センスや撮影技術に対する反省は別途実施の予定。
そして動きを再現するのだが。
見た目の記憶も 「確かこんな感じだったような…」 って程度だが、動きの記憶はもっとぼんやりしている。 なんか格好良かった記憶はあるのだが。 クリックしたものが正面に来たような… スライダーがあったような… そんな状態。 しかし別に正確に再現したい訳ではないからね。 ふんわり覚えているものをふんわり作る。
で、直接選択とスライダーだが、勿論どっちも有って良い。 ここで扱う11枚程度なら直接選択が早い。 でも1000枚あったら無理。 大量のブラウズならスライダーが必須だが、しかしこっちは精度が微妙。 行き過ぎたり戻り過ぎたり。 結局、スライダーでざっくり当たりをつけて、最後は直接選択することになるんだよな。
よし、両方使えるようにしよう。
ということで作ったのがこれ。











html
<div class="stage smpl3">
<div class="covers">
<div class="cover cv00" data-n="-5"><img src="00.jpg" /></div>
<div class="cover cv01" data-n="-4"><img src="01.jpg" /></div>
<div class="cover cv02" data-n="-3"><img src="02.jpg" /></div>
<div class="cover cv03" data-n="-2"><img src="03.jpg" /></div>
<div class="cover cv04" data-n="-1"><img src="04.jpg" /></div>
<div class="cover cv05" data-n="0" ><img src="05.jpg" /></div>
<div class="cover cv06" data-n="1" ><img src="06.jpg" /></div>
<div class="cover cv07" data-n="2" ><img src="07.jpg" /></div>
<div class="cover cv08" data-n="3" ><img src="08.jpg" /></div>
<div class="cover cv09" data-n="4" ><img src="09.jpg" /></div>
<div class="cover cv10" data-n="5" ><img src="10.jpg" /></div>
</div>
</div>
<div class="ctrl">
<input type="range" id="rng" min="0" max="10" />
</div>
css(追加分のみ)
.stage .covers .cover img {
object-fit: cover;
object-position: center center;
width: 100%;
height: 100%;
}
.smpl3 .cover {
border: 1px solid transparent;
}
.smpl3 .cover:hover {
border: 1px solid #ff0000;
}
JavaScript
window.onload = () => {
const cvs = document.querySelectorAll( ".smpl3 .cover" );
const rng = document.getElementById( "rng" );
cvs.forEach( ( cv, i ) => {
cv.addEventListener( "click", () => mvCvs( i ), false );
} );
rng.addEventListener( "input", ( ev ) => mvCvs( ev.target.value ), false );
function mvCvs( picCvIdx ) {
const picN = parseInt( cvs[ picCvIdx ].dataset.n );
if ( !picN ) return;
cvs.forEach( ( cv ) => {
const curN = parseInt( cv.dataset.n );
const nxtN = curN - picN;
cv.animate(
[
{ transform: trnCss( curN ) },
{ transform: trnCss( nxtN ) }
],
{
fill: "forwards",
duration: 300
}
);
cv.dataset.n = nxtN;
} );
rng.value = picCvIdx;
}
function trnCss( n ) {
const x = Math.sign( n ) * ( 150 + 30 * ( Math.abs( n ) - 1 ) );
const y = Math.sign( n ) * ( -70 );
const z = n ? -20 : 100;
return `translateZ( ${z}px ) translateX( ${x}px ) rotateY( ${y}deg )`;
}
}
ロジックに特に難しいところは無いと思うが、それはついさっきまでどう実装するか考えていたから思うのであって、暫くすると綺麗さっぱり忘れるんだよな。 ということで説明を残しておく。 明日の自分のために。
まずは位置の考え方。
カバーの並びを、正面向きを0とした数直線の整数位置とする。 そして選択したカバーを次の正面、つまり0とする。 こうすると、ある状態でどれかのカバーを選択した時、選択した位置の値を各位置それぞれで引くことで、次の状態を得られる。
例えば、初期配置は全11枚のちょうど真ん中を正面にしているので、こう。
-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5この状態で左から三番目(-3)を選択した場合、新しい配置はこう。
-2. -1, 0, 1, 2, 3, 4, 5, 6, 7, 8選択した位置の値 -3 を数列の各値から引いた結果が、新しい数列になっている。 なんか長々と説明しているが、要は数直線上の並行移動。
次に変形。
変形の css の定義(transform)は、それぞれの位置で決まる。
中央。
translateZ( 100px ) translateX( 0px ) rotateY( 0deg );
中央から数えて左側N番目。
translateZ( -20px ) translateX( -150px - ( N - 1 ) * 30px ) rotateY( 70deg );
中央から数えて右側N番目。
translateZ( 20px ) translateX( 150px + ( N - 1 ) * 30px ) rotateY( -70deg );
都合の良いことに、正面か右か左か、何番目か、そうした位置と変形の式が上手く噛み合うのだな。 って、そりゃ都合が良くなるようにしているからなのだが、これを都合よく利用して、現位置の transform と新位置の transform を決め、アニメーションの始点・終点とする。 これを各カバーで。
MDN の解説だと、アニメーション終了時の状態を保持するのに
fill: "forwards"
だけではなく
commitStyles()
を使った方が良いとあった。
前者だけだと
ブラウザーがアニメーションの状態を無期限に、あるいは自動的に除去されるまで維持しなければならない
ことになり、後者によって
完了したスタイルはアニメーションの状態ではなく style 属性の値として取り込まれます
とのこと。
一般的にはそうすべきなのだろうが、今回はアニメーション後の状態を後の処理で使用しないんだよな。 今回のカバーフローでは、現新どちらも毎回位置に応じた transform の設定値として作っているからね。 ということで、コードの簡潔さを優先してスルーした。
以上、朧げな記憶からの再現ではあるが、なかなかそれっぽいものが出来たと概ね満足している。 動きについては。 格好良さという点では微妙。 最初のワイヤーフレームは良かったのに、写真の所為で数段落ちてしまったのがなぁ…。
写真、ちゃんと学んでみようかな。