using different component

master
Maksim 2019-01-09 16:25:01 -08:00
rodzic fd495faa69
commit 847365d9b8
13 zmienionych plików z 1604 dodań i 383 usunięć

Wyświetl plik

@ -64,6 +64,14 @@ module.exports = {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
},
{
test: /\.scss$/,
use: [
'vue-style-loader',
'css-loader',
'sass-loader'
]
}
]
},

Wyświetl plik

@ -4,6 +4,8 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>image-editor</title>
<link rel="stylesheet" href="https://unpkg.com/font-awesome@4.7.0/css/font-awesome.min.css">
</head>
<body>
<div id="app"></div>

942
package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -35,6 +35,8 @@
"build": "node build/build.js"
},
"dependencies": {
"cropperjs": "^1.4.3",
"normalize.css": "^8.0.1",
"pica": "^5.0.0",
"vue": "^2.5.2"
},
@ -56,6 +58,7 @@
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"node-notifier": "^5.1.2",
"node-sass": "^4.11.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.13",
@ -63,6 +66,7 @@
"postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
"sass-loader": "^7.1.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"uglifyjs-webpack-plugin": "^1.1.1",

Wyświetl plik

@ -1,27 +1,100 @@
<template>
<div id="app">
<ImageProvider/>
<header class="header">
<span class="title">Photo Editor</span>
<navbar :data="data" @change="change"></navbar>
</header>
<main class="main">
<editor v-if="data.loaded" ref="editor" :data="data"></editor>
<loader v-else ref="loader" :data="data"></loader>
</main>
</div>
</template>
<script>
import ImageProvider from './components/ImageProvider'
import Navbar from './components/Navbar'
import Loader from './components/Loader'
import Editor from './components/Editor'
export default {
name: 'App',
components: {
ImageProvider
}
navbar: Navbar ,
loader: Loader,
editor: Editor
},
data() {
return {
data: {
cropped: false,
cropping: false,
loaded: false,
name: '',
previousUrl: '',
type: '',
url: '',
},
};
},
methods: {
change(action) {
const { editor } = this.$refs;
switch (action) {
case 'crop':
editor.crop();
break;
case 'clear':
editor.clear();
break;
case 'restore':
editor.restore();
break;
case 'remove':
editor.reset();
break;
default:
}
},
},
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
<style scoped>
.app {
bottom: 0;
left: 0;
position: absolute;
top: 0;
right: 0;
}
.header {
background-color: #666;
height: 3rem;
overflow: hidden;
padding-left: 1rem;
padding-right: 1rem;
position: relative;
z-index: 1;
}
@media (min-width: 768px) {
.header {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
}
.title {
color: #fff;
display: block;
float: left;
font-size: 1.125rem;
line-height: 3rem;
}
.main {
background-color: #333;
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 3rem;
}
</style>

Wyświetl plik

@ -0,0 +1,355 @@
<template>
<div class="editor">
<div class="canvas" @dblclick="dblclick">
<img ref="image" :alt="data.name" :src="data.url" @load="start">
</div>
<div class="toolbar" v-if="cropper" @click="click">
<button class="toolbar__button" data-action="move" title="Move (M)"><span class="fa fa-arrows"></span></button>
<button class="toolbar__button" data-action="crop" title="Crop (C)"><span class="fa fa-crop"></span></button>
<button class="toolbar__button" data-action="zoom-in" title="Zoom In (I)"><span class="fa fa-search-plus"></span></button>
<button class="toolbar__button" data-action="zoom-out" title="Zoom Out (O)"><span class="fa fa-search-minus"></span></button>
<button class="toolbar__button" data-action="rotate-left" title="Rotate Left (L)"><span class="fa fa-rotate-left"></span></button>
<button class="toolbar__button" data-action="rotate-right" title="Rotate Right (R)"><span class="fa fa-rotate-right"></span></button>
<button class="toolbar__button" data-action="flip-horizontal" title="Flip Horizontal (H)"><span class="fa fa-arrows-h"></span></button>
<button class="toolbar__button" data-action="flip-vertical" title="Flip Vertical (V)"><span class="fa fa-arrows-v"></span></button>
</div>
</div>
</template>
<script>
import Cropper from 'cropperjs';
export default {
props: {
data: {
type: Object,
default: () => ({}),
},
},
data() {
return {
canvasData: null,
cropBoxData: null,
croppedData: null,
cropper: null,
};
},
methods: {
click({ target }) {
const { cropper } = this;
const action = target.getAttribute('data-action') || target.parentElement.getAttribute('data-action');
switch (action) {
case 'move':
case 'crop':
cropper.setDragMode(action);
break;
case 'zoom-in':
cropper.zoom(0.1);
break;
case 'zoom-out':
cropper.zoom(-0.1);
break;
case 'rotate-left':
cropper.rotate(-90);
break;
case 'rotate-right':
cropper.rotate(90);
break;
case 'flip-horizontal':
cropper.scaleX(-cropper.getData().scaleX || -1);
break;
case 'flip-vertical':
cropper.scaleY(-cropper.getData().scaleY || -1);
break;
default:
}
},
keydown(e) {
switch (e.key) {
// Undo crop
case 'z':
if (e.ctrlKey) {
e.preventDefault();
this.restore();
}
break;
// Delete the image
case 'Delete':
this.reset();
break;
default:
}
const { cropper } = this;
if (!cropper) {
return;
}
switch (e.key) {
// Crop the image
case 'Enter':
this.crop();
break;
// Clear crop area
case 'Escape':
this.clear();
break;
// Move to the left
case 'ArrowLeft':
e.preventDefault();
cropper.move(-1, 0);
break;
// Move to the top
case 'ArrowUp':
e.preventDefault();
cropper.move(0, -1);
break;
// Move to the right
case 'ArrowRight':
e.preventDefault();
cropper.move(1, 0);
break;
// Move to the bottom
case 'ArrowDown':
e.preventDefault();
cropper.move(0, 1);
break;
// Enter crop mode
case 'c':
cropper.setDragMode('crop');
break;
// Enter move mode
case 'm':
cropper.setDragMode('move');
break;
// Zoom in
case 'i':
cropper.zoom(0.1);
break;
// Zoom out
case 'o':
cropper.zoom(-0.1);
break;
// Rotate left
case 'l':
cropper.rotate(-90);
break;
// Rotate right
case 'r':
cropper.rotate(90);
break;
// Flip horizontal
case 'h':
cropper.scaleX(-cropper.getData().scaleX || -1);
break;
// Flip vertical
case 'v':
cropper.scaleY(-cropper.getData().scaleY || -1);
break;
default:
}
},
dblclick(e) {
if (e.target.className.indexOf('cropper-face') >= 0) {
e.preventDefault();
e.stopPropagation();
this.crop();
}
},
start() {
const { data } = this;
if (data.cropped) {
return;
}
this.cropper = new Cropper(this.$refs.image, {
autoCrop: false,
dragMode: 'move',
background: false,
aspectRatio: 1,
viewMode: 1,
movable: false,
ready: () => {
if (this.croppedData) {
this.cropper
.crop()
.setData(this.croppedData)
.setCanvasData(this.canvasData)
.setCropBoxData(this.cropBoxData);
this.croppedData = null;
this.canvasData = null;
this.cropBoxData = null;
}
this.cropper.setDragMode('crop');
},
crop: ({ detail }) => {
if (detail.width > 0 && detail.height > 0 && !data.cropping) {
this.update({
cropping: true,
});
}
},
});
},
stop() {
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
},
crop() {
const { cropper, data } = this;
if (data.cropping) {
this.croppedData = cropper.getData();
this.canvasData = cropper.getCanvasData();
this.cropBoxData = cropper.getCropBoxData();
this.update({
cropped: true,
cropping: false,
previousUrl: data.url,
url: cropper.getCroppedCanvas(data.type === 'image/png' ? {} : {
fillColor: '#fff',
}).toDataURL(data.type),
});
this.stop();
}
},
clear() {
if (this.data.cropping) {
this.cropper.clear();
this.update({
cropping: false,
});
}
},
restore() {
if (this.data.cropped) {
this.update({
cropped: false,
previousUrl: '',
url: this.data.previousUrl,
});
}
},
reset() {
this.stop();
this.update({
cropped: false,
cropping: false,
loaded: false,
name: '',
previousUrl: '',
type: '',
url: '',
});
},
update(data) {
Object.assign(this.data, data);
},
},
mounted() {
window.addEventListener('keydown', (this.onKeydown = this.keydown.bind(this)));
},
beforeDestroy() {
window.removeEventListener('keydown', this.onKeydown);
this.stop();
},
};
</script>
<style scoped lang="scss">
.editor {
height: 100%;
}
.canvas {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
& > img {
max-height: 100%;
max-width: 100%;
}
}
.toolbar {
background-color: rgba(0, 0, 0, .5);
bottom: 1rem;
color: #fff;
height: 2rem;
left: 50%;
margin-left: -8rem;
position: absolute;
width: 16rem;
z-index: 2015;
}
.toolbar__button {
background-color: transparent;
border-width: 0;
color: #fff;
cursor: pointer;
display: block;
float: left;
font-size: .875rem;
height: 2rem;
text-align: center;
width: 2rem;
&:focus {
outline: none;
}
&:hover {
background-color: #0074d9;
color: #fff;
}
}
</style>

Wyświetl plik

@ -1,113 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h2>Essential Links</h2>
<ul>
<li>
<a
href="https://vuejs.org"
target="_blank"
>
Core Docs
</a>
</li>
<li>
<a
href="https://forum.vuejs.org"
target="_blank"
>
Forum
</a>
</li>
<li>
<a
href="https://chat.vuejs.org"
target="_blank"
>
Community Chat
</a>
</li>
<li>
<a
href="https://twitter.com/vuejs"
target="_blank"
>
Twitter
</a>
</li>
<br>
<li>
<a
href="http://vuejs-templates.github.io/webpack/"
target="_blank"
>
Docs for This Template
</a>
</li>
</ul>
<h2>Ecosystem</h2>
<ul>
<li>
<a
href="http://router.vuejs.org/"
target="_blank"
>
vue-router
</a>
</li>
<li>
<a
href="http://vuex.vuejs.org/"
target="_blank"
>
vuex
</a>
</li>
<li>
<a
href="http://vue-loader.vuejs.org/"
target="_blank"
>
vue-loader
</a>
</li>
<li>
<a
href="https://github.com/vuejs/awesome-vue"
target="_blank"
>
awesome-vue
</a>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

Wyświetl plik

@ -1,213 +0,0 @@
<template>
<div>
<img v-bind:src="imagePreview" alt="">
<input type="file" @change="updatePreview">
<canvas ref="originalCanvas"></canvas>
<canvas ref="resizedCanvas" width="300" height="300"></canvas>
</div>
</template>
<script>
import Pica from 'pica';
import getOrientation from '../lib/imageUtils';
export default {
name: "FileUploader",
data: function(){
return {
offscreenImage: null,
uploadedFile: {
name: '',
size: 0,
type: '',
},
uploadedImage: {
width: 0,
height: 0,
data: [],
orientation: 0
},
imagePreview: '',
pica: undefined
}
},
created: function(){
//this.offscreenImage = new Image();
const img = new Image();
this.offscreenImage = img;
img.onload = () => {
console.log(img);
};
this.pica = Pica();
},
methods: {
orientImage(canvas, ctx, orientation) {
let width = canvas.width;
let height = canvas.height;
if (!orientation || orientation > 8) return;
if (orientation > 4) {
ctx.canvas.width = height;
ctx.canvas.height = width;
}
switch (orientation) {
case 2:
// Horizontal flip
ctx.translate(width, 0);
ctx.scale(-1, 1);
break;
case 3:
// rotate 180 degrees left
ctx.translate(width, height);
ctx.rotate(Math.PI);
break;
case 4:
// Vertical flip
ctx.translate(0, height);
ctx.scale(1, -1);
break;
case 5:
// Vertical flip + rotate right
ctx.rotate(0.5 * Math.PI);
ctx.scale(1, -1);
break;
case 6:
// Rotate right
ctx.rotate(0.5 * Math.PI);
ctx.translate(0, -height);
break;
case 7:
// Horizontal flip + rotate right
ctx.rotate(0.5 * Math.PI);
ctx.translate(width, -height);
ctx.scale(-1, 1);
break;
case 8:
// Rotate left
ctx.rotate(-0.5 * Math.PI);
ctx.translate(-width, 0);
break;
default:
}
},
openUpload() {
//
},
loadImage(file){
const ctx = this.$refs.originalCanvas.getContext('2d');
let orientation;
//image = new Image();
// this.offscreenImage.onerror = () => { N.wire.emit('notify', t('err_image_invalid')); };
this.offscreenImage.onload = () => {
this.$refs.originalCanvas.width = this.offscreenImage.width;
this.$refs.originalCanvas.height = this.offscreenImage.height;
this.orientImage(this.$refs.originalCanvas, ctx, this.uploadedImage.orientation);
let imageWidth = this.$refs.originalCanvas.width;
let imageHeight = this.$refs.originalCanvas.height;
///let canvasCropped = document.createElement('canvas');
//canvasCropped.width = 500;
//canvasCropped.height = 500;
//let ctxCropped = canvasCropped.getContext('2d');
//console.log(this.pica);
ctx.drawImage(this.offscreenImage, 0, 0, this.offscreenImage.width, this.offscreenImage.height);
this.pica.resize(this.$refs.originalCanvas, this.$refs.resizedCanvas)
.then(() => this.pica.toBlob(this.$refs.resizedCanvas, 'image/jpeg', 90))
.then(function (blob) {
console.log(blob);
})
.catch((e) => {
console.log(e);
//N.wire.emit('notify', t('err_image_invalid'));
throw 'CANCELED';
});
//this.resizeImage();
};
let reader = new FileReader();
reader.onloadend = e => {
this.uploadedImage.orientation = getOrientation(e.target.result);
this.offscreenImage.src = window.URL.createObjectURL(file);
};
reader.readAsArrayBuffer(file);
},
updatePreview(e) {
let files = e.target.files;
const allowedFiles = ['image/jpeg', 'image/png', 'image/jpg'];
//const pica = Pica();
if (files.length === 0) {
console.log('No files were selected');
}
if (!allowedFiles.includes(files[0].type)){
console.log(`${files[0].name} is not an image`);
return;
}
const file = files[0];
this.loadImage(file);
// let source = document.createElement('canvas');
// let dest = document.createElement('canvas');
//
// source.width = 1600;
// source.height = 1600;
//
// dest.width = 400;
// dest.height = 400;
// const reader = new FileReader();
//
// reader.onloadend = e => {
// //let fileData = new Uint8Array(reader.result);
// this.offscreenImage.src = window.URL.createObjectURL(file);
//
// }
//reader.onload = (e) => {
// this.imagePreview = this.imageData = e.target.result;
//source.getContext('2d').drawImage(img, cropX, 0, width, img.height);
//pica.resize(this.imagePreview, this.$refs.resizedPreview)
// .then(result => console.log('resize done!'));
//};
//reader.readAsDataURL(files[0]);
}
}
}
</script>
<style scoped>
</style>

Wyświetl plik

@ -0,0 +1,104 @@
<template>
<div class="loader" @change="change" @dragover="dragover" @drop="drop">
<p>Drop image here or
<label class="browse">browse...
<input class="sr-only" id="file" type="file" accept="image/*">
</label>
</p>
</div>
</template>
<script>
const URL = window.URL || window.webkitURL;
export default {
props: {
data: {
type: Object,
default: () => ({}),
},
},
methods: {
read(files) {
return new Promise((resolve, reject) => {
if (!files || files.length === 0) {
resolve();
return;
}
const file = files[0];
if (/^image\/\w+$/.test(file.type)) {
if (URL) {
resolve({
loaded: true,
name: file.name,
type: file.type,
url: URL.createObjectURL(file),
});
} else {
reject(new Error('Your browser is not supported.'));
}
} else {
reject(new Error('Please choose an image file.'));
}
});
},
change({ target }) {
this.read(target.files).then((data) => {
target.value = '';
this.update(data);
}).catch((e) => {
target.value = '';
this.alert(e);
});
},
dragover(e) {
e.preventDefault();
},
drop(e) {
e.preventDefault();
this.read(e.dataTransfer.files).catch(this.alert);
},
alert(e) {
window.alert(e && e.message ? e.message : e);
},
update(data) {
Object.assign(this.data, data);
},
},
};
</script>
<style scoped lang="scss">
.loader {
display: table;
height: 100%;
overflow: hidden;
width: 100%;
& > p {
color: #999;
display: table-cell;
text-align: center;
vertical-align: middle;
}
}
.browse {
color: #0074d9;
cursor: pointer;
margin-left: .25rem;
&:hover {
color: #08f;
text-decoration: underline;
}
}
</style>

Wyświetl plik

@ -0,0 +1,76 @@
<template>
<div class="navbar">
<nav class="nav" @click="click">
<label class="nav__button" for="file" title="Upload" role="button" v-if="!data.loaded"><span class="fa fa-upload"></span></label>
<button type="button" class="nav__button" data-action="restore" title="Undo (Ctrl + Z)" v-if="data.cropped"><span class="fa fa-undo"></span></button>
<button type="button" class="nav__button nav__button--danger" data-action="remove" title="Delete (Delete)" v-if="data.loaded && !data.cropping"><span class="fa fa-trash"></span></button>
<button type="button" class="nav__button nav__button--danger" data-action="clear" title="Cancel (Esc)" v-if="data.cropping"><span class="fa fa-ban"></span></button>
<button type="button" class="nav__button nav__button--success" data-action="crop" title="OK (Enter)" v-if="data.cropping"><span class="fa fa-check"></span></button>
<a class="nav__button nav__button--success" title="Download" :download="data.name" :href="data.url" v-if="downloadable && data.loaded"><span class="fa fa-download"></span></a>
<a class="nav__button" href="https://github.com/fengyuanchen/photo-editor" title="View on GitHub"><span class="fa fa-github"></span></a>
</nav>
</div>
</template>
<script>
export default {
data() {
return {
downloadable: typeof document.createElement('a').download !== 'undefined',
};
},
props: {
data: {
type: Object,
default: () => ({}),
},
},
methods: {
click({ target }) {
const action = target.getAttribute('data-action') || target.parentElement.getAttribute('data-action');
if (action) {
this.$emit('change', action);
}
},
},
};
</script>
<style scoped lang="scss">
.navbar {
float: right;
}
.nav__button {
background-color: transparent;
border-width: 0;
color: #fff;
cursor: pointer;
display: block;
float: left;
height: 3rem;
line-height: 3rem;
text-align: center;
width: 3rem;
&:focus {
outline: none;
}
&:hover {
background-color: #0074d9;
color: #fff;
}
}
.nav--success:hover {
background-color: #2ecc40;
}
.nav--danger:hover {
background-color: #ff4136;
}
</style>

Wyświetl plik

@ -1,42 +0,0 @@
export default function getOrientation(data) {
let view = new DataView(data);
if (view.getUint16(0, false) !== 0xFFD8)
{
return -2;
}
let length = view.byteLength, offset = 2;
while (offset < length)
{
if (view.getUint16(offset+2, false) <= 8) return -1;
let marker = view.getUint16(offset, false);
offset += 2;
if (marker === 0xFFE1)
{
if (view.getUint32(offset += 2, false) !== 0x45786966)
{
return -1;
}
let little = view.getUint16(offset += 6, false) === 0x4949;
offset += view.getUint32(offset + 4, little);
let tags = view.getUint16(offset, little);
offset += 2;
for (let i = 0; i < tags; i++)
{
if (view.getUint16(offset + (i * 12), little) === 0x0112)
{
return view.getUint16(offset + (i * 12) + 8, little);
}
}
}
else if ((marker & 0xFF00) !== 0xFF00)
{
break;
}
else
{
offset += view.getUint16(offset, false);
}
}
return -1;
}

Wyświetl plik

@ -1,7 +1,10 @@
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import 'normalize.css'
import Vue from 'vue'
import App from './App'
import './styles/index.css'
import 'cropperjs/dist/cropper.css';
Vue.config.productionTip = false

Wyświetl plik

@ -0,0 +1,26 @@
*,
::before,
::after {
box-sizing: border-box;
}
body {
background-color: #fff;
color: #212529;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
margin: 0;
}
.sr-only {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
overflow: hidden;
padding: 0;
position: absolute;
white-space: nowrap;
width: 1px;
}