diff --git a/cli/collage.jpg b/cli/collage.jpg new file mode 100644 index 0000000..9ddf34f Binary files /dev/null and b/cli/collage.jpg differ diff --git a/cli/collages.ts b/cli/collages.ts new file mode 100644 index 0000000..bdc96c4 --- /dev/null +++ b/cli/collages.ts @@ -0,0 +1,27 @@ +import { CollageModes, CollageModeType, collageModeType } from "../src/common/collages.ts"; +import { CanvasRenderingContext2D, createCanvas, Image, ImageBitmap, loadImage } from "https://deno.land/x/canvas/mod.ts"; +import { CollageContext, CollageImage } from "../src/common/types.ts"; + +export class ProxyImage implements CollageImage { + private image: Image; + public readonly height: number; + public readonly width: number; + + constructor(image: Image) { + this.image = image; + this.width = image.width(); + this.height = image.height(); + } + + public get(): Image { + return this.image; + } +} + +export type CastCanvasRenderingContext = CanvasRenderingContext2D & CollageContext; + +export default class BrowserCollageModes extends CollageModes { + drawImage(ctx: CastCanvasRenderingContext, image: ProxyImage, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void { + ctx.drawImage(image.get(), sx, sy, sw, sh, dx, dy, dw, dh); + } +} \ No newline at end of file diff --git a/cli/import_map.json b/cli/import_map.json new file mode 100644 index 0000000..80e42af --- /dev/null +++ b/cli/import_map.json @@ -0,0 +1,6 @@ +{ + "imports": { + "@/common/types": "../src/common/types.ts", + "@/common/utils": "../src/common/utils.ts" + } +} \ No newline at end of file diff --git a/cli/main.ts b/cli/main.ts new file mode 100644 index 0000000..eb6f70d --- /dev/null +++ b/cli/main.ts @@ -0,0 +1,42 @@ +import { parse } from "https://deno.land/std@0.106.0/flags/mod.ts"; +import { createCanvas, loadImage } from "https://deno.land/x/canvas/mod.ts"; +import { CollageModes, CollageModeType, collageModeType } from "../src/common/collages.ts"; +import DenoCollageModes, { CastCanvasRenderingContext, ProxyImage } from "./collages.ts"; +import { CollageConfig } from "../src/common/types.ts"; +import { choice, shuffle } from "../src/common/utils.ts"; + +const args = parse(Deno.args, { + alias: { + 'o': 'output' + } +}); + +const images: ProxyImage[] = (await Promise.all(args["_"].map((imageURL) => loadImage(imageURL.toString())))).map((image) => new ProxyImage(image)); + +if (images.length < 2) { + console.error("kollagen needs at least 2 images to work."); + Deno.exit(1); +} + +const modes = new DenoCollageModes(); + +const modeKey: CollageModeType = args["mode"] || choice(collageModeType); +const mode = modes.modes[modeKey]; + +console.log(`Creating a "${mode.name}" collage from ${images.length} images...`); +console.debug(`Images: ${args["_"].join(", ")}`); + +const canvas = createCanvas(args["width"] || 640, args["height"] || 640); +const context = canvas.getContext("2d"); + +const collageConfig: CollageConfig = { + numImages: args["n"] +} + +const shuffledImages = shuffle(images); +const segments = mode.getSegments(context as CastCanvasRenderingContext, collageConfig, shuffledImages); +mode.place(context as CastCanvasRenderingContext, shuffledImages, segments); + +const output = args["output"] || "collage.jpg"; +console.log(`Saving to "${output}"...`); +await Deno.writeFile(output, canvas.toBuffer()); \ No newline at end of file diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 0000000..0caaad6 --- /dev/null +++ b/cli/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "deno.ns" + ] + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0b01147..3bdaa7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "kollagen", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/collages.ts b/src/collages.ts index 95d6650..8b5c7ae 100644 --- a/src/collages.ts +++ b/src/collages.ts @@ -1,224 +1,7 @@ -import {CollageConfig, CollageMode, Segment} from "@/types"; -import {choice, randint, range, shuffle} from "@/utils"; +import { CollageModes } from "./common/collages"; -export const collageModeType = [ - "clean_grid", "chaos_grid", - "row", "irow", - "col", "icol", - "concentric_factor", "concentric_spaced", - "blend" -] as const; -export type CollageModeType = typeof collageModeType[number]; - -function cleanDraw(ctx: CanvasRenderingContext2D, image: ImageBitmap, - x: number, y: number, w: number, h: number) { - const scaleRatio = Math.max(w / image.width, h / image.height); - ctx.drawImage( - 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 - ); -} - -function getGridSegments(ctx: CanvasRenderingContext2D, 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 - }; - }); -} - - -function cleanPlace(ctx: CanvasRenderingContext2D, images: ImageBitmap[], segments: Segment[]) { - segments.forEach((segment, idx) => { - cleanDraw(ctx, images[idx], segment.x, segment.y, segment.w, segment.h); - }); -} - -const modes: { [key in CollageModeType]: CollageMode } = { - "clean_grid": { - name: "Clean Grid", - minImages: 4, - forceConfig: { - numImages: 4 - }, - getSegments: getGridSegments, - place: cleanPlace - }, - "chaos_grid": { - name: "Irregular Grid", - minImages: 4, - forceConfig: { - numImages: 4 - }, - getSegments: 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); - ctx.drawImage(image, - 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: cleanPlace - }, - "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: cleanPlace - }, - "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: cleanPlace - }, - "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: cleanPlace - }, - "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: cleanPlace - }, - "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: cleanPlace - }, - "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"]); - cleanPlace(ctx, images, segments); - } +export default class BrowserCollageModes extends CollageModes { + drawImage(ctx: CanvasRenderingContext2D, image: ImageBitmap, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void { + ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh); } -}; - -export default modes; +} \ No newline at end of file diff --git a/src/common/collages.ts b/src/common/collages.ts new file mode 100644 index 0000000..b026a25 --- /dev/null +++ b/src/common/collages.ts @@ -0,0 +1,222 @@ +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; +} \ No newline at end of file diff --git a/src/common/types.ts b/src/common/types.ts new file mode 100644 index 0000000..cce4e42 --- /dev/null +++ b/src/common/types.ts @@ -0,0 +1,31 @@ +export interface CollageMode { + name: string; + minImages: number; + getSegments: (ctx: C, config?: CollageConfig, images?: I[]) => Segment[]; + place: (ctx: C, images: I[], segments: Segment[]) => void; + forceConfig?: CollageConfig; +} + +export interface CollageConfig { + numImages?: number; +} + +export interface Segment { + x: number; + y: number; + w: number; + h: number; +} + +export interface CollageContext { + globalCompositeOperation: string; + canvas: { + width: number; + height: number; + }; +} + +export interface CollageImage { + width: number; + height: number; +} \ No newline at end of file diff --git a/src/utils.ts b/src/common/utils.ts similarity index 91% rename from src/utils.ts rename to src/common/utils.ts index 836364d..c90de72 100644 --- a/src/utils.ts +++ b/src/common/utils.ts @@ -18,7 +18,7 @@ export function randint(n: number) { return Math.floor(Math.random() * n); } -export function choice(arr: T[]): T { +export function choice(arr: readonly T[]): T { return arr[randint(arr.length)]; } diff --git a/src/components/Collage.vue b/src/components/Collage.vue index aa29fbf..0872151 100644 --- a/src/components/Collage.vue +++ b/src/components/Collage.vue @@ -16,7 +16,7 @@
    -
  • import {Component, Prop, Vue, Watch} from "vue-property-decorator"; -import collageModes, {CollageModeType} from "../collages"; -import {CollageConfig, CollageMode, Segment} from "@/types"; -import {choice, shuffle} from "@/utils"; +import {CollageModeType} from "../common/collages"; +import {CollageConfig, CollageMode, Segment} from "../common/types"; +import {choice, shuffle} from "../common/utils"; +import BrowserCollageModes from "../collages"; type DisplayCollageModeType = CollageModeType | & "shuffle" | & "recursive"; @@ -98,19 +99,19 @@ export default class Collage extends Vue { private currentModeType: DisplayCollageModeType = "shuffle"; private lastActiveModeTypes: CollageModeType[] = []; private excludedModes: CollageModeType[] = []; - private modes = collageModes; + private modes = new BrowserCollageModes(); private get minImages() { if (this.currentModeType === "shuffle" || this.currentModeType === "recursive") { - return Math.min(...Object.values(this.modes).map((mode) => mode.minImages)); + return Math.min(...Object.values(this.modes.modes).map((mode) => mode.minImages)); } else { - return this.modes[this.currentModeType].minImages; + return this.modes.modes[this.currentModeType].minImages; } } private get lastMode() { if (this.lastActiveModeTypes.length === 1) { - return this.modes[this.lastActiveModeTypes[0]]; + return this.modes.modes[this.lastActiveModeTypes[0]]; } } @@ -131,18 +132,18 @@ export default class Collage extends Vue { if (this.images.length >= this.minImages) { this.reset(); - const permissibleModeKeys = (Object.keys(collageModes) as CollageModeType[]) - .filter(k => !this.excludedModes.includes(k) && collageModes[k].minImages <= this.images.length); + const permissibleModeKeys = (Object.keys(this.modes.modes) as CollageModeType[]) + .filter(k => !this.excludedModes.includes(k) && this.modes.modes[k].minImages <= this.images.length); if (this.currentModeType !== "recursive") { - let mode: CollageMode; + let mode: CollageMode; if (this.currentModeType === "shuffle") { const randomModeType = choice(permissibleModeKeys); this.lastActiveModeTypes = [randomModeType]; - mode = collageModes[randomModeType]; + mode = this.modes.modes[randomModeType]; } else { this.lastActiveModeTypes = [this.currentModeType]; - mode = this.modes[this.currentModeType]; + mode = this.modes.modes[this.currentModeType]; } const shuffledImages = shuffle(this.images); const segments = mode.getSegments(this.context, this.collageConfig, shuffledImages); @@ -152,18 +153,18 @@ export default class Collage extends Vue { const shuffledImages = shuffle(this.images); const rootSegment: Segment = {x: 0, y: 0, w: this.context.canvas.width, h: this.context.canvas.height}; const processSegment = async (segment: Segment, level: number): Promise => { - console.log(segment, level); + console.debug(segment, level); if (segment === rootSegment || level <= this.recursiveConfig.level - 1) { let canvas = document.createElement("canvas"); canvas.width = segment.w; canvas.height = segment.h; let modeKey = choice(permissibleModeKeys); - console.log(modeKey); + console.debug(modeKey); this.lastActiveModeTypes.push(modeKey); - let mode = this.modes[modeKey]; + let mode = this.modes.modes[modeKey]; let ctx = canvas.getContext("2d") as CanvasRenderingContext2D; let segments = mode.getSegments(ctx); - console.log(segments); + console.debug(segments); let bitmaps = await Promise.all(segments.map((segment) => processSegment(segment, level + 1))); mode.place(ctx, bitmaps, segments); return await createImageBitmap(canvas); @@ -180,7 +181,7 @@ export default class Collage extends Vue { } }; processSegment(rootSegment, 0).then((finalCollage) => { - console.log(finalCollage); + console.debug(finalCollage); this.context.drawImage(finalCollage, 0, 0); }).catch((error) => { alert(error); diff --git a/src/types.d.ts b/src/types.d.ts deleted file mode 100644 index fc9dde7..0000000 --- a/src/types.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface CollageMode { - name: string; - minImages: number; - getSegments: (ctx: CanvasRenderingContext2D, config?: CollageConfig, images?: ImageBitmap[]) => Segment[]; - place: (ctx: CanvasRenderingContext2D, images: ImageBitmap[], segments: Segment[]) => void; - forceConfig?: CollageConfig; -} - -export interface CollageConfig { - numImages?: number; -} - -export interface Segment { - x: number; - y: number; - w: number; - h: number; -}