initial commit
This commit is contained in:
parent
cdaf3f9c50
commit
9850decf24
7 changed files with 463 additions and 74 deletions
32
src/App.vue
32
src/App.vue
|
@ -1,29 +1,37 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<img alt="Vue logo" src="./assets/logo.png">
|
<picker @images="images = $event"/>
|
||||||
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
|
<collage :images="images"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
import {Component, Vue} from "vue-property-decorator";
|
||||||
import HelloWorld from './components/HelloWorld.vue';
|
import Picker from "@/components/Picker.vue";
|
||||||
|
import Collage from "@/components/Collage.vue";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
HelloWorld,
|
Picker,
|
||||||
|
Collage
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class App extends Vue {}
|
export default class App extends Vue {
|
||||||
|
private images: ImageBitmap[] = [];
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0 2em;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
font-family: monospace;
|
||||||
-webkit-font-smoothing: antialiased;
|
display: flex;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
flex-direction: column;
|
||||||
text-align: center;
|
justify-content: space-evenly;
|
||||||
color: #2c3e50;
|
min-height: 100vh;
|
||||||
margin-top: 60px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
129
src/collages.ts
Normal file
129
src/collages.ts
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import {CollageMode} from "@/types";
|
||||||
|
import {randint, shuffle} from "@/utils";
|
||||||
|
|
||||||
|
const collageModeType = ["grid", "row", "irow", "col", "icol"] 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modes: { [key in CollageModeType]: CollageMode } = {
|
||||||
|
"grid": {
|
||||||
|
name: "Regular Grid",
|
||||||
|
minImages: 4,
|
||||||
|
forceConfig: {
|
||||||
|
numImages: 4
|
||||||
|
},
|
||||||
|
place: (ctx, images, config) => {
|
||||||
|
const quadrantSize = [ctx.canvas.width / 2, ctx.canvas.height / 2];
|
||||||
|
const selectedImages = shuffle(images).slice(0, 4);
|
||||||
|
shuffle(selectedImages.map((image, idx) => [image, idx] as [ImageBitmap, number]))
|
||||||
|
.forEach(([image, 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;
|
||||||
|
}
|
||||||
|
if (config.cleanCrops) {
|
||||||
|
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
|
||||||
|
} else {
|
||||||
|
const scaleRatio = Math.max(quadrantSize[0] / image.width, quadrantSize[1] / image.height);
|
||||||
|
ctx.drawImage(image,
|
||||||
|
x - (image.width * scaleRatio / 2), y - (image.height * scaleRatio / 2),
|
||||||
|
image.width * scaleRatio, image.height * scaleRatio);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"row": {
|
||||||
|
name: "Regular Row",
|
||||||
|
minImages: 2,
|
||||||
|
forceConfig: {
|
||||||
|
cleanCrops: true
|
||||||
|
},
|
||||||
|
place: (ctx, images, config) => {
|
||||||
|
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
||||||
|
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height];
|
||||||
|
selectedImages.forEach((image, idx) => {
|
||||||
|
const x = idx * quadrantSize[0] + quadrantSize[0] / 2;
|
||||||
|
const y = quadrantSize[1] / 2;
|
||||||
|
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"irow": {
|
||||||
|
name: "Irregular Row",
|
||||||
|
minImages: 2,
|
||||||
|
forceConfig: {
|
||||||
|
cleanCrops: true
|
||||||
|
},
|
||||||
|
place: (ctx, images, config) => {
|
||||||
|
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
||||||
|
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height];
|
||||||
|
selectedImages.forEach((image, idx) => {
|
||||||
|
const x = idx * quadrantSize[0] + quadrantSize[0] / 2;
|
||||||
|
const y = quadrantSize[1] / 2;
|
||||||
|
const w = Math.min(ctx.canvas.width / 2,
|
||||||
|
quadrantSize[1] + Math.random() * (quadrantSize[1] - (quadrantSize[1] / image.height) * image.width));
|
||||||
|
cleanDraw(ctx, image, x, y, w, quadrantSize[1]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"col": {
|
||||||
|
name: "Regular Column",
|
||||||
|
minImages: 2,
|
||||||
|
forceConfig: {
|
||||||
|
cleanCrops: true
|
||||||
|
},
|
||||||
|
place: (ctx, images, config) => {
|
||||||
|
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
||||||
|
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length];
|
||||||
|
selectedImages.forEach((image, idx) => {
|
||||||
|
const x = quadrantSize[0] / 2;
|
||||||
|
const y = idx * quadrantSize[1] + quadrantSize[1] / 2;
|
||||||
|
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"icol": {
|
||||||
|
name: "Irregular Column",
|
||||||
|
minImages: 2,
|
||||||
|
forceConfig: {
|
||||||
|
cleanCrops: true
|
||||||
|
},
|
||||||
|
place: (ctx, images, config) => {
|
||||||
|
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
||||||
|
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length];
|
||||||
|
selectedImages.forEach((image, idx) => {
|
||||||
|
const x = quadrantSize[0] / 2;
|
||||||
|
const y = idx * quadrantSize[1] + quadrantSize[1] / 2;
|
||||||
|
const h = Math.min(ctx.canvas.height / 2,
|
||||||
|
quadrantSize[0] + Math.random() * (quadrantSize[0] - (quadrantSize[0] / image.width) * image.height));
|
||||||
|
cleanDraw(ctx, image, x, y, quadrantSize[0], h);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default modes;
|
190
src/components/Collage.vue
Normal file
190
src/components/Collage.vue
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
<template>
|
||||||
|
<div class="collage">
|
||||||
|
<div class="canvas">
|
||||||
|
<canvas id="canvas" ref="canvas" :width="canvas.width" :height="canvas.height"></canvas>
|
||||||
|
<div class="canvas-size">
|
||||||
|
<label>
|
||||||
|
Width:
|
||||||
|
<input type="number" step="16" min="128" v-model="canvas.width">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Height:
|
||||||
|
<input type="number" step="16" min="128" v-model="canvas.height">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="modes">
|
||||||
|
<label v-for="(mode, idx) in modes"
|
||||||
|
:class="{disabled: images.length < mode.minImages,
|
||||||
|
selected: idx === currentModeType,
|
||||||
|
lastActive: idx === lastActiveModeType}">
|
||||||
|
{{mode.name}} ({{mode.minImages}})
|
||||||
|
<input type="radio" :value="idx" v-model="currentModeType">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button :disabled="images.length < currentMode.minImages" @click="renderCollage">REPAINT</button>
|
||||||
|
<hr>
|
||||||
|
<div class="config">
|
||||||
|
<label class="config-numimages">
|
||||||
|
#N of images:
|
||||||
|
<input type="number" :min="currentMode.minImages" :max="images.length"
|
||||||
|
placeholder="RND"
|
||||||
|
:disabled="Object.keys(forceConfig).includes('numImages')"
|
||||||
|
v-model="forceConfig.numImages || collageConfig.numImages">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox"
|
||||||
|
:disabled="Object.keys(forceConfig).includes('cleanCrops')"
|
||||||
|
v-model="forceConfig.cleanCrops || collageConfig.cleanCrops">
|
||||||
|
Crop cleanly
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
|
||||||
|
import collageModes, {CollageModeType} from "../collages";
|
||||||
|
import {CollageConfig, CollageMode} from "@/types";
|
||||||
|
|
||||||
|
type DisplayCollageModeType = CollageModeType | & "shuffle";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Collage extends Vue {
|
||||||
|
@Prop({required: true}) private images!: ImageBitmap[];
|
||||||
|
private context!: CanvasRenderingContext2D;
|
||||||
|
private canvas = {
|
||||||
|
width: 640,
|
||||||
|
height: 640
|
||||||
|
};
|
||||||
|
private collageConfig: CollageConfig = {
|
||||||
|
numImages: undefined,
|
||||||
|
cleanCrops: false
|
||||||
|
};
|
||||||
|
private currentModeType: DisplayCollageModeType = "shuffle";
|
||||||
|
private lastActiveModeType: DisplayCollageModeType | null = null;
|
||||||
|
private modes: { [key in DisplayCollageModeType]: CollageMode } = {
|
||||||
|
...collageModes,
|
||||||
|
"shuffle": {
|
||||||
|
name: "Shuffle all!",
|
||||||
|
minImages: Math.min(...Object.values(collageModes).map(m => m.minImages)),
|
||||||
|
place: (ctx, images, config) => {
|
||||||
|
const permissibleModeKeys = Object.keys(collageModes)
|
||||||
|
.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() {
|
||||||
|
return this.lastActiveModeType ? this.modes[this.lastActiveModeType] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get forceConfig() {
|
||||||
|
return this.lastMode ? this.lastMode.forceConfig || {} : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private mounted() {
|
||||||
|
const canvas = (this.$refs.canvas as HTMLCanvasElement);
|
||||||
|
this.context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch("images")
|
||||||
|
@Watch("currentMode")
|
||||||
|
@Watch("collageConfig", {deep: true})
|
||||||
|
private renderCollage() {
|
||||||
|
if (this.images.length >= this.currentMode.minImages) {
|
||||||
|
this.lastActiveModeType = this.currentModeType;
|
||||||
|
this.currentMode.place(this.context, this.images, this.collageConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
|
<style scoped>
|
||||||
|
.collage {
|
||||||
|
margin: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canvas {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-size {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls label {
|
||||||
|
font-size: 14pt;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modes input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button, .controls hr, .controls .config {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls .config {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-numimages input {
|
||||||
|
width: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
color: gray;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lastActive {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,58 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="hello">
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
<p>
|
|
||||||
For a guide and recipes on how to configure / customize this project,<br>
|
|
||||||
check out the
|
|
||||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
|
||||||
</p>
|
|
||||||
<h3>Installed CLI Plugins</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Essential Links</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
|
||||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
|
||||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
|
||||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
|
||||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Ecosystem</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
|
||||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
|
||||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class HelloWorld extends Vue {
|
|
||||||
@Prop() private msg!: string;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped>
|
|
||||||
h3 {
|
|
||||||
margin: 40px 0 0;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
90
src/components/Picker.vue
Normal file
90
src/components/Picker.vue
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<template>
|
||||||
|
<div class="picker">
|
||||||
|
<div class="images">
|
||||||
|
<!--suppress HtmlUnknownTarget -->
|
||||||
|
<img v-for="file in files" :alt="file.name" :src="dataURLs[file.name]" @click="confirmDelete(file)"/>
|
||||||
|
</div>
|
||||||
|
<input class="browse" type="file" accept="image/*" multiple @change="loadImages($event)">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Vue, Watch} from "vue-property-decorator";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Picker extends Vue {
|
||||||
|
private files: File[] = [];
|
||||||
|
private dataURLs: { [key: string]: string } = {};
|
||||||
|
private bitmaps: { [key: string]: ImageBitmap } = {};
|
||||||
|
|
||||||
|
private loadImages(event: Event) {
|
||||||
|
Object.values(this.dataURLs).forEach((url) => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
this.dataURLs = {};
|
||||||
|
|
||||||
|
this.files = Array.from((event.target as HTMLInputElement).files || []);
|
||||||
|
this.files.forEach((file: File) => {
|
||||||
|
this.dataURLs[file.name] = URL.createObjectURL(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
const bitmapPromises: Promise<[string, ImageBitmap]>[] =
|
||||||
|
Object.entries(this.dataURLs).map(([filename, url]) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const image = new Image();
|
||||||
|
image.onload = () => {
|
||||||
|
createImageBitmap(image).then((bitmap) => {
|
||||||
|
resolve([filename, bitmap]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
image.src = url;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(bitmapPromises).then((bitmaps) => {
|
||||||
|
bitmaps.forEach(([filename, bitmap]) => {
|
||||||
|
this.bitmaps[filename] = bitmap;
|
||||||
|
});
|
||||||
|
this.$emit("images", Object.values(this.bitmaps));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private confirmDelete(file: File) {
|
||||||
|
if (window.confirm(`Remove ${file.name}?`)) {
|
||||||
|
this.files.splice(this.files.indexOf(file), 1);
|
||||||
|
URL.revokeObjectURL(this.dataURLs[file.name]);
|
||||||
|
delete this.dataURLs[file.name];
|
||||||
|
delete this.bitmaps[file.name];
|
||||||
|
|
||||||
|
this.$emit("images", Object.values(this.bitmaps));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
|
<style scoped>
|
||||||
|
.picker {
|
||||||
|
border: 1px solid black;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.images {
|
||||||
|
min-height: 120px;
|
||||||
|
display: flex;
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.images img {
|
||||||
|
height: 120px;
|
||||||
|
margin: .5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browse {
|
||||||
|
min-width: 16em;
|
||||||
|
margin: 2em;
|
||||||
|
}
|
||||||
|
</style>
|
11
src/types.d.ts
vendored
Normal file
11
src/types.d.ts
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export interface CollageMode {
|
||||||
|
name: string;
|
||||||
|
minImages: number;
|
||||||
|
place: (ctx: CanvasRenderingContext2D, images: ImageBitmap[], config: CollageConfig) => void;
|
||||||
|
forceConfig?: CollageConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollageConfig {
|
||||||
|
numImages?: number;
|
||||||
|
cleanCrops?: boolean;
|
||||||
|
}
|
19
src/utils.ts
Normal file
19
src/utils.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Shuffles array in place.
|
||||||
|
* @param {Array} a items An array containing the items.
|
||||||
|
*/
|
||||||
|
export function shuffle<T>(a: T[]): T[] {
|
||||||
|
let j, x, i;
|
||||||
|
let b = Array.from(a);
|
||||||
|
for (i = b.length - 1; i > 0; i--) {
|
||||||
|
j = Math.floor(Math.random() * (i + 1));
|
||||||
|
x = b[i];
|
||||||
|
b[i] = b[j];
|
||||||
|
b[j] = x;
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randint(n: number) {
|
||||||
|
return Math.floor(Math.random() * n);
|
||||||
|
}
|
Loading…
Reference in a new issue