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 {
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<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(
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();

View file

@ -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,
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 as CastCanvasRenderingContext, images, segments);
);
mode.place(context, images, segments);
}
const output = args["output"] || "collage.png";
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";
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<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> } = {
"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,
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<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;
}

View file

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