major refactor (add segments) in preparation for recursive collages

This commit is contained in:
Tomáš Mládek 2020-07-17 16:11:40 +02:00
parent 3866522404
commit 7cf393af71
4 changed files with 189 additions and 135 deletions

View file

@ -1,5 +1,5 @@
import {CollageMode} from "@/types"; import {CollageConfig, CollageMode, Segment} from "@/types";
import {choice, randint, shuffle} from "@/utils"; import {choice, randint, range, shuffle} from "@/utils";
const collageModeType = [ const collageModeType = [
"clean_grid", "chaos_grid", "clean_grid", "chaos_grid",
@ -22,27 +22,40 @@ function cleanDraw(ctx: CanvasRenderingContext2D, image: ImageBitmap,
); );
} }
function getGridPoints(ctx: CanvasRenderingContext2D, idx: number) { function getGridSegments(ctx: CanvasRenderingContext2D, config?: CollageConfig) {
let x!: number, y!: number; return [0, 1, 2, 3].map((idx) => {
switch (idx) { let x!: number, y!: number;
case 0: switch (idx) {
x = ctx.canvas.width * .25; case 0:
y = ctx.canvas.height * .25; x = ctx.canvas.width * .25;
break; y = ctx.canvas.height * .25;
case 1: break;
x = ctx.canvas.width * .75; case 1:
y = ctx.canvas.height * .25; x = ctx.canvas.width * .75;
break; y = ctx.canvas.height * .25;
case 2: break;
x = ctx.canvas.width * .25; case 2:
y = ctx.canvas.height * .75; x = ctx.canvas.width * .25;
break; y = ctx.canvas.height * .75;
case 3: break;
x = ctx.canvas.width * .75; case 3:
y = ctx.canvas.height * .75; x = ctx.canvas.width * .75;
break; y = ctx.canvas.height * .75;
} break;
return [x, y]; }
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 } = { const modes: { [key in CollageModeType]: CollageMode } = {
@ -52,14 +65,8 @@ const modes: { [key in CollageModeType]: CollageMode } = {
forceConfig: { forceConfig: {
numImages: 4 numImages: 4
}, },
place: (ctx, images, config) => { getSegments: getGridSegments,
const quadrantSize = [ctx.canvas.width / 2, ctx.canvas.height / 2]; place: cleanPlace
const selectedImages = shuffle(images).slice(0, 4);
selectedImages.forEach((image, idx) => {
const [x, y] = getGridPoints(ctx, idx);
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
});
}
}, },
"chaos_grid": { "chaos_grid": {
name: "Irregular Grid", name: "Irregular Grid",
@ -67,125 +74,149 @@ const modes: { [key in CollageModeType]: CollageMode } = {
forceConfig: { forceConfig: {
numImages: 4 numImages: 4
}, },
place: (ctx, images, config) => { getSegments: getGridSegments,
const quadrantSize = [ctx.canvas.width / 2, ctx.canvas.height / 2]; place: (ctx, images, segments) => {
const selectedImages = shuffle(images).slice(0, 4); const shuffledImages = shuffle(images);
shuffle(selectedImages.map((image, idx) => [image, idx] as [ImageBitmap, number])) shuffle(segments.map((segment, idx) => [segment, idx] as [Segment, number]))
.forEach(([image, idx]) => { .forEach(([segment, idx]) => {
const [x, y] = getGridPoints(ctx, idx); const image = shuffledImages[idx];
const scaleRatio = Math.max(quadrantSize[0] / image.width, quadrantSize[1] / image.height); const scaleRatio = Math.max(segment.w / image.width, segment.h / image.height);
ctx.drawImage(image, ctx.drawImage(image,
x - (image.width * scaleRatio / 2), y - (image.height * scaleRatio / 2), segment.x - (image.width * scaleRatio / 2), segment.y - (image.height * scaleRatio / 2),
image.width * scaleRatio, image.height * scaleRatio); image.width * scaleRatio, image.height * scaleRatio);
}); });
} },
}, },
"row": { "row": {
name: "Regular Row", name: "Regular Row",
minImages: 2, minImages: 2,
place: (ctx, images, config) => { getSegments: (ctx, config, images) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2); const numImages = Math.min(images?.length || 0, config?.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height]; const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
selectedImages.forEach((image, idx) => { return range(numImages).map((idx) => {
const x = idx * quadrantSize[0] + quadrantSize[0] / 2; return {
const y = quadrantSize[1] / 2; x: idx * segmentSize[0] + segmentSize[0] / 2,
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]); y: segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1]
};
}); });
} },
place: cleanPlace
}, },
"irow": { "irow": {
name: "Irregular Row", name: "Irregular Row",
minImages: 2, minImages: 2,
place: (ctx, images, config) => { getSegments: (ctx, config, images) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2); const numImages = Math.min(images?.length || 0, config?.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height]; const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
selectedImages.forEach((image, idx) => { return range(numImages).map((idx) => {
const x = idx * quadrantSize[0] + quadrantSize[0] / 2; const irregularWidth = images ?
const y = quadrantSize[1] / 2; segmentSize[0] + Math.random() * ((segmentSize[1] / images[idx].height * images[idx].width) - segmentSize[0]) :
const w = Math.min(ctx.canvas.width / 2, segmentSize[0] + Math.random() * segmentSize[0] * .5;
quadrantSize[1] + Math.random() * (quadrantSize[1] - (quadrantSize[1] / image.height) * image.width)); return {
cleanDraw(ctx, image, x, y, w, quadrantSize[1]); 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": { "col": {
name: "Regular Column", name: "Regular Column",
minImages: 2, minImages: 2,
place: (ctx, images, config) => { getSegments: (ctx, config, images) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2); const numImages = Math.min(images?.length || 0, config?.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length]; const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
selectedImages.forEach((image, idx) => { return range(numImages).map((idx) => {
const x = quadrantSize[0] / 2; return {
const y = idx * quadrantSize[1] + quadrantSize[1] / 2; x: segmentSize[0] / 2,
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]); y: idx * segmentSize[1] + segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1]
};
}); });
} },
place: cleanPlace
}, },
"icol": { "icol": {
name: "Irregular Column", name: "Irregular Column",
minImages: 2, minImages: 2,
place: (ctx, images, config) => { getSegments: (ctx, config, images) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2); const numImages = Math.min(images?.length || 0, config?.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length]; const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
selectedImages.forEach((image, idx) => { return range(numImages).map((idx) => {
const x = quadrantSize[0] / 2; const irregularHeight = images ?
const y = idx * quadrantSize[1] + quadrantSize[1] / 2; segmentSize[1] + Math.random() * ((segmentSize[0] / images[idx].width * images[idx].height) - segmentSize[1]) :
const h = Math.min(ctx.canvas.height / 2, segmentSize[1] + Math.random() * segmentSize[1] * .5;
quadrantSize[0] + Math.random() * (quadrantSize[0] - (quadrantSize[0] / image.width) * image.height)); return {
cleanDraw(ctx, image, x, y, quadrantSize[0], h); 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": { "concentric_factor": {
name: "Constant factor concentric", name: "Constant factor concentric",
minImages: 2, minImages: 2,
place: (ctx, images, config) => { getSegments: (ctx, config, images) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2); const numImages = Math.min(images?.length || 0, config?.numImages || randint(4) + 2);
const x = ctx.canvas.width / 2;
const y = ctx.canvas.height / 2;
let factor: number; let factor: number;
if (Math.random() > .5) { if (Math.random() > .5) {
factor = choice([1 / Math.sqrt(2), .5, .88]); factor = choice([1 / Math.sqrt(2), .5, .88]);
} else { } else {
factor = 1 - (1 / selectedImages.length); factor = 1 - (1 / numImages);
} }
selectedImages.forEach((image, idx) => { return range(numImages).map((idx) => {
const ratio = Math.pow(factor, idx); const ratio = Math.pow(factor, idx);
cleanDraw(ctx, image, x, y, ctx.canvas.width * ratio, ctx.canvas.height * ratio); 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": { "concentric_spaced": {
name: "Equally spaced concentric", name: "Equally spaced concentric",
minImages: 2, minImages: 2,
place: (ctx, images, config) => { getSegments: (ctx, config, images) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2); const numImages = Math.min(images?.length || 0, config?.numImages || randint(2) + 2);
return range(numImages).map((idx) => {
selectedImages.forEach((image, idx) => { return {
cleanDraw( x: ctx.canvas.width / 2,
ctx, image, y: ctx.canvas.height / 2,
ctx.canvas.width / 2, w: ctx.canvas.width - (ctx.canvas.width / numImages * idx),
ctx.canvas.height / 2, h: ctx.canvas.height - (ctx.canvas.height / numImages * idx),
ctx.canvas.width - (ctx.canvas.width / selectedImages.length * idx), };
ctx.canvas.height - (ctx.canvas.height / selectedImages.length * idx),
);
}); });
} },
place: cleanPlace
}, },
"blend": { "blend": {
name: "Blending", name: "Blending",
minImages: 2, minImages: 2,
place: (ctx, images, config) => { getSegments: (ctx, config, images) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(2) + 2); const numImages = Math.min(images?.length || 0, config?.numImages || randint(2) + 2);
ctx.globalCompositeOperation = choice(["difference", "saturation", "soft-light", "overlay"]); return range(numImages).map((_) => {
selectedImages.forEach((image) => { return {
cleanDraw( x: ctx.canvas.width / 2,
ctx, image, y: ctx.canvas.height / 2,
ctx.canvas.width / 2, ctx.canvas.height / 2, w: ctx.canvas.width,
ctx.canvas.width, ctx.canvas.height h: ctx.canvas.height
); };
}); });
},
place: (ctx, images, segments) => {
ctx.globalCompositeOperation = choice(["difference", "saturation", "soft-light", "overlay"]);
cleanPlace(ctx, images, segments);
} }
} }
}; };

View file

@ -22,13 +22,19 @@
{{mode.name}} {{mode.name}}
<input type="radio" :value="idx" v-model="currentModeType"> <input type="radio" :value="idx" v-model="currentModeType">
</label> </label>
<hr>
<label :class="{disabled: images.length < minImages,
selected: 'shuffle' === currentModeType}">
!SHUFFLE ALL!
<input type="radio" value="shuffle" v-model="currentModeType">
</label>
</div> </div>
<button :disabled="images.length < currentMode.minImages" @click="renderCollage">REPAINT</button> <button :disabled="images.length < minImages" @click="renderCollage">REPAINT</button>
<hr> <hr>
<div class="config"> <div class="config">
<label class="config-numimages"> <label class="config-numimages">
#N of images: #N of images:
<input type="number" :min="currentMode.minImages" :max="images.length" <input type="number" :min="minImages" :max="images.length"
placeholder="RND" placeholder="RND"
:disabled="Object.keys(forceConfig).includes('numImages')" :disabled="Object.keys(forceConfig).includes('numImages')"
v-model="forceConfig.numImages || collageConfig.numImages"> v-model="forceConfig.numImages || collageConfig.numImages">
@ -42,6 +48,7 @@
import {Component, Prop, Vue, Watch} from "vue-property-decorator"; import {Component, Prop, Vue, Watch} from "vue-property-decorator";
import collageModes, {CollageModeType} from "../collages"; import collageModes, {CollageModeType} from "../collages";
import {CollageConfig, CollageMode} from "@/types"; import {CollageConfig, CollageMode} from "@/types";
import {choice, shuffle} from "@/utils";
type DisplayCollageModeType = CollageModeType | & "shuffle"; type DisplayCollageModeType = CollageModeType | & "shuffle";
@ -57,30 +64,15 @@ export default class Collage extends Vue {
numImages: undefined numImages: undefined
}; };
private currentModeType: DisplayCollageModeType = "shuffle"; private currentModeType: DisplayCollageModeType = "shuffle";
private lastActiveModeType: DisplayCollageModeType | null = null; private lastActiveModeType: CollageModeType | null = null;
private modes: { [key in DisplayCollageModeType]: CollageMode } = { private modes = collageModes;
...collageModes,
"shuffle": { private get minImages() {
name: "Shuffle all!", if (this.currentModeType === "shuffle") {
minImages: Math.min(...Object.values(collageModes).map(m => m.minImages)), return Math.min(...Object.values(this.modes).map((mode) => mode.minImages));
place: (ctx, images, config) => { } else {
const permissibleModeKeys = Object.keys(collageModes) return this.modes[this.currentModeType].minImages;
.filter(k => collageModes[k as CollageModeType].minImages <= images.length) as CollageModeType[];
const randomModeType = permissibleModeKeys[Math.floor(Math.random() * permissibleModeKeys.length)];
const randomMode = collageModes[randomModeType];
this.setLastActiveModeType(randomModeType);
randomMode.place(ctx, images, config);
}
} }
};
// wtf vue?
private setLastActiveModeType(lastActiveModeType: any) {
this.lastActiveModeType = lastActiveModeType;
}
private get currentMode() {
return this.modes[this.currentModeType];
} }
private get lastMode() { private get lastMode() {
@ -97,13 +89,26 @@ export default class Collage extends Vue {
} }
@Watch("images") @Watch("images")
@Watch("currentMode") @Watch("currentModeType")
@Watch("collageConfig", {deep: true}) @Watch("collageConfig", {deep: true})
private renderCollage() { private renderCollage() {
if (this.images.length >= this.currentMode.minImages) { if (this.images.length >= this.minImages) {
this.lastActiveModeType = this.currentModeType;
this.reset(); this.reset();
this.currentMode.place(this.context, this.images, this.collageConfig); this.lastActiveModeType = this.currentModeType === "shuffle" ? null : this.currentModeType;
let mode: CollageMode;
if (this.currentModeType === "shuffle") {
const permissibleModeKeys = Object.keys(collageModes)
.filter(k => collageModes[k as CollageModeType].minImages <= this.images.length) as CollageModeType[];
const randomModeType = choice(permissibleModeKeys);
this.lastActiveModeType = randomModeType;
mode = collageModes[randomModeType];
} else {
mode = this.modes[this.currentModeType];
}
const shuffledImages = shuffle(this.images);
const segments = mode.getSegments(this.context, this.collageConfig, shuffledImages);
mode.place(this.context, shuffledImages, segments);
} }
} }
@ -156,6 +161,12 @@ export default class Collage extends Vue {
align-items: center; align-items: center;
} }
.controls .modes hr {
margin-top: .5rem;
width: 100%;
color: lightgray;
}
.controls label { .controls label {
font-size: 14pt; font-size: 14pt;
cursor: pointer; cursor: pointer;

10
src/types.d.ts vendored
View file

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

View file

@ -21,3 +21,7 @@ export function randint(n: number) {
export function choice<T>(arr: T[]): T { export function choice<T>(arr: T[]): T {
return arr[randint(arr.length)]; return arr[randint(arr.length)];
} }
export function range(n: number): number[] {
return Array.from({length: n}, (x, i) => i);
}