Согласно вашему третьему пункту, изображения могут быть созданы динамически с использованием javascript .
Итак, давайте возьмем вашу первую идею, просто нарисовав ее на холсте,но вместо того, чтобы генерировать много холстов, создайте один, на котором мы будем рисовать полную сетку.
Вместо того, чтобы хранить каждое изображение в виде пикселей, мы просто сохраним то, что необходимо для его фактического создания.В итоге это будет просто набор координат, и, если повезет, мы сможем сохранить его в объектах с достаточно схожими формами, чтобы движки js могли оптимизировать его наилучшим образом.
Все, что нам нужно сделать, это вычислить, сколько из этих картинок может появиться на нашем экране, поместить их в сетку и отобразить только те, которые действительно видимы.(Поскольку маловероятно, что вы когда-либо сможете визуализировать все эти изображения одновременно).
(function() {
const pic_width = 150;
const pic_height = 75;
const padding = 10;
const colors = Array.from({
length: 150
}, _ => '#' + Math.random().toString(16).substr(2, 6));
// This offscreen canvas is only used to avoid clipping the main one
// It is shared by all instances of Pic
const pic_drawer = Object.assign(document.createElement('canvas'), {
width: pic_width,
height: pic_height
}).getContext('2d');
pic_drawer.textAlign = 'center';
pic_drawer.textBaseline = 'hanging';
/*
Our Picture class.
Holds only some coords in memory
Redraws itself from scratch every time
*/
class Pic {
constructor(index) {
this.id = index;
const max_rad = 25;
const min_rad = 3;
// just hold some coords of points
this.points = Array.from({
length: Math.random() * 20 + 5
}, _ => ({
color: colors[(Math.random() * colors.length) | 0],
x: Math.random() * pic_width,
y: Math.random() * pic_height,
r: (Math.random() * (max_rad - min_rad)) + min_rad
}));
}
draw(ctx) {
// render all our points on the small canvas so the clipping is easy done
const pts = this.points;
pic_drawer.clearRect(0, 0, pic_width, pic_height);
pic_drawer.beginPath();
pic_drawer.fillStyle = pts[0].color;
for (let pt of pts) {
if (pic_drawer.fillStyle !== pt.color) {
pic_drawer.fill();
pic_drawer.fillStyle = pt.color;
pic_drawer.beginPath();
}
pic_drawer.moveTo(pt.x + pt.r, pt.y);
pic_drawer.arc(pt.x, pt.y, pt.r, 0, Math.PI * 2);
}
pic_drawer.fillText(this.id, pic_width / 2, pic_height / 2);
// draw back on main context
ctx.drawImage(pic_drawer.canvas, 0, 0);
}
}
/*
The Grid instance will hold all our Pics
It will get responsible for the main canvas' size and scroll
And for the disposition of all our Pics and their rendering
Exposes a *dirty* flag so the outside anim loop can know when it needs update
*/
class Grid {
constructor() {
this.dirty = true;
this.scrollTop = 0;
this.pics = [];
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
window.addEventListener('resize', this.resize.bind(this), {
passive: true
});
// ToDo: implement touch events...
this.canvas.addEventListener('wheel', e => {
e.preventDefault();
this.scrollTop += e.deltaY;
if (this.scrollTop < 0) this.scrollTop = 0;
this.dirty = true;
}, {
// passive: true // (disabled for SO's iframe)
})
this.resize();
}
resize() {
this.width = this.canvas.width = innerWidth;
this.height = this.canvas.height = innerHeight;
this.dirty = true;
}
update() {
// update only the grid info
// number of columns that can fit in screen
this.columns = Math.floor(this.width / (pic_width + padding * 2));
// number of rows (ceil + 1 to get partial previous and next ones too)
this.rows = Math.ceil(this.height / (pic_height + padding * 2)) + 1;
const floating_row_index = this.scrollTop / (pic_height + padding * 2);
const first_row_index = Math.floor(floating_row_index);
// the index of the first Pic that will get drawn
this.first_visible_pic = first_row_index * this.columns;
// floating scroll
this.y_offset = (first_row_index - floating_row_index) * (pic_height + padding * 2);
// center
this.x_offset = (this.width - (this.columns * (pic_width + padding * 2))) / 2;
}
draw() {
const ctx = this.ctx;
// clear
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, this.width, this.height);
// iterate through our cells
for (let y = 0; y < this.rows; y++) {
for (let x = 0; x < this.columns; x++) {
// get the Pic at given index
const index = (y * this.columns + x) + this.first_visible_pic;
// in case it doesn't exist yet, create it
if (!this.pics[index]) {
this.pics[index] = new Pic(index);
}
// move our context at cell's coords
ctx.setTransform(
1, 0, 0,
1,
x * (pic_width + padding * 2) + padding + this.x_offset,
y * (pic_height + padding * 2) + padding + this.y_offset
);
// draw our Pic
this.pics[index].draw(ctx);
// border...
ctx.strokeRect(0, 0, pic_width, pic_height);
}
}
}
}
const grid = new Grid();
document.body.append(grid.canvas);
function anim() {
// only if it has changed
if (grid.dirty) {
grid.update(); // update the grid
grid.draw(); // draw it
grid.dirty = false; // clean the flag
}
requestAnimationFrame(anim); // check again at next screen refresh
}
anim();
})();
:root,body,canvas{margin:0;padding:0;line-height:0}