cli - allow multiple modes to be selected, recursive collages

also use module augmentation to simplify types and all

TODO: make web app also use the same recursive code
This commit is contained in:
Tomáš Mládek 2021-09-20 22:38:04 +02:00
parent 2647238410
commit 33603eabff
4 changed files with 165 additions and 40 deletions

View file

@ -1,24 +1,34 @@
import { CollageModes } from "../src/common/collages.ts"; import { CollageModes } from "../src/common/collages.ts";
import { import {
CanvasRenderingContext2D, CanvasRenderingContext2D,
EmulatedCanvas2D,
Image, Image,
} from "https://deno.land/x/canvas/mod.ts"; } 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"; import { init } from "https://deno.land/x/canvas@v1.3.0/mod.ts";
const canvasKit = await init(); const canvasKit = await init();
export class ProxyImage implements CollageImage { export class ProxyImage implements CollageImage {
private filepath: string; private filepath: string | null;
private _image: Image | undefined; private _image: Image | undefined;
constructor(filepath: string) { constructor(input: string | Image) {
this.filepath = filepath; if (typeof input === "string") {
this.filepath = input;
} else {
this.filepath = null;
this._image = input;
}
} }
public get image(): Image { public get image(): Image {
if (!this._image) { if (!this._image) {
const image = canvasKit.MakeImageFromEncoded(Deno.readFileSync(this.filepath)) const image = canvasKit.MakeImageFromEncoded(
Deno.readFileSync(this.filepath!),
);
if (!image) { if (!image) {
throw Error(`Failed loading ${this.filepath}!`); throw Error(`Failed loading ${this.filepath}!`);
} }
@ -36,14 +46,41 @@ export class ProxyImage implements CollageImage {
} }
} }
export type CastCanvasRenderingContext = declare module "https://deno.land/x/canvas/mod.ts" {
& CanvasRenderingContext2D interface HTMLCanvasElement {
& CollageContext; 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<ProxyImage> {
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<CastCanvasRenderingContext, ProxyImage> {
drawImage( drawImage(
ctx: CastCanvasRenderingContext, ctx: CanvasRenderingContext2D,
image: ProxyImage, image: ProxyImage,
sx: number, sx: number,
sy: number, sy: number,
@ -57,3 +94,5 @@ export default class BrowserCollageModes
ctx.drawImage(image.image, sx, sy, sw, sh, dx, dy, dw, dh); ctx.drawImage(image.image, sx, sy, sw, sh, dx, dy, dw, dh);
} }
} }
export const denoCollageModes = new DenoCollageModes();

View file

@ -1,8 +1,8 @@
import { parse } from "https://deno.land/std@0.106.0/flags/mod.ts"; 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 { createCanvas } from "https://deno.land/x/canvas@v1.3.0/mod.ts";
import { CollageModeType, collageModeType } from "../src/common/collages.ts"; import { collageModeType, DisplayCollageModeType, displayCollageModeType, isCollageModeType, isDisplayCollageModeType } from "../src/common/collages.ts";
import DenoCollageModes, { import {
CastCanvasRenderingContext, denoCollageModes,
ProxyImage, ProxyImage,
} from "./collages.ts"; } from "./collages.ts";
import { CollageConfig } from "../src/common/types.ts"; import { CollageConfig } from "../src/common/types.ts";
@ -17,11 +17,11 @@ const args = parse(Deno.args, {
"m": "mode", "m": "mode",
"r": "recursive", "r": "recursive",
}, },
boolean: ["recursive"] boolean: ["recursive"],
}); });
if (args["mode"] === true) { if (args["mode"] === true) {
console.log(collageModeType.join(", ")); console.log(displayCollageModeType.join(", "));
Deno.exit(0); Deno.exit(0);
} }
@ -35,9 +35,7 @@ args["_"].forEach((arg) => {
if (Deno.statSync(arg).isDirectory) { if (Deno.statSync(arg).isDirectory) {
Array.from( Array.from(
walkSync(arg, { walkSync(arg, {
maxDepth: args["recursive"] maxDepth: args["recursive"] ? Infinity : 1,
? Infinity
: 1,
includeDirs: false, includeDirs: false,
includeFiles: true, includeFiles: true,
exts: includeExtensions.length ? includeExtensions : undefined, 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 images: ProxyImage[] = shuffledFiles.map((file) => new ProxyImage(file));
const modes = new DenoCollageModes(); const allModeKeys: DisplayCollageModeType[] = [];
if (args["mode"]) {
const modeKey: CollageModeType = args["mode"] || choice(collageModeType); (args["mode"] as string).split(",").map((m) => m.trim()).forEach((m) => {
const mode = modes.modes[modeKey]; if (isDisplayCollageModeType(m)) {
allModeKeys.push(m)
console.log( } else {
`Creating a "${mode.name}" collage, choosing from ${shuffledFiles.length} files...`, throw Error(`"${m}" is not a valid collage mode.`);
); }
// console.debug(`Images: ${shuffledFiles.join(", ")}`); })
} else {
allModeKeys.push(...displayCollageModeType);
}
const canvas = createCanvas(args["width"] || 640, args["height"] || 640); const canvas = createCanvas(args["width"] || 640, args["height"] || 640);
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
@ -73,13 +74,29 @@ const context = canvas.getContext("2d");
const collageConfig: CollageConfig = { const collageConfig: CollageConfig = {
numImages: args["n"], numImages: args["n"],
}; };
const modeKey: DisplayCollageModeType = choice(allModeKeys);
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( const segments = mode.getSegments(
context as CastCanvasRenderingContext, context,
collageConfig, collageConfig,
images, images,
); );
mode.place(context as CastCanvasRenderingContext, images, segments); mode.place(context, images, segments);
}
const output = args["output"] || "collage.png"; const output = args["output"] || "collage.png";
console.log(`Saving to "${output}"...`); console.log(`Saving to "${output}"...`);

View file

@ -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"; import { choice, randint, range, shuffle } from "@/common/utils";
export const collageModeType = [ export const collageModeType = [
@ -9,8 +9,23 @@ export const collageModeType = [
"blend" "blend"
] as const; ] as const;
export type CollageModeType = typeof collageModeType[number]; 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<C extends CollageContext, I extends CollageImage> { 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<C, I> } = { readonly modes: { [key in CollageModeType]: CollageMode<C, I> } = {
"clean_grid": { "clean_grid": {
name: "Clean Grid", name: "Clean Grid",
@ -174,6 +189,52 @@ export abstract class CollageModes<C extends CollageContext, I extends CollageIm
} }
}; };
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<I> => {
// 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, private cleanDraw(ctx: C, image: I,
x: number, y: number, w: number, h: number) { x: number, y: number, w: number, h: number) {
const scaleRatio = Math.max(w / image.width, h / image.height); const scaleRatio = Math.max(w / image.width, h / image.height);
@ -218,5 +279,9 @@ export abstract class CollageModes<C extends CollageContext, I extends CollageIm
}); });
} }
abstract createCanvas(w: number, h: number): CS
abstract canvasToImage(canvas: CS): PromiseLike<I>
abstract drawImage(ctx: C, image: I, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void; abstract drawImage(ctx: C, image: I, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;
} }

View file

@ -17,12 +17,16 @@ export interface Segment {
h: number; h: number;
} }
export interface CollageContext { export interface CollageContext {
globalCompositeOperation: string; globalCompositeOperation: string;
canvas: { canvas: CollageCanvas
}
export interface CollageCanvas {
width: number; width: number;
height: number; height: number;
}; getContext: (x: '2d') => CollageContext
} }
export interface CollageImage { export interface CollageImage {