kollagen/src/common/collages.ts

222 lines
9.3 KiB
TypeScript

import { CollageConfig, CollageContext, CollageImage, CollageMode, Segment } from "@/common/types";
import { choice, randint, range, shuffle } from "@/common/utils";
export const collageModeType = [
"clean_grid", "chaos_grid",
"row", "irow",
"col", "icol",
"concentric_factor", "concentric_spaced",
"blend"
] as const;
export type CollageModeType = typeof collageModeType[number];
export abstract class CollageModes<C extends CollageContext, I extends CollageImage> {
readonly modes: { [key in CollageModeType]: CollageMode<C, I> } = {
"clean_grid": {
name: "Clean Grid",
minImages: 4,
forceConfig: {
numImages: 4
},
getSegments: this.getGridSegments,
place: this.cleanPlace.bind(this)
},
"chaos_grid": {
name: "Irregular Grid",
minImages: 4,
forceConfig: {
numImages: 4
},
getSegments: this.getGridSegments,
place: (ctx, images, segments) => {
const shuffledImages = shuffle(images);
shuffle(segments.map((segment, idx) => [segment, idx] as [Segment, number]))
.forEach(([segment, idx]) => {
const image = shuffledImages[idx];
const scaleRatio = Math.max(segment.w / image.width, segment.h / image.height);
this.drawImage(ctx, image, 0, 0, image.width, image.height,
segment.x - (image.width * scaleRatio / 2), segment.y - (image.height * scaleRatio / 2),
image.width * scaleRatio, image.height * scaleRatio);
});
},
},
"row": {
name: "Regular Row",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
return range(numImages).map((idx) => {
return {
x: idx * segmentSize[0] + segmentSize[0] / 2,
y: segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1]
};
});
},
place: this.cleanPlace.bind(this)
},
"irow": {
name: "Irregular Row",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
return range(numImages).map((idx) => {
const irregularWidth = images ?
segmentSize[0] + Math.random() * ((segmentSize[1] / images[idx].height * images[idx].width) - segmentSize[0]) :
segmentSize[0] + Math.random() * segmentSize[0] * .5;
return {
x: idx * segmentSize[0] + segmentSize[0] / 2,
y: segmentSize[1] / 2,
w: Math.min(ctx.canvas.width / 2, irregularWidth),
h: segmentSize[1],
};
});
},
place: this.cleanPlace.bind(this)
},
"col": {
name: "Regular Column",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
return range(numImages).map((idx) => {
return {
x: segmentSize[0] / 2,
y: idx * segmentSize[1] + segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1]
};
});
},
place: this.cleanPlace.bind(this)
},
"icol": {
name: "Irregular Column",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
return range(numImages).map((idx) => {
const irregularHeight = images ?
segmentSize[1] + Math.random() * ((segmentSize[0] / images[idx].width * images[idx].height) - segmentSize[1]) :
segmentSize[1] + Math.random() * segmentSize[1] * .5;
return {
x: segmentSize[0] / 2,
y: idx * segmentSize[1] + segmentSize[1] / 2,
w: segmentSize[0],
h: Math.min(ctx.canvas.height / 2, irregularHeight),
};
});
},
place: this.cleanPlace.bind(this)
},
"concentric_factor": {
name: "Constant factor concentric",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
let factor: number;
if (Math.random() > .5) {
factor = choice([1 / Math.sqrt(2), .5, .88]);
} else {
factor = 1 - (1 / numImages);
}
return range(numImages).map((idx) => {
const ratio = Math.pow(factor, idx);
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width * ratio,
h: ctx.canvas.height * ratio
};
});
},
place: this.cleanPlace.bind(this)
},
"concentric_spaced": {
name: "Equally spaced concentric",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(2) + 2);
return range(numImages).map((idx) => {
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width - (ctx.canvas.width / numImages * idx),
h: ctx.canvas.height - (ctx.canvas.height / numImages * idx),
};
});
},
place: this.cleanPlace.bind(this)
},
"blend": {
name: "Blending",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(2) + 2);
return range(numImages).map((_) => {
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width,
h: ctx.canvas.height
};
});
},
place: (ctx, images, segments) => {
ctx.globalCompositeOperation = choice(["difference", "saturation", "soft-light", "overlay"]);
this.cleanPlace(ctx, images, segments);
}
}
};
private cleanDraw(ctx: C, image: I,
x: number, y: number, w: number, h: number) {
const scaleRatio = Math.max(w / image.width, h / image.height);
this.drawImage(ctx, image, image.width / 2 - w / scaleRatio / 2, image.height / 2 - h / scaleRatio / 2,
w / scaleRatio, h / scaleRatio,
x - w / 2, y - h / 2,
w, h);
}
private cleanPlace(ctx: C, images: I[], segments: Segment[]) {
segments.forEach((segment, idx) => {
this.cleanDraw(ctx, images[idx], segment.x, segment.y, segment.w, segment.h);
});
}
private getGridSegments(ctx: C, config?: CollageConfig) {
return [0, 1, 2, 3].map((idx) => {
let x!: number, y!: number;
switch (idx) {
case 0:
x = ctx.canvas.width * .25;
y = ctx.canvas.height * .25;
break;
case 1:
x = ctx.canvas.width * .75;
y = ctx.canvas.height * .25;
break;
case 2:
x = ctx.canvas.width * .25;
y = ctx.canvas.height * .75;
break;
case 3:
x = ctx.canvas.width * .75;
y = ctx.canvas.height * .75;
break;
}
return {
x, y,
w: ctx.canvas.width / 2,
h: ctx.canvas.height / 2
};
});
}
abstract drawImage(ctx: C, image: I, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;
}