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:
parent
2647238410
commit
33603eabff
4 changed files with 165 additions and 40 deletions
|
@ -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();
|
||||
|
|
59
cli/main.ts
59
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,
|
||||
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}"...`);
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue