initial commit

This commit is contained in:
Tomáš Mládek 2020-07-15 22:00:19 +02:00
parent cdaf3f9c50
commit 9850decf24
7 changed files with 463 additions and 74 deletions

View file

@ -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
View 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
View 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>

View file

@ -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
View 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
View 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
View 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);
}