mirror of
https://github.com/MarcZierle/photo-log-frontend.git
synced 2025-04-09 05:44:37 +00:00
add cropper snapping points image
This commit is contained in:
parent
2add4c42a2
commit
82cd293f59
src
assets
components
router
views
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 (image error) 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">
|
||||
<cropper
|
||||
class="cropper"
|
||||
:src="src"
|
||||
:stencil-props="aspect_ratio"
|
||||
:src="snap_to_inters ? srcImageIntersections : src"
|
||||
:stencil-component="this.$options.components.FreeSnapStencil"
|
||||
:stencil-props="stencilProps"
|
||||
:auto-zoom="true"
|
||||
@change="change"
|
||||
ref="cropper"
|
||||
@ -27,6 +28,7 @@
|
||||
<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 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>
|
||||
</div>
|
||||
@ -34,39 +36,103 @@
|
||||
|
||||
<script>
|
||||
import {Cropper} from 'vue-advanced-cropper'
|
||||
import FreeSnapStencil from '../components/FreeSnapStencil.vue'
|
||||
import 'vue-advanced-cropper/dist/style.css'
|
||||
|
||||
|
||||
/* eslint-disable vue/no-unused-components */
|
||||
export default {
|
||||
components: {
|
||||
Cropper,
|
||||
FreeSnapStencil,
|
||||
},
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
inters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
srcImgElement: null,
|
||||
srcImgLoaded: false,
|
||||
preview_data_url: null,
|
||||
lock_aspect_ratio: true,
|
||||
free_transform: false,
|
||||
snap_to_inters: false,
|
||||
bbox: [],
|
||||
rotate_angle: 0,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.srcImgElement = new Image()
|
||||
this.srcImgElement.src = this.src
|
||||
this.srcImgElement.crossOrigin = 'Anonymous'
|
||||
this.srcImgElement.onload = () => {
|
||||
this.srcImgLoaded = true
|
||||
}
|
||||
},
|
||||
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) {
|
||||
return {
|
||||
aspectRatio: 1/1.41421356
|
||||
aspectRatio: 1/1.41421356,
|
||||
intersections: this.inters
|
||||
}
|
||||
}
|
||||
return {}
|
||||
return {intersections: this.inters}
|
||||
}
|
||||
},
|
||||
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) {
|
||||
let top_left = [
|
||||
coordinates.left,
|
||||
@ -88,8 +154,10 @@ export default {
|
||||
return [top_left, top_right, bottom_right, bottom_left]
|
||||
},
|
||||
change({ coordinates, canvas }) {
|
||||
this.preview_data_url = canvas.toDataURL()
|
||||
this.bbox = this.coordinates_to_bbox(coordinates)
|
||||
try {
|
||||
this.preview_data_url = canvas.toDataURL()
|
||||
this.bbox = this.coordinates_to_bbox(coordinates)
|
||||
} catch (e) {return}
|
||||
},
|
||||
update_rotate() {
|
||||
this.$refs.cropper.appliedImageTransforms= {
|
||||
|
@ -10,6 +10,7 @@ import LogsList from '../views/LogsList.vue'
|
||||
import CreateLog from '../views/CreateLog.vue'
|
||||
import CameraCapture from '../views/CameraCapture.vue'
|
||||
import ManagePhotos from '../views/ManagePhotos.vue'
|
||||
import DevView from '../views/DevView.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@ -44,6 +45,11 @@ const routes = [
|
||||
name: 'ManagePhotos',
|
||||
component: ManagePhotos
|
||||
},
|
||||
{
|
||||
path: '/dev',
|
||||
name: 'Development',
|
||||
component: DevView
|
||||
},
|
||||
]
|
||||
|
||||
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">
|
||||
<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>
|
||||
<PhotoItem
|
||||
@ -44,6 +47,7 @@
|
||||
<PhotoCropper
|
||||
style="max-width:75vw;margin:auto;"
|
||||
:src="current_photo_src"
|
||||
:inters="current_photo_intersections"
|
||||
ref="cropper"
|
||||
/>
|
||||
|
||||
@ -148,6 +152,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
current_photo: null,
|
||||
current_photo_intersections: null,
|
||||
|
||||
showChangeGroupModal: false,
|
||||
showCropModal: false,
|
||||
@ -157,6 +162,8 @@ export default {
|
||||
new_group: null,
|
||||
current_ocr_text: '',
|
||||
current_photo_src: null,
|
||||
|
||||
is_group_loading: new Map(),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -199,8 +206,10 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
loadPhotosInGroup(group_data) {
|
||||
this.is_group_loading.set(group_data.name, true)
|
||||
if (group_data.expanded)
|
||||
this.$store.dispatch('loadPhotosInGroup', group_data.name)
|
||||
.finally(() => this.is_group_loading.set(group_data.name, false))
|
||||
},
|
||||
getPhotoSrcById(photo_id){
|
||||
for (const index in this.photos) {
|
||||
@ -221,7 +230,9 @@ export default {
|
||||
change_crop_modal(photo_id) {
|
||||
this.current_photo = photo_id
|
||||
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) {
|
||||
this.current_photo = photo_id
|
||||
|
Loading…
Reference in New Issue
Block a user