autoformat

master
Tomáš Mládek 2022-01-19 21:26:06 +01:00
parent f24047b33b
commit c89080ff00
No known key found for this signature in database
GPG Key ID: ED21612889E75EC5
2 changed files with 390 additions and 277 deletions

View File

@ -25,7 +25,7 @@ const args = parse(Deno.args, {
include: "*.png, *.jpg", include: "*.png, *.jpg",
output: "collage.png", output: "collage.png",
rl: 2, rl: 2,
rr: false rr: false,
}, },
}); });
@ -36,7 +36,7 @@ if (args["mode"] === true) {
const files: Set<string> = new Set(); const files: Set<string> = new Set();
const includeExtensions = Array.from( const includeExtensions = Array.from(
String(args["include"]).matchAll(/\*\.([\w]+)/g), String(args["include"]).matchAll(/\*\.([\w]+)/g)
).map(([_, group]) => group); ).map(([_, group]) => group);
args["_"].forEach((arg) => { args["_"].forEach((arg) => {
@ -48,7 +48,7 @@ args["_"].forEach((arg) => {
includeDirs: false, includeDirs: false,
includeFiles: true, includeFiles: true,
exts: includeExtensions.length ? includeExtensions : undefined, exts: includeExtensions.length ? includeExtensions : undefined,
}), })
).forEach((entry) => files.add(entry.path)); ).forEach((entry) => files.add(entry.path));
} else { } else {
files.add(arg); files.add(arg);
@ -64,7 +64,9 @@ 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 allModeKeys = args["mode"] ? parseDisplayCollageModes(args["mode"]) : displayCollageModeType; const allModeKeys = args["mode"]
? parseDisplayCollageModes(args["mode"])
: displayCollageModeType;
const canvas = createCanvas(args["width"], args["height"]); const canvas = createCanvas(args["width"], args["height"]);
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
@ -76,7 +78,7 @@ const modeKey: DisplayCollageModeType = choice(allModeKeys);
if (modeKey === "recursive") { if (modeKey === "recursive") {
console.log( console.log(
`Creating a recursive collage, choosing from ${shuffledFiles.length} files...`, `Creating a recursive collage, choosing from ${shuffledFiles.length} files...`
); );
await denoCollageModes.recursiveDraw(context, images, { await denoCollageModes.recursiveDraw(context, images, {
modes: args["rm"] ? parseCollageModes(args["rm"]) : collageModeType, modes: args["rm"] ? parseCollageModes(args["rm"]) : collageModeType,
@ -86,17 +88,18 @@ if (modeKey === "recursive") {
} else { } else {
const mode = denoCollageModes.modes[modeKey]; const mode = denoCollageModes.modes[modeKey];
console.log( console.log(
`Creating a "${mode.name}" collage, choosing from ${shuffledFiles.length} files...`, `Creating a "${mode.name}" collage, choosing from ${shuffledFiles.length} files...`
);
const segments = mode.getSegments(
context,
collageConfig,
images,
); );
const segments = mode.getSegments(context, collageConfig, images);
mode.place(context, images, segments); mode.place(context, images, segments);
console.log(`Used: ${images.slice(0, segments.length).map((img) => img.path).join(", ")}`) console.log(
`Used: ${images
.slice(0, segments.length)
.map((img) => img.path)
.join(", ")}`
);
} }
const output = args["output"]; const output = args["output"];
console.log(`Saving to "${output}"...`); console.log(`Saving to "${output}"...`);
await Deno.writeFile(output, canvas.toBuffer()); await Deno.writeFile(output, canvas.toBuffer());

View File

@ -1,287 +1,397 @@
import { CollageCanvas, 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 = [
"clean_grid", "chaos_grid", "clean_grid",
"row", "irow", "chaos_grid",
"col", "icol", "row",
"concentric_factor", "concentric_spaced", "irow",
"blend" "col",
"icol",
"concentric_factor",
"concentric_spaced",
"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 { export function isCollageModeType(value: string): value is CollageModeType {
return (collageModeType as readonly string[]).includes(value); return (collageModeType as readonly string[]).includes(value);
} }
export const displayCollageModeType = [...collageModeType, "recursive"] as const; export const displayCollageModeType = [
...collageModeType,
"recursive",
] as const;
export type DisplayCollageModeType = typeof displayCollageModeType[number]; export type DisplayCollageModeType = typeof displayCollageModeType[number];
export function isDisplayCollageModeType(value: string): value is DisplayCollageModeType { export function isDisplayCollageModeType(
return (displayCollageModeType as readonly string[]).includes(value); value: string
): value is DisplayCollageModeType {
return (displayCollageModeType as readonly string[]).includes(value);
} }
export interface RecursiveCollageConfig { export interface RecursiveCollageConfig {
level: number; level: number;
repeat: boolean; repeat: boolean;
modes: readonly CollageModeType[] modes: readonly CollageModeType[];
} }
export abstract class CollageModes<C extends CollageContext, I extends CollageImage, CS extends CollageCanvas> { export abstract class CollageModes<
readonly modes: { [key in CollageModeType]: CollageMode<C, I> } = { C extends CollageContext,
"clean_grid": { I extends CollageImage,
name: "Clean Grid", CS extends CollageCanvas
minImages: 4, > {
forceConfig: { readonly modes: { [key in CollageModeType]: CollageMode<C, I> } = {
numImages: 4 clean_grid: {
}, name: "Clean Grid",
getSegments: this.getGridSegments, minImages: 4,
place: this.cleanPlace.bind(this) forceConfig: {
}, numImages: 4,
"chaos_grid": { },
name: "Irregular Grid", getSegments: this.getGridSegments,
minImages: 4, place: this.cleanPlace.bind(this),
forceConfig: { },
numImages: 4 chaos_grid: {
}, name: "Irregular Grid",
getSegments: this.getGridSegments, minImages: 4,
place: (ctx, images, segments) => { forceConfig: {
const shuffledImages = shuffle(images); numImages: 4,
shuffle(segments.map((segment, idx) => [segment, idx] as [Segment, number])) },
.forEach(([segment, idx]) => { getSegments: this.getGridSegments,
const image = shuffledImages[idx]; place: (ctx, images, segments) => {
const scaleRatio = Math.max(segment.w / image.width, segment.h / image.height); const shuffledImages = shuffle(images);
this.drawImage(ctx, image, 0, 0, image.width, image.height, shuffle(
segment.x - (image.width * scaleRatio / 2), segment.y - (image.height * scaleRatio / 2), segments.map((segment, idx) => [segment, idx] as [Segment, number])
image.width * scaleRatio, image.height * scaleRatio); ).forEach(([segment, idx]) => {
}); const image = shuffledImages[idx];
}, const scaleRatio = Math.max(
}, segment.w / image.width,
"row": { segment.h / image.height
name: "Regular Row", );
minImages: 2, this.drawImage(
getSegments: (ctx, config, images) => { ctx,
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2); image,
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height]; 0,
return range(numImages).map((idx) => { 0,
return { image.width,
x: idx * segmentSize[0] + segmentSize[0] / 2, image.height,
y: segmentSize[1] / 2, segment.x - (image.width * scaleRatio) / 2,
w: segmentSize[0], segment.y - (image.height * scaleRatio) / 2,
h: segmentSize[1] image.width * scaleRatio,
}; image.height * scaleRatio
}); );
}, });
place: this.cleanPlace.bind(this) },
}, },
"irow": { row: {
name: "Irregular Row", name: "Regular Row",
minImages: 2, minImages: 2,
getSegments: (ctx, config, images) => { getSegments: (ctx, config, images) => {
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2); const numImages = Math.min(
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height]; images?.length || Infinity,
return range(numImages).map((idx) => { config?.numImages || randint(4) + 2
const irregularWidth = images ? );
segmentSize[0] + Math.random() * ((segmentSize[1] / images[idx].height * images[idx].width) - segmentSize[0]) : const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
segmentSize[0] + Math.random() * segmentSize[0] * .5; return range(numImages).map((idx) => {
return { return {
x: idx * segmentSize[0] + segmentSize[0] / 2, x: idx * segmentSize[0] + segmentSize[0] / 2,
y: segmentSize[1] / 2, y: segmentSize[1] / 2,
w: Math.min(ctx.canvas.width / 2, irregularWidth), w: segmentSize[0],
h: segmentSize[1], h: segmentSize[1],
}; };
}); });
}, },
place: this.cleanPlace.bind(this) place: this.cleanPlace.bind(this),
}, },
"col": { irow: {
name: "Regular Column", name: "Irregular Row",
minImages: 2, minImages: 2,
getSegments: (ctx, config, images) => { getSegments: (ctx, config, images) => {
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2); const numImages = Math.min(
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages]; images?.length || Infinity,
return range(numImages).map((idx) => { config?.numImages || randint(4) + 2
return { );
x: segmentSize[0] / 2, const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
y: idx * segmentSize[1] + segmentSize[1] / 2, return range(numImages).map((idx) => {
w: segmentSize[0], const irregularWidth = images
h: segmentSize[1] ? segmentSize[0] +
}; Math.random() *
}); ((segmentSize[1] / images[idx].height) * images[idx].width -
}, segmentSize[0])
place: this.cleanPlace.bind(this) : segmentSize[0] + Math.random() * segmentSize[0] * 0.5;
}, return {
"icol": { x: idx * segmentSize[0] + segmentSize[0] / 2,
name: "Irregular Column", y: segmentSize[1] / 2,
minImages: 2, w: Math.min(ctx.canvas.width / 2, irregularWidth),
getSegments: (ctx, config, images) => { h: segmentSize[1],
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2); };
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages]; });
return range(numImages).map((idx) => { },
const irregularHeight = images ? place: this.cleanPlace.bind(this),
segmentSize[1] + Math.random() * ((segmentSize[0] / images[idx].width * images[idx].height) - segmentSize[1]) : },
segmentSize[1] + Math.random() * segmentSize[1] * .5; col: {
return { name: "Regular Column",
x: segmentSize[0] / 2, minImages: 2,
y: idx * segmentSize[1] + segmentSize[1] / 2, getSegments: (ctx, config, images) => {
w: segmentSize[0], const numImages = Math.min(
h: Math.min(ctx.canvas.height / 2, irregularHeight), images?.length || Infinity,
}; config?.numImages || randint(4) + 2
}); );
}, const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
place: this.cleanPlace.bind(this) return range(numImages).map((idx) => {
}, return {
"concentric_factor": { x: segmentSize[0] / 2,
name: "Constant factor concentric", y: idx * segmentSize[1] + segmentSize[1] / 2,
minImages: 2, w: segmentSize[0],
getSegments: (ctx, config, images) => { h: segmentSize[1],
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2); };
let factor: number; });
if (Math.random() > .5) { },
factor = choice([1 / Math.sqrt(2), .5, .88]); place: this.cleanPlace.bind(this),
} else { },
factor = 1 - (1 / numImages); icol: {
} name: "Irregular Column",
return range(numImages).map((idx) => { minImages: 2,
const ratio = Math.pow(factor, idx); getSegments: (ctx, config, images) => {
return { const numImages = Math.min(
x: ctx.canvas.width / 2, images?.length || Infinity,
y: ctx.canvas.height / 2, config?.numImages || randint(4) + 2
w: ctx.canvas.width * ratio, );
h: ctx.canvas.height * ratio const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
}; return range(numImages).map((idx) => {
}); const irregularHeight = images
}, ? segmentSize[1] +
place: this.cleanPlace.bind(this) Math.random() *
}, ((segmentSize[0] / images[idx].width) * images[idx].height -
"concentric_spaced": { segmentSize[1])
name: "Equally spaced concentric", : segmentSize[1] + Math.random() * segmentSize[1] * 0.5;
minImages: 2, return {
getSegments: (ctx, config, images) => { x: segmentSize[0] / 2,
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(2) + 2); y: idx * segmentSize[1] + segmentSize[1] / 2,
return range(numImages).map((idx) => { w: segmentSize[0],
return { h: Math.min(ctx.canvas.height / 2, irregularHeight),
x: ctx.canvas.width / 2, };
y: ctx.canvas.height / 2, });
w: ctx.canvas.width - (ctx.canvas.width / numImages * idx), },
h: ctx.canvas.height - (ctx.canvas.height / numImages * idx), place: this.cleanPlace.bind(this),
}; },
}); concentric_factor: {
}, name: "Constant factor concentric",
place: this.cleanPlace.bind(this) minImages: 2,
}, getSegments: (ctx, config, images) => {
"blend": { const numImages = Math.min(
name: "Blending", images?.length || Infinity,
minImages: 2, config?.numImages || randint(4) + 2
getSegments: (ctx, config, images) => { );
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(2) + 2); let factor: number;
return range(numImages).map((_) => { if (Math.random() > 0.5) {
return { factor = choice([1 / Math.sqrt(2), 0.5, 0.88]);
x: ctx.canvas.width / 2, } else {
y: ctx.canvas.height / 2, factor = 1 - 1 / numImages;
w: ctx.canvas.width,
h: ctx.canvas.height
};
});
},
place: (ctx, images, segments) => {
ctx.globalCompositeOperation = choice(["difference", "saturation", "soft-light", "overlay"]);
this.cleanPlace(ctx, images, segments);
}
} }
return range(numImages).map((idx) => {
const ratio = Math.pow(factor, idx);
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width * ratio,
h: ctx.canvas.height * ratio,
};
});
},
place: this.cleanPlace.bind(this),
},
concentric_spaced: {
name: "Equally spaced concentric",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(2) + 2
);
return range(numImages).map((idx) => {
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width - (ctx.canvas.width / numImages) * idx,
h: ctx.canvas.height - (ctx.canvas.height / numImages) * idx,
};
});
},
place: this.cleanPlace.bind(this),
},
blend: {
name: "Blending",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(2) + 2
);
return range(numImages).map((_) => {
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width,
h: ctx.canvas.height,
};
});
},
place: (ctx, images, segments) => {
ctx.globalCompositeOperation = choice([
"difference",
"saturation",
"soft-light",
"overlay",
]);
this.cleanPlace(ctx, images, segments);
},
},
};
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";
}
}
}
}; };
public async recursiveDraw(context: C, images: I[], recursiveConfig: RecursiveCollageConfig) { const result = await processSegment(rootSegment, 0);
const localImages = images.concat(); this.drawImage(
const rootSegment: Segment = { context,
x: 0, result,
y: 0, 0,
w: context.canvas.width, 0,
h: context.canvas.height, result.width,
}; result.height,
const processSegment = async ( 0,
segment: Segment, 0,
level: number context.canvas.width,
): Promise<I> => { context.canvas.height
// 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); private cleanDraw(
this.drawImage(context, result, 0, 0, result.width, result.height, 0, 0, context.canvas.width, context.canvas.height); ctx: C,
} image: I,
x: number,
y: number,
w: number,
h: number
) {
const scaleRatio = Math.max(w / image.width, h / image.height);
this.drawImage(
ctx,
image,
image.width / 2 - w / scaleRatio / 2,
image.height / 2 - h / scaleRatio / 2,
w / scaleRatio,
h / scaleRatio,
x - w / 2,
y - h / 2,
w,
h
);
}
private cleanDraw(ctx: C, image: I, private cleanPlace(ctx: C, images: I[], segments: Segment[]) {
x: number, y: number, w: number, h: number) { segments.forEach((segment, idx) => {
const scaleRatio = Math.max(w / image.width, h / image.height); this.cleanDraw(
this.drawImage(ctx, image, image.width / 2 - w / scaleRatio / 2, image.height / 2 - h / scaleRatio / 2, ctx,
w / scaleRatio, h / scaleRatio, images[idx],
x - w / 2, y - h / 2, segment.x,
w, h); segment.y,
} segment.w,
segment.h
);
});
}
private cleanPlace(ctx: C, images: I[], segments: Segment[]) { private getGridSegments(ctx: C, config?: CollageConfig) {
segments.forEach((segment, idx) => { return [0, 1, 2, 3].map((idx) => {
this.cleanDraw(ctx, images[idx], segment.x, segment.y, segment.w, segment.h); let x!: number, y!: number;
}); switch (idx) {
} case 0:
x = ctx.canvas.width * 0.25;
y = ctx.canvas.height * 0.25;
break;
case 1:
x = ctx.canvas.width * 0.75;
y = ctx.canvas.height * 0.25;
break;
case 2:
x = ctx.canvas.width * 0.25;
y = ctx.canvas.height * 0.75;
break;
case 3:
x = ctx.canvas.width * 0.75;
y = ctx.canvas.height * 0.75;
break;
}
return {
x,
y,
w: ctx.canvas.width / 2,
h: ctx.canvas.height / 2,
};
});
}
private getGridSegments(ctx: C, config?: CollageConfig) { abstract createCanvas(w: number, h: number): CS;
return [0, 1, 2, 3].map((idx) => {
let x!: number, y!: number;
switch (idx) {
case 0:
x = ctx.canvas.width * .25;
y = ctx.canvas.height * .25;
break;
case 1:
x = ctx.canvas.width * .75;
y = ctx.canvas.height * .25;
break;
case 2:
x = ctx.canvas.width * .25;
y = ctx.canvas.height * .75;
break;
case 3:
x = ctx.canvas.width * .75;
y = ctx.canvas.height * .75;
break;
}
return {
x, y,
w: ctx.canvas.width / 2,
h: ctx.canvas.height / 2
};
});
}
abstract createCanvas(w: number, h: number): CS abstract canvasToImage(canvas: CS): PromiseLike<I>;
abstract canvasToImage(canvas: CS): PromiseLike<I> abstract drawImage(
ctx: C,
abstract drawImage(ctx: C, image: I, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void; image: I,
} sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number
): void;
}