1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ふわふわしたUIをつくろう

Last updated at Posted at 2025-04-21

はじめに

昔どこかのWebサイトで見た「ふわふわしたUI」を作ってみました。なんと呼べばいいのか分からないのでこのまま行きます。

完成図

完成図はこちら
image.png

ソースコード

HTML

point

#fuwaAreaをふわふわの中心点を決めるために設定。
その中の.fuwaは独立したそれぞれの要素。今回は画像を入れていますが、文字などの要素も使えます。.fuwaの中なら複数の要素を入れても大丈夫です。

<div id="fuwaArea">
        <div class="fuwa">
            <img src="https://icooon-mono.com/i/icon_11343/icon_113431_64.png">
        </div>
        <div class="fuwa">
            <img src="https://icooon-mono.com/i/icon_11343/icon_113431_64.png">
        </div>
        <div class="fuwa">
            <img src="https://icooon-mono.com/i/icon_11343/icon_113431_64.png">
        </div>
    </div>

CSS

こちらは簡単に。

point

今回のコードでは、エリアの中心に向かって移動するようにしかしていないため、エリアの形状は正方形が適しています。

#fuwaArea {
    width: 400px;
    height: 400px;
    margin: 0 auto;
    position: relative;
    border: 2px dashed #ccc;
    border-radius: 50%;

}

.fuwa {
    position: absolute;
    cursor: grab;
}

.fuwa img {
    width: 64px;
    height: 64px;
}

JavaScript

point

複数の要素に対して同じように移動を定義しているのでループで書いちゃいます。オブジェクトごとに移動パラメータを変更したい場合は工夫してください。

function animateFuwa() {
    const fuwaList = document.querySelectorAll('.fuwa');
    fuwaList.forEach(fuwa => {
        const fuwaArea = document.getElementById('fuwaArea');
        const fuwaAreaRect = fuwaArea.getBoundingClientRect();
        const fuwaAreaRectWidth = fuwaAreaRect.width;
        const fuwaAreaRectHeight = fuwaAreaRect.height;
        const fuwaAreaRectCenterX = fuwaAreaRect.left + fuwaAreaRectWidth / 2;
        const fuwaAreaRectCenterY = fuwaAreaRect.top + fuwaAreaRectHeight / 2;


        const rect = fuwa.getBoundingClientRect();
        const centerX = rect.left + rect.width / 2;
        const centerY = rect.top + rect.height / 2;

        const fuwaAreaRectCenterDistance = Math.sqrt(
            Math.pow(centerX - fuwaAreaRectCenterX, 2) +
            Math.pow(centerY - fuwaAreaRectCenterY, 2)
        );

        const fuwaAreaRectCenterAngle = Math.atan2(centerY - fuwaAreaRectCenterY, centerX - fuwaAreaRectCenterX);
        const fuwaAreaRectCenterSpeed = Math.pow(fuwaAreaRectCenterDistance, 2) / 10000;
        let moveX = - Math.cos(fuwaAreaRectCenterAngle) * fuwaAreaRectCenterSpeed;
        let moveY = - Math.sin(fuwaAreaRectCenterAngle) * fuwaAreaRectCenterSpeed;

        let otherFuwaList = Array.from(fuwaList).filter(obj => obj !== fuwa);

        otherFuwaList.forEach(otherFuwa => {
            const otherRect = otherFuwa.getBoundingClientRect();
            const otherX = otherRect.left + otherRect.width / 2;
            const otherY = otherRect.top + otherRect.height / 2;

            const dx = centerX - otherX;
            const dy = centerY - otherY;
            const angle = Math.atan2(dy, dx);
            const spanX = dx - fuwaAreaRectWidth / 2 - otherRect.width / 2;
            const spanY = dy - fuwaAreaRectHeight / 2 - otherRect.height / 2;
            const spanDistance = Math.sqrt(spanX * spanX + spanY * spanY);
            // 距離に応じてスピード調整(近いほど大きく動く)
            const speed = Math.pow(Math.max(1, 10 - spanDistance), 2) / 2;

            moveX += Math.cos(angle) * speed;
            moveY += Math.sin(angle) * speed;
        });

        // 現在の transform を取得して相対的に動かす
        const currentTransform = getComputedStyle(fuwa).transform;
        let currentX = 0, currentY = 0;

        if (currentTransform !== 'none') {
            const matrix = new DOMMatrix(currentTransform);
            currentX = matrix.m41;
            currentY = matrix.m42;
        }

        fuwa.style.transform = `translate(${currentX + moveX}px, ${currentY + moveY}px)`;
    });

    requestAnimationFrame(animateFuwa); // 続ける
}

// スタート
requestAnimationFrame(animateFuwa);

補遺

せっかく動くようにしたのに開始時だけじゃつまらない。ということで、手動でのドラッグをできるようにしてみましょう。

document.querySelectorAll('.fuwa').forEach(enableDrag);
function enableDrag(element) {
    let isDragging = false;

    element.addEventListener('mousedown', (e) => {
        isDragging = true;
        e.preventDefault(); // 選択防止
    });

    document.addEventListener('mousemove', (e) => {
        if (isDragging) {
            const transform = getComputedStyle(element).transform;
            let currentX = 0, currentY = 0;

            if (transform !== 'none') {
                const matrix = new DOMMatrix(transform);
                currentX = matrix.m41;
                currentY = matrix.m42;
            }
            const rect = element.getBoundingClientRect();
            const offsetX = rect.width / 2;
            const offsetY = rect.height / 2;
            const fuwaArea = document.getElementById('fuwaArea');
            const fuwaAreaRect = fuwaArea.getBoundingClientRect();
            const x = e.pageX - offsetX - fuwaAreaRect.left;
            const y = e.pageY - offsetY - fuwaAreaRect.top;

            element.style.transform = `translate(${x}px, ${y}px)`;
        }
    });

    document.addEventListener('mouseup', () => {
        isDragging = false;
    });
}

完成!!

これでふわふわしたUIを作成することができました。
ライブラリなどは使用していないため、このまま自由に使えるはずです

完成図(再掲)

問題点

  • スマホ対応出来ていない
  • 長時間開きっぱなしにしていると動作が重くなる(謎)

完成図はこちら
image.png

修正

ドラッグ可能にする関数をanimationの中で呼び出していたので、1秒当たり60回のイベントリスナーがセットされていました。再帰の外でドラッグ可能にするように変更しました。

1
0
1

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?