Skip to content

Commit 0f41576

Browse files
authored
Add education example (tldraw#6334)
This PR adds an "education" use case example. ### Change type - [ ] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [x] `other`
1 parent ae54a01 commit 0f41576

3 files changed

Lines changed: 592 additions & 0 deletions

File tree

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import { memo, useCallback, useEffect, useRef, useState } from 'react'
2+
import { Box, Editor, TLCameraOptions, TLComponents, Tldraw, track, useEditor } from 'tldraw'
3+
import 'tldraw/tldraw.css'
4+
import './education-canvas.css'
5+
6+
// Fixed camera options to prevent zooming/panning
7+
const CAMERA_OPTIONS: Partial<TLCameraOptions> = {
8+
isLocked: false,
9+
constraints: {
10+
initialZoom: 'fit-max',
11+
baseZoom: 'fit-max',
12+
bounds: {
13+
x: 0,
14+
y: 0,
15+
w: 600,
16+
h: 600,
17+
},
18+
behavior: { x: 'contain', y: 'contain' },
19+
padding: { x: 100, y: 100 },
20+
origin: { x: 0.5, y: 0.5 },
21+
},
22+
}
23+
24+
const CameraSetup = track(() => {
25+
const editor = useEditor()
26+
27+
useEffect(() => {
28+
if (!editor) return
29+
editor.run(() => {
30+
editor.zoomToBounds(new Box(0, 0, 600, 600), {
31+
inset: 150,
32+
})
33+
editor.setCameraOptions(CAMERA_OPTIONS)
34+
editor.setCamera(editor.getCamera(), {
35+
immediate: true,
36+
})
37+
// editor.updateInstanceState({
38+
// isGridMode: true,
39+
// })
40+
})
41+
}, [editor])
42+
43+
return null
44+
})
45+
46+
const TICKS = 8
47+
48+
const CartesianGrid = memo(function CartesianGrid() {
49+
return (
50+
<svg
51+
className="cartesian-grid"
52+
width="600"
53+
height="600"
54+
viewBox="0 0 600 600"
55+
stroke="#aaa"
56+
color="#aaa"
57+
>
58+
{Array.from({ length: TICKS * 2 + 1 }).map((_, i) => {
59+
const step = 600 / (TICKS * 2)
60+
const opacity = i === TICKS ? 1 : 0.16
61+
return (
62+
<g key={i + '_line'}>
63+
<line x1={0} y1={i * step} x2={600} y2={i * step} strokeWidth="1" opacity={opacity} />
64+
<line x1={i * step} y1={0} x2={i * step} y2={600} strokeWidth="1" opacity={opacity} />
65+
</g>
66+
)
67+
})}
68+
<g>
69+
{Array.from({ length: TICKS * 2 + 1 }).map((_, i) => {
70+
const index = i
71+
if (i - TICKS === 0) return null
72+
const y = 600 - index * (600 / (TICKS * 2))
73+
return (
74+
<g key={i + '_textx'}>
75+
<text
76+
key={index}
77+
x={312}
78+
y={y}
79+
dy="0.3em"
80+
fontFamily="Arial"
81+
textAnchor="start"
82+
letterSpacing=".25em"
83+
stroke="none"
84+
fill="#aaa"
85+
fontWeight="bold"
86+
>
87+
{-TICKS + index}
88+
</text>
89+
<line x1={295} y1={y} x2={305} y2={y} strokeWidth="2" />
90+
</g>
91+
)
92+
})}
93+
{Array.from({ length: TICKS * 2 + 1 }).map((_, i) => {
94+
const index = i
95+
if (i - TICKS === 0) return null
96+
const x = index * (600 / (TICKS * 2))
97+
return (
98+
<g key={i + '_texty'}>
99+
<text
100+
key={index}
101+
x={x}
102+
y={320}
103+
dy="0.3em"
104+
fontFamily="Arial"
105+
textAnchor="middle"
106+
stroke="none"
107+
fill="#aaa"
108+
fontWeight="bold"
109+
>
110+
{-TICKS + index}
111+
</text>
112+
<line x1={x} y1={295} x2={x} y2={305} strokeWidth="2" strokeLinecap="round" />
113+
</g>
114+
)
115+
})}
116+
</g>
117+
</svg>
118+
)
119+
})
120+
121+
const components: TLComponents = {
122+
OnTheCanvas: () => <CartesianGrid />,
123+
}
124+
125+
export default function EducationCanvasExample() {
126+
const [answers, setAnswers] = useState({
127+
partB: '',
128+
partC: '',
129+
})
130+
131+
const handleAnswerChange = (part: keyof typeof answers, value: string) => {
132+
setAnswers((prev) => ({ ...prev, [part]: value }))
133+
}
134+
135+
const rEditor = useRef<Editor | null>(null)
136+
const handleMount = useCallback((editor: Editor) => {
137+
rEditor.current = editor
138+
}, [])
139+
140+
const handleSubmit = useCallback(async () => {
141+
// Normalize answers for comparison
142+
const normalizeAnswer = (answer: string) => {
143+
return answer.toLowerCase().replace(/[^a-z0-9(),.-]/g, '')
144+
}
145+
146+
// Check Part B - Area (accept 8, 8 square units, 8 units², etc.)
147+
const normalizedB = normalizeAnswer(answers.partB)
148+
const isPartBCorrect =
149+
normalizedB.includes('8') &&
150+
(normalizedB.includes('square') || normalizedB.includes('unit') || normalizedB === '8')
151+
152+
// Check Part C - Coordinates (accept (0,7), (0, 7), 0,7, etc.)
153+
const normalizedC = normalizeAnswer(answers.partC)
154+
const isPartCCorrect =
155+
normalizedC.includes('0') &&
156+
normalizedC.includes('7') &&
157+
(normalizedC.includes('(0,7)') ||
158+
normalizedC.includes('0,7') ||
159+
normalizedC.match(/0.*7/) ||
160+
normalizedC.match(/7.*0/))
161+
162+
if (isPartBCorrect && isPartCCorrect) {
163+
alert('Good job! Both answers are correct!')
164+
} else if (isPartBCorrect || isPartCCorrect) {
165+
let message = 'Good progress! '
166+
if (isPartBCorrect) message += 'Part B is correct. '
167+
if (isPartCCorrect) message += 'Part C is correct. '
168+
if (!isPartBCorrect) message += 'Check your area calculation for Part B.'
169+
if (!isPartCCorrect) message += 'Check your coordinates for Part C.'
170+
alert(message)
171+
} else {
172+
alert('Please check your answers and try again.')
173+
}
174+
175+
// Do something with the answers
176+
const editor = rEditor.current
177+
if (editor) {
178+
// For example, get the canvas content and the answers and send it to the server.
179+
// const result = {
180+
// answers: {
181+
// partA: await editor.toImage(editor.getCurrentPageShapes()),
182+
// partB: answers.partB,
183+
// partC: answers.partC,
184+
// },
185+
// }
186+
// console.log(result)
187+
}
188+
}, [answers])
189+
190+
return (
191+
<div className="education-container">
192+
{/* Question Panel - Left Half */}
193+
<div className="question-panel">
194+
<div className="question-content">
195+
<h1 className="main-title">Mathematics - Geometry</h1>
196+
197+
<div className="question-card">
198+
<h2 className="question-title">Question 1</h2>
199+
<p className="question-text">
200+
A triangle ABC has vertices at A(2, 3), B(6, 3), and C(4, 7).
201+
</p>
202+
203+
<div className="question-part">
204+
<p className="question-text">
205+
<strong>Part A:</strong> Draw triangle ABC on the coordinate grid.
206+
</p>
207+
</div>
208+
209+
<div className="question-part">
210+
<p className="question-text">
211+
<strong>Part B:</strong> Calculate the area of triangle ABC.
212+
</p>
213+
<div className="answer-input-group">
214+
<label className="answer-label">
215+
<strong>Answer:</strong>
216+
</label>
217+
<input
218+
type="text"
219+
className="answer-input"
220+
placeholder="Enter the area (include units)"
221+
value={answers.partB}
222+
onChange={(e) => handleAnswerChange('partB', e.target.value)}
223+
/>
224+
</div>
225+
</div>
226+
227+
<div className="question-part">
228+
<p className="question-text">
229+
<strong>Part C:</strong> Find the coordinates of point D such that ABCD forms a
230+
parallelogram.
231+
</p>
232+
<div className="answer-input-group">
233+
<label className="answer-label">
234+
<strong>Answer:</strong>
235+
</label>
236+
<input
237+
type="text"
238+
className="answer-input"
239+
placeholder="Enter coordinates as (x, y)"
240+
value={answers.partC}
241+
onChange={(e) => handleAnswerChange('partC', e.target.value)}
242+
/>
243+
</div>
244+
</div>
245+
246+
<button className="submit-button" onClick={handleSubmit}>
247+
Submit Answers
248+
</button>
249+
</div>
250+
251+
<div className="instructions-card">
252+
<h3 className="instructions-title">Instructions:</h3>
253+
<ul className="instructions-list">
254+
<li>Use the drawing canvas on the right to sketch your solution</li>
255+
<li>
256+
You can use the draw tool <kbd>D</kbd> to draw points and the line tool <kbd>L</kbd>{' '}
257+
to draw lines
258+
</li>
259+
<li>
260+
Use the text tool <kbd>T</kbd> to label points and write calculations
261+
</li>
262+
<li>Show all your working clearly</li>
263+
<li>Enter your final answers in the answer boxes above</li>
264+
</ul>
265+
</div>
266+
</div>
267+
</div>
268+
269+
{/* Canvas Panel - Right Half */}
270+
<div className="canvas-panel">
271+
<div className="canvas-container">
272+
<Tldraw persistenceKey="education-canvas" components={components} onMount={handleMount}>
273+
<CameraSetup />
274+
</Tldraw>
275+
</div>
276+
</div>
277+
</div>
278+
)
279+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
title: Education Canvas
3+
component: ./EducationCanvasExample.tsx
4+
category: use-cases
5+
priority: 1
6+
keywords: [education, math, geometry, GCSE, teaching, learning, canvas, fixed camera]
7+
---
8+
9+
An educational application template with a math question on the left and a drawing canvas on the right.
10+
11+
---
12+
13+
This example demonstrates how to create an educational application using tldraw. It features:
14+
15+
- **Split Layout**: Question panel on the left, drawing canvas on the right
16+
- **Fixed Camera**: Canvas has constrained bounds to keep students focused on the drawing area
17+
- **GCSE Math Question**: A geometry problem suitable for GCSE-level mathematics
18+
- **Grid Background**: Coordinate grid to help with plotting points and shapes
19+
- **Educational Styling**: Clean, professional styling suitable for educational environments
20+
21+
Perfect for creating interactive math worksheets, geometry exercises, or any educational content that requires visual problem-solving.

0 commit comments

Comments
 (0)