🐕
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