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 { readonly modes: { [key in CollageModeType]: CollageMode } = { "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; }