Skip to content

Commit be784b6

Browse files
Add canvas and drawing (#363)
* lgtm * Update config.js put the original url back * update import statements * may have fixed imports, npm run build works locally * Apply suggestions from code review remove print debugging statement --------- Co-authored-by: Yibei Chen <[email protected]>
1 parent 389ccc5 commit be784b6

File tree

8 files changed

+436
-16
lines changed

8 files changed

+436
-16
lines changed

babel.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
module.exports = {
2-
presets: ["@vue/cli-plugin-babel/preset"]
2+
presets: [
3+
'@vue/cli-plugin-babel/preset'
4+
]
35
};

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/CanvasInput.vue

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<template>
2+
<div class="canvas-container">
3+
<canvas ref="canvas" :width="width" :height="height" @mousedown="startDrawing" @mousemove="draw" @mouseup="stopDrawing" @mouseleave="stopDrawing"></canvas>
4+
<div class="canvas-controls">
5+
<button @click="clearCanvas" class="btn btn-secondary">Clear</button>
6+
<input type="color" v-model="currentColor" title="Choose color">
7+
<input type="range" v-model="lineWidth" min="1" max="20" title="Line width">
8+
</div>
9+
</div>
10+
</template>
11+
12+
<script>
13+
export default {
14+
name: 'CanvasInput',
15+
props: {
16+
value: {
17+
type: String,
18+
default: ''
19+
},
20+
width: {
21+
type: Number,
22+
default: 800
23+
},
24+
height: {
25+
type: Number,
26+
default: 400
27+
},
28+
backgroundImage: {
29+
type: String,
30+
default: ''
31+
}
32+
},
33+
data() {
34+
return {
35+
canvas: null,
36+
ctx: null,
37+
isDrawing: false,
38+
currentColor: '#000000',
39+
lineWidth: 5,
40+
lastX: 0,
41+
lastY: 0,
42+
image: null
43+
}
44+
},
45+
mounted() {
46+
this.canvas = this.$refs.canvas;
47+
this.ctx = this.canvas.getContext('2d');
48+
this.ctx.lineCap = 'round';
49+
this.ctx.lineJoin = 'round';
50+
51+
// Load background image if provided
52+
if (this.backgroundImage) {
53+
this.image = new Image();
54+
this.image.onload = () => {
55+
// Draw the image maintaining aspect ratio
56+
const scale = Math.min(
57+
this.width / this.image.width,
58+
this.height / this.image.height
59+
);
60+
const x = (this.width - this.image.width * scale) / 2;
61+
const y = (this.height - this.image.height * scale) / 2;
62+
this.ctx.drawImage(
63+
this.image,
64+
x, y,
65+
this.image.width * scale,
66+
this.image.height * scale
67+
);
68+
};
69+
this.image.src = this.backgroundImage;
70+
}
71+
72+
// Load existing drawing if value exists
73+
if (this.value) {
74+
const img = new Image();
75+
img.onload = () => {
76+
this.ctx.drawImage(img, 0, 0);
77+
};
78+
img.src = this.value;
79+
}
80+
},
81+
methods: {
82+
startDrawing(event) {
83+
this.isDrawing = true;
84+
const rect = this.canvas.getBoundingClientRect();
85+
this.lastX = event.clientX - rect.left;
86+
this.lastY = event.clientY - rect.top;
87+
},
88+
draw(event) {
89+
if (!this.isDrawing) return;
90+
91+
const rect = this.canvas.getBoundingClientRect();
92+
const currentX = event.clientX - rect.left;
93+
const currentY = event.clientY - rect.top;
94+
95+
this.ctx.beginPath();
96+
this.ctx.strokeStyle = this.currentColor;
97+
this.ctx.lineWidth = this.lineWidth;
98+
this.ctx.moveTo(this.lastX, this.lastY);
99+
this.ctx.lineTo(currentX, currentY);
100+
this.ctx.stroke();
101+
102+
this.lastX = currentX;
103+
this.lastY = currentY;
104+
105+
this.$emit('input', this.canvas.toDataURL());
106+
},
107+
stopDrawing() {
108+
this.isDrawing = false;
109+
},
110+
clearCanvas() {
111+
this.ctx.clearRect(0, 0, this.width, this.height);
112+
// Redraw background image if it exists
113+
if (this.image) {
114+
const scale = Math.min(
115+
this.width / this.image.width,
116+
this.height / this.image.height
117+
);
118+
const x = (this.width - this.image.width * scale) / 2;
119+
const y = (this.height - this.image.height * scale) / 2;
120+
this.ctx.drawImage(
121+
this.image,
122+
x, y,
123+
this.image.width * scale,
124+
this.image.height * scale
125+
);
126+
}
127+
this.$emit('input', this.canvas.toDataURL());
128+
}
129+
}
130+
}
131+
</script>
132+
133+
<style scoped>
134+
.canvas-container {
135+
display: flex;
136+
flex-direction: column;
137+
align-items: center;
138+
gap: 1rem;
139+
margin: 1rem 0;
140+
}
141+
142+
canvas {
143+
border: 1px solid #ccc;
144+
background: white;
145+
cursor: crosshair;
146+
}
147+
148+
.canvas-controls {
149+
display: flex;
150+
gap: 1rem;
151+
align-items: center;
152+
}
153+
154+
input[type="color"] {
155+
width: 50px;
156+
height: 30px;
157+
padding: 0;
158+
border: none;
159+
border-radius: 4px;
160+
cursor: pointer;
161+
}
162+
163+
input[type="range"] {
164+
width: 100px;
165+
}
166+
167+
.btn {
168+
padding: 0.5rem 1rem;
169+
border: none;
170+
border-radius: 4px;
171+
cursor: pointer;
172+
font-size: 0.9rem;
173+
}
174+
175+
.btn-secondary {
176+
background-color: #6c757d;
177+
color: white;
178+
}
179+
180+
.btn-secondary:hover {
181+
background-color: #5a6268;
182+
}
183+
</style>

src/components/InputSelector/InputSelector.vue

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,16 @@
172172
:init="init" v-on:valueChanged="sendData"/>
173173
</div>
174174

175+
<!-- If type is canvas drawing -->
176+
<div v-else-if="inputType === 'canvas'">
177+
<CanvasInput
178+
:constraints="valueConstraints"
179+
:selected_language="selected_language"
180+
:init="init"
181+
:backgroundImage="ui.backgroundImage"
182+
v-on:valueChanged="sendData"/>
183+
</div>
184+
175185
<!-- If type is a slider -->
176186
<div v-else-if="inputType === 'slider'">
177187
<SliderInput
@@ -255,18 +265,17 @@ import IntegerInput from '../Inputs/WebIntegerInput/';
255265
import FloatInput from '../Inputs/WebFloatInput/';
256266
import RangeInput from '../Inputs/RangeInput/';
257267
import DateInput from '../Inputs/YearInput/';
258-
import DocumentUpload from '../Inputs/DocumentUpload';
259-
import MultiTextInput from '../Inputs/MultiTextInput';
260-
import SliderInput from '../Inputs/SliderInput';
261-
import TimeRange from '../Inputs/TimeRange';
262-
import SelectInput from '../Inputs/SelectInput';
263-
// import AudioCheck from '../Inputs/AudioCheck';
264-
import StaticReadOnly from '../Inputs/StaticReadOnly';
265-
import SaveData from '../Inputs/SaveData/SaveData';
266-
import StudySign from '../StudySign/StudySign';
267-
// import Static from '../Inputs/Static';
268-
import EmailInput from '../Inputs/EmailInput';
269-
import ParticipantId from '../Inputs/ParticipantId/ParticipantId';
268+
import DocumentUpload from '../Inputs/DocumentUpload/';
269+
import MultiTextInput from '../Inputs/MultiTextInput/';
270+
import SliderInput from '../Inputs/SliderInput/';
271+
import TimeRange from '../Inputs/TimeRange/';
272+
import SelectInput from '../Inputs/SelectInput/';
273+
import StaticReadOnly from '../Inputs/StaticReadOnly/';
274+
import SaveData from '../Inputs/SaveData/';
275+
import StudySign from '../StudySign/';
276+
import EmailInput from '../Inputs/EmailInput/';
277+
import ParticipantId from '../Inputs/ParticipantId/';
278+
import CanvasInput from '../Inputs/CanvasInput/';
270279
271280
272281
export default {
@@ -308,6 +317,11 @@ export default {
308317
ipAddress: {
309318
type: String,
310319
},
320+
ui: {
321+
type: Object,
322+
required: false,
323+
default: () => ({})
324+
}
311325
},
312326
components: {
313327
ParticipantId,
@@ -328,7 +342,7 @@ export default {
328342
TimeRange,
329343
SelectInput,
330344
StaticReadOnly,
331-
// Static,
345+
CanvasInput,
332346
},
333347
data() {
334348
return {

0 commit comments

Comments
 (0)