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 { 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();
|
||||||
|
|
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 { 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);
|
||||||
|
|
||||||
const segments = mode.getSegments(
|
if (modeKey === "recursive") {
|
||||||
context as CastCanvasRenderingContext,
|
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,
|
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}"...`);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue