Skip to content

Commit ba79f58

Browse files
Add custom handwriting data collector tool
1 parent 173a53f commit ba79f58

1 file changed

Lines changed: 294 additions & 0 deletions

File tree

data_collector.html

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>INKFORGE Handwriting Collector</title>
8+
<style>
9+
body {
10+
font-family: system-ui, -apple-system, sans-serif;
11+
max-width: 800px;
12+
margin: 0 auto;
13+
padding: 20px;
14+
background: #f5f5f5;
15+
}
16+
17+
.container {
18+
background: white;
19+
padding: 20px;
20+
border-radius: 8px;
21+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
22+
}
23+
24+
h1 {
25+
color: #333;
26+
margin-top: 0;
27+
}
28+
29+
.prompt-box {
30+
font-size: 1.2em;
31+
margin: 20px 0;
32+
padding: 15px;
33+
background: #eef2ff;
34+
border-radius: 6px;
35+
color: #3730a3;
36+
}
37+
38+
canvas {
39+
border: 2px solid #cbd5e1;
40+
border-radius: 4px;
41+
cursor: crosshair;
42+
background: white;
43+
touch-action: none;
44+
/* Prevent scrolling when drawing on touch devices */
45+
}
46+
47+
.controls {
48+
margin-top: 20px;
49+
display: flex;
50+
gap: 10px;
51+
}
52+
53+
button {
54+
padding: 10px 20px;
55+
font-size: 1em;
56+
border: none;
57+
border-radius: 4px;
58+
cursor: pointer;
59+
font-weight: bold;
60+
}
61+
62+
.btn-clear {
63+
background: #ef4444;
64+
color: white;
65+
}
66+
67+
.btn-save {
68+
background: #10b981;
69+
color: white;
70+
}
71+
72+
.btn-next {
73+
background: #3b82f6;
74+
color: white;
75+
}
76+
77+
button:hover {
78+
opacity: 0.9;
79+
}
80+
81+
.setup {
82+
margin-bottom: 20px;
83+
padding-bottom: 20px;
84+
border-bottom: 2px solid #eee;
85+
}
86+
87+
input {
88+
padding: 8px;
89+
font-size: 1em;
90+
border: 1px solid #ccc;
91+
border-radius: 4px;
92+
}
93+
</style>
94+
</head>
95+
96+
<body>
97+
98+
<div class="container">
99+
<h1>INKFORGE Handwriting Collector</h1>
100+
101+
<div class="setup">
102+
<label><strong>Writer ID:</strong></label>
103+
<input type="text" id="writerId" value="me" placeholder="e.g., me, friend1">
104+
<span style="font-size:0.9em;color:#666;margin-left:10px;">Used to separate different handwriting
105+
styles.</span>
106+
</div>
107+
108+
<div>
109+
<strong>Please write the following sentence:</strong>
110+
<div class="prompt-box" id="promptText">The quick brown fox jumps over the lazy dog.</div>
111+
</div>
112+
113+
<canvas id="canvas" width="760" height="200"></canvas>
114+
115+
<div class="controls">
116+
<button class="btn-clear" onclick="clearCanvas()">Clear Canvas</button>
117+
<button class="btn-save" onclick="saveData()">Save & Download</button>
118+
<button class="btn-next" onclick="nextPrompt()">Next Sentence</button>
119+
</div>
120+
121+
<div style="margin-top: 20px; font-size: 0.9em; color: #666;">
122+
<strong>Instructions:</strong> Write on the canvas above using a mouse, stylus, or touch. Click "Save &
123+
Download" when done. It will download an XML file of your strokes.
124+
</div>
125+
</div>
126+
127+
<script>
128+
const canvas = document.getElementById('canvas');
129+
const ctx = canvas.getContext('2d');
130+
const writerInput = document.getElementById('writerId');
131+
const promptText = document.getElementById('promptText');
132+
133+
let isDrawing = false;
134+
let strokes = []; // Array of strokes. Each stroke is an array of points.
135+
let currentStroke = [];
136+
let startTime = 0;
137+
let fileCounter = 0;
138+
139+
// A list of example sentences that cover a lot of letters.
140+
const prompts = [
141+
"The quick brown fox jumps over the lazy dog.",
142+
"Pack my box with five dozen liquor jugs.",
143+
"How vexingly quick daft zebras jump!",
144+
"Sphinx of black quartz, judge my vow.",
145+
"Two driven jocks help fax my big quiz.",
146+
"A wizard's job is to vex chumps quickly in fog.",
147+
"Hello world, this is my custom handwriting data!",
148+
"Numbers test: 0 1 2 3 4 5 6 7 8 9."
149+
];
150+
let promptIndex = 0;
151+
152+
// Set up canvas style
153+
ctx.lineWidth = 2;
154+
ctx.lineCap = 'round';
155+
ctx.lineJoin = 'round';
156+
ctx.strokeStyle = '#000';
157+
158+
function getCoordinates(e) {
159+
const rect = canvas.getBoundingClientRect();
160+
if (e.touches && e.touches.length > 0) {
161+
return {
162+
x: e.touches[0].clientX - rect.left,
163+
y: e.touches[0].clientY - rect.top
164+
};
165+
}
166+
return {
167+
x: e.clientX - rect.left,
168+
y: e.clientY - rect.top
169+
};
170+
}
171+
172+
function startDrawing(e) {
173+
e.preventDefault();
174+
isDrawing = true;
175+
currentStroke = [];
176+
if (strokes.length === 0) startTime = Date.now();
177+
178+
const coords = getCoordinates(e);
179+
ctx.beginPath();
180+
ctx.moveTo(coords.x, coords.y);
181+
182+
currentStroke.push({ x: coords.x, y: coords.y, time: Date.now() - startTime });
183+
}
184+
185+
function draw(e) {
186+
if (!isDrawing) return;
187+
e.preventDefault();
188+
189+
const coords = getCoordinates(e);
190+
ctx.lineTo(coords.x, coords.y);
191+
ctx.stroke();
192+
193+
currentStroke.push({ x: coords.x, y: coords.y, time: Date.now() - startTime });
194+
}
195+
196+
function stopDrawing(e) {
197+
if (!isDrawing) return;
198+
e.preventDefault();
199+
isDrawing = false;
200+
201+
if (currentStroke.length > 0) {
202+
strokes.push(currentStroke);
203+
}
204+
}
205+
206+
// Event Listeners for Mouse
207+
canvas.addEventListener('mousedown', startDrawing);
208+
canvas.addEventListener('mousemove', draw);
209+
canvas.addEventListener('mouseup', stopDrawing);
210+
canvas.addEventListener('mouseout', stopDrawing);
211+
212+
// Event Listeners for Touch/Stylus
213+
canvas.addEventListener('touchstart', startDrawing, { passive: false });
214+
canvas.addEventListener('touchmove', draw, { passive: false });
215+
canvas.addEventListener('touchend', stopDrawing);
216+
217+
function clearCanvas() {
218+
ctx.clearRect(0, 0, canvas.width, canvas.height);
219+
strokes = [];
220+
}
221+
222+
function nextPrompt() {
223+
promptIndex = (promptIndex + 1) % prompts.length;
224+
promptText.innerText = prompts[promptIndex];
225+
clearCanvas();
226+
}
227+
228+
function generateXML() {
229+
let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<WhiteboardCapture>\n <StrokeSet>\n`;
230+
231+
for (const stroke of strokes) {
232+
xml += ` <Stroke>\n`;
233+
for (const pt of stroke) {
234+
// Formatting to 2 decimal places to match IAM style sizes roughly
235+
xml += ` <Point x="${Math.round(pt.x)}" y="${Math.round(pt.y)}" time="${pt.time}" />\n`;
236+
}
237+
xml += ` </Stroke>\n`;
238+
}
239+
240+
xml += ` </StrokeSet>\n</WhiteboardCapture>`;
241+
return xml;
242+
}
243+
244+
function generateTranscription(lineId, text) {
245+
// Simple transcription format the script understands:
246+
// writer-form-line "transcription text"
247+
return `${lineId} "${text}"\n`;
248+
}
249+
250+
function downloadFile(filename, content, mimeType) {
251+
const blob = new Blob([content], { type: mimeType });
252+
const url = URL.createObjectURL(blob);
253+
const a = document.createElement('a');
254+
a.href = url;
255+
a.download = filename;
256+
document.body.appendChild(a);
257+
a.click();
258+
document.body.removeChild(a);
259+
URL.revokeObjectURL(url);
260+
}
261+
262+
function saveData() {
263+
if (strokes.length === 0) {
264+
alert("Canvas is empty! Please write something first.");
265+
return;
266+
}
267+
268+
const writer = writerInput.value.trim() || "me";
269+
const formId = String(fileCounter).padStart(3, '0');
270+
const lineId = `${writer}-${formId}-00`; // e.g., me-000-00
271+
272+
const text = promptText.innerText;
273+
274+
// 1. Generate XML
275+
const xmlContent = generateXML();
276+
downloadFile(`${lineId}.xml`, xmlContent, 'application/xml');
277+
278+
// 2. Generate Transcription Text
279+
// Note: preprocess.py looks for writer-form.txt (e.g., me-000.txt)
280+
const txtFilename = `${writer}-${formId}.txt`;
281+
const txtContent = generateTranscription(lineId, text);
282+
283+
// Use a short timeout so the browser allows multiple downloads
284+
setTimeout(() => {
285+
downloadFile(txtFilename, txtContent, 'text/plain');
286+
fileCounter++;
287+
nextPrompt();
288+
}, 500);
289+
}
290+
</script>
291+
292+
</body>
293+
294+
</html>

0 commit comments

Comments
 (0)