Skip to content

Commit 1fcfd9d

Browse files
committed
Atoms - Add annotator user option utility component
1 parent 7bab924 commit 1fcfd9d

File tree

1 file changed

+283
-0
lines changed

1 file changed

+283
-0
lines changed

src/atoms/PenAndPaper.vue

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
<script setup>
2+
import { ref, watch, onMounted, onBeforeUnmount, computed, nextTick } from "vue";
3+
import { XMLNS } from "../lib";
4+
import BaseIcon from "./BaseIcon.vue";
5+
import { lightenHexColor } from "../lib";
6+
import ColorPicker from "./ColorPicker.vue";
7+
8+
const props = defineProps({
9+
parent: {
10+
type: HTMLElement
11+
},
12+
backgroundColor: {
13+
type: String,
14+
default: '#FFFFFF'
15+
},
16+
color: {
17+
type: String,
18+
default: '#2D353C'
19+
},
20+
active: {
21+
type: Boolean,
22+
default: false,
23+
}
24+
});
25+
26+
const emit = defineEmits(['close']);
27+
28+
const stack = ref([]);
29+
const redoStack = ref([]);
30+
31+
const viewBox = ref("0 0 0 0");
32+
33+
const currentColor = ref(props.color)
34+
35+
const buttonBorderColor = computed(() => {
36+
return lightenHexColor(props.color, 0.6);
37+
});
38+
39+
function setViewBox({ width, height }) {
40+
viewBox.value = `0 0 ${width} ${height}`;
41+
}
42+
43+
const resizeObserver = ref(null);
44+
45+
onMounted(() => {
46+
nextTick(() => {
47+
if (props.parent) {
48+
resizeObserver.value = new ResizeObserver((entries) => {
49+
for (const entry of entries) {
50+
const { width, height } = entry.contentRect;
51+
setViewBox({ width, height });
52+
}
53+
});
54+
resizeObserver.value.observe(props.parent);
55+
56+
const { width, height } = props.parent.getBoundingClientRect();
57+
setViewBox({ width, height });
58+
}
59+
})
60+
});
61+
62+
onBeforeUnmount(() => {
63+
if (resizeObserver.value) resizeObserver.value.disconnect();
64+
});
65+
66+
watch(
67+
() => props.parent,
68+
(v) => {
69+
if (!v) return;
70+
const { width, height } = props.parent.getBoundingClientRect();
71+
setViewBox({ width, height });
72+
},
73+
{ immediate: true }
74+
);
75+
76+
const isDrawing = ref(false);
77+
const currentPath = ref("");
78+
const svgElement = ref(null);
79+
80+
function startDrawing(event) {
81+
if (!svgElement.value) return;
82+
isDrawing.value = true;
83+
const { x, y } = getRelativePosition(event);
84+
currentPath.value = `M ${x} ${y}`;
85+
}
86+
87+
function draw(event) {
88+
if (!isDrawing.value || !svgElement.value) return;
89+
90+
const { x, y } = getRelativePosition(event);
91+
currentPath.value += ` ${x} ${y}`;
92+
}
93+
94+
function stopDrawing() {
95+
if (isDrawing.value) {
96+
stack.value.push(currentPath.value);
97+
redoStack.value = [];
98+
currentPath.value = "";
99+
}
100+
isDrawing.value = false;
101+
}
102+
103+
function getRelativePosition(event) {
104+
if (!svgElement.value) return { x: 0, y: 0 };
105+
106+
const svgRect = svgElement.value.getBoundingClientRect();
107+
let clientX, clientY;
108+
109+
if (event.touches && event.touches.length) {
110+
clientX = event.touches[0].clientX;
111+
clientY = event.touches[0].clientY;
112+
} else {
113+
clientX = event.clientX;
114+
clientY = event.clientY;
115+
}
116+
117+
return {
118+
x: clientX - svgRect.left,
119+
y: clientY - svgRect.top,
120+
};
121+
}
122+
123+
const showRedoButton = ref(false);
124+
125+
function deleteLastDraw() {
126+
if (stack.value.length > 0) {
127+
const lastPath = stack.value.pop();
128+
redoStack.value.push(lastPath);
129+
}
130+
}
131+
132+
function redoLastDraw() {
133+
if (redoStack.value.length > 0) {
134+
const lastUndonePath = redoStack.value.pop();
135+
stack.value.push(lastUndonePath);
136+
}
137+
}
138+
139+
function reset() {
140+
stack.value = [];
141+
redoStack.value = [];
142+
}
143+
</script>
144+
145+
<template>
146+
<div
147+
v-if="active"
148+
data-html2canvas-ignore
149+
:class="{
150+
'vue-ui-pen-and-paper-actions': true,
151+
'visible': active
152+
}"
153+
>
154+
<button
155+
class="vue-ui-pen-and-paper-action"
156+
:style="{
157+
backgroundColor: backgroundColor,
158+
border: `1px solid ${buttonBorderColor}`
159+
}"
160+
@click="emit('close')"
161+
>
162+
<BaseIcon name="close" :stroke="color"/>
163+
</button>
164+
<button
165+
:class="{
166+
'vue-ui-pen-and-paper-action': true,
167+
}"
168+
style="padding: 0 !important"
169+
>
170+
<ColorPicker v-model:value="currentColor" />
171+
172+
</button>
173+
<button
174+
:class="{
175+
'vue-ui-pen-and-paper-action': true,
176+
'vue-ui-pen-and-paper-action-disabled': !stack.length
177+
}"
178+
:disabled="!stack.length"
179+
:style="{
180+
backgroundColor: backgroundColor,
181+
border: `1px solid ${buttonBorderColor}`
182+
}"
183+
@click="deleteLastDraw"
184+
>
185+
<BaseIcon name="restart" :stroke="color"/>
186+
</button>
187+
<button
188+
:class="{
189+
'vue-ui-pen-and-paper-action': true,
190+
'vue-ui-pen-and-paper-action-disabled': !redoStack.length
191+
}"
192+
:style="{
193+
backgroundColor: backgroundColor,
194+
border: `1px solid ${buttonBorderColor}`
195+
}"
196+
@click="redoLastDraw"
197+
>
198+
<BaseIcon name="restart" :stroke="color" style="transform: scaleX(-1)"/>
199+
</button>
200+
<button
201+
:class="{
202+
'vue-ui-pen-and-paper-action': true,
203+
'vue-ui-pen-and-paper-action-disabled': !stack.length
204+
}"
205+
class="vue-ui-pen-and-paper-action"
206+
:style="{
207+
backgroundColor: backgroundColor,
208+
border: `1px solid ${buttonBorderColor}`
209+
}"
210+
@click="reset"
211+
>
212+
<BaseIcon name="trash" :stroke="color"/>
213+
</button>
214+
</div>
215+
<svg
216+
ref="svgElement"
217+
:xmlns="XMLNS"
218+
:viewBox="viewBox"
219+
:class="{
220+
'vue-ui-pen-and-paper': true,
221+
inactive: !active,
222+
}"
223+
@mousedown="startDrawing"
224+
@mousemove="draw"
225+
@mouseup="stopDrawing"
226+
@mouseleave="stopDrawing"
227+
@touchstart.prevent="startDrawing"
228+
@touchmove.prevent="draw"
229+
@touchend="stopDrawing"
230+
>
231+
<path class="vue-ui-pen-and-paper-path" v-for="path in stack" :key="path" :d="path" :stroke="currentColor" fill="none" />
232+
<path class="vue-ui-pen-and-paper-path vue-ui-pen-and-paper-path-drawing" v-if="isDrawing" :d="currentPath" :stroke="currentColor" fill="none" />
233+
</svg>
234+
</template>
235+
236+
<style scoped>
237+
.vue-ui-pen-and-paper {
238+
position: absolute;
239+
top: 0;
240+
left: 0;
241+
width: 100%;
242+
height: 100%;
243+
background: transparent;
244+
cursor: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAABg2lDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpSIVh2YQcchQnSyIijhKFYtgobQVWnUwufQLmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi6OSk6CIl/i8ptIjx4Lgf7+497t4BQrPKNKtnAtB020wn4lIuvyqFXhGGiAhCiMnMMpKZxSx8x9c9Any9i/Es/3N/jgG1YDEgIBHPMcO0iTeIZzZtg/M+scjKskp8Tjxu0gWJH7muePzGueSywDNFM5ueJxaJpVIXK13MyqZGPE0cVTWd8oWcxyrnLc5atc7a9+QvDBf0lQzXaY4ggSUkkYIEBXVUUIWNGK06KRbStB/38Q+7/hS5FHJVwMixgBo0yK4f/A9+d2sVpya9pHAc6H1xnI9RILQLtBqO833sOK0TIPgMXOkdf60JzH6S3uho0SNgcBu4uO5oyh5wuQMMPRmyKbtSkKZQLALvZ/RNeSByC/Sveb2193H6AGSpq+Ub4OAQGCtR9rrPu/u6e/v3TLu/H5C7crM1WjgWAAAABmJLR0QAqwB5AHWF+8OUAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH5gwUExIUagzGcQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAABfSURBVBjTldAxDoNQDIPhL0+q1L33P1AvAhN7xfK6WAgoLfSfrNiykpQtE+7RLzx2vgF9D3o8lWDmn1QVVMP0LZQGmNtqp1/cmou0XHdG/+sYeGZwFBqPCub8rkcvvAGvsi1VYarR8wAAAABJRU5ErkJggg==') 5 5, auto;
245+
z-index: 0;
246+
}
247+
.inactive {
248+
pointer-events: none;
249+
}
250+
.vue-ui-pen-and-paper-actions {
251+
position: absolute;
252+
top: 50%;
253+
left: 0;
254+
transform: translateY(-50%);
255+
z-index: 1;
256+
display: flex;
257+
flex-direction: column;
258+
gap: 4px;
259+
}
260+
.vue-ui-pen-and-paper-action {
261+
display: flex;
262+
align-items:center;
263+
justify-content:center;
264+
height: 32px;
265+
width: 32px;
266+
padding: 2px;
267+
transition: all 0.2s ease-in-out;
268+
cursor: pointer;
269+
}
270+
.vue-ui-pen-and-paper-action:hover {
271+
box-shadow: 2px 2px 6px rgba(0,0,0,0.3);
272+
}
273+
.vue-ui-pen-and-paper-action-disabled {
274+
opacity: 0.5;
275+
cursor: not-allowed;
276+
}
277+
.vue-ui-pen-and-paper-path {
278+
stroke-linecap: round;
279+
}
280+
.vue-ui-pen-and-paper-path-drawing {
281+
stroke-width: 2;
282+
}
283+
</style>

0 commit comments

Comments
 (0)