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",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
227
src/collages.ts
227
src/collages.ts
|
@ -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
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function choice<T>(arr: T[]): T {
|
||||
export function choice<T>(arr: readonly T[]): T {
|
||||
return arr[randint(arr.length)];
|
||||
}
|
||||
|
|
@ -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
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