Hobby Graphics

CSS ボーダー付きの二等辺三角形を描きたい

要約先日書いたCSSを使いつつ、枠線・ボーダー付きの二等辺三角形を描くCSSを書きました。ボーダー幅を設定すると内側にひと回り小さな二等辺三角形を描き、位置を合わせます。

※不等辺三角形には対応していません。

ひと回り小さい二等辺三角形を描く

Inner-triangleの斜辺inner-hypotenuseは外側の斜辺から頂点側の差と底辺側の差を引くことにしました。なお、ここでは「頂点の角=2つの等辺に挟まれた角」、「斜辺=二等辺三角形の等辺」、「底辺=頂点の角の対向の辺」としています。数学の定義とは異なります。

上の左は頂点側の差、右は底辺側の差の図です。頂点側の差を「top-gap」、底辺側の差を「bottom-gap」としています。2つの図からtop-gapは「ボーダーの幅 / tan(θ/2)」、bottom-gapは「ボーダーの幅 / tan(90°- θ/2)」と「ボーダーの幅 / sin(90°- θ/2)」の和で計算できるとわかりました。なので、外側の三角形の斜辺を「triangle-hypotenuse」、内側の三角形の斜辺を「inner-hypotenuse」とすると
inner-hypotenuse = triangle-hypotenuse – top-gap – bottom-gap
となります。二等辺三角形の幅と高さを求める式は前回のものを流用していて、頂点の角の角度をθとすると、二等辺三角形の「幅=斜辺×cos(90°-θ/2)×2」、「高さ=斜辺×sin(90°-θ/2)」です。

外側と内側の三角形の位置を揃える

外側と内側の三角形の位置を揃える方法として、positionを使うものと、gridを使うものの二つが考えられます。このうちpositionを使った場合には、内側の三角形をy軸方向で調整するには底辺の位置を外側の三角形からボーダーの幅と同じ距離だけ離せば位置が揃います。
gridでセンター揃えにするのもシンプルで使いやすく、私は普段gridをよく使います。ですが、今回は少し面倒な調整が必要になりました。

上の左図は四角形と三角形それぞれにボーダーをつけた時の図です。内側と外側の図形の位置関係は、四角形の場合では天地左右の空き全てが等距離ならば均等のボーダー幅を設定することができます。でも、三角形の場合は天地の空きを調整しないとボーダーの幅が均等になりません。グリッドでレイアウトし、place-items: center; で中央揃えにすると、仕様上2つの要素の幅と高さの差をそれぞれ均等に割り振るため、頂点側(斜辺側)の空きは意図しているよりも狭くなり、底辺の空き(=ボーダー幅)は逆に広くなってしまいます。
上の右図は何も調整しない場合のplace-items: center; の模式図です。外側の三角形が明るいグレー、高さの差を均等に割り振られた内側の三角形の位置が濃いグレー、ボーダー幅が正しく表示された状態の内側の三角形の位置が白となっています。
「外側と内側の三角形の高さの差」から「天地のボーダー幅」を差し引いたものが Ⓐ、それを天地に等分に割り振った位置に内側の三角形が配置されていることになります。差分が Ⓑ なので、それをtransform: translate();で調整することにしました。

See the Pen bordered triangle 01 / 境界線付き三角形 01 grid使用 by 岩橋直人 (@lynbvavk-the-selector) on CodePen.

index.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>bordered triangle</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/yakuhanjp@3.3.1/dist/css/yakuhanjp.min.css" />
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
        <link
            href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap"
            rel="stylesheet"
        />
        <link rel="stylesheet" href="bordered-triangle.css" />
    </head>
    <body>
        <main>
            <h1>境界線付き三角形</h1>
            <div class="triangle-container">
                <p><span class="triangle bordered theta-90"></span>θ=90°</p>
                <p><span class="triangle bordered theta-60"></span>θ=60°</p>
                <p><span class="triangle bordered theta-30"></span>θ=30°</p>
            </div>
            <div class="triangle-container">
                <p><span class="triangle bordered theta-60"></span>no option class</p>
                <p><span class="triangle bordered theta-60 to-bottom"></span>to-bottom</p>
                <p><span class="triangle bordered theta-60 to-left"></span>to-left</p>
                <p><span class="triangle bordered theta-60 to-right"></span>to-right</p>
            </div>
        </main>
    </body>
</html>

style.css

@charset "UTF-8";
/* reset style */
body,
main,
h1,
h2,
div,
p,
span {
    padding: 0;
    margin: 0;
}
/* style */
body {
    font-family: YakuHanJP, "Roboto", "游ゴシック", "Yu Gothic", YuGothic, "メイリオ", Meiryo, "メイリオ", Meiryo,
        "Hiragino Kaku Gothic ProN", "Hiragino Kaku Gothic Pro", sans-serif;
    padding-top: 3em;
    display: grid;
    grid-template-columns: minmax(5vw, 1fr) minmax(33vw, 640px) minmax(5vw, 1fr);
    background-color: #f7f7f7;
}
h1,
h2 {
    font-size: 1.5em;
    font-weight: normal;
    text-align: center;
    margin-bottom: 1em;
}
main,
.sub {
    grid-column: 2;
    margin-bottom: 6em;
}
p {
    text-align: center;
}
.triangle-container {
    display: grid;
    grid-auto-flow: roq;
    row-gap: 1.5em;
    justify-items: center;
    align-items: end;
    @media screen and (min-width: 768px) {
        grid-auto-flow: column;
        column-gap: 0.75em;
    }
    &:last-child {
        margin-top: 2em;
    }
}
/* triangle */
/* variables */
.triangle,
.triangle:after,
.triangle:before {
    --triangle-hypotenuse: 150px;
    --triangle-border-width: 10px;
    --triangle-color: #222222;
    --inner-triangle-color: #fa0505;
    --half-theta: calc(var(--theta) / 2);

    --triangle-width: calc(var(--triangle-hypotenuse) * cos(90deg - var(--half-theta)) * 2);
    --triangle-height: calc(var(--triangle-hypotenuse) * sin(90deg - var(--half-theta)));
    --bottom-gap: calc(
        var(--triangle-border-width) / tan(90deg - var(--half-theta)) + var(--triangle-border-width) /
            sin((90deg - var(--half-theta)))
    );
    --top-gap: calc(var(--triangle-border-width) / tan(var(--half-theta)));
    --inner-hypotenuse: calc(var(--triangle-hypotenuse) - var(--top-gap) - var(--bottom-gap));
    --inner-triangle-width: calc(var(--inner-hypotenuse) * cos(90deg - var(--half-theta)) * 2);
    --inner-triangle-height: calc(var(--inner-hypotenuse) * sin(90deg - var(--half-theta)));

    --adjust-gap: calc((var(--triangle-border-width) / sin(var(--half-theta)) - var(--triangle-border-width)) / 2);
}
.theta-90 {
    --theta: 90deg;
}
.theta-60 {
    --theta: 60deg;
}
.theta-30 {
    --theta: 30deg;
}

.triangle {
    margin-bottom: 0.25em;
    display: block;
    width: var(--triangle-width);
    height: var(--triangle-height);
    background-color: var(--triangle-color);
    clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
    display: grid;
    place-items: center;
}

/* inner triangle */
.bordered::after {
    display: block;
    content: "";
    width: var(--inner-triangle-width);
    height: var(--inner-triangle-height);
    background-color: var(--inner-triangle-color);
    clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
    transform: translate(0, calc(var(--adjust-gap) * 1)); /* .to-bottom の時は -1 をかける */
}

/* .to-bottom .to-left .to-right */
.to-bottom {
    clip-path: polygon(0% 0%, 100% 0%, 50% 100%);
    &:after {
        clip-path: polygon(0% 0%, 100% 0%, 50% 100%);
        transform: translate(0, calc(var(--adjust-gap) * -1));
    }
}
.to-left,
.to-right {
    width: var(--triangle-height);
    height: var(--triangle-width);
    clip-path: polygon(0% 50%, 100% 0%, 100% 100%);
    &:after {
        width: var(--inner-triangle-height);
        height: var(--inner-triangle-width);
        clip-path: polygon(0% 50%, 100% 0%, 100% 100%);
        transform: translate(calc(var(--adjust-gap) * 1), 0); /* .to-right の時は -1 をかける */
    }
}
.to-right {
    clip-path: polygon(0% 0%, 100% 50%, 0% 100%);
    &:after {
        clip-path: polygon(0% 0%, 100% 50%, 0% 100%);
        transform: translate(calc(var(--adjust-gap) * -1), 0);
    }
}

二等辺三角形限定ですが、できました

三角形の向きを変更する場合には位置調整の向き変更するなどの必要がありますが、頂点の角度と斜辺の幅、ボーダーの幅を指定すると枠線付きの二等辺三角形が描けるようになりました。単位はem指定でも問題ありません。
translate()の中でcalc()を使って–adjust-gapに1をかけています。これは逆三角形の時などにはかける値を-1に変更して負の値にするためです。今後の仕事で装飾として三角形を使う場合に流用するか、あるいはそこまでいかなくても計算を理解する補助になってくれそうです。

blog 一覧へ