deno cli version (ugly) proof of concept

master
Tomáš Mládek 2021-09-15 19:08:53 +02:00
parent 79f9108f34
commit 054f9a9218
12 changed files with 362 additions and 260 deletions

BIN
cli/collage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

27
cli/collages.ts Normal file
View File

@ -0,0 +1,27 @@
import { CollageModes, CollageModeType, collageModeType } from "../src/common/collages.ts";
import { CanvasRenderingContext2D, createCanvas, Image, ImageBitmap, loadImage } from "https://deno.land/x/canvas/mod.ts";
import { CollageContext, CollageImage } from "../src/common/types.ts";
export class ProxyImage implements CollageImage {
private image: Image;
public readonly height: number;
public readonly width: number;
constructor(image: Image) {
this.image = image;
this.width = image.width();
this.height = image.height();
}
public get(): Image {
return this.image;
}
}
export type CastCanvasRenderingContext = CanvasRenderingContext2D & CollageContext;
export default class BrowserCollageModes extends CollageModes<CastCanvasRenderingContext, ProxyImage> {
drawImage(ctx: CastCanvasRenderingContext, image: ProxyImage, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void {
ctx.drawImage(image.get(), sx, sy, sw, sh, dx, dy, dw, dh);
}
}

6
cli/import_map.json Normal file
View File

@ -0,0 +1,6 @@
{
"imports": {
"@/common/types": "../src/common/types.ts",
"@/common/utils": "../src/common/utils.ts"
}
}

42
cli/main.ts Normal file
View File

@ -0,0 +1,42 @@
import { parse } from "https://deno.land/std@0.106.0/flags/mod.ts";
import { createCanvas, loadImage } from "https://deno.land/x/canvas/mod.ts";
import { CollageModes, CollageModeType, collageModeType } from "../src/common/collages.ts";
import DenoCollageModes, { CastCanvasRenderingContext, ProxyImage } from "./collages.ts";
import { CollageConfig } from "../src/common/types.ts";
import { choice, shuffle } from "../src/common/utils.ts";
const args = parse(Deno.args, {
alias: {
'o': 'output'
}
});
const images: ProxyImage[] = (await Promise.all(args["_"].map((imageURL) => loadImage(imageURL.toString())))).map((image) => new ProxyImage(image));
if (images.length < 2) {
console.error("kollagen needs at least 2 images to work.");
Deno.exit(1);
}
const modes = new DenoCollageModes();
const modeKey: CollageModeType = args["mode"] || choice(collageModeType);
const mode = modes.modes[modeKey];
console.log(`Creating a "${mode.name}" collage from ${images.length} images...`);
console.debug(`Images: ${args["_"].join(", ")}`);
const canvas = createCanvas(args["width"] || 640, args["height"] || 640);
const context = canvas.getContext("2d");
const collageConfig: CollageConfig = {
numImages: args["n"]
}
const shuffledImages = shuffle(images);
const segments = mode.getSegments(context as CastCanvasRenderingContext, collageConfig, shuffledImages);
mode.place(context as CastCanvasRenderingContext, shuffledImages, segments);
const output = args["output"] || "collage.jpg";
console.log(`Saving to "${output}"...`);
await Deno.writeFile(output, canvas.toBuffer());

8
cli/tsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"lib": [
"dom",
"deno.ns"
]
}
}

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "kollagen",
"version": "0.1.0",
"version": "0.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,224 +1,7 @@
import {CollageConfig, CollageMode, Segment} from "@/types";
import {choice, randint, range, shuffle} from "@/utils";
import { CollageModes } from "./common/collages";
export const collageModeType = [
"clean_grid", "chaos_grid",
"row", "irow",
"col", "icol",
"concentric_factor", "concentric_spaced",
"blend"
] as const;
export type CollageModeType = typeof collageModeType[number];
function cleanDraw(ctx: CanvasRenderingContext2D, image: ImageBitmap,
x: number, y: number, w: number, h: number) {
const scaleRatio = Math.max(w / image.width, h / image.height);
ctx.drawImage(
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
);
}
function getGridSegments(ctx: CanvasRenderingContext2D, config?: CollageConfig) {
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
};
});
}
function cleanPlace(ctx: CanvasRenderingContext2D, images: ImageBitmap[], segments: Segment[]) {
segments.forEach((segment, idx) => {
cleanDraw(ctx, images[idx], segment.x, segment.y, segment.w, segment.h);
});
}
const modes: { [key in CollageModeType]: CollageMode } = {
"clean_grid": {
name: "Clean Grid",
minImages: 4,
forceConfig: {
numImages: 4
},
getSegments: getGridSegments,
place: cleanPlace
},
"chaos_grid": {
name: "Irregular Grid",
minImages: 4,
forceConfig: {
numImages: 4
},
getSegments: getGridSegments,
place: (ctx, images, segments) => {
const shuffledImages = shuffle(images);
shuffle(segments.map((segment, idx) => [segment, idx] as [Segment, number]))
.forEach(([segment, idx]) => {
const image = shuffledImages[idx];
const scaleRatio = Math.max(segment.w / image.width, segment.h / image.height);
ctx.drawImage(image,
segment.x - (image.width * scaleRatio / 2), segment.y - (image.height * scaleRatio / 2),
image.width * scaleRatio, image.height * scaleRatio);
});
},
},
"row": {
name: "Regular Row",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
return range(numImages).map((idx) => {
return {
x: idx * segmentSize[0] + segmentSize[0] / 2,
y: segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1]
};
});
},
place: cleanPlace
},
"irow": {
name: "Irregular Row",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
return range(numImages).map((idx) => {
const irregularWidth = images ?
segmentSize[0] + Math.random() * ((segmentSize[1] / images[idx].height * images[idx].width) - segmentSize[0]) :
segmentSize[0] + Math.random() * segmentSize[0] * .5;
return {
x: idx * segmentSize[0] + segmentSize[0] / 2,
y: segmentSize[1] / 2,
w: Math.min(ctx.canvas.width / 2, irregularWidth),
h: segmentSize[1],
};
});
},
place: cleanPlace
},
"col": {
name: "Regular Column",
minImages: 2,
getSegments: (ctx, config, images) => {
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) => {
return {
x: segmentSize[0] / 2,
y: idx * segmentSize[1] + segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1]
};
});
},
place: cleanPlace
},
"icol": {
name: "Irregular Column",
minImages: 2,
getSegments: (ctx, config, images) => {
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 ?
segmentSize[1] + Math.random() * ((segmentSize[0] / images[idx].width * images[idx].height) - segmentSize[1]) :
segmentSize[1] + Math.random() * segmentSize[1] * .5;
return {
x: segmentSize[0] / 2,
y: idx * segmentSize[1] + segmentSize[1] / 2,
w: segmentSize[0],
h: Math.min(ctx.canvas.height / 2, irregularHeight),
};
});
},
place: cleanPlace
},
"concentric_factor": {
name: "Constant factor concentric",
minImages: 2,
getSegments: (ctx, config, images) => {
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]);
} else {
factor = 1 - (1 / numImages);
}
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: cleanPlace
},
"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: cleanPlace
},
"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"]);
cleanPlace(ctx, images, segments);
}
export default class BrowserCollageModes extends CollageModes<CanvasRenderingContext2D, ImageBitmap> {
drawImage(ctx: CanvasRenderingContext2D, image: ImageBitmap, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void {
ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
}
};
export default modes;
}

222
src/common/collages.ts Normal file
View File

@ -0,0 +1,222 @@
import { CollageConfig, CollageContext, CollageImage, CollageMode, Segment } from "@/common/types";
import { choice, randint, range, shuffle } from "@/common/utils";
export const collageModeType = [
"clean_grid", "chaos_grid",
"row", "irow",
"col", "icol",
"concentric_factor", "concentric_spaced",
"blend"
] as const;
export type CollageModeType = typeof collageModeType[number];
export abstract class CollageModes<C extends CollageContext, I extends CollageImage> {
readonly modes: { [key in CollageModeType]: CollageMode<C, I> } = {
"clean_grid": {
name: "Clean Grid",
minImages: 4,
forceConfig: {
numImages: 4
},
getSegments: this.getGridSegments,
place: this.cleanPlace.bind(this)
},
"chaos_grid": {
name: "Irregular Grid",
minImages: 4,
forceConfig: {
numImages: 4
},
getSegments: this.getGridSegments,
place: (ctx, images, segments) => {
const shuffledImages = shuffle(images);
shuffle(segments.map((segment, idx) => [segment, idx] as [Segment, number]))
.forEach(([segment, idx]) => {
const image = shuffledImages[idx];
const scaleRatio = Math.max(segment.w / image.width, segment.h / image.height);
this.drawImage(ctx, image, 0, 0, image.width, image.height,
segment.x - (image.width * scaleRatio / 2), segment.y - (image.height * scaleRatio / 2),
image.width * scaleRatio, image.height * scaleRatio);
});
},
},
"row": {
name: "Regular Row",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
return range(numImages).map((idx) => {
return {
x: idx * segmentSize[0] + segmentSize[0] / 2,
y: segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1]
};
});
},
place: this.cleanPlace.bind(this)
},
"irow": {
name: "Irregular Row",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
return range(numImages).map((idx) => {
const irregularWidth = images ?
segmentSize[0] + Math.random() * ((segmentSize[1] / images[idx].height * images[idx].width) - segmentSize[0]) :
segmentSize[0] + Math.random() * segmentSize[0] * .5;
return {
x: idx * segmentSize[0] + segmentSize[0] / 2,
y: segmentSize[1] / 2,
w: Math.min(ctx.canvas.width / 2, irregularWidth),
h: segmentSize[1],
};
});
},
place: this.cleanPlace.bind(this)
},
"col": {
name: "Regular Column",
minImages: 2,
getSegments: (ctx, config, images) => {
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) => {
return {
x: segmentSize[0] / 2,
y: idx * segmentSize[1] + segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1]
};
});
},
place: this.cleanPlace.bind(this)
},
"icol": {
name: "Irregular Column",
minImages: 2,
getSegments: (ctx, config, images) => {
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 ?
segmentSize[1] + Math.random() * ((segmentSize[0] / images[idx].width * images[idx].height) - segmentSize[1]) :
segmentSize[1] + Math.random() * segmentSize[1] * .5;
return {
x: segmentSize[0] / 2,
y: idx * segmentSize[1] + segmentSize[1] / 2,
w: segmentSize[0],
h: Math.min(ctx.canvas.height / 2, irregularHeight),
};
});
},
place: this.cleanPlace.bind(this)
},
"concentric_factor": {
name: "Constant factor concentric",
minImages: 2,
getSegments: (ctx, config, images) => {
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]);
} else {
factor = 1 - (1 / numImages);
}
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);
}
}
};
private cleanDraw(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 cleanPlace(ctx: C, images: I[], segments: Segment[]) {
segments.forEach((segment, idx) => {
this.cleanDraw(ctx, images[idx], segment.x, segment.y, segment.w, segment.h);
});
}
private getGridSegments(ctx: C, config?: CollageConfig) {
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 drawImage(ctx: C, image: I, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;
}

31
src/common/types.ts Normal file
View File

@ -0,0 +1,31 @@
export interface CollageMode<C extends CollageContext, I extends CollageImage> {
name: string;
minImages: number;
getSegments: (ctx: C, config?: CollageConfig, images?: I[]) => Segment[];
place: (ctx: C, images: I[], segments: Segment[]) => void;
forceConfig?: CollageConfig;
}
export interface CollageConfig {
numImages?: number;
}
export interface Segment {
x: number;
y: number;
w: number;
h: number;
}
export interface CollageContext {
globalCompositeOperation: string;
canvas: {
width: number;
height: number;
};
}
export interface CollageImage {
width: number;
height: number;
}

View File

@ -18,7 +18,7 @@ export function randint(n: number) {
return Math.floor(Math.random() * n);
}
export function choice<T>(arr: T[]): T {
export function choice<T>(arr: readonly T[]): T {
return arr[randint(arr.length)];
}

View File

@ -16,7 +16,7 @@
<div class="controls">
<div class="modes">
<ul class="modes-list">
<li v-for="(mode, idx) in modes"
<li v-for="(mode, idx) in modes.modes"
:class="['mode', {
disabled: images.length < mode.minImages,
excluded: excludedModes.includes(idx),
@ -74,9 +74,10 @@
<script lang="ts">
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
import collageModes, {CollageModeType} from "../collages";
import {CollageConfig, CollageMode, Segment} from "@/types";
import {choice, shuffle} from "@/utils";
import {CollageModeType} from "../common/collages";
import {CollageConfig, CollageMode, Segment} from "../common/types";
import {choice, shuffle} from "../common/utils";
import BrowserCollageModes from "../collages";
type DisplayCollageModeType = CollageModeType | & "shuffle" | & "recursive";
@ -98,19 +99,19 @@ export default class Collage extends Vue {
private currentModeType: DisplayCollageModeType = "shuffle";
private lastActiveModeTypes: CollageModeType[] = [];
private excludedModes: CollageModeType[] = [];
private modes = collageModes;
private modes = new BrowserCollageModes();
private get minImages() {
if (this.currentModeType === "shuffle" || this.currentModeType === "recursive") {
return Math.min(...Object.values(this.modes).map((mode) => mode.minImages));
return Math.min(...Object.values(this.modes.modes).map((mode) => mode.minImages));
} else {
return this.modes[this.currentModeType].minImages;
return this.modes.modes[this.currentModeType].minImages;
}
}
private get lastMode() {
if (this.lastActiveModeTypes.length === 1) {
return this.modes[this.lastActiveModeTypes[0]];
return this.modes.modes[this.lastActiveModeTypes[0]];
}
}
@ -131,18 +132,18 @@ export default class Collage extends Vue {
if (this.images.length >= this.minImages) {
this.reset();
const permissibleModeKeys = (Object.keys(collageModes) as CollageModeType[])
.filter(k => !this.excludedModes.includes(k) && collageModes[k].minImages <= this.images.length);
const permissibleModeKeys = (Object.keys(this.modes.modes) as CollageModeType[])
.filter(k => !this.excludedModes.includes(k) && this.modes.modes[k].minImages <= this.images.length);
if (this.currentModeType !== "recursive") {
let mode: CollageMode;
let mode: CollageMode<any, any>;
if (this.currentModeType === "shuffle") {
const randomModeType = choice(permissibleModeKeys);
this.lastActiveModeTypes = [randomModeType];
mode = collageModes[randomModeType];
mode = this.modes.modes[randomModeType];
} else {
this.lastActiveModeTypes = [this.currentModeType];
mode = this.modes[this.currentModeType];
mode = this.modes.modes[this.currentModeType];
}
const shuffledImages = shuffle(this.images);
const segments = mode.getSegments(this.context, this.collageConfig, shuffledImages);
@ -152,18 +153,18 @@ export default class Collage extends Vue {
const shuffledImages = shuffle(this.images);
const rootSegment: Segment = {x: 0, y: 0, w: this.context.canvas.width, h: this.context.canvas.height};
const processSegment = async (segment: Segment, level: number): Promise<ImageBitmap> => {
console.log(segment, level);
console.debug(segment, level);
if (segment === rootSegment || level <= this.recursiveConfig.level - 1) {
let canvas = document.createElement("canvas");
canvas.width = segment.w;
canvas.height = segment.h;
let modeKey = choice(permissibleModeKeys);
console.log(modeKey);
console.debug(modeKey);
this.lastActiveModeTypes.push(modeKey);
let mode = this.modes[modeKey];
let mode = this.modes.modes[modeKey];
let ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
let segments = mode.getSegments(ctx);
console.log(segments);
console.debug(segments);
let bitmaps = await Promise.all(segments.map((segment) => processSegment(segment, level + 1)));
mode.place(ctx, bitmaps, segments);
return await createImageBitmap(canvas);
@ -180,7 +181,7 @@ export default class Collage extends Vue {
}
};
processSegment(rootSegment, 0).then((finalCollage) => {
console.log(finalCollage);
console.debug(finalCollage);
this.context.drawImage(finalCollage, 0, 0);
}).catch((error) => {
alert(error);

18
src/types.d.ts vendored
View File

@ -1,18 +0,0 @@
export interface CollageMode {
name: string;
minImages: number;
getSegments: (ctx: CanvasRenderingContext2D, config?: CollageConfig, images?: ImageBitmap[]) => Segment[];
place: (ctx: CanvasRenderingContext2D, images: ImageBitmap[], segments: Segment[]) => void;
forceConfig?: CollageConfig;
}
export interface CollageConfig {
numImages?: number;
}
export interface Segment {
x: number;
y: number;
w: number;
h: number;
}