🐕

p5jsで超ぬるぬるな画像ビュワーを作成する方法 本当に60fps??

に公開

技術的概要

読み込んだ画像について、最初に表示場所の事前計算する。

表示領域に入った画像以外すべて表示計算しないようにしパフォーマンスを最大化する。

scrollを目標地点に対する、非線形関数に通すことで、60hzモニターでもノンストレスなスクロールを実現。

AIに書いてもらいましたが、P5jsで実装するというコアアイデアや、その他実装の工夫当、やはり最適な実装を指示する必要があり、完全に手放しに実装することは難しい印象です。

このような画像ビュワーをhtml,css,jsで実装したことがある方はとても驚くと思います。
ぬるぬるです。

prompt

p5jsで画像ビュワー をさくせいする。
画像は、ファイル選択で複数取得可能。
ファイル情報をもとに横幅がcanvasの横幅と同じになるように事前計算しておく。
offset + そのファイルの高さをすべて計算し、配列に保持しておく。
スクロールを疑似的に再現し、表示領域に入ったら描画、抜けたら描画しないをように実装する。
描画処理をできるだけ軽量化したい。



スクロール処理の変更をします。
目標地点を決め、更新座標 = (目標地点 - 現在地点) * 0.1 + 現在地点という処理に書き換えて。
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>p5.js Image Viewer</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            overflow: hidden; /* スクロールバーを非表示にする */
            display: flex;
            justify-content: center;
            align-items: center;
            background-color: #222;
        }
        canvas {
            display: block;
            border: 1px solid #444;
        }
        #file-input-container {
            position: absolute;
            top: 20px;
            left: 20px;
            z-index: 100;
        }
    </style>
    <!-- p5.jsライブラリをCDNから読み込む -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
</head>
<body>
    <div id="file-input-container">
        <input type="file" id="file-input" multiple />
    </div>

    <script>
        // p5.jsで画像ビューアーを作成する
        let images = []; // p5.Imageオブジェクトを格納する配列
        let imageInfo = []; // 画像の描画情報を保持する配列
        let totalHeight = 0; // すべての画像を合わせた仮想的な総高さ
        let scrollPos = 0; // 現在のスクロール位置
        let targetScrollPos = 0; // 目標のスクロール位置
        let fileInput; // ファイル選択ボタンのDOM要素
        let messageText = '画像を選択してください。'; // メッセージ表示用のテキスト
        let font;
        let image_margin_bottom = 100;
        let fs = 120;

        // setup()関数の前に実行されるpreload()は、今回は使用しないため、空にします。
        // もしフォントなどのアセットを読み込む場合はここに記述します。
        function preload() {}

        function setup() {
            // ウィンドウサイズに合わせてキャンバスを作成
            createCanvas(windowWidth, windowHeight);
            background(220);

            // HTML側のファイル選択ボタンを取得し、p5.jsで扱う
            fileInput = select('#file-input');
            fileInput.changed(handleFile);

            // テキストのスタイルを設定
            textAlign(CENTER, CENTER);
            textSize(20);

            // ウィンドウリサイズ時の処理を設定
            windowResized();
             frameRate(fs);
        }

        function draw() {
            background(220); // フレームごとに背景をクリア

            if (images.length === 0) {
                // 画像がまだ読み込まれていない場合はメッセージを表示
                fill(50);
                noStroke();
                text(messageText, width / 2, height / 2);
            } else {
                // スムーズなスクロール処理
                scrollPos = (targetScrollPos - scrollPos) * (0.05) * 60 / fs + scrollPos;

                // スクロール位置を調整
                let maxScroll = totalHeight - height;
                if (maxScroll < 0) maxScroll = 0;
                scrollPos = constrain(scrollPos, 0, maxScroll);
                targetScrollPos = constrain(targetScrollPos, 0, maxScroll);

                // 描画処理の最適化: 表示領域内の画像のみを描画
                for (let i = 0; i < imageInfo.length; i++) {
                    let info = imageInfo[i];
                    let img = info.img;
                    let yOffset = info.yOffset;
                    let scaledHeight = info.scaledHeight;

                    // 画像の一部でも表示領域に入っていれば描画
                    if (yOffset + scaledHeight > scrollPos && yOffset < scrollPos + height) {
                        image(img, 0, yOffset - scrollPos, width, scaledHeight);
                    }
                }
            }
        }

        // ファイル選択時のコールバック関数
        function handleFile() {
            // p5.jsのcreateFileInputではなく、HTML側のinput要素のfilesプロパティを使用
            let files = fileInput.elt.files;
            if (files.length === 0) return;

            // 既存のデータをクリア
            images = [];
            imageInfo = [];
            totalHeight = 0;
            scrollPos = 0;
            targetScrollPos = 0;
            messageText = '読み込み中...';

            for (let i = 0; i < files.length; i++) {
                let file = files[i];
                if (file.type.startsWith('image/')) {
                    // Blob URLを生成してloadImageに渡す
                    let blobUrl = URL.createObjectURL(file);
                    loadImage(blobUrl, (img) => {
                        // 画像の幅をキャンバスの幅に合わせる
                        const scaledHeight = img.height * (width / img.width);

                        const info = {
                            img: img,
                            scaledHeight: scaledHeight,
                            yOffset: totalHeight,
                        };

                        imageInfo.push(info);
                        images.push(img);
                        totalHeight += scaledHeight + image_margin_bottom;

                        // メッセージを更新
                        messageText = `画像: ${images.length}`;
                    }, () => {
                        console.error('画像の読み込みに失敗しました。');
                    });
                }
            }
        }

        // マウスホイールイベントでスクロールを処理
        function mouseWheel(event) {
            targetScrollPos += event.deltaY;
            targetScrollPos = constrain(targetScrollPos, 0, totalHeight - height);
            return false; // ブラウザのデフォルトスクロールを防ぐ
        }

        // ウィンドウがリサイズされたときの処理
        function windowResized() {
            resizeCanvas(windowWidth, windowHeight);

            if (images.length > 0) {
                totalHeight = 0;
                for (let i = 0; i < images.length; i++) {
                    let img = images[i];
                    const newScaledHeight = img.height * (width / img.width);
                    imageInfo[i].yOffset = totalHeight;
                    imageInfo[i].scaledHeight = newScaledHeight;
                    totalHeight += newScaledHeight + image_margin_bottom;
                }
            }
            // リサイズ後、スクロール位置を再計算
            targetScrollPos = constrain(targetScrollPos, 0, totalHeight - height);
        }



        // ドラッグ中のフラグ
let mouse_isDragging = false;

// 前回の座標を保持する変数
let mouse_lastX = 0;
let mouse_lastY = 0;

// 移動量を保持する変数
let mouse_deltaX = 0;
let mouse_deltaY = 0;

// マウスがクリックされた時
document.addEventListener('mousedown', (event) => {
    mouse_isDragging = true;
    // 初回は現在の座標を前回の座標として設定
    mouse_lastX = event.clientX;
    mouse_lastY = event.clientY;
});

// マウスが移動した時
document.addEventListener('mousemove', (event) => {
    if (!mouse_isDragging) return;

    // 現在の座標と前回の座標の差を計算
    mouse_deltaX = event.clientX - mouse_lastX;
    mouse_deltaY = event.clientY - mouse_lastY;

    console.log(`移動量 X: ${mouse_deltaX}, Y: ${mouse_deltaY}`);
 targetScrollPos -= mouse_deltaY * 4;
            targetScrollPos = constrain(targetScrollPos, 0, totalHeight - height);
    // 計算が終わったら、現在の座標を次回の計算のために保存
    mouse_lastX = event.clientX;
    mouse_lastY = event.clientY;
});

// マウスのボタンが離された時(ドラッグ終了)
document.addEventListener('mouseup', () => {
    mouse_isDragging = false;
});
    </script>
</body>
</html>

Discussion