Отзывчивый canvas
Чтобы адаптировать содержимое холста, лучше использовать эталонный размер экрана.Это представляет идеальный размер дисплея, в котором просматривается ваш контент.
Ссылка используется для расчета того, как отображать контент на дисплеях, которые не соответствуют идеальному размеру.
В примерепод объектом reference
определяет отображение ссылок и предоставляет методы для изменения размера холста, а также для масштабирования и позиционирования содержимого.
После определения ссылки вы можете расположить и изменить размер содержимого для отображения ссылок.
Например, константа imageMaxSize = 512
устанавливает максимальный размер (ширину или высоту) изображения.512 относится к эталонному дисплею (1920, 1080).Фактический размер отображаемого изображения зависит от размера страницы.
Устанавливает матрицу, которая используется для преобразования содержимого в соответствии с размером дисплея.Вместо того, чтобы использовать верхний левый угол экрана в качестве источника (0,0), он устанавливает центр холста в качестве источника.
В этом примере можно указать, как холст реагирует на разрешение экрана, const scaleMethod
можно установить на
"fit"
, что обеспечит отображение всего контента (при условии, что он соответствует ссылке).Тем не менее, могут быть пустые области сверху внизу или слева и справа от содержимого, если аспект отображения отличается от эталонного. "fill"
гарантирует, что содержимое заполнит дисплей.Однако часть содержимого (вверху и внизу или слева и справа) может быть обрезано, если формат изображения не соответствует эталону.
Позиционирование изображений.
Для этого требуется толькомассив, который содержит положение и размер изображения относительно эталонного дисплея.
В примере массив displayList
, расширяющий массив, имеет функцию
add(image,x,y)
это добавляет изображение в список.X и y представляют положение центра изображения и относятся к исходному эталонному дисплею (центру холста)
Когда изображение добавляется, его эталонный размер вычисляется из его естественного размера
draw(ctx)
нарисует все элементы в списке отображения, используя эталонную матрицу для масштабирования и позиционирования изображений.
Рендеринг
Вместорендеринг на холст ad-hock используется цикл рендеринга updateCanvas
, который обеспечивает обновление контента синхронно с оборудованием дисплея.Убедитесь, что если у вас есть анимированный контент, он не создает артефактов (сдвиг, мерцание)
Чтобы предотвратить рендеринг для ненужного рисования контента, цикл рендеринга будет рендерить контент, только когда семафор update
установлен в true
.Например, при изменении размера холста содержимое должно быть визуализировано.Это достигается простой установкой update=true
Вместо использования события resize для изменения размера холста, цикл рендеринга проверяет, соответствует ли размер холста размеру страницы.Если совпадение отсутствует, то размер холста изменяется.это сделано потому, что событие изменения размера не синхронизируется с оборудованием дисплея и приведет к низкому качеству рендеринга во время изменения размера дисплея.это также гарантирует, что холст не будет изменен больше чем один раз между рамками дисплея.
Пример
requestAnimationFrame(updateCanvas);
const ctx = canvas.getContext('2d');
const SCALE_METHOD = "fit";
const images = [];
const ALPHA_FADE_IN_SPEED = 0.04; // for fade in out approx time use
// seconds = (0.016666 / ALPHA_FADE_IN_SPEED)
const FADE_OVERLAP = 0.4; // fraction of fade time. NOT less or equal to
// ALPHA_FADE_IN_SPEED and not greater equal to 0.5
const IMAGE_MAX_SIZE = 480; // image isze in pixel of reference display
const IMAGE_MIN_SIZE = IMAGE_MAX_SIZE * 0.8;
const IMAGE_SCALE_FLICK = IMAGE_MAX_SIZE * 0.05;
// sigmoid curve return val 0-1. P is power.
// 0 < p < 1 curve eases center
// 1 == p linear curve
// 1 < p curve eases out from 0 and into 1
Math.sCurve = (u, p = 2) => u <= 0 ? 0 : u >= 1 ? 1 : u ** p / (u ** p + (1 - u) ** p);
// Simple spring
// constructor(u,[a,[d,[t]]])
// u is spring position
// a is acceleration default 0.1
// d is dampening default 0.9
// t is spring target (equalibrium) default t = u
// properties
// u current spring length
// flick(v) // adds movement to spring
// step(u) gets next value of spring. target defaults to this.target
Math.freeSpring = (u, a = 0.3 , d = 0.65, t = u) => ({
u,
v : 0,
set target(v) { t = v },
flick(v) { this.v = v * (1/d) *(1/a)},
step(u = t) { return this.u += (this.v = (this.v += (u - this.u) * a) * d) }
})
var update = false;
const reference = {
get width() { return 1920 }, // ideal display resolution
get height() { return 1080 },
matrix: [1, 0, 0, 1, 0, 0],
resize(method, width = innerWidth, height = innerHeight) {
method = method.toLowerCase();
var scale = 1; // one to one of reference
if (method === "fit") {
scale = Math.min(width / reference.width, height / reference.height);
} else if (method === "fill") {
scale = Math.max(width / reference.width, height / reference.height);
}
const mat = reference.matrix;
mat[3] = mat[0] = scale;
mat[4] = width / 2;
mat[5] = height / 2;
canvas.width = width;
canvas.height = height;
update = true;
},
checkSize() {
if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
reference.resize(SCALE_METHOD);
}
},
};
{
let count = 0;
[
'https://images.pexels.com/photos/1313267/pexels-photo-1313267.jpeg?cs=srgb&dl=food-fruit-green-1313267.jpg&fm=jpg',
'https://images.pexels.com/photos/2965413/pexels-photo-2965413.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/2196602/pexels-photo-2196602.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/2955490/pexels-photo-2955490.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940'
].forEach(src => {
count++;
const img = new Image;
img.src = src;
img.addEventListener("load", () => {
images.push(img);
if (! --count) { setup() }
})
img.addEventListener("error", () => {if (! --count) { setup() }});
});
}
const displayList = Object.assign([], {
add(image, x, y) {
var item;
var w = image.naturalWidth;
var h = image.naturalHeight;
const scale = Math.min(IMAGE_MAX_SIZE / w, IMAGE_MAX_SIZE / h);
w *= scale;
h *= scale;
displayList.push(item = {
image, x, y, w, h,
fading: false,
alpha: 0,
alphaStep: 0,
onAlphaReady: undefined,
scaleFX: Math.freeSpring(IMAGE_MIN_SIZE)
});
displayList.fadeQueue.push(item);
return item;
},
fadeQueue: [],
draw(ctx) {
var curvePower = 2
ctx.setTransform(...reference.matrix);
for (const item of displayList) {
if (item.fading) {
item.alpha += item.alphaStep;
curvePower = item.alphaStep > 0 ? 2 : 2;
if (item.onAlphaReady && (
(item.alphaStep < 0 && item.alpha <= FADE_OVERLAP) ||
(item.alphaStep > 0 && item.alpha >= 1 - FADE_OVERLAP))) {
item.onAlphaReady(item);
item.onAlphaReady = undefined;
} else if (item.alpha <= 0 || item.alpha >= 1) {
item.fading = false;
}
update = true;
}
ctx.globalAlpha = Math.sCurve(item.alpha, curvePower);
const s = item.scaleFX.step() / IMAGE_MAX_SIZE;
ctx.drawImage(item.image, item.x - item.w / 2 * s, item.y - item.h / 2 * s, item.w * s, item.h * s);
}
ctx.globalAlpha = 1;
ctx.setTransform(1, 0, 0, 1, 0, 0); // default transform
}
});
function fadeNextImage() {
const next = displayList.fadeQueue.shift();
if(next.alpha < 0.5) { // Start fade in
next.scaleFX.flick(IMAGE_SCALE_FLICK);
next.scaleFX.target = IMAGE_MAX_SIZE;
next.alphaStep = ALPHA_FADE_IN_SPEED;
} else { // Start fade out
next.scaleFX.flick(IMAGE_SCALE_FLICK);
next.scaleFX.target = IMAGE_MIN_SIZE;
next.alphaStep = -ALPHA_FADE_IN_SPEED;
}
next.onAlphaReady = fadeNextImage;
next.fading = true;
displayList.fadeQueue.push(next);
}
function setup() {
const repeat = 2;
var i, len = images.length;
const distX = (reference.width - IMAGE_MAX_SIZE) * 0.45;
const distY = (reference.height - IMAGE_MAX_SIZE) * 0.45;
for (i = 0; i < len * repeat; i++) {
const ang = i / (len * repeat) * Math.PI * 2 - Math.PI / 2;
displayList.add(images[i % len], Math.cos(ang) * distX, Math.sin(ang) * distY);
}
fadeNextImage();
}
function clearCanvas() {
ctx.globalAlpha = 1;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
function loading(time) {
clearCanvas();
ctx.font = "12px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.strokeStyle = "#aaa";
ctx.fillStyle = "white";
ctx.setTransform(1,0,0,1,ctx.canvas.width / 2, ctx.canvas.height / 2);
ctx.fillText("loading",0,0);
ctx.beginPath();
ctx.lineWidth = 2;
ctx.lineCap = "round";
const pos = time + Math.cos(time) * 0.25 + 1;
ctx.arc(0 ,0, 24, pos, pos + Math.cos(time * 0.1) * 0.5 + 1);
ctx.stroke();
}
function updateCanvas(time) {
reference.checkSize()
if(!displayList.length) {
loading(time / 100);
} else if (update) {
update = false;
clearCanvas();
displayList.draw(ctx);
}
requestAnimationFrame(updateCanvas);
}
canvas {
position: absolute;
top: 0px;
left: 0px;
background: black;
}
<canvas id="canvas"></canvas>