kopia lustrzana https://github.com/msurguy/SquiggleCam
using different component
rodzic
fd495faa69
commit
847365d9b8
|
@ -64,6 +64,14 @@ module.exports = {
|
||||||
limit: 10000,
|
limit: 10000,
|
||||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: [
|
||||||
|
'vue-style-loader',
|
||||||
|
'css-loader',
|
||||||
|
'sass-loader'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<title>image-editor</title>
|
<title>image-editor</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/font-awesome@4.7.0/css/font-awesome.min.css">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
Plik diff jest za duży
Load Diff
|
@ -35,6 +35,8 @@
|
||||||
"build": "node build/build.js"
|
"build": "node build/build.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cropperjs": "^1.4.3",
|
||||||
|
"normalize.css": "^8.0.1",
|
||||||
"pica": "^5.0.0",
|
"pica": "^5.0.0",
|
||||||
"vue": "^2.5.2"
|
"vue": "^2.5.2"
|
||||||
},
|
},
|
||||||
|
@ -56,6 +58,7 @@
|
||||||
"friendly-errors-webpack-plugin": "^1.6.1",
|
"friendly-errors-webpack-plugin": "^1.6.1",
|
||||||
"html-webpack-plugin": "^2.30.1",
|
"html-webpack-plugin": "^2.30.1",
|
||||||
"node-notifier": "^5.1.2",
|
"node-notifier": "^5.1.2",
|
||||||
|
"node-sass": "^4.11.0",
|
||||||
"optimize-css-assets-webpack-plugin": "^3.2.0",
|
"optimize-css-assets-webpack-plugin": "^3.2.0",
|
||||||
"ora": "^1.2.0",
|
"ora": "^1.2.0",
|
||||||
"portfinder": "^1.0.13",
|
"portfinder": "^1.0.13",
|
||||||
|
@ -63,6 +66,7 @@
|
||||||
"postcss-loader": "^2.0.8",
|
"postcss-loader": "^2.0.8",
|
||||||
"postcss-url": "^7.2.1",
|
"postcss-url": "^7.2.1",
|
||||||
"rimraf": "^2.6.0",
|
"rimraf": "^2.6.0",
|
||||||
|
"sass-loader": "^7.1.0",
|
||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
"shelljs": "^0.7.6",
|
"shelljs": "^0.7.6",
|
||||||
"uglifyjs-webpack-plugin": "^1.1.1",
|
"uglifyjs-webpack-plugin": "^1.1.1",
|
||||||
|
|
97
src/App.vue
97
src/App.vue
|
@ -1,27 +1,100 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ImageProvider from './components/ImageProvider'
|
import Navbar from './components/Navbar'
|
||||||
|
import Loader from './components/Loader'
|
||||||
|
import Editor from './components/Editor'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
#app {
|
.app {
|
||||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
bottom: 0;
|
||||||
-webkit-font-smoothing: antialiased;
|
left: 0;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
position: absolute;
|
||||||
text-align: center;
|
top: 0;
|
||||||
color: #2c3e50;
|
right: 0;
|
||||||
margin-top: 60px;
|
}
|
||||||
}
|
.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>
|
</style>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,7 +1,10 @@
|
||||||
// The Vue build version to load with the `import` command
|
// The Vue build version to load with the `import` command
|
||||||
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
|
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
|
||||||
|
import 'normalize.css'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import './styles/index.css'
|
||||||
|
import 'cropperjs/dist/cropper.css';
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
Ładowanie…
Reference in New Issue