<anime-image
src="https://storage.googleapis.com/ys-notebook-public/ec-perfect-mobile-performance-score.avif"
animeTime=8
expansionRatio=1.3
/>
'use client'; // next.js で使用する場合に必要
const DefaultAnimeTime = 15; // アニメーション時間(秒)
const DefaultExpansionRatio = 1.15; // 拡大比率。1 以上の値にする。1.15 ならば15%拡大。実際の表示はオリジナルの85%
const startPaint = (info) => {
info.startTime = undefined;
const img = new Image();
img.addEventListener("load", () => {
// ワーク(オフスクリーン)canvas 作成、設定。チラつき防止。
if (info.workCanvas == undefined) info.workCanvas = document.createElement("canvas");
const aspect = img.naturalHeight / img.naturalWidth;
const dstW = info.clientWidth * info.dpr * info.expansionRatio;
const dstH = dstW * aspect;
info.workCanvas.width = dstW;
info.workCanvas.height = dstH;
const workCtx = info.workCanvas.getContext('2d', { alpha: false });
workCtx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, dstW, dstH);
info.canvasW = info.clientWidth;
info.canvasH = info.canvasW * aspect;
info.canvas.width = info.canvasW * info.dpr;
info.canvas.height = info.canvasH * info.dpr;
info.canvas.style.width = info.canvasW + 'px';
info.canvas.style.height = info.canvasH + 'px';
// 多重動作抑止。初回、または50 msec 以内に画面更新していなければ描画する
if (info.last_t == undefined || performance.now() - info.last_t > 50) animate(info);
});
img.src = info.src;
}
const animate = (info) => {
let frame = requestAnimationFrame(function loop(t) {
if (paint(info, t)) {
frame = requestAnimationFrame(loop);
}
});
return () => {
cancelAnimationFrame(frame);
};
}
const paint = (info, t) => {
info.last_t = t;
if (info.startTime === undefined) {
info.startTime = t;
info.beforeTime = t;
info.count = 0;
// 1秒間に移動する比率
info.ratioPerSec = (info.expansionRatio - 1.0) * 0.5 / info.animeTime;
}
const elapsed = (t - info.startTime) * 0.001; // 秒単位
if (false) { // fps 計測ロジック
++info.count;
if ((t - info.beforeTime) > 1000.0) {
console.log(info.src + ": " + elapsed + ": "
+ Math.round(info.count / ((t - info.beforeTime) * 0.001)) + " fps");
info.beforeTime = t;
info.count = 0;
}
}
const workC = info.workCanvas;
info.pxPerSecX = info.canvas.width * info.ratioPerSec;
info.pxPerSecY = info.canvas.height * info.ratioPerSec;
info.x = info.pxPerSecX * -elapsed;
info.y = info.canvas.height - workC.height + (info.pxPerSecY * elapsed);
if (workC.width != 0) {
info.canvas.getContext('2d').drawImage(workC, info.x, info.y, workC.width, workC.height);
}
if (info.x < ((info.canvas.width - workC.width) * 0.5)) {
return false;
}
return true;
}
class AnimeImage extends HTMLElement {
constructor() {
super();
// info にインスタンス毎に必要なパラメータを入れる。
this.info = {};
// devicePixelRatio=1 のディスプレイでも2 をセットした方が綺麗に見えたので、最小で2 とする。
this.info.dpr = window.devicePixelRatio >= 2 ? window.devicePixelRatio : 2;
this.getAttribute("animeTime")?.length > 0 ?
this.info.animeTime = this.getAttribute("animeTime") : this.info.animeTime = DefaultAnimeTime;
this.getAttribute("expansionRatio")?.length > 0 ?
this.info.expansionRatio = this.getAttribute("expansionRatio") : this.info.expansionRatio = DefaultExpansionRatio;
this.getAttribute("style")?.length > 0 ?
this.style = this.getAttribute("style") : this.style = "display:flex;";
this.info.src = this.getAttribute("src");
}
connectedCallback() {
this.info.canvas = document.createElement("canvas");
this.append(this.info.canvas);
this.info.clientWidth = this.clientWidth;
startPaint(this.info);
addEventListener('resize', (e) => {
this.info.clientWidth = this.clientWidth;
startPaint(this.info);
})
this.addEventListener('click', (e) => {
startPaint(e.target.parentElement.info);
})
}
}
customElements.define("anime-image", AnimeImage);
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.