398 lines
11 KiB
TypeScript
398 lines
11 KiB
TypeScript
import {
|
|
CollageCanvas,
|
|
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 function isCollageModeType(value: string): value is CollageModeType {
|
|
return (collageModeType as readonly string[]).includes(value);
|
|
}
|
|
|
|
export const displayCollageModeType = [
|
|
...collageModeType,
|
|
"recursive",
|
|
] as const;
|
|
export type DisplayCollageModeType = typeof displayCollageModeType[number];
|
|
export function isDisplayCollageModeType(
|
|
value: string
|
|
): value is DisplayCollageModeType {
|
|
return (displayCollageModeType as readonly string[]).includes(value);
|
|
}
|
|
|
|
export interface RecursiveCollageConfig {
|
|
level: number;
|
|
repeat: boolean;
|
|
modes: readonly CollageModeType[];
|
|
}
|
|
|
|
export abstract class CollageModes<
|
|
C extends CollageContext,
|
|
I extends CollageImage,
|
|
CS extends CollageCanvas
|
|
> {
|
|
readonly modes: { [key in CollageModeType]: CollageMode<C, I> } = {
|
|
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] * 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] * 0.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() > 0.5) {
|
|
factor = choice([1 / Math.sqrt(2), 0.5, 0.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);
|
|
},
|
|
},
|
|
};
|
|
|
|
public async recursiveDraw(
|
|
context: C,
|
|
images: I[],
|
|
recursiveConfig: RecursiveCollageConfig
|
|
) {
|
|
const localImages = images.concat();
|
|
const rootSegment: Segment = {
|
|
x: 0,
|
|
y: 0,
|
|
w: context.canvas.width,
|
|
h: context.canvas.height,
|
|
};
|
|
const processSegment = async (
|
|
segment: Segment,
|
|
level: number
|
|
): Promise<I> => {
|
|
// console.debug(segment, level);
|
|
if (segment === rootSegment || level <= recursiveConfig.level - 1) {
|
|
const canvas = this.createCanvas(segment.w, segment.h);
|
|
const modeKey = choice(recursiveConfig.modes);
|
|
// console.debug(modeKey);
|
|
const mode = this.modes[modeKey];
|
|
const ctx = canvas.getContext("2d") as C;
|
|
const segments = mode.getSegments(ctx);
|
|
// console.debug(segments);
|
|
const bitmaps = await Promise.all(
|
|
segments.map((segment: Segment) => processSegment(segment, level + 1))
|
|
);
|
|
mode.place(ctx, bitmaps, segments);
|
|
return await this.canvasToImage(canvas);
|
|
} else {
|
|
if (recursiveConfig.repeat) {
|
|
return choice(localImages);
|
|
} else {
|
|
if (localImages.length > 0) {
|
|
return localImages.pop() as I;
|
|
} else {
|
|
throw "RAN OUT OF IMAGES";
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = await processSegment(rootSegment, 0);
|
|
this.drawImage(
|
|
context,
|
|
result,
|
|
0,
|
|
0,
|
|
result.width,
|
|
result.height,
|
|
0,
|
|
0,
|
|
context.canvas.width,
|
|
context.canvas.height
|
|
);
|
|
}
|
|
|
|
private cleanDraw(
|
|
ctx: C,
|
|
image: I,
|
|
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 * 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,
|
|
};
|
|
});
|
|
}
|
|
|
|
abstract createCanvas(w: number, h: number): CS;
|
|
|
|
abstract canvasToImage(canvas: CS): PromiseLike<I>;
|
|
|
|
abstract drawImage(
|
|
ctx: C,
|
|
image: I,
|
|
sx: number,
|
|
sy: number,
|
|
sw: number,
|
|
sh: number,
|
|
dx: number,
|
|
dy: number,
|
|
dw: number,
|
|
dh: number
|
|
): void;
|
|
}
|