14
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

kurogoma939のひとりアドベントカレンダーAdvent Calendar 2024

Day 15

デザイナーの角丸へのこだわりに応えよう【HTML/CSS/JavaScript編】

Last updated at Posted at 2024-12-14

はじめに

先日以下のツイートを拝見しました。

「スーパー楕円...?なんだそれ?」と思うと同時に、「デザイナーからこれ要求されたらどうしよ」とも思ったので実装方法を検討してみました。

上記の記事にもありますが、スーパー楕円のように美しい角丸系は以下の方程式で導き出せるようです。
このnが2.5の時にスーパー楕円になるようです。

\left|\frac{x}{a}\right|^n + \left|\frac{y}{b}\right|^n = 1

数式が出てきて頭が痛くなる方もいるかもしれませんが理解できなくて大丈夫です!

では、早速実装方法です。
前編はHTML, CSS, JavaScriptでの実装方法について記載します。
後編はモバイル向けにFlutterでの実装方法を調査します!

HTML, CSSでの実装

sample.html
<html>
<head>
    <title>SQUARE</title>
    <link rel="stylesheet" type="text/css" href="sample.css">
</html>
<body>
    <div class="container">
        <div class="normal"></div>
        <div class="smooth"></div>
        <div class="super-ellipse"></div>
    </div>
</body>
sample.css
.container {
    margin: 28px;
    display: flex;
}

.normal, .smooth, .super-ellipse {
    margin: 20px;
    width: 200px;
    height: 200px;
    background: rgb(48, 48, 48);
}

.normal {
    border-radius: 40px;
}

.smooth {
    clip-path: path("M100,0 C195,0 200,10 200,100 C200,190 190,200 100,200 C10,200 0,190 0,100 C0,10 10,0 100,0 Z");
}

.super-ellipse {
    clip-path: path("M100,0 C180,0 200,20 200,100 C200,180 180,200 100,200 C20,200 0,180 0,100 C0,15 15,0 100,0 Z");
}

以下が結果です。

スクリーンショット 2024-12-14 23.13.40.png

コードにある通り、複雑な曲線はclip-pathを用いて実装が可能です。

clip-path: path("M100,0 C195,0 200,10 200,100 C200,190 190,200 100,200 C10,200 0,190 0,100 C0,10 10,0 100,0 Z")

この数値については、今回半径を200pxとしているので200pxを基準とした数値をとっています。
そのため、こちらを流用する場合はそれぞれの1辺の長さに合わせて数値の調整をしてください。

JavaScript

嫌な人は見なくて大丈夫ですが、先ほどの方程式を解いていき、それを参考に実装してみます。

方程式

\left|\frac{x}{a}\right|^n + \left|\frac{y}{b}\right|^n = 1

(1) yを求めるため移項する

\left|\frac{x}{a}\right|^n + \left|\frac{y}{b}\right|^n = 1 \implies \left|\frac{y}{b}\right|^n = 1 - \left|\frac{x}{a}\right|^n

(2) n乗根を打ち消す

\left|\frac{y}{b}\right| = \left(1 - \left|\frac{x}{a}\right|^n\right)^{\frac{1}{n}}

(3) bをかけてyの式が出せる

|y| = b\left(1 - \left|\frac{x}{a}\right|^n\right)^{1/n}

JSのコード

複雑かもしれませんが、a, b, nを変えてもらえば良いだけです。

<canvas id="myCanvas" width="400" height="400"></canvas>
<script>
    // ********** パラメータ設定 **********
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');
    const a = 100;   // x方向スケール
    const b = 100;   // y方向スケール
    const n = 2.5;     // 楕円形状パラメータ(n=2.5 でスーパー楕円化など)
    const step = 1;  // x座標をこのピクセル刻みでサンプリング
    const centerX = canvas.width / 2;
    const centerY = canvas.height / 2;

    // ********** 描画関数定義 **********
    /**
     * 指定した n, a, b に基づいて、スーパー楕円(もしくは一般的なn乗楕円)の上半分または下半分を描画する。
     * @param {CanvasRenderingContext2D} ctx コンテキスト
     * @param {number} a x方向の半径
     * @param {number} b y方向の半径
     * @param {number} n 楕円パラメータ
     * @param {number} step x座標をサンプリングするピクセル刻み
     * @param {boolean} isUpper true なら上半分、false なら下半分を描く
     */
    function drawSuperEllipseHalf(ctx, a, b, n, step, isUpper) {
        const direction = isUpper ? 1 : -1; 
        // xを -a から a まで動かすが、上半分は左→右、下半分は右→左へと回すことで閉路を形成
        const start = isUpper ? -a : a;
        const end = isUpper ? a : -a;
        const increment = isUpper ? step : -step;

        for (let x = start; isUpper ? (x <= end) : (x >= end); x += increment) {
            const ratio = Math.abs(x / a);
            const yPart = 1 - Math.pow(ratio, n);
            if (yPart < 0) continue;

            const yVal = b * Math.pow(yPart, 1/n);
            const drawX = centerX + x;
            const drawY = centerY + (isUpper ? -yVal : yVal);

            if ((isUpper && x === -a) || (!isUpper && x === a)) {
                // パス移動開始
                ctx.moveTo(drawX, drawY);
            } else {
                ctx.lineTo(drawX, drawY);
            }
        }
    }

    // ********** 実際の描画処理 **********
    ctx.beginPath();
    // 上半分を描画
    drawSuperEllipseHalf(ctx, a, b, n, step, true);
    // 下半分を描画
    drawSuperEllipseHalf(ctx, a, b, n, step, false);

    ctx.closePath();
    ctx.fillStyle = 'rgb(48,48,48)';
    ctx.fill();
</script>
n = 4.5 n = 3 n = 2.5
スクリーンショット 2024-12-15 0.03.41.png スクリーンショット 2024-12-15 0.03.26.png スクリーンショット 2024-12-15 0.03.07.png

もう少しスマートな書き方があれば後日更新させていただきます。
あくまで数式ベースな感じなのでCSSらしい書き方などができればベストですね。

14
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?