merge-images-grid
Version:
Merge images on canvas,looks likes grid template!Can be used both of node and browser.
317 lines (314 loc) • 13 kB
JavaScript
'use strict';
class CanvasGrid {
constructor(props) {
this.template = [];
this.colTemplate = [];
this.colWidths = [];
this.rowHeights = [];
const { width, height, padding, alignItems, justifyItems, list, col, canvas, gap, bgColor, widthType, heightType, objectFit } = props;
this.itemWidth = width;
this.itemHeight = height;
const _padding = handlePadding(padding);
this.top = _padding[0];
this.right = _padding[1];
this.bottom = _padding[2];
this.left = _padding[3];
this.alignItems = alignItems || 'center';
this.justifyItems = justifyItems || 'center';
this.list = list || [];
this.col = col || 3;
this.canvas = canvas;
this.ctx = this.canvas.getContext('2d');
this.bgColor = bgColor;
this.widthType = widthType || 'max';
if (this.widthType === 'fixed' && width == null) {
this.widthType = 'max';
}
this.heightType = heightType || 'auto';
if (this.heightType === 'fixed' && height == null) {
this.heightType = 'max';
}
this.objectFit = objectFit || 'contain';
this.gap = handleGap(gap);
this.initTemplate();
this.render();
}
//计算布局状态
initTemplate() {
const { list, itemHeight, itemWidth, col, gap } = this;
const template = [];
let rowIndex = 0;
let colIndex = 0;
for (let i = 0, l = list.length; i < l; i++) {
const item = list[i];
if (template[rowIndex]?.[colIndex]) {
colIndex++;
if (colIndex >= col) {
colIndex = 0;
rowIndex++;
}
i--;
continue;
}
const row = template[rowIndex] || [];
const colSpan = item.colSpan ?? 1;
const rowSpan = item.rowSpan ?? 1;
const imgWidth = item.image?.width;
const imgHeight = item.image?.height;
const realWidth = item.width ?? (imgWidth || itemWidth || 0);
const realHeight = item.height ?? (imgHeight || itemHeight || 0);
const width = (realWidth - (colSpan - 1) * gap[1]) / (colSpan || 1); //当前单元格的单位width,非最终,最终以该列最大的为主
const height = (realHeight - (rowSpan - 1) * gap[0]) / (rowSpan || 1); //当前单元格的单位height,非最终
const rowItem = {
...item,
colSpan,
rowSpan,
width: (typeof itemWidth === 'number' && width < itemWidth) ? itemWidth : width,
height: (typeof itemHeight === 'number' && height < itemHeight) ? itemHeight : height,
realWidth,
realHeight,
maxWidth: 0,
maxHeight: 0,
justifyItems: item.justifyItems || this.justifyItems,
alignItems: item.alignItems || this.alignItems,
objectFit: item.objectFit || this.objectFit,
};
row[colIndex] = rowItem;
template[rowIndex] = row;
const maxSpan = Math.min(colSpan, col - colIndex);
for (let k = 0; k < rowSpan; k++) {
const row = template[rowIndex + k] || [];
for (let j = 0; j < maxSpan; j++) {
row[colIndex + j] = row[colIndex + j] || this.getEmpty(rowItem.width || 0, rowItem.height || 0);
}
template[rowIndex + k] = row;
}
colIndex += colSpan;
if (colIndex >= col) {
colIndex = 0;
rowIndex++;
}
}
this.template = template;
this.colTemplate = this.getColTemplate();
this.setFitWH();
}
render() {
this.setSsize();
const { ctx, bgColor, canvas } = this;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (bgColor) {
ctx.save();
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
this.renderGrid();
}
/** 追加数据 */
appendData(list) {
this.list = this.list.concat(list);
this.initTemplate();
this.render();
}
renderGrid() {
const { colWidths, rowHeights, left, top, ctx, template, gap } = this;
ctx.save();
ctx.translate(left, top);
for (let rowIndex = 0, l = template.length, y = 0; rowIndex < l; rowIndex++) {
const row = template[rowIndex];
ctx.save();
ctx.translate(0, y);
const rowHeight = rowHeights[rowIndex];
for (let colIndex = 0, _l = row.length, x = 0; colIndex < _l; colIndex++) {
const { image, realWidth, realHeight, justifyItems, alignItems, maxWidth, maxHeight, objectFit } = row[colIndex] || {};
const colWidth = colWidths[colIndex];
if (image && maxWidth && maxHeight && realHeight && realWidth) {
let dx = realWidth;
let dy = realHeight;
let left = 0, top = 0;
if (objectFit === 'contain') {
const scale = Math.max(realWidth / maxWidth, realHeight / maxHeight);
dx = realWidth / scale;
dy = realHeight / scale;
}
else if (objectFit === 'fill') {
dx = maxWidth;
dy = maxHeight;
}
else if (objectFit === 'auto') {
const scale = Math.max(realWidth / maxWidth, realHeight / maxHeight);
if (scale > 1) {
dx = realWidth / scale;
dy = realHeight / scale;
}
}
if (justifyItems === 'center') {
left = (maxWidth - dx) * 0.5;
}
else if (justifyItems === 'end') {
left = maxWidth - dx;
}
if (alignItems === 'center') {
top = (maxHeight - dy) * 0.5;
}
else if (alignItems === 'end') {
top = maxHeight - dy;
}
ctx.save();
ctx.translate(x, 0);
ctx.drawImage(image, left, top, dx, dy);
// ctx.drawImage(image, 0, 0, realWidth, realHeight, left, top, dx, dy);
ctx.restore();
}
x += colWidth + gap[1];
}
ctx.restore();
y += rowHeight + gap[0];
}
ctx.restore();
}
/** 设置canvas大小 */
setSsize() {
const { gap, colWidths, rowHeights } = this;
const gapWidth = gap[1] * (colWidths.length - 1);
const gapHeight = gap[0] * (rowHeights.length - 1);
const contentWidth = colWidths.reduce((a, b) => a + b);
const contentHeight = rowHeights.reduce((a, b) => a + b);
this.canvas.width = gapWidth + contentWidth + gap[1] * 2;
this.canvas.height = gapHeight + contentHeight + gap[0] * 2;
}
//确定行列宽高
setFitWH() {
const { template, colTemplate, gap, col, widthType, heightType, itemWidth, itemHeight } = this;
const colWidths = [];
const rowHeights = [];
for (const row of colTemplate) {
const maxWidth = Math.max(...row.map(v => v?.width || 0).filter(v => typeof v === 'number'));
colWidths.push(maxWidth);
}
for (const row of template) {
const maxHeight = Math.max(...row.map(v => v?.height || 0).filter(v => typeof v === 'number'));
rowHeights.push(maxHeight);
}
if (widthType === 'max') {
const max = Math.max(...colWidths);
colWidths.fill(max, 0, colWidths.length);
}
else if (widthType === 'auto') {
for (let rowIndex = 0, l = template.length; rowIndex < l; rowIndex++) {
const row = template[rowIndex];
for (let colIndex = 0, l = row.length; colIndex < l; colIndex++) {
const item = row[colIndex];
if (!item)
continue;
const { colSpan } = item;
if (colSpan >= 2) {
const max = Math.max(...colWidths.slice(colIndex, colIndex + colSpan));
colWidths.fill(max, colIndex, colIndex + colSpan);
}
}
}
}
else if (widthType === 'fixed' && typeof itemWidth === 'number') {
colWidths.fill(itemWidth, 0, colWidths.length);
}
if (heightType === 'max') {
const max = Math.max(...rowHeights);
rowHeights.fill(max, 0, rowHeights.length);
}
else if (heightType === 'auto') {
for (let rowIndex = 0, l = colTemplate.length; rowIndex < l; rowIndex++) {
const row = colTemplate[rowIndex];
for (let colIndex = 0, l = row.length; colIndex < l; colIndex++) {
const item = row[colIndex];
if (!item)
continue;
const { rowSpan } = item;
if (rowSpan >= 2) {
const max = Math.max(...rowHeights.slice(colIndex, colIndex + rowSpan));
rowHeights.fill(max, colIndex, colIndex + rowSpan);
}
}
}
}
else if (heightType === 'fixed' && typeof itemHeight === 'number') {
rowHeights.fill(itemHeight, 0, rowHeights.length);
}
//生成当前图形的最大绘图宽高
for (let rowIndex = 0, l = template.length; rowIndex < l; rowIndex++) {
const row = template[rowIndex];
for (let colIndex = 0, l = row.length; colIndex < l; colIndex++) {
const item = row[colIndex];
if (!item)
continue;
const { rowSpan, colSpan } = item;
if (rowSpan && colSpan) {
const gapWidth = (rowSpan - 1) * gap[0];
const gapHeight = (colSpan - 1) * gap[1];
let colSpanWidth = 0;
const maxSpan = Math.min(colSpan, col - colIndex);
for (let i = 0; i < maxSpan; i++) {
colSpanWidth += colWidths[colIndex + i];
}
let rowSpanWidth = 0;
for (let i = 0; i < rowSpan; i++) {
rowSpanWidth += rowHeights[rowIndex + i];
}
item.maxWidth = gapWidth + colSpanWidth;
item.maxHeight = gapHeight + rowSpanWidth;
}
}
}
this.colWidths = colWidths;
this.rowHeights = rowHeights;
}
//行列变换
getColTemplate() {
const { template } = this;
const list = [];
const realCol = template?.[0]?.length || 0;
for (let colIndex = 0; colIndex < realCol; colIndex++) {
const row = [];
for (let j = 0, l = template.length; j < l; j++) {
const item = template[j][colIndex];
item && row.push(item);
}
list.push(row);
}
return list;
}
getEmpty(width, height) {
return {
width,
height,
justifyItems: 'center',
alignItems: 'center',
rowSpan: 0,
colSpan: 0,
realHeight: 0,
realWidth: 0,
maxWidth: 0,
maxHeight: 0,
};
}
}
function handlePadding(padding) {
if (typeof padding === 'number') {
return [padding, padding, padding, padding];
}
else if (padding instanceof Array && padding.length) {
return [...padding, ...padding, ...padding, ...padding].slice(0, 4);
}
return [10, 10, 10, 10];
}
function handleGap(gap) {
if (gap instanceof Array && gap.length >= 2) {
return gap;
}
else if (typeof gap === 'number') {
return [gap, gap];
}
return [10, 10];
}
module.exports = CanvasGrid;