deno cli version (ugly) proof of concept
This commit is contained in:
parent
79f9108f34
commit
054f9a9218
12 changed files with 362 additions and 260 deletions
BIN
cli/collage.jpg
Normal file
BIN
cli/collage.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 950 KiB |
27
cli/collages.ts
Normal file
27
cli/collages.ts
Normal 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
6
cli/import_map.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"@/common/types": "../src/common/types.ts",
|
||||||
|
"@/common/utils": "../src/common/utils.ts"
|
||||||
|
}
|
||||||
|
}
|
42
cli/main.ts
Normal file
42
cli/main.ts
Normal 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
8
cli/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"deno.ns"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "kollagen",
|
"name": "kollagen",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
225
src/collages.ts
225
src/collages.ts
|
@ -1,224 +1,7 @@
|
||||||
import {CollageConfig, CollageMode, Segment} from "@/types";
|
import { CollageModes } from "./common/collages";
|
||||||
import {choice, randint, range, shuffle} from "@/utils";
|
|
||||||
|
|
||||||
export const collageModeType = [
|
export default class BrowserCollageModes extends CollageModes<CanvasRenderingContext2D, ImageBitmap> {
|
||||||
"clean_grid", "chaos_grid",
|
drawImage(ctx: CanvasRenderingContext2D, image: ImageBitmap, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void {
|
||||||
"row", "irow",
|
ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
|
||||||
"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 modes;
|
|
||||||
|
|
222
src/common/collages.ts
Normal file
222
src/common/collages.ts
Normal 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
31
src/common/types.ts
Normal 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;
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ export function randint(n: number) {
|
||||||
return Math.floor(Math.random() * n);
|
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)];
|
return arr[randint(arr.length)];
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="modes">
|
<div class="modes">
|
||||||
<ul class="modes-list">
|
<ul class="modes-list">
|
||||||
<li v-for="(mode, idx) in modes"
|
<li v-for="(mode, idx) in modes.modes"
|
||||||
:class="['mode', {
|
:class="['mode', {
|
||||||
disabled: images.length < mode.minImages,
|
disabled: images.length < mode.minImages,
|
||||||
excluded: excludedModes.includes(idx),
|
excluded: excludedModes.includes(idx),
|
||||||
|
@ -74,9 +74,10 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
|
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
|
||||||
import collageModes, {CollageModeType} from "../collages";
|
import {CollageModeType} from "../common/collages";
|
||||||
import {CollageConfig, CollageMode, Segment} from "@/types";
|
import {CollageConfig, CollageMode, Segment} from "../common/types";
|
||||||
import {choice, shuffle} from "@/utils";
|
import {choice, shuffle} from "../common/utils";
|
||||||
|
import BrowserCollageModes from "../collages";
|
||||||
|
|
||||||
type DisplayCollageModeType = CollageModeType | & "shuffle" | & "recursive";
|
type DisplayCollageModeType = CollageModeType | & "shuffle" | & "recursive";
|
||||||
|
|
||||||
|
@ -98,19 +99,19 @@ export default class Collage extends Vue {
|
||||||
private currentModeType: DisplayCollageModeType = "shuffle";
|
private currentModeType: DisplayCollageModeType = "shuffle";
|
||||||
private lastActiveModeTypes: CollageModeType[] = [];
|
private lastActiveModeTypes: CollageModeType[] = [];
|
||||||
private excludedModes: CollageModeType[] = [];
|
private excludedModes: CollageModeType[] = [];
|
||||||
private modes = collageModes;
|
private modes = new BrowserCollageModes();
|
||||||
|
|
||||||
private get minImages() {
|
private get minImages() {
|
||||||
if (this.currentModeType === "shuffle" || this.currentModeType === "recursive") {
|
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 {
|
} else {
|
||||||
return this.modes[this.currentModeType].minImages;
|
return this.modes.modes[this.currentModeType].minImages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private get lastMode() {
|
private get lastMode() {
|
||||||
if (this.lastActiveModeTypes.length === 1) {
|
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) {
|
if (this.images.length >= this.minImages) {
|
||||||
this.reset();
|
this.reset();
|
||||||
|
|
||||||
const permissibleModeKeys = (Object.keys(collageModes) as CollageModeType[])
|
const permissibleModeKeys = (Object.keys(this.modes.modes) as CollageModeType[])
|
||||||
.filter(k => !this.excludedModes.includes(k) && collageModes[k].minImages <= this.images.length);
|
.filter(k => !this.excludedModes.includes(k) && this.modes.modes[k].minImages <= this.images.length);
|
||||||
|
|
||||||
if (this.currentModeType !== "recursive") {
|
if (this.currentModeType !== "recursive") {
|
||||||
let mode: CollageMode;
|
let mode: CollageMode<any, any>;
|
||||||
if (this.currentModeType === "shuffle") {
|
if (this.currentModeType === "shuffle") {
|
||||||
const randomModeType = choice(permissibleModeKeys);
|
const randomModeType = choice(permissibleModeKeys);
|
||||||
this.lastActiveModeTypes = [randomModeType];
|
this.lastActiveModeTypes = [randomModeType];
|
||||||
mode = collageModes[randomModeType];
|
mode = this.modes.modes[randomModeType];
|
||||||
} else {
|
} else {
|
||||||
this.lastActiveModeTypes = [this.currentModeType];
|
this.lastActiveModeTypes = [this.currentModeType];
|
||||||
mode = this.modes[this.currentModeType];
|
mode = this.modes.modes[this.currentModeType];
|
||||||
}
|
}
|
||||||
const shuffledImages = shuffle(this.images);
|
const shuffledImages = shuffle(this.images);
|
||||||
const segments = mode.getSegments(this.context, this.collageConfig, shuffledImages);
|
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 shuffledImages = shuffle(this.images);
|
||||||
const rootSegment: Segment = {x: 0, y: 0, w: this.context.canvas.width, h: this.context.canvas.height};
|
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> => {
|
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) {
|
if (segment === rootSegment || level <= this.recursiveConfig.level - 1) {
|
||||||
let canvas = document.createElement("canvas");
|
let canvas = document.createElement("canvas");
|
||||||
canvas.width = segment.w;
|
canvas.width = segment.w;
|
||||||
canvas.height = segment.h;
|
canvas.height = segment.h;
|
||||||
let modeKey = choice(permissibleModeKeys);
|
let modeKey = choice(permissibleModeKeys);
|
||||||
console.log(modeKey);
|
console.debug(modeKey);
|
||||||
this.lastActiveModeTypes.push(modeKey);
|
this.lastActiveModeTypes.push(modeKey);
|
||||||
let mode = this.modes[modeKey];
|
let mode = this.modes.modes[modeKey];
|
||||||
let ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
let ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||||
let segments = mode.getSegments(ctx);
|
let segments = mode.getSegments(ctx);
|
||||||
console.log(segments);
|
console.debug(segments);
|
||||||
let bitmaps = await Promise.all(segments.map((segment) => processSegment(segment, level + 1)));
|
let bitmaps = await Promise.all(segments.map((segment) => processSegment(segment, level + 1)));
|
||||||
mode.place(ctx, bitmaps, segments);
|
mode.place(ctx, bitmaps, segments);
|
||||||
return await createImageBitmap(canvas);
|
return await createImageBitmap(canvas);
|
||||||
|
@ -180,7 +181,7 @@ export default class Collage extends Vue {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
processSegment(rootSegment, 0).then((finalCollage) => {
|
processSegment(rootSegment, 0).then((finalCollage) => {
|
||||||
console.log(finalCollage);
|
console.debug(finalCollage);
|
||||||
this.context.drawImage(finalCollage, 0, 0);
|
this.context.drawImage(finalCollage, 0, 0);
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
alert(error);
|
alert(error);
|
||||||
|
|
18
src/types.d.ts
vendored
18
src/types.d.ts
vendored
|
@ -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;
|
|
||||||
}
|
|
Loading…
Reference in a new issue