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