diff --git a/cli/collages.ts b/cli/collages.ts index 3b535b5..eeb3111 100644 --- a/cli/collages.ts +++ b/cli/collages.ts @@ -1,24 +1,34 @@ import { CollageModes } from "../src/common/collages.ts"; import { CanvasRenderingContext2D, + EmulatedCanvas2D, Image, } from "https://deno.land/x/canvas/mod.ts"; -import { CollageContext, CollageImage } from "../src/common/types.ts"; +import { + CollageImage, +} from "../src/common/types.ts"; import { init } from "https://deno.land/x/canvas@v1.3.0/mod.ts"; const canvasKit = await init(); export class ProxyImage implements CollageImage { - private filepath: string; + private filepath: string | null; private _image: Image | undefined; - constructor(filepath: string) { - this.filepath = filepath; + constructor(input: string | Image) { + if (typeof input === "string") { + this.filepath = input; + } else { + this.filepath = null; + this._image = input; + } } public get image(): Image { if (!this._image) { - const image = canvasKit.MakeImageFromEncoded(Deno.readFileSync(this.filepath)) + const image = canvasKit.MakeImageFromEncoded( + Deno.readFileSync(this.filepath!), + ); if (!image) { throw Error(`Failed loading ${this.filepath}!`); } @@ -36,14 +46,41 @@ export class ProxyImage implements CollageImage { } } -export type CastCanvasRenderingContext = - & CanvasRenderingContext2D - & CollageContext; +declare module "https://deno.land/x/canvas/mod.ts" { + interface HTMLCanvasElement { + width: number; + height: number; + getContext(x: '2d'): CanvasRenderingContext2D + } +} + +export class DenoCollageModes + extends CollageModes< + CanvasRenderingContext2D, + ProxyImage, + EmulatedCanvas2D + > { + + createCanvas(w: number, h: number): EmulatedCanvas2D { + const canvas = canvasKit.MakeCanvas(Math.round(w), Math.round(h)); + if (!canvas) { + throw Error("Error initializing canvas."); + } + return canvas; + } + + canvasToImage(canvas: EmulatedCanvas2D): PromiseLike { + const image = canvasKit.MakeImageFromEncoded( + canvas.toBuffer(), + ); + if (!image) { + throw Error("Something went wrong converting canvas to image."); + } + return Promise.resolve(new ProxyImage(image)); + } -export default class BrowserCollageModes - extends CollageModes { drawImage( - ctx: CastCanvasRenderingContext, + ctx: CanvasRenderingContext2D, image: ProxyImage, sx: number, sy: number, @@ -57,3 +94,5 @@ export default class BrowserCollageModes ctx.drawImage(image.image, sx, sy, sw, sh, dx, dy, dw, dh); } } + +export const denoCollageModes = new DenoCollageModes(); diff --git a/cli/main.ts b/cli/main.ts index 02291ec..9587de9 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -1,8 +1,8 @@ import { parse } from "https://deno.land/std@0.106.0/flags/mod.ts"; import { createCanvas } from "https://deno.land/x/canvas@v1.3.0/mod.ts"; -import { CollageModeType, collageModeType } from "../src/common/collages.ts"; -import DenoCollageModes, { - CastCanvasRenderingContext, +import { collageModeType, DisplayCollageModeType, displayCollageModeType, isCollageModeType, isDisplayCollageModeType } from "../src/common/collages.ts"; +import { + denoCollageModes, ProxyImage, } from "./collages.ts"; import { CollageConfig } from "../src/common/types.ts"; @@ -17,11 +17,11 @@ const args = parse(Deno.args, { "m": "mode", "r": "recursive", }, - boolean: ["recursive"] + boolean: ["recursive"], }); if (args["mode"] === true) { - console.log(collageModeType.join(", ")); + console.log(displayCollageModeType.join(", ")); Deno.exit(0); } @@ -35,9 +35,7 @@ args["_"].forEach((arg) => { if (Deno.statSync(arg).isDirectory) { Array.from( walkSync(arg, { - maxDepth: args["recursive"] - ? Infinity - : 1, + maxDepth: args["recursive"] ? Infinity : 1, includeDirs: false, includeFiles: true, exts: includeExtensions.length ? includeExtensions : undefined, @@ -57,15 +55,18 @@ const shuffledFiles = shuffle(Array.from(files)); const images: ProxyImage[] = shuffledFiles.map((file) => new ProxyImage(file)); -const modes = new DenoCollageModes(); - -const modeKey: CollageModeType = args["mode"] || choice(collageModeType); -const mode = modes.modes[modeKey]; - -console.log( - `Creating a "${mode.name}" collage, choosing from ${shuffledFiles.length} files...`, -); -// console.debug(`Images: ${shuffledFiles.join(", ")}`); +const allModeKeys: DisplayCollageModeType[] = []; +if (args["mode"]) { + (args["mode"] as string).split(",").map((m) => m.trim()).forEach((m) => { + if (isDisplayCollageModeType(m)) { + allModeKeys.push(m) + } else { + throw Error(`"${m}" is not a valid collage mode.`); + } + }) +} else { + allModeKeys.push(...displayCollageModeType); +} const canvas = createCanvas(args["width"] || 640, args["height"] || 640); const context = canvas.getContext("2d"); @@ -73,13 +74,29 @@ const context = canvas.getContext("2d"); const collageConfig: CollageConfig = { numImages: args["n"], }; +const modeKey: DisplayCollageModeType = choice(allModeKeys); -const segments = mode.getSegments( - context as CastCanvasRenderingContext, - collageConfig, - images, -); -mode.place(context as CastCanvasRenderingContext, images, segments); +if (modeKey === "recursive") { + console.log( + `Creating a recursive collage, choosing from ${shuffledFiles.length} files...`, + ); + await denoCollageModes.recursiveDraw(context, images, { + modes: collageModeType, + repeat: true, + level: 3 + }) +} else { + const mode = denoCollageModes.modes[modeKey]; + console.log( + `Creating a "${mode.name}" collage, choosing from ${shuffledFiles.length} files...`, + ); + const segments = mode.getSegments( + context, + collageConfig, + images, + ); + mode.place(context, images, segments); +} const output = args["output"] || "collage.png"; console.log(`Saving to "${output}"...`); diff --git a/src/common/collages.ts b/src/common/collages.ts index b026a25..f9522bc 100644 --- a/src/common/collages.ts +++ b/src/common/collages.ts @@ -1,4 +1,4 @@ -import { CollageConfig, CollageContext, CollageImage, CollageMode, Segment } from "@/common/types"; +import { CollageCanvas, CollageConfig, CollageContext, CollageImage, CollageMode, Segment } from "@/common/types"; import { choice, randint, range, shuffle } from "@/common/utils"; export const collageModeType = [ @@ -9,8 +9,23 @@ export const collageModeType = [ "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 abstract class CollageModes { +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 { readonly modes: { [key in CollageModeType]: CollageMode } = { "clean_grid": { name: "Clean Grid", @@ -174,6 +189,52 @@ export abstract class CollageModes => { + // 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); @@ -218,5 +279,9 @@ export abstract class CollageModes + 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 index cce4e42..0bc5300 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -17,12 +17,16 @@ export interface Segment { h: number; } + export interface CollageContext { globalCompositeOperation: string; - canvas: { - width: number; - height: number; - }; + canvas: CollageCanvas +} + +export interface CollageCanvas { + width: number; + height: number; + getContext: (x: '2d') => CollageContext } export interface CollageImage {