Introduced Custom Card designer
Based on vuejs advanced cropper
This commit is contained in:
30
package-lock.json
generated
30
package-lock.json
generated
@@ -3134,6 +3134,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"classnames": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
|
||||
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
|
||||
},
|
||||
"clean-css": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
||||
@@ -4091,6 +4096,11 @@
|
||||
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
|
||||
"dev": true
|
||||
},
|
||||
"debounce": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
|
||||
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
@@ -4566,6 +4576,11 @@
|
||||
"stream-shift": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"easy-bem": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/easy-bem/-/easy-bem-1.1.1.tgz",
|
||||
"integrity": "sha512-GJRqdiy2h+EXy6a8E6R+ubmqUM08BK0FWNq41k24fup6045biQ8NXxoXimiwegMQvFFV3t1emADdGNL1TlS61A=="
|
||||
},
|
||||
"easy-stack": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz",
|
||||
@@ -5361,6 +5376,11 @@
|
||||
"schema-utils": "^2.5.0"
|
||||
}
|
||||
},
|
||||
"file-saver": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||
},
|
||||
"filesize": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
|
||||
@@ -11857,6 +11877,16 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
|
||||
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
|
||||
},
|
||||
"vue-advanced-cropper": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-advanced-cropper/-/vue-advanced-cropper-1.10.1.tgz",
|
||||
"integrity": "sha512-obEcM5DvDE4VfX32p21X3Kk5gGo9nbEttm+SEgk4X380SSS5JNWkzjSfWLBYQnZ+9pI2BKsuISbymeH1P68dbg==",
|
||||
"requires": {
|
||||
"classnames": "^2.2.6",
|
||||
"debounce": "^1.2.0",
|
||||
"easy-bem": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"vue-cli-plugin-vuetify": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.4.2.tgz",
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"file-saver": "^2.0.5",
|
||||
"vue": "^2.6.11",
|
||||
"vue-advanced-cropper": "^1.10.1",
|
||||
"vuetify": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
22
src/App.vue
22
src/App.vue
@@ -33,9 +33,24 @@
|
||||
</v-app-bar>
|
||||
|
||||
<v-main>
|
||||
<Designer/>
|
||||
<v-tabs centered>
|
||||
<v-tabs-slider color="yellow"></v-tabs-slider>
|
||||
|
||||
<v-tab>
|
||||
Profile Designer
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
Custom Card Designer
|
||||
</v-tab>
|
||||
<v-tab-item>
|
||||
<Designer/>
|
||||
</v-tab-item>
|
||||
<v-tab-item>
|
||||
<CardDesigner/>
|
||||
</v-tab-item>
|
||||
</v-tabs>
|
||||
</v-main>
|
||||
<v-footer class="footer"
|
||||
<v-footer fixed class="footer"
|
||||
color="secondary"
|
||||
|
||||
elevation="5"
|
||||
@@ -47,12 +62,13 @@
|
||||
|
||||
<script>
|
||||
import Designer from './components/Designer';
|
||||
import CardDesigner from './components/CardDesigner';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
|
||||
components: {
|
||||
Designer,
|
||||
Designer, CardDesigner
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
|
||||
BIN
src/assets/cards/akashi_placeholder.jpg
Normal file
BIN
src/assets/cards/akashi_placeholder.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 271 KiB |
BIN
src/assets/cards/overlay.png
Normal file
BIN
src/assets/cards/overlay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
184
src/components/CardDesigner.vue
Normal file
184
src/components/CardDesigner.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div>
|
||||
<Intro header="Card Designer" body="This Tool is currently being build..." />
|
||||
<v-row justify="center" class="pa-10">
|
||||
<div class="button-wrapper">
|
||||
<v-btn class="button" @click="$refs.file.click()">
|
||||
<input v-show="false" type="file" ref="file" @change="loadImage($event)" accept="image/*">
|
||||
Load image
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-row>
|
||||
<v-row justify="center" class="pa-10">
|
||||
<cropper ref="cropper"
|
||||
class="cropper"
|
||||
@change="onChange"
|
||||
:debounce="false"
|
||||
:src="image.src"
|
||||
:stencil-props="{
|
||||
handlers: {},
|
||||
movable: false,
|
||||
scalable: false,
|
||||
aspectRatio: 300/500,
|
||||
}"
|
||||
:resize-image="{
|
||||
adjustStencil: false
|
||||
}"
|
||||
image-restriction="stencil"
|
||||
/>
|
||||
<div class="card-container">
|
||||
<img id="frame" src="../assets/cards/overlay.png" alt="">
|
||||
<Preview id="card"
|
||||
:image="result.image"
|
||||
:coordinates="result.coordinates"
|
||||
|
||||
/>
|
||||
<div id="card" :style="{ backgroundImage : 'url(' + result.image + ')' }"></div>
|
||||
</div>
|
||||
<v-btn class="button ma-10" @click="crop()">
|
||||
Save Image
|
||||
</v-btn>
|
||||
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Cropper, Preview } from 'vue-advanced-cropper';
|
||||
import 'vue-advanced-cropper/dist/style.css';
|
||||
import { saveAs } from 'file-saver';
|
||||
import Intro from './Intro.vue';
|
||||
|
||||
// This function is used to detect the actual image type,
|
||||
function getMimeType(file, fallback = null) {
|
||||
const byteArray = (new Uint8Array(file)).subarray(0, 4);
|
||||
let header = '';
|
||||
for (let i = 0; i < byteArray.length; i++) {
|
||||
header += byteArray[i].toString(16);
|
||||
}
|
||||
switch (header) {
|
||||
case "89504e47":
|
||||
return "image/png";
|
||||
case "47494638":
|
||||
return "image/gif";
|
||||
case "ffd8ffe0":
|
||||
case "ffd8ffe1":
|
||||
case "ffd8ffe2":
|
||||
case "ffd8ffe3":
|
||||
case "ffd8ffe8":
|
||||
return "image/jpeg";
|
||||
default:
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Cropper, Preview, Intro
|
||||
},
|
||||
data: () => ({
|
||||
image: {
|
||||
src: null,
|
||||
type: null
|
||||
},
|
||||
result: {
|
||||
coordinates: null,
|
||||
image: null,
|
||||
dataUrl: null
|
||||
}
|
||||
}),
|
||||
methods: {
|
||||
crop() {
|
||||
const { canvas } = this.$refs.cropper.getResult();
|
||||
canvas.toBlob((blob) => {
|
||||
saveAs(blob);
|
||||
}, this.image.type);
|
||||
},
|
||||
onChange({ coordinates, image }) {
|
||||
this.result = {
|
||||
coordinates,
|
||||
image
|
||||
};
|
||||
},
|
||||
reset() {
|
||||
this.image = {
|
||||
src: null,
|
||||
type: null
|
||||
}
|
||||
},
|
||||
loadImage(event) {
|
||||
// Reference to the DOM input element
|
||||
const { files } = event.target;
|
||||
// Ensure that you have a file before attempting to read it
|
||||
if (files && files[0]) {
|
||||
// 1. Revoke the object URL, to allow the garbage collector to destroy the uploaded before file
|
||||
if (this.image.src) {
|
||||
URL.revokeObjectURL(this.image.src)
|
||||
}
|
||||
// 2. Create the blob link to the file to optimize performance:
|
||||
const blob = URL.createObjectURL(files[0]);
|
||||
|
||||
// 3. The steps below are designated to determine a file mime type to use it during the
|
||||
// getting of a cropped image from the canvas. You can replace it them by the following string,
|
||||
// but the type will be derived from the extension and it can lead to an incorrect result:
|
||||
//
|
||||
// this.image = {
|
||||
// src: blob;
|
||||
// type: files[0].type
|
||||
// }
|
||||
|
||||
// Create a new FileReader to read this image binary data
|
||||
const reader = new FileReader();
|
||||
// Define a callback function to run, when FileReader finishes its job
|
||||
reader.onload = (e) => {
|
||||
// Note: arrow function used here, so that "this.image" refers to the image of Vue component
|
||||
this.image = {
|
||||
// Set the image source (it will look like blob:http://example.com/2c5270a5-18b5-406e-a4fb-07427f5e7b94)
|
||||
src: blob,
|
||||
// Determine the image type to preserve it during the extracting the image from canvas:
|
||||
type: getMimeType(e.target.result, files[0].type),
|
||||
};
|
||||
};
|
||||
// Start the reader job - read file as a data url (base64 format)
|
||||
reader.readAsArrayBuffer(files[0]);
|
||||
}
|
||||
},
|
||||
},
|
||||
destroyed() {
|
||||
// Revoke the object URL, to allow the garbage collector to destroy the uploaded before file
|
||||
if (this.image.src) {
|
||||
URL.revokeObjectURL(this.image.src)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
//FIXME: Outsource styles
|
||||
.cropper {
|
||||
min-height: 500px;
|
||||
max-height: 600px;
|
||||
max-width: 400px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
height: 500px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
#frame {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
max-height: 100%;
|
||||
}
|
||||
#card {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-size: contain;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user