import { CollageCanvas, 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 function isCollageModeType(value: string): value is CollageModeType { return (collageModeType as readonly string[]).includes(value); } export const displayCollageModeType = [ ...collageModeType, "recursive", ] as const; export type DisplayCollageModeType = typeof displayCollageModeType[number]; export function isDisplayCollageModeType( value: string ): value is DisplayCollageModeType { return (displayCollageModeType as readonly string[]).includes(value); } export interface RecursiveCollageConfig { level: number; repeat: boolean; modes: readonly CollageModeType[]; } export abstract class CollageModes< C extends CollageContext, I extends CollageImage, CS extends CollageCanvas > { 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] * 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] * 0.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() > 0.5) { factor = choice([1 / Math.sqrt(2), 0.5, 0.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); }, }, }; public async recursiveDraw( context: C, images: I[], recursiveConfig: RecursiveCollageConfig ) { const localImages = images.concat(); const rootSegment: Segment = { x: 0, y: 0, w: context.canvas.width, h: context.canvas.height, }; const processSegment = async ( segment: Segment, level: number ): Promise => { // console.debug(segment, level); if (segment === rootSegment || level <= recursiveConfig.level - 1) { const canvas = this.createCanvas(segment.w, segment.h); const modeKey = choice(recursiveConfig.modes); // console.debug(modeKey); const mode = this.modes[modeKey]; const ctx = canvas.getContext("2d") as C; const segments = mode.getSegments(ctx); // console.debug(segments); const bitmaps = await Promise.all( segments.map((segment: Segment) => processSegment(segment, level + 1)) ); mode.place(ctx, bitmaps, segments); return await this.canvasToImage(canvas); } else { if (recursiveConfig.repeat) { return choice(localImages); } else { if (localImages.length > 0) { return localImages.pop() as I; } else { throw "RAN OUT OF IMAGES"; } } } }; const result = await processSegment(rootSegment, 0); this.drawImage( context, result, 0, 0, result.width, result.height, 0, 0, context.canvas.width, context.canvas.height ); } 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 * 0.25; y = ctx.canvas.height * 0.25; break; case 1: x = ctx.canvas.width * 0.75; y = ctx.canvas.height * 0.25; break; case 2: x = ctx.canvas.width * 0.25; y = ctx.canvas.height * 0.75; break; case 3: x = ctx.canvas.width * 0.75; y = ctx.canvas.height * 0.75; break; } return { x, y, w: ctx.canvas.width / 2, h: ctx.canvas.height / 2, }; }); } abstract createCanvas(w: number, h: number): CS; abstract canvasToImage(canvas: CS): PromiseLike; abstract drawImage( ctx: C, image: I, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number ): void; }