CSSの底力を(もう一度)見せてやる
チラシを見ると頭の中でCSSを組み始めてしまうCSSオタクですこんにちわ。
というわけで今回もHTMLとCSSだけで正多面体描いていこうと思います。
例によってSVGもCANVASもjsも使いません。
(というかsvgでもthree.jsでも大変さは変わらない気がするけど)
ただHTMLとCSSを素で記述するのは大変なので、
PUGとStylusを利用して記述していきます。
準備
まず3Dで十全に動かす準備として、入念に要素を入れ子にします。
.container
.view
.view__inner
// 多面体全体
.solid
// 多面体の各平面
.face
.view
transform-style preserve-3d
transition all .2s linear 0s
animation rotation 10s linear 0s infinite
&:hover
animation-play-state paused
&__inner
transform-style preserve-3d
transition all .2s linear 0s
@keyframes rotation
from
transform rotateY(0deg)
to
transform rotateY(360deg)
.view
は回転用のレイヤーになります。
.view__inner
はコントローラー(後述)用のレイヤーです。
.solid
からが多面体本体となります。
.face
は各平面で、各多面体によって数が変わります。
正六面体と基本となる記述を書く
手始めに一番簡単な正六面体から始めましょう。
全ての面が直角に接しているのでやりやすいです。
正六面体を描くと共に各多面体に共通する部分の記述も一緒に書いていきます。
.solidの記述
.solid.solid--hexa
- for (var v = 1; v <= 6; v++)
.face.face--hexa.face--square(class=`face--${v}` data-num = v)
六面体なので.face
を6個作ります。
stylus側では再利用しやすいように一辺の長さを変数に格納しておきます。
$w = 100px
.solid
position absolute
top 50%
left 50%
backface-visibility visible
transform-style preserve-3d
transform-origin 50% 50% 0
&--hexa
width $w
height $w
margin-left - ($w / 2)
margin-top - ($w / 2)
各多面体で共通となる部分は.solid
に、
正六面体固有の部分は.solid--hexa
に記述しました。
.faceの記述
次に.face
の記述です。
$w = 100px
.solid
position absolute
top 50%
left 50%
backface-visibility visible
transform-style preserve-3d
transform-origin 50% 50% 0
&--hexa
width $w
height $w
margin-left - ($w / 2)
margin-top - ($w / 2)
+ .face
+ position absolute
+ top 0
+ left 0
+ width 100%
+ height 100%
+ background-color rgba(#fff, .15)
+ color rgba(#fff, .75)
+ &::before
+ position absolute
+ left 50%
+ content attr(data-num)
+ margin-top -15px
+ margin-left -15px
+ width 30px
+ height 30px
+ border-radius 15px
+ box-sizing border-box
+ text-align center
+ line-height 30px
+ &--square
+ transform-origin 50% 50% 0
+ &::before
+ top 50%
+ &--hexa
+ border 1px solid rgba(#fff, .75)
面に対して共通になる部分は.face
に、
正方形独自の部分は.face--square
に、
正六面体独自の設定は.face--hexa
に分けて記述しました。
まぁ正方形は正六面体でしか使わないので別に分けなくても良いんですがこう書いておくとわかりやすいので。
.face::before
の部分は各面に丸囲み数字を入れるためのものです。
必要ない場合は消してください。
この丸囲み数字を面の中心に置きたいのですが、面の中心は各多角形によって異なるため、
.face--square:before
の方のtop
で指定しています(left
は共通)。
各面の記述
では各面を3D移動していきます。
$w = 100px
.solid
position absolute
top 50%
left 50%
backface-visibility visible
transform-style preserve-3d
transform-origin 50% 50% 0
&--hexa
width $w
height $w
margin-left - ($w / 2)
margin-top - ($w / 2)
.face
position absolute
top 0
left 0
width 100%
height 100%
background-color rgba(#fff, .15)
color rgba(#fff, .75)
&::before
position absolute
left 50%
content attr(data-num)
margin-top -15px
margin-left -15px
width 30px
height 30px
border-radius 15px
box-sizing border-box
text-align center
line-height 30px
&--square
transform-origin 50% 50% 0
&::before
top 50%
&--hexa
border 1px solid rgba(#fff, .75)
+ for num in (0..1)
+ &^[0]--{num * 5 + 1}
+ transform rotateX((num - 1) * 180deg + 90deg) translateZ($w / 2)
+ for num in (2..5)
+ &^[0]--{num}
+ transform rotateY((num - 2) * 90deg) translateZ($w / 2)
基本的にどの多面体であっても、z軸(画面の奥→手前)方向に内接球の半径分引き出してから、XYZ軸のどれかで回転することによって所定の場所に移動させることになります。
正六面体の内接球の半径は一辺の1/2になるので簡単です。
translateZ($w / 2)
の記述で手前側に引き出しています。
そのあと、1番目と6番目の面を上と下に90度回転(X軸回転)、
残りの面はY軸回転で90度ずつずらしています。
これで正六面体は完成です。
ただし、この状態だと面がY軸を直角になっているため回してもなんとなくカッコ良くありません。
そこで、頂点を結んだ対角線をY軸と一致させることでカッコ良く回したいと思います。
多面体の軸を回転移動させる
正多面体自体の中心と頂点を結ぶ線は外接球(circumscribed sphere)の半径と等しく、また正多面体自体の中心と各面の中心を結ぶ線は内接球(inscribed sphere)の半径と等しくなります。
この二つの半径は面の正多角形の対角線を隔て直角を構成しているため、二つの線が交わる角度は逆コサイン関数により求めることができます。
正六面体では、一辺の長さを1とした場合、
内接球の半径は一辺の半分(1/2)です。
また外接級半径は正方形の対角線の半分(√2/2)と一辺の半分(1/2)からなる直角三角形の斜辺となるので三平方の定理で求めることができます。
つまり、
\begin{align}
内接球半径& = \frac{1}{2} \\
外接球半径& = \sqrt{\bigl(\frac{\sqrt{2}}{2}\bigr)^2+\bigl(\frac{1}{2}\bigr)^2} \\
&= \sqrt{\frac{2}{4}+\frac{1}{4}} \\
&= \sqrt{\frac{3}{4}} \\
&= \frac{\sqrt{3}}{2} \\
\end{align}
となるので、これらが構成する角度は、
\begin{align}
\arccos\frac{\frac{1}{2}}{\frac{\sqrt{3}}{2}}& = \arccos\frac{1}{\sqrt{3}} \\
& \approx 54.73561031724534°
\end{align}
Math.acos(1 / (3 ** 0.5)) / Math.PI * 180
// -> 54.73561031724534
となります。
stylusは三角関数(sin, cos, tan)と冪乗演算子(**)を搭載していますが、逆三角関数(asin, acos, atan)は搭載されていないため、この計算をstylus上で行うことはできません。
しょうがないのでjsで計算した値を変数に入れて使いました。
※ 本来はわざわざ度(deg)に直さずラジアン(rad)のままでもcssで使えますが、日本人的に度の方が理解しやすいので変換しています。
※ ちなみにcss自体でも「CSS Values and Units Module Level 4」で三角関数や逆三角関数などの数々の数学関数の実装が提案されていますが(→参照)、現在のところブラウザへの搭載状況は芳しくありません。
この角度は「マジックアングル(magic angle)」と呼ばれており、NMR(核磁気共鳴)やMRIにおいて重要な角度らしいです。マジックアングルと呼ばれるのはこの54.7356度だけなのですが、他の名称を知らないので以後、各多面体の外接球半径と内接球半径がなす角度をマジックアングルと呼称することにします。
(参考:[wikipwdia] Magic angle(英語記事))
(正式名称誰か教えてください)
$w = 100px
+ $ma = 54.73561031724534deg
.solid
position absolute
top 50%
left 50%
backface-visibility visible
transform-style preserve-3d
transform-origin 50% 50% 0
&--hexa
width $w
height $w
margin-left - ($w / 2)
margin-top - ($w / 2)
+ transform rotate3d(1, 0, 1, $ma)
.face
position absolute
top 0
left 0
width 100%
height 100%
background-color rgba(#fff, .15)
color rgba(#fff, .75)
&::before
position absolute
left 50%
content attr(data-num)
margin-top -15px
margin-left -15px
width 30px
height 30px
border-radius 15px
box-sizing border-box
text-align center
line-height 30px
&--square
transform-origin 50% 50% 0
&::before
top 50%
&--hexa
border 1px solid rgba(#fff, .75)
for num in (0..1)
&^[0]--{num * 5 + 1}
transform rotateX((num - 1) * 180deg + 90deg) translateZ($solid.hexa.rs * $w.hexa)
for num in (2..5)
&^[0]--{num}
transform rotateY((num - 2) * 90deg) translateZ($solid.hexa.rs * $w.hexa)
回転させるにあたって、正六面体のみ、頂点が一つもXYZ軸を通らないのでrotateX()
、rotateY()
、rotateZ()
ではなく、回転軸を指定できるrotate3D()
を使います(他では使いません)。
回転軸の指定は原点(0, 0, 0)からXZ面の手前右側に45度進む線(1, 0, 1)です。この軸に沿ってマジックアングル分回転させると頂点がY軸上に来るようになります。
コントローラーの記述
ただ回すだけでも面白くないので、コントローラーで色々な設定をON/OFFできるようにします。
+ -
+ const
+ controlList = [
+ {
+ id: 'perspective',
+ name: 'pers',
+ checked: false
+ },
+ {
+ id: 'rotatex90',
+ name: 'x90deg',
+ checked: false
+ },
+ {
+ id: 'rotatez45',
+ name: 'z45deg',
+ checked: false
+ },
+ {
+ id: 'stop',
+ name: 'stop',
+ checked: false
+ },
+ {
+ id: 'background',
+ name: 'bg',
+ checked: true
+ },
+ {
+ id: 'number',
+ name: 'num',
+ checked: true
+ }
+ ];
+ each obj in controlList
+ input(type='checkbox', id=obj.id, checked=obj.checked)
+ .control
+ each obj in controlList
+ label.control__label(for=obj.id)=obj.name
.container
.view
.view__inner
.solid.solid--hexa
- for (var v = 1; v <= 6; v++)
.face.face--hexa.face--square(class=`face--${v}` data-num = v)
input
display none
.control
position absolute
top 0
left 0
padding 0 8px 8px 0
color #fff
flex-flow row wrap
z-index 10
font-size 0
&__label
margin 8px 0 0 8px
display inline-block
background-color #888
padding 0 8px
border-radius 8px
line-height 1.4
text-transform uppercase
cursor pointer
font-size 12px
line-height 20px
#perspective:checked
& ~ .control [for=perspective]
background-color #88f
& ~ .container
perspective 500px
#stop:checked
& ~ .control [for=stop]
background-color #88f
& ~ .container .view
animation-play-state paused
#rotatex90:checked
& ~ .control [for=rotatex90]
background-color #88f
& ~ .container .view__inner
transform rotateX(90deg)
#rotatez45:checked
& ~ .control [for=rotatez45]
background-color #88f
& ~ .container .view__inner
transform rotateZ(45deg)
#rotatex90:checked ~ #rotatez45:checked
& ~ .container .view__inner
transform rotateZ(45deg) rotateX(90deg)
#background:checked
& ~ .control [for=background]
background-color #88f
#background:not(:checked)
& ~ .container .face
background-color transparent
#number:checked
& ~ .control [for=number]
background-color #88f
#number:not(:checked)
& ~ .container .face:before
opacity 0
これでちょっと傾けたり、パースペクティブ(遠近法)をON/OFFできるようになりました。
正六面体と基本の記述はこれで完成です。
See the Pen hedrons by ichimonzi (@ichimonzi) on CodePen.
※全部をいったん一つのcodepenに描いてから一つずつ独立させる、という方法を採ったので、書き方が若干冗長になっています。
この調子、どんどん描こう、多面体。
正四面体を描く
正六面体以外を描くにあたって問題となることがあります。
それは当たり前の話なのですが、正六面体以外、構成する面が長方形じゃない、ということです。
cssのclip-path
プロパティを使うことで長方形から任意の形に変形することはできますが、このプロパティは面(背景色)は指定できても境界線(枠線)は指定できません。
そこで、枠線用の要素を線の数だけ用意することで、これに対処したいと思います。
.solid.solid--tetra
- for (var v = 1; v <= 4; v++)
.face.face--tetra.face--tri(class=`face--${v}` data-num = v)
- for (var x = 1; x <= 3; x++)
.face__border(class=`face__border--${x}`)
.face
の中に.face__border
を辺の数だけ設置します、正四面体の場合構成面は正三角形なので.face__border
は三つとなります。
正三角形の設定
今回から設定が複雑になっていくので、変数はオブジェクト型で書いていきます。
まず面に対しての三角形の変数群です。
$w = {
tetra: 100px
}
$face = {
tri: {
h: sin(60deg),
rl: 1 / (cos(30deg) * 2),
rs: tan(30deg) / 2
}
}
長方形の底辺が正三角形の一辺と接する時にちょうどおさまる長方形の幅と高さを考えます。
幅は一辺と等しくなるので改めて計算する必要はありません。高さは正三角形の高さと等しくなります。
これを$face.tri.h
とします。
これに一辺の長さ$w.tetra
をかければ、一辺の長さを後で変更した時も一緒に変更されるようになります。
同じく外接円半径を$face.tri.rl
、内接円半径を$face.tri.rs
に入れておきます。
.face
position absolute
top 0
left 0
width 100%
height 100%
&::before
position absolute
left 50%
content attr(data-num)
margin-top -15px
margin-left -15px
width 30px
height 30px
border-radius 15px
box-sizing border-box
text-align center
line-height 30px
+ &__border
+ position absolute
+ top 0
+ left 0
+ width 100%
+ height 100%
+ box-sizing border-box
+ &--tri
+ clip-path polygon(
+ 50% 0,
+ 100% 100%,
+ 0 100%
+ )
+ transform-origin 50% (100% / $face.tri.h * $face.tri.rl) 0
+ &::before
+ top (100% / $face.tri.h * $face.tri.rl)
+ & ^[0]__border
+ transform-origin 50% (100% / $face.tri.h * $face.tri.rl) 0
+ width 100%
+ for num in (1..3)
+ &--{num}
+ transform rotateZ((num - 1) * 120deg)
正三角形の場合、clip-path
はかなり単純です。高ささえ正しければ、上辺の中点と右下、左下を結ぶだけで正三角形ができます。
transform-origin
のY座標やtop
は外接円半径 / 外接長方形の高さ
をパーセンテージで入れておきます。
\frac{\frac{1}{2\cos30°}}{\sin60°} \approx 66.66666663347881\%
一応計算値を書き出しましたが、ここはstylusが自動でやってくれます。
あとはz軸に沿って120°ずつ回すことで、枠線用要素.face__border
が所定の位置に移動してくれます。
これで正三角形の用意ができました。
この正三角形は正八面体と正二十面体でも使うことになります。
.solidと.faceの残りの設定
.solid
position absolute
top 50%
left 50%
backface-visibility visible
transform-style preserve-3d
transform-origin 50% 50% 0
+ &--tetra
+ width $w.tetra
+ height $face.tri.h * $w.tetra
+ margin-left - ($w.tetra / 2)
+ margin-top - ($face.tri.h * $w.tetra / 2)
.solidに関しては特筆すべき部分はありません。
高さが正六面体と異なり$wではなくなりました。
続いて正四面体用の変数群です。
$solid = {
tetra: {
rl: sqrt(3 / 8),
rs: (1 / sqrt(24)),
da: 70.52877936550931deg
}
}
Math.atan(Math.sqrt(8)) / Math.PI * 180
// -> 70.52877936550931
(参照:[wikipedia] 正四面体)
ここら辺から自前の計算ではよくわからなくなってくるのでwikipedia要参照。
便利だぜwikipedia。
外接球半径を$solid.tetra.rl
、内接球半径を$solid.tetra.rs
としています。
sqrt()
関数は自作関数です。
いちいち冪乗演算子で書くのが面倒だったので用意しました。
sqrt(num)
num ** (1/2)
$solid.tetra.da
は二面角(dihedral angle)といって多面体の隣り合う面同士がなす角度です。
正四面体の場合はこの角度だけで各面の移動ができます。
.face
position absolute
top 0
left 0
width 100%
height 100%
&::before
position absolute
left 50%
content attr(data-num)
margin-top -15px
margin-left -15px
width 30px
height 30px
border-radius 15px
box-sizing border-box
text-align center
line-height 30px
&__border
position absolute
top 0
left 0
width 100%
height 100%
box-sizing border-box
&--tri
clip-path polygon(
50% 0,
100% 100%,
0 100%
)
transform-origin 50% (100% / $face.tri.h * $face.tri.rl) 0
&::before
top (100% / $face.tri.h * $face.tri.rl)
& ^[0]__border
transform-origin 50% (100% / $face.tri.h * $face.tri.rl) 0
width 100%
for num in (1..3)
&--{num}
transform rotateZ((num - 1) * 120deg)
+ &--tetra
+ background-color rgba(#fff, .15)
+ color rgba(#fff, .75)
+ &::before
+ border 1px solid rgba(#fff, .75)
+ & ^[0]__border
+ border-bottom 1px solid rgba(#fff, .75)
+ &^[0]--1
+ transform rotateY(180deg) rotateX(-90deg) translateZ($solid.tetra.rs * $w.tetra)
+ for num in (0..2)
+ &^[0]--{num + 2}
+ transform rotateY(num * 120deg) rotateX(90deg - $solid.tetra.da) translateZ($solid.tetra.rs * $w.tetra)
全ての面を$solid.tetra.rs * $w.tetra
で内接球半径分手前に押し出します。
そして1枚目の面はY軸回転で逆さまにしたあと下に90°移動させます。
これは面の辺同士を接するようにするためです。
2〜4枚目の面は90°から二面角を引いた分だけX軸で傾け、Y軸回転で120°ずつずらします。
これで正四面体は完成です。
最初から頂点がY軸を通っているので軸調整は必要ないです。
ちなみに正四面体のみ、頂点と多面体の中心点を通る線が反対側の頂点を通りません。
See the Pen hedrons by ichimonzi (@ichimonzi) on CodePen.
正八面体を描く
正八面体も正四面体と同じく正三角形で構成されるため、上で書いた正三角形のコードが流用できます。
pugは以下になります。
.solid.solid--octa
- for (var v = 1; v <= 8; v++)
.face.face--octa.face--tri(class=`face--${v}` data-num = v)
- for (var x = 1; x <= 3; x++)
.face__border(class=`face__border--${x}`)
正八面体の変数群は以下です。
$solid = {
octa: {
rl: (1 / sqrt(2)),
rs: (1 / sqrt(6)),
da: 109.47122063449069deg,
ma: 35.264389682754654deg
}
}
(参考:[wikipedia 正八面体]、[wikipedia] Octahedron(英語))
(※ 二面角が何故か日本語版には載っていなかった)
二面角($solid.octa.da
)とマジックアングル($solid.octa.ma
)は以下の計算で求められます。
\begin{align}
da &= \arccos (−\frac{1}{3}) \\
&\approx 109.47122063449069deg \\
ma &= \arccos \frac{\frac{1}{\sqrt{6}}}{\frac{1}{\sqrt{2}}} \\
&= \arccos \frac{\sqrt{2}}{\sqrt{6}} \\
&\approx 54.73561031724534deg
\end{align}
Math.acos(-1 / 3) / Math.PI * 180
// 109.47122063449069
Math.acos(Math.sqrt(2) / Math.sqrt(6)) / Math.PI * 180
// 54.73561031724534
.solidの設定
.solid
position absolute
top 50%
left 50%
backface-visibility visible
transform-style preserve-3d
transform-origin 50% 50% 0
+ &--octa
+ width $w.octa
+ height $face.tri.h * $w.octa
+ margin-left - ($w.octa / 2)
+ margin-top - ($face.tri.h * $w.octa / 2)
+ transform translateY(- $face.tri.rs * $w.octa / 2)
正三角形の中点は外接長方形の中点より低いのでその分のギャップをtranslateY
で調整しています。
ギャップは内接円半径の半分です。
.faceの設定
.face
position absolute
top 0
left 0
width 100%
height 100%
&::before
position absolute
left 50%
content attr(data-num)
margin-top -15px
margin-left -15px
width 30px
height 30px
border-radius 15px
box-sizing border-box
text-align center
line-height 30px
&__border
position absolute
top 0
left 0
width 100%
height 100%
box-sizing border-box
&--tri
clip-path polygon(
50% 0,
100% 100%,
0 100%
)
transform-origin 50% (100% / $face.tri.h * $face.tri.rl) 0
&::before
top (100% / $face.tri.h * $face.tri.rl)
& ^[0]__border
transform-origin 50% (100% / $face.tri.h * $face.tri.rl) 0
width 100%
for num in (1..3)
&--{num}
transform rotateZ((num - 1) * 120deg)
+ &--octa
+ background-color rgba(#fff, .15)
+ color rgba(#fff, .75)
+ &::before
+ border 1px solid rgba(#fff, .75)
+ & ^[0]__border
+ border-bottom 1px solid rgba(#fff, .75)
+ for num in (1..4)
+ &^[0]--{num}
+ transform rotateY((num - 1) * 90deg) rotateX(90deg - $solid.octa.ma) translateZ($solid.octa.rs * $w.octa)
+ &^[0]--{num + 4}
+ transform rotateY((num - 1) * 90deg) rotateX(-90deg + $solid.octa.ma) translateZ($solid.octa.rs * $w.octa) rotateZ(180deg)
全ての面を$solid.octa.rs * $w.octa
で内接球半径分手前に押し出します。
そのあと90°上に倒したのちマジックアングル分戻すことで頂点の一つがY軸と重なります。
この状態でY軸に沿って90°ずつ回転させれば上半分の配置が終了します。
下半分は逆に90°下に倒したのちマジックアングル分戻し、同じくY軸に沿って90°ずつ回転させれば完了です。
これで正八面体が完成しました。
See the Pen hedrons by ichimonzi (@ichimonzi) on CodePen.
正十二面体を描く
そろそろ自分が何のためにこんなことをしているのかよくわからなくなってくる頃合いですが、
深く考えてはいけません。
ラマヌジャンのようにナーマギリ女神に導かれるままコードを書き進めましょう(?)
.solid.solid--dodeca
- for (var v = 1; v <= 12; v++)
.face.face--dodeca.face--penta(class=`face--${v}` data-num = v)
- for (var x = 1; x <= 5; x++)
.face__border(class=`face__border--${x}`)
正十二面体は英語では「ドデカヘドロン(dodecahedron)」と言います。
かっこいいね!
面の数は12、構成する正多角形はこれまで出てこなかった正五角形です。
なので新しく正五角形用の設定を書きます。
正五角形の設定
正五角形用の変数群は以下になります。
$face = {
penta: {
w: (cos(72deg) * 2) + 1,
h: (tan(72deg) / 2),
rl: 1 / (cos(54deg) * 2),
rs: tan(54deg) / 2,
v1y: sin(36deg),
v2x: cos(72deg)
}
}
底辺に正五角形の一辺が接する外接長方形を作ります。
幅は$face.penta.w
、高さは$face.penta.h
となり、いよいよ幅も高さも一辺の長さとは異なることになります。
$face.penta.rl
は外接円半径、$face.penta.rs
は内接円半径です。
$face.penta.v1y
は上辺とも底辺とも接しない2つの頂点のY座標、
$face.penta.v2x
は底辺と接する二つの頂点のX座標計算用の数値です。
.face
position absolute
top 0
left 0
width 100%
height 100%
&::before
position absolute
left 50%
content attr(data-num)
margin-top -15px
margin-left -15px
width 30px
height 30px
border-radius 15px
box-sizing border-box
text-align center
line-height 30px
&__border
position absolute
top 0
left 0
width 100%
height 100%
box-sizing border-box
+ &--penta
+ clip-path polygon(
+ 50% 0,
+ 100% (100% / $face.penta.h * $face.penta.v1y),
+ (100% / $face.penta.w * ($face.penta.v2x+ 1)) 100%,
+ (100% / $face.penta.w * $face.penta.v2x) 100%,
+ 0 (100% / $face.penta.h * $face.penta.v1y)
+ )
+ transform-origin 50% (100% / $face.penta.h * $face.penta.rl) 0
+ &^[0]::before
+ top (100% / $face.penta.h * $face.penta.rl)
+ & ^[0]__border
+ transform-origin 50% (100% / $face.penta.h * $face.penta.rl) 0
+ width 100%
+ for num in (1..5)
+ &--{num}
+ transform rotateZ((num - 1) * 72deg)
clip-path
プロパティは最初の点が上辺の中心でそれ以降v1y
とv2x
を交互に使いながら指定します。
あとは正三角形の時とあまり変わりません。
transform-origin
のY座標やtop
プロパティも同じく外接円半径/外接長方形の高さ
となります。
枠線用要素は72°ずつ回転させることで所定の位置に収まります。
.solidの設定
$solid = {
dodeca: {
rl: ((sqrt(15) + sqrt(3)) / 4),
rs: sqrt((25 + (11 * sqrt(5))) / 40),
da: 116.56505117707799deg,
ma: 37.3773681406497deg
}
}
(参考:[wikipedia 正十二面体])
二面角($solid.dodeca.da
)とマジックアングル($solid.dodeca.ma
)は以下の計算で求められます。
\begin{align}
da &= \arccos (−\frac{1}{\sqrt{5}}) \\
&\approx 116.56505117707799deg \\
ma &= \arccos \frac{\sqrt{\frac{25+11\sqrt{5}}{40}}}{\frac{\sqrt{15}+\sqrt{3}}{4}} \\
&= \arccos \frac{2\sqrt{\frac{25+11\sqrt{5}}{10}}}{\sqrt{15}+\sqrt{3}} \\
&\approx 37.37736814064969deg
\end{align}
Math.acos(-1 / Math.sqrt(5)) / Math.PI * 180
// 116.56505117707799
Math.acos((Math.sqrt((25 + (11 * Math.sqrt(5))) / 10) * 2) / (Math.sqrt(15) + Math.sqrt(3))) / Math.PI * 180
// 37.37736814064969
...何この式超怖い。
.solid
のstylusは以下になります。
.solid
position absolute
top 50%
left 50%
backface-visibility visible
transform-style preserve-3d
transform-origin 50% 50% 0
+ &--dodeca
+ width $face.penta.w * $w.dodeca
+ height $face.penta.h * $w.dodeca
+ margin-left - ($face.penta.w * $w.dodeca / 2)
+ margin-top - ($face.penta.h * $w.dodeca / 2)
ギャップ調整については後述します。
.faceの設定
.face
position absolute
top 0
left 0
width 100%
height 100%
&::before
position absolute
left 50%
content attr(data-num)
margin-top -15px
margin-left -15px
width 30px
height 30px
border-radius 15px
box-sizing border-box
text-align center
line-height 30px
&__border
position absolute
top 0
left 0
width 100%
height 100%
box-sizing border-box
&--penta
clip-path polygon(
50% 0,
100% (100% / $face.penta.h * $face.penta.v1y),
(100% / $face.penta.w * ($face.penta.v2x+ 1)) 100%,
(100% / $face.penta.w * $face.penta.v2x) 100%,
0 (100% / $face.penta.h * $face.penta.v1y)
)
transform-origin 50% (100% / $face.penta.h * $face.penta.rl) 0
&^[0]::before
top (100% / $face.penta.h * $face.penta.rl)
& ^[0]__border
transform-origin 50% (100% / $face.penta.h * $face.penta.rl) 0
width 100%
for num in (1..5)
&--{num}
transform rotateZ((num - 1) * 72deg)
+ &--dodeca
+ background-color rgba(#fff, .15)
+ color rgba(#fff, .75)
+ &::before
+ border 1px solid rgba(#fff, .75)
+ & ^[0]__border
+ border-bottom 1px solid rgba(#fff, .75)
+ for num in (0..1)
+ &^[0]--{num * 6 + 1}
+ transform rotateZ(num * 180deg) rotateY(num * 180deg) translateZ($solid.dodeca.rs * $w.dodeca)
+ for num in (1..5)
+ &^[0]--{num + 1}
+ transform rotateZ((num - 1) * 72deg) rotateX(180deg + $solid.dodeca.da) translateZ($solid.dodeca.rs * $w.dodeca) rotateZ(180deg)
+ &^[0]--{num + 7}
+ transform rotateZ((num - 1) * 72deg + 36deg) rotateX(- $solid.dodeca.da) translateZ($solid.dodeca.rs * $w.dodeca)
いよいよ難解になってまいりました。
全ての面を$solid.dodeca.rs * $w.dodeca
で内接球半径分手前に押し出します。
今回は1枚目と7枚目を中心として、残りをそれぞれ5枚ずつ周りに配置していくイメージです。
1枚目と7枚目はそれぞれ上下逆さまに原点越しに相対させます。
2枚目〜6枚目は中心とする面の上下逆状態から180°から二面角を引いた角度だけ逆方向に倒します。
そのあとそれぞれを72°ずつ回転させれば1枚目の周りに配置できます。
8枚目〜12枚目は180°から「180°から二面角を引いた角度」を引いた角度になるのでつまり二面角のマイナス値です。
さらに全てを36°傾けたあと72°ずつ回転させれば7枚目の周りに配置できます。
回転軸の調整
.solid
position absolute
top 50%
left 50%
backface-visibility visible
transform-style preserve-3d
transform-origin 50% 50% 0
&--dodeca
width $face.penta.w * $w.dodeca
height $face.penta.h * $w.dodeca
margin-left - ($face.penta.w * $w.dodeca / 2)
margin-top - ($face.penta.h * $w.dodeca / 2)
+ transform rotateX(90deg - $solid.dodeca.ma) translateY(-($face.penta.rl - $face.penta.rs) * $w.dodeca / 2)
頂点をY軸まで倒すには、90°上側に倒したあとマジックアングルの角度だけ引き戻す、ということが必要になります。
また正五角形とその外接長方形の中心はY座標が異なるため、このギャップをtranslateY
で調整します。
以上で正十二面体が完成となります。
See the Pen dodecahedron by ichimonzi (@ichimonzi) on CodePen.
ぜひパースペクティブをONにして見てほしい。
圧倒的な立体感。
すごい(語彙力の欠如)。
正二十面体を描く
長かったですねここまで。
では最後に張り切って二重面体を描いていきたいと思います。
.solid.solid--icosa
- for (var v = 1; v <= 20; v++)
.face.face--icosa.face--tri(class=`face--${v}` data-num = v)
- for (var x = 1; x <= 3; x++)
.face__border(class=`face__border--${x}`)
正十二面体は英語では「イコサヘドロン(icosahedron)」と言います。
かっこいいね!
正十二面体の構成面は正三角形なのでこれまた前に作ったものを流用しましょう。
変数群は以下になります。
.solidの設定
$solid = {
icosa: {
rl: (sqrt(10 + (2 * sqrt(5))) / 4),
rs: (((3 * sqrt(3)) + sqrt(15)) / 12),
da: 138.1896851042214deg,
ma: 37.37736814064969deg
}
}
(参考:[wikipedia 正二十面体]、[wikipedia Regular icosahedron(英語)])
何回も「面体」と打ち込んでるせいで明太子食べたくなってきました。
二面角($solid.icosa.da
)とマジックアングル($solid.icosa.ma
)は以下の計算で求められます。
\begin{align}
da &= \arccos (−\frac{\sqrt{5}}{3}) \\
&\approx 138.1896851042214° \\
ma &= \arccos \frac{\frac{3\sqrt{3}+\sqrt{15}}{12}}{\frac{\sqrt{10+2\sqrt{5}}}{4}} \\
&= \arccos \frac{3\sqrt{3}+\sqrt{15}}{3\sqrt{10+2\sqrt{5}}} \\
&\approx 37.37736814064969°
\end{align}
Math.acos(-(Math.sqrt(5) / 3)) / Math.PI * 180
// 138.1896851042214
Math.acos(((3 * Math.sqrt(3)) + Math.sqrt(15)) / (3 * Math.sqrt(10 + (2 * Math.sqrt(5))))) / Math.PI * 180
// 37.37736814064969
実はマジックアングルが正十二面体と一緒です。
ということはアークコサインの中の分数もどうにかすれば同じ形になるはずなのですが
よくわかりませんでした。
....一応計算はできてるので次に進みます。
.solidの設定
.solid
position absolute
top 50%
left 50%
backface-visibility visible
transform-style preserve-3d
transform-origin 50% 50% 0
+ &--icosa
+ width $w.icosa
+ height $face.tri.h * $w.icosa
+ margin-left - ($w.icosa / 2)
+ margin-top - ($face.tri.h * $w.icosa / 2)
+ transform rotateX(90deg) translateY(- $face.tri.rs * $w.icosa / 2)
正八面体と同じくY軸方向(軸を倒す前のZ軸方向)にギャップがあるのでそれを直しています。
…本来ならこれで良いはずなのですが、ちょうど90°倒した時だけChromeでレンダリングの不具合が起こるようなので、
もう一つ先の頂点までさらに移動させます。
.solid
position absolute
top 50%
left 50%
backface-visibility visible
transform-style preserve-3d
transform-origin 50% 50% 0
+ &--icosa
+ width $w.icosa
+ height $face.tri.h * $w.icosa
+ margin-left - ($w.icosa / 2)
+ margin-top - ($face.tri.h * $w.icosa / 2)
- transform rotateX(90deg) translateY(- $face.tri.rs * $w.icosa / 2)
+ transform rotateX(270deg + ($solid.icosa.ma * 2) - $solid.icosa.da) translateY(- $face.tri.rs * $w.icosa / 2)
レンダリンクの不具合の理由はよくわかりませんでした…。
.faceの設定
.face
position absolute
top 0
left 0
width 100%
height 100%
&::before
position absolute
left 50%
content attr(data-num)
margin-top -15px
margin-left -15px
width 30px
height 30px
border-radius 15px
box-sizing border-box
text-align center
line-height 30px
&__border
position absolute
top 0
left 0
width 100%
height 100%
box-sizing border-box
&--tri
clip-path polygon(
50% 0,
100% 100%,
0 100%
)
transform-origin 50% (100% / $face.tri.h * $face.tri.rl) 0
&::before
top (100% / $face.tri.h * $face.tri.rl)
& ^[0]__border
transform-origin 50% (100% / $face.tri.h * $face.tri.rl) 0
width 100%
for num in (1..3)
&--{num}
transform rotateZ((num - 1) * 120deg)
+ &--icosa
+ background-color rgba(#fff, .15)
+ color rgba(#fff, .75)
+ &::before
+ border 1px solid rgba(#fff, .75)
+ & ^[0]__border
+ border-bottom 1px solid rgba(#fff, .75)
+ for num in (1..5)
+ &^[0]--{num}
+ transform rotateZ((num - 1) * 72deg) rotateX(- $solid.icosa.ma) translateZ($solid.icosa.rs * $w.icosa)
+ &^[0]--{num + 5}
+ transform rotateZ((num - 1) * 72deg) rotateX(180deg - $solid.icosa.ma) translateZ($solid.icosa.rs * $w.icosa)
+ &^[0]--{num + 10}
+ transform rotateZ((num - 1) * 72deg) rotateX(-(180deg - $solid.icosa.da) - $solid.icosa.ma) translateZ($solid.icosa.rs * $w.icosa) rotateZ(180deg)
+ &^[0]--{num + 15}
+ transform rotateZ((num - 1) * 72deg) rotateX($solid.icosa.da - $solid.icosa.ma) translateZ($solid.icosa.rs * $w.icosa) rotateZ(180deg)
流石に今までで一番難解です。
今まで通り全ての面は$solid.dodeca.rs * $w.icosaで内接球半径分手前に押し出しています。
5枚ずつに分けて考えます。
まずマジックアングル分動かして頂点の一つをZ軸に接するようにします。
この状態でZ軸を回転軸にして72°ずつ動かせば1枚目から5枚目を配置できます。
6枚目から10枚目は1枚目から5枚目のまだ接する面がない辺に接地させます。
つまりマジックアングル分+「180°-二面角」分動かします。
11枚目〜20枚目はこれと180°違うことをやれば自然と相対した形に配置できます。
とうとう正二十面体も完成しました。
これを素のCSSで書いていたら発狂していたでしょう。
stylus万歳。
See the Pen hedrons by ichimonzi (@ichimonzi) on CodePen.
まとめ
全5種の多面体を一堂に集めて回してみました。
全ての多面体は一辺の長さを$w
で定義しており、ここを変えるだけで多面体の大きさを変えられます。
こちらの方はコントローラーを大幅に強化しています。
「same radius」をONにしたあと「opacity off」をしてみると、ゲームのポリゴンがバグったみたいなの正多面体同士の干渉具合が見られます。面と面が干渉して凸凹になっていて、ちゃんとCSSで3D描けるんだなってちょっと感動します。
あとこちらにはおまけとして「切頂二十面体(truncated icosahedron)」を追加しました。
「Truncated ICOSA」を押すと正二十面体と正十二面体を部品としてサッカーボール型が組み上がるのが見れるはずです。
See the Pen hedrons by ichimonzi (@ichimonzi) on CodePen.
ちなみにJSで3D座標をいじりたい方はDOMMatrix APIというものが用意されています。これを使って正二十面体の最初の5枚を指定する例をコメントアウトで書いておきました。(最終的にはcssのtransform: matrix3d()
に変換しています)。
作ってみて
三角関数と逆三角関数と平方根を一生分使い切った気がする。
拡大してよく見てみると面どうしがちゃんとくっついてないように見えるのは、
CSSの限界なのか、計算と座標移動が複雑すぎて値が狂ってしまうのか、
…それとも書き手の計算ミスなのか。
終わりに
Chromeで特定の角度でなぜか面が表示されなくなる、という不具合に悩まされ、場所によっては迂遠な記述をせざるを得なかったのが心残りです。
CSSでやることじゃねぇなと思いました。