mirror of
https://github.com/MarcZierle/photo-log-frontend.git
synced 2025-04-17 17:34:38 +00:00
add cropper snapping points image
This commit is contained in:
parent
2add4c42a2
commit
82cd293f59
47
src/assets/handler.svg
Normal file
47
src/assets/handler.svg
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="540.398px" height="540.398px" viewBox="0 0 540.398 540.398" style="enable-background:new 0 0 540.398 540.398;"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M242.207,276.315H88.398c-12.087,0-14.951,6.928-6.407,15.473l29.357,29.357L2.565,429.929
|
||||||
|
c-3.421,3.414-3.421,8.959,0,12.373l95.527,95.533c3.421,3.416,8.96,3.416,12.375,0L219.25,429.054l29.358,29.357
|
||||||
|
c8.543,8.537,15.471,5.672,15.471-6.414V298.194C264.079,286.114,254.281,276.315,242.207,276.315z"/>
|
||||||
|
<path d="M442.306,2.562c-3.421-3.415-8.96-3.415-12.374,0L321.143,111.345l-29.358-29.357c-8.537-8.544-15.465-5.673-15.465,6.407
|
||||||
|
v153.802c0,12.081,9.798,21.879,21.873,21.879H452c12.087,0,14.951-6.928,6.407-15.472l-29.357-29.357l108.783-108.783
|
||||||
|
c3.421-3.415,3.421-8.959,0-12.375L442.306,2.562z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
136
src/components/FreeSnapStencil.vue
Normal file
136
src/components/FreeSnapStencil.vue
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="circle-stencil"
|
||||||
|
:style="style"
|
||||||
|
>
|
||||||
|
<draggable-element
|
||||||
|
class="circle-stencil__handler"
|
||||||
|
@drag="onResize"
|
||||||
|
@drag-end="onResizeEnd"
|
||||||
|
>
|
||||||
|
<img :src="require('../assets/handler.svg')" @mousedown.prevent>
|
||||||
|
</draggable-element>
|
||||||
|
<draggable-area @move="onMove" @move-end="onMoveEnd">
|
||||||
|
<stencil-preview
|
||||||
|
class="circle-stencil__preview"
|
||||||
|
:image="image"
|
||||||
|
:coordinates="coordinates"
|
||||||
|
:width="stencilCoordinates.width"
|
||||||
|
:height="stencilCoordinates.height"
|
||||||
|
:transitions="transitions"
|
||||||
|
/>
|
||||||
|
</draggable-area>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
DraggableElement,
|
||||||
|
DraggableArea,
|
||||||
|
StencilPreview,
|
||||||
|
ResizeEvent
|
||||||
|
} from 'vue-advanced-cropper'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FreeSnapStencil',
|
||||||
|
components: {
|
||||||
|
StencilPreview,
|
||||||
|
DraggableArea,
|
||||||
|
DraggableElement
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
image: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
coordinates: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
transitions: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
stencilCoordinates: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
intersections: {
|
||||||
|
type: Array,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
style() {
|
||||||
|
const { height, width, left, top } = this.stencilCoordinates
|
||||||
|
const style = {
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
transform: `translate(${left}px, ${top}px)`
|
||||||
|
}
|
||||||
|
if (this.transitions && this.transitions.enabled) {
|
||||||
|
style.transition = `${this.transitions.time}ms ${this.transitions.timingFunction}`
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onMove(moveEvent) {
|
||||||
|
this.$emit('move', moveEvent)
|
||||||
|
},
|
||||||
|
onMoveEnd() {
|
||||||
|
this.$emit('move-end')
|
||||||
|
},
|
||||||
|
onResize(dragEvent) {
|
||||||
|
const shift = dragEvent.shift()
|
||||||
|
|
||||||
|
const widthResize = shift.left
|
||||||
|
const heightResize = -shift.top
|
||||||
|
|
||||||
|
this.$emit('resize', new ResizeEvent(
|
||||||
|
{
|
||||||
|
left: widthResize,
|
||||||
|
right: widthResize,
|
||||||
|
top: heightResize,
|
||||||
|
bottom: heightResize,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
compensate: true,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
},
|
||||||
|
onResizeEnd() {
|
||||||
|
this.$emit('resize-end')
|
||||||
|
},
|
||||||
|
aspectRatios() {
|
||||||
|
return {
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.circle-stencil {
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: move;
|
||||||
|
position: absolute;
|
||||||
|
border: dashed 2px white;
|
||||||
|
box-sizing: border-box;
|
||||||
|
&__handler {
|
||||||
|
position: absolute;
|
||||||
|
right: 15%;
|
||||||
|
top: 14%;
|
||||||
|
z-index: 1;
|
||||||
|
cursor: ne-resize;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transform: translate(50%, -50%);
|
||||||
|
}
|
||||||
|
&__preview {
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -3,8 +3,9 @@
|
|||||||
<n-space justify="start">
|
<n-space justify="start">
|
||||||
<cropper
|
<cropper
|
||||||
class="cropper"
|
class="cropper"
|
||||||
:src="src"
|
:src="snap_to_inters ? srcImageIntersections : src"
|
||||||
:stencil-props="aspect_ratio"
|
:stencil-component="this.$options.components.FreeSnapStencil"
|
||||||
|
:stencil-props="stencilProps"
|
||||||
:auto-zoom="true"
|
:auto-zoom="true"
|
||||||
@change="change"
|
@change="change"
|
||||||
ref="cropper"
|
ref="cropper"
|
||||||
@ -27,6 +28,7 @@
|
|||||||
<n-slider v-model:value="rotate_angle" @update:value="update_rotate" :step="1" :min="-180" :max="180" />
|
<n-slider v-model:value="rotate_angle" @update:value="update_rotate" :step="1" :min="-180" :max="180" />
|
||||||
<n-checkbox :disabled="free_transform" v-model:checked="lock_aspect_ratio">lock aspect ratio to flip chart size</n-checkbox>
|
<n-checkbox :disabled="free_transform" v-model:checked="lock_aspect_ratio">lock aspect ratio to flip chart size</n-checkbox>
|
||||||
<n-checkbox v-model:checked="free_transform">free transform handles</n-checkbox>
|
<n-checkbox v-model:checked="free_transform">free transform handles</n-checkbox>
|
||||||
|
<n-checkbox v-model:checked="snap_to_inters">snap to intersections</n-checkbox>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
@ -34,39 +36,103 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {Cropper} from 'vue-advanced-cropper'
|
import {Cropper} from 'vue-advanced-cropper'
|
||||||
|
import FreeSnapStencil from '../components/FreeSnapStencil.vue'
|
||||||
import 'vue-advanced-cropper/dist/style.css'
|
import 'vue-advanced-cropper/dist/style.css'
|
||||||
|
|
||||||
|
|
||||||
|
/* eslint-disable vue/no-unused-components */
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Cropper,
|
Cropper,
|
||||||
|
FreeSnapStencil,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
src: {
|
src: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
}
|
},
|
||||||
|
inters: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
srcImgElement: null,
|
||||||
|
srcImgLoaded: false,
|
||||||
preview_data_url: null,
|
preview_data_url: null,
|
||||||
lock_aspect_ratio: true,
|
lock_aspect_ratio: true,
|
||||||
free_transform: false,
|
free_transform: false,
|
||||||
|
snap_to_inters: false,
|
||||||
bbox: [],
|
bbox: [],
|
||||||
rotate_angle: 0,
|
rotate_angle: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.srcImgElement = new Image()
|
||||||
|
this.srcImgElement.src = this.src
|
||||||
|
this.srcImgElement.crossOrigin = 'Anonymous'
|
||||||
|
this.srcImgElement.onload = () => {
|
||||||
|
this.srcImgLoaded = true
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
aspect_ratio() {
|
srcImageIntersections() {
|
||||||
|
if (!this.srcImgLoaded) {
|
||||||
|
return this.src
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvasElement = document.createElement('canvas')
|
||||||
|
canvasElement.width = this.srcImgElement.width
|
||||||
|
canvasElement.height = this.srcImgElement.height
|
||||||
|
|
||||||
|
var ctx = canvasElement.getContext('2d')
|
||||||
|
ctx.drawImage(this.srcImgElement, 0,0)
|
||||||
|
|
||||||
|
this.drawIntersections(ctx)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return ctx.canvas.toDataURL()
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
return this.src
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stencilProps() {
|
||||||
if (this.lock_aspect_ratio) {
|
if (this.lock_aspect_ratio) {
|
||||||
return {
|
return {
|
||||||
aspectRatio: 1/1.41421356
|
aspectRatio: 1/1.41421356,
|
||||||
|
intersections: this.inters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {}
|
return {intersections: this.inters}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
drawIntersections(ctx) {
|
||||||
|
for (let i=0; i < this.inters.length; i++) {
|
||||||
|
let x = this.inters[i][0]
|
||||||
|
let y = this.inters[i][1]
|
||||||
|
this.drawCircle(
|
||||||
|
ctx, x,y,
|
||||||
|
8, 'rgba(255,255,255,0.9)',
|
||||||
|
'rgba(255,255,255,0.4)', 25)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawCircle(ctx, x, y, radius, fill, stroke, strokeWidth) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(x, y, radius, 0, 2 * Math.PI, false)
|
||||||
|
if (fill) {
|
||||||
|
ctx.fillStyle = fill
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
if (stroke) {
|
||||||
|
ctx.lineWidth = strokeWidth
|
||||||
|
ctx.strokeStyle = stroke
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
},
|
||||||
coordinates_to_bbox(coordinates) {
|
coordinates_to_bbox(coordinates) {
|
||||||
let top_left = [
|
let top_left = [
|
||||||
coordinates.left,
|
coordinates.left,
|
||||||
@ -88,8 +154,10 @@ export default {
|
|||||||
return [top_left, top_right, bottom_right, bottom_left]
|
return [top_left, top_right, bottom_right, bottom_left]
|
||||||
},
|
},
|
||||||
change({ coordinates, canvas }) {
|
change({ coordinates, canvas }) {
|
||||||
this.preview_data_url = canvas.toDataURL()
|
try {
|
||||||
this.bbox = this.coordinates_to_bbox(coordinates)
|
this.preview_data_url = canvas.toDataURL()
|
||||||
|
this.bbox = this.coordinates_to_bbox(coordinates)
|
||||||
|
} catch (e) {return}
|
||||||
},
|
},
|
||||||
update_rotate() {
|
update_rotate() {
|
||||||
this.$refs.cropper.appliedImageTransforms= {
|
this.$refs.cropper.appliedImageTransforms= {
|
||||||
|
@ -10,6 +10,7 @@ import LogsList from '../views/LogsList.vue'
|
|||||||
import CreateLog from '../views/CreateLog.vue'
|
import CreateLog from '../views/CreateLog.vue'
|
||||||
import CameraCapture from '../views/CameraCapture.vue'
|
import CameraCapture from '../views/CameraCapture.vue'
|
||||||
import ManagePhotos from '../views/ManagePhotos.vue'
|
import ManagePhotos from '../views/ManagePhotos.vue'
|
||||||
|
import DevView from '../views/DevView.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@ -44,6 +45,11 @@ const routes = [
|
|||||||
name: 'ManagePhotos',
|
name: 'ManagePhotos',
|
||||||
component: ManagePhotos
|
component: ManagePhotos
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/dev',
|
||||||
|
name: 'Development',
|
||||||
|
component: DevView
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
35
src/views/DevView.vue
Normal file
35
src/views/DevView.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">Development</h1>
|
||||||
|
|
||||||
|
<PhotoCropper
|
||||||
|
style="max-width:75vw;margin:auto;"
|
||||||
|
:src="photo_src"
|
||||||
|
ref="cropper"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useMeta } from 'vue-meta'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
|
||||||
|
import PhotoCropper from '@/components/PhotoCropper'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DevView',
|
||||||
|
components: {
|
||||||
|
PhotoCropper,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
useMeta({ title: 'Development' })
|
||||||
|
const message = useMessage()
|
||||||
|
return { message }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
photo_src: 'https://server.riezel.com/static/original_images/9b5be3b7-7cf9-4720-ad5e-7d516c7e03c1.jpg'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
@ -11,7 +11,10 @@
|
|||||||
:name="group.id">
|
:name="group.id">
|
||||||
<template #header-extra>{{ group.date }}</template>
|
<template #header-extra>{{ group.date }}</template>
|
||||||
|
|
||||||
<p v-if="!photos[group.id]">loading...</p>
|
<div style="text-align: center">
|
||||||
|
<p v-if="is_group_loading.get(group.id)">loading...</p>
|
||||||
|
<p v-else-if="!photos[group.id]">no photos in group found</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<n-space>
|
<n-space>
|
||||||
<PhotoItem
|
<PhotoItem
|
||||||
@ -44,6 +47,7 @@
|
|||||||
<PhotoCropper
|
<PhotoCropper
|
||||||
style="max-width:75vw;margin:auto;"
|
style="max-width:75vw;margin:auto;"
|
||||||
:src="current_photo_src"
|
:src="current_photo_src"
|
||||||
|
:inters="current_photo_intersections"
|
||||||
ref="cropper"
|
ref="cropper"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -148,6 +152,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
current_photo: null,
|
current_photo: null,
|
||||||
|
current_photo_intersections: null,
|
||||||
|
|
||||||
showChangeGroupModal: false,
|
showChangeGroupModal: false,
|
||||||
showCropModal: false,
|
showCropModal: false,
|
||||||
@ -157,6 +162,8 @@ export default {
|
|||||||
new_group: null,
|
new_group: null,
|
||||||
current_ocr_text: '',
|
current_ocr_text: '',
|
||||||
current_photo_src: null,
|
current_photo_src: null,
|
||||||
|
|
||||||
|
is_group_loading: new Map(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -199,8 +206,10 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadPhotosInGroup(group_data) {
|
loadPhotosInGroup(group_data) {
|
||||||
|
this.is_group_loading.set(group_data.name, true)
|
||||||
if (group_data.expanded)
|
if (group_data.expanded)
|
||||||
this.$store.dispatch('loadPhotosInGroup', group_data.name)
|
this.$store.dispatch('loadPhotosInGroup', group_data.name)
|
||||||
|
.finally(() => this.is_group_loading.set(group_data.name, false))
|
||||||
},
|
},
|
||||||
getPhotoSrcById(photo_id){
|
getPhotoSrcById(photo_id){
|
||||||
for (const index in this.photos) {
|
for (const index in this.photos) {
|
||||||
@ -221,7 +230,9 @@ export default {
|
|||||||
change_crop_modal(photo_id) {
|
change_crop_modal(photo_id) {
|
||||||
this.current_photo = photo_id
|
this.current_photo = photo_id
|
||||||
this.showCropModal = true
|
this.showCropModal = true
|
||||||
this.current_photo_src = this.$store.getters.photoById(photo_id).original_image
|
let photo = this.$store.getters.photoById(photo_id)
|
||||||
|
this.current_photo_src = photo.original_image
|
||||||
|
this.current_photo_intersections = photo.intersections
|
||||||
},
|
},
|
||||||
change_ocr_modal(photo_id) {
|
change_ocr_modal(photo_id) {
|
||||||
this.current_photo = photo_id
|
this.current_photo = photo_id
|
||||||
|
Loading…
Reference in New Issue
Block a user