@edadma/logo 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@edadma/logo",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "Logo programming language interpreter with React component",
5
- "main": "dist/logo.js",
6
- "module": "dist/logo.js",
7
- "types": "dist/types/index.d.ts",
5
+ "main": "dist/main.js",
6
+ "module": "dist/main.js",
7
+ "types": "src/index.d.ts",
8
8
  "files": [
9
9
  "dist",
10
+ "src",
10
11
  "react"
11
12
  ],
12
13
  "exports": {
13
14
  ".": {
14
- "import": "./dist/logo.js",
15
- "types": "./dist/types/index.d.ts"
15
+ "import": "./dist/main.js",
16
+ "types": "./src/index.d.ts"
16
17
  },
17
18
  "./react": {
18
19
  "import": "./react/index.ts",
@@ -21,7 +22,7 @@
21
22
  },
22
23
  "scripts": {
23
24
  "build": "npm run build:scala && npm run build:react",
24
- "build:scala": "cd .. && sbt logoJS/fullLinkJS && cp js/target/scala-3.7.4/logo-opt/main.js js/dist/logo.js",
25
+ "build:scala": "cd .. && sbt logoJS/fullLinkJS && cp js/target/scala-3.7.4/logo-opt/main.js js/target/scala-3.7.4/logo-opt/main.js.map js/dist/",
25
26
  "build:react": "tsc --project tsconfig.react.json",
26
27
  "prepublishOnly": "npm run build"
27
28
  },
package/src/Logo.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ /** Drawing data returned by the Logo interpreter */
2
+ export interface LogoDrawing {
3
+ lines: Array<{
4
+ x1: number
5
+ y1: number
6
+ x2: number
7
+ y2: number
8
+ color: string
9
+ width: number
10
+ }>
11
+ labels: Array<{
12
+ x: number
13
+ y: number
14
+ heading: number
15
+ text: string
16
+ }>
17
+ }
18
+
19
+ /** Current state of the turtle */
20
+ export interface TurtleState {
21
+ x: number
22
+ y: number
23
+ heading: number
24
+ visible: boolean
25
+ }
26
+
27
+ /** Logo interpreter class from Scala.js */
28
+ export declare class Logo {
29
+ constructor(canvas: HTMLCanvasElement)
30
+
31
+ /** Run a complete Logo program */
32
+ run(program: string): void
33
+
34
+ /** Execute a single Logo command */
35
+ execute(command: string): void
36
+
37
+ /** Clear the canvas and reset turtle position */
38
+ clear(): void
39
+
40
+ /** Render the current drawing to the canvas */
41
+ render(): void
42
+
43
+ /** Enable or disable path-based rendering (smoother curves) */
44
+ setPathRendering(enabled: boolean): void
45
+
46
+ /** Enable or disable automatic rendering after each command */
47
+ setAutoRender(enabled: boolean): void
48
+
49
+ /** Set the canvas background color */
50
+ setBackgroundColor(color: string): void
51
+
52
+ /** Set the default pen color (used after clear) */
53
+ setForegroundColor(color: string): void
54
+
55
+ /** Set a callback for print output */
56
+ setOutputHandler(handler: (text: string) => void): void
57
+
58
+ /** Get the current drawing data */
59
+ getDrawing(): LogoDrawing
60
+
61
+ /** Get the current turtle state */
62
+ getTurtle(): TurtleState
63
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { Logo, type LogoDrawing, type TurtleState } from './Logo'
@@ -0,0 +1,307 @@
1
+ package io.github.edadma.logo
2
+
3
+ import scala.scalajs.js
4
+ import scala.scalajs.js.annotation.*
5
+ import org.scalajs.dom
6
+ import org.scalajs.dom.html
7
+
8
+ import scala.math.Pi
9
+
10
+ @js.native
11
+ trait LogoDrawing extends js.Object:
12
+ val lines: js.Array[LineData] = js.native
13
+ val labels: js.Array[LabelData] = js.native
14
+ val arcs: js.Array[ArcData] = js.native
15
+
16
+ @js.native
17
+ trait ArcData extends js.Object:
18
+ val x: Double = js.native
19
+ val y: Double = js.native
20
+ val heading: Double = js.native
21
+ val angle: Double = js.native
22
+ val radius: Double = js.native
23
+ val color: String = js.native
24
+ val width: Double = js.native
25
+
26
+ @js.native
27
+ trait LineData extends js.Object:
28
+ val x1: Double = js.native
29
+ val y1: Double = js.native
30
+ val x2: Double = js.native
31
+ val y2: Double = js.native
32
+ val color: String = js.native
33
+ val width: Double = js.native
34
+
35
+ @js.native
36
+ trait LabelData extends js.Object:
37
+ val x: Double = js.native
38
+ val y: Double = js.native
39
+ val heading: Double = js.native
40
+ val text: String = js.native
41
+
42
+ @js.native
43
+ trait TurtleState extends js.Object:
44
+ val x: Double = js.native
45
+ val y: Double = js.native
46
+ val heading: Double = js.native
47
+ val visible: Boolean = js.native
48
+
49
+ @JSExportTopLevel("Logo")
50
+ class LogoJS(canvas: html.Canvas) extends js.Object:
51
+ private val ctx = canvas.getContext("2d").asInstanceOf[dom.CanvasRenderingContext2D]
52
+ private var usePathRendering: Boolean = true
53
+ private var autoRender: Boolean = true
54
+ private var initialized: Boolean = false
55
+ private var backgroundColor: String = "white"
56
+
57
+ private val logo = new Logo:
58
+ def event(): Unit = if initialized && autoRender then render()
59
+
60
+ initialized = true
61
+
62
+ /** Run a Logo program */
63
+ def run(program: String): Unit =
64
+ logo.interp(program)
65
+ if !autoRender then render()
66
+
67
+ /** Execute a single command */
68
+ def execute(command: String): Unit =
69
+ logo.interp(command)
70
+ if !autoRender then render()
71
+
72
+ /** Clear the screen and reset turtle */
73
+ def clear(): Unit =
74
+ logo.clearscreen()
75
+ render()
76
+
77
+ /** Set whether to use path-based rendering (smoother) or line-based */
78
+ def setPathRendering(enabled: Boolean): Unit =
79
+ usePathRendering = enabled
80
+ render()
81
+
82
+ /** Set whether to auto-render after each command */
83
+ def setAutoRender(enabled: Boolean): Unit =
84
+ autoRender = enabled
85
+
86
+ /** Set the canvas background color */
87
+ def setBackgroundColor(color: String): Unit =
88
+ backgroundColor = color
89
+ render()
90
+
91
+ /** Set the default pen color (used after clear) */
92
+ def setForegroundColor(color: String): Unit =
93
+ val rgb = parseColor(color)
94
+ logo.setDefaultColor(rgb)
95
+ render()
96
+
97
+ /** Parse a CSS color string to RGB tuple */
98
+ private def parseColor(color: String): (Int, Int, Int) =
99
+ // Use canvas to parse any CSS color
100
+ ctx.fillStyle = color
101
+ val parsed = ctx.fillStyle.asInstanceOf[String]
102
+ if parsed.startsWith("#") then
103
+ val hex = parsed.drop(1)
104
+ if hex.length == 6 then
105
+ val r = Integer.parseInt(hex.substring(0, 2), 16)
106
+ val g = Integer.parseInt(hex.substring(2, 4), 16)
107
+ val b = Integer.parseInt(hex.substring(4, 6), 16)
108
+ (r, g, b)
109
+ else (0, 0, 0)
110
+ else (0, 0, 0)
111
+
112
+ /** Set a callback for print output (instead of console) */
113
+ def setOutputHandler(handler: js.Function1[String, Unit]): Unit =
114
+ logo.setOutputHandler(s => handler(s))
115
+
116
+ /** Clear the output handler (print goes to console) */
117
+ def clearOutputHandler(): Unit =
118
+ logo.clearOutputHandler()
119
+
120
+ /** Force a render */
121
+ def render(): Unit =
122
+ val width = canvas.width
123
+ val height = canvas.height
124
+
125
+ // Clear canvas
126
+ ctx.fillStyle = backgroundColor
127
+ ctx.fillRect(0, 0, width, height)
128
+
129
+ // Set up coordinate system (origin at center, y-up)
130
+ ctx.save()
131
+ ctx.translate(width / 2.0, height / 2.0)
132
+ ctx.scale(1, -1)
133
+
134
+ if usePathRendering then renderWithPaths()
135
+ else renderWithLines()
136
+
137
+ // Draw turtle
138
+ logo.turtle match
139
+ case Some((x, y, heading)) => drawTurtle(x, y, heading)
140
+ case None =>
141
+
142
+ ctx.restore()
143
+
144
+ /** Get the current drawing as data (for custom rendering) */
145
+ def getDrawing(): LogoDrawing =
146
+ val lines = js.Array[LineData]()
147
+ val labels = js.Array[LabelData]()
148
+ val arcs = js.Array[ArcData]()
149
+
150
+ logo.drawing.foreach {
151
+ case DrawLine(x1, y1, x2, y2, (r, g, b), width) =>
152
+ lines.push(js.Dynamic.literal(
153
+ x1 = x1, y1 = y1, x2 = x2, y2 = y2,
154
+ color = s"rgb($r,$g,$b)", width = width
155
+ ).asInstanceOf[LineData])
156
+ case DrawLabel(x, y, heading, text) =>
157
+ labels.push(js.Dynamic.literal(
158
+ x = x, y = y, heading = heading, text = text
159
+ ).asInstanceOf[LabelData])
160
+ case DrawArc(x, y, heading, angle, radius, (r, g, b), width) =>
161
+ arcs.push(js.Dynamic.literal(
162
+ x = x, y = y, heading = heading, angle = angle, radius = radius,
163
+ color = s"rgb($r,$g,$b)", width = width
164
+ ).asInstanceOf[ArcData])
165
+ }
166
+
167
+ js.Dynamic.literal(lines = lines, labels = labels, arcs = arcs).asInstanceOf[LogoDrawing]
168
+
169
+ /** Get the current turtle state */
170
+ def getTurtle(): TurtleState =
171
+ logo.turtle match
172
+ case Some((x, y, heading)) =>
173
+ js.Dynamic.literal(x = x, y = y, heading = heading, visible = true).asInstanceOf[TurtleState]
174
+ case None =>
175
+ js.Dynamic.literal(x = 0, y = 0, heading = Pi / 2, visible = false).asInstanceOf[TurtleState]
176
+
177
+ private def renderWithPaths(): Unit =
178
+ case class Style(color: (Int, Int, Int), width: Double)
179
+
180
+ var currentStyle: Option[Style] = None
181
+ var pathStarted = false
182
+ var lastX: Double = 0
183
+ var lastY: Double = 0
184
+
185
+ def flushPath(): Unit =
186
+ if pathStarted && currentStyle.isDefined then
187
+ val Style((r, g, b), width) = currentStyle.get
188
+ ctx.strokeStyle = s"rgb($r,$g,$b)"
189
+ ctx.lineWidth = width
190
+ ctx.lineCap = "round"
191
+ ctx.lineJoin = "round"
192
+ ctx.stroke()
193
+ pathStarted = false
194
+ currentStyle = None
195
+
196
+ logo.drawing.foreach {
197
+ case DrawLine(x1, y1, x2, y2, color, width) =>
198
+ val style = Style(color, width)
199
+
200
+ if !currentStyle.contains(style) then
201
+ flushPath()
202
+ currentStyle = Some(style)
203
+ ctx.beginPath()
204
+ ctx.moveTo(x1, y1)
205
+ pathStarted = true
206
+ lastX = x1
207
+ lastY = y1
208
+
209
+ // Check for discontinuity
210
+ if !pathStarted || lastX != x1 || lastY != y1 then
211
+ if !pathStarted then
212
+ ctx.beginPath()
213
+ pathStarted = true
214
+ ctx.moveTo(x1, y1)
215
+
216
+ ctx.lineTo(x2, y2)
217
+ lastX = x2
218
+ lastY = y2
219
+
220
+ case DrawArc(x, y, heading, angleDeg, radius, color, width) =>
221
+ flushPath()
222
+ renderArc(x, y, heading, angleDeg, radius, color, width)
223
+
224
+ case DrawLabel(x, y, heading, text) =>
225
+ flushPath()
226
+ renderLabel(x, y, heading, text)
227
+ }
228
+
229
+ flushPath()
230
+
231
+ private def renderWithLines(): Unit =
232
+ logo.drawing.foreach {
233
+ case DrawLine(x1, y1, x2, y2, (r, g, b), width) =>
234
+ ctx.strokeStyle = s"rgb($r,$g,$b)"
235
+ ctx.lineWidth = width
236
+ ctx.beginPath()
237
+ ctx.moveTo(x1, y1)
238
+ ctx.lineTo(x2, y2)
239
+ ctx.stroke()
240
+
241
+ case DrawArc(x, y, heading, angleDeg, radius, (r, g, b), width) =>
242
+ renderArc(x, y, heading, angleDeg, radius, (r, g, b), width)
243
+
244
+ case DrawLabel(x, y, heading, text) =>
245
+ renderLabel(x, y, heading, text)
246
+ }
247
+
248
+ private def renderLabel(x: Double, y: Double, heading: Double, text: String): Unit =
249
+ ctx.save()
250
+ ctx.translate(x, y)
251
+ ctx.rotate(heading)
252
+ ctx.scale(1, -1)
253
+ ctx.fillStyle = "black"
254
+ ctx.font = "20px sans-serif"
255
+ ctx.fillText(text, 0, 0)
256
+ ctx.restore()
257
+
258
+ private def renderArc(x: Double, y: Double, heading: Double, angleDeg: Double, radius: Double, color: (Int, Int, Int), width: Double): Unit =
259
+ val (r, g, b) = color
260
+ // For positive angle: center is to the left of turtle (perpendicular)
261
+ // For negative angle: center is to the right
262
+ val sign = if angleDeg >= 0 then 1.0 else -1.0
263
+ val absAngle = math.abs(angleDeg)
264
+
265
+ // Center is perpendicular to turtle's heading
266
+ // heading is in radians (internal: 0=east, CCW positive)
267
+ // Perpendicular to the left: heading + π/2
268
+ val perpAngle = heading + sign * Pi / 2
269
+ val cx = x + radius * math.cos(perpAngle)
270
+ val cy = y + radius * math.sin(perpAngle)
271
+
272
+ // Start angle: from center to turtle position
273
+ val startAngle = math.atan2(y - cy, x - cx)
274
+
275
+ // End angle: sweep by angleDeg (converted to radians)
276
+ // Positive angleDeg = CCW on circle (which is forward/right from turtle's view)
277
+ val sweepRad = math.toRadians(absAngle)
278
+ val endAngle = if angleDeg >= 0 then startAngle - sweepRad else startAngle + sweepRad
279
+
280
+ ctx.strokeStyle = s"rgb($r,$g,$b)"
281
+ ctx.lineWidth = width
282
+ ctx.lineCap = "round"
283
+ ctx.beginPath()
284
+ // counterclockwise parameter: true for positive angle (sweep is subtracted)
285
+ ctx.arc(cx, cy, radius, startAngle, endAngle, angleDeg >= 0)
286
+ ctx.stroke()
287
+
288
+ private def drawTurtle(x: Double, y: Double, heading: Double): Unit =
289
+ val w = 15.0
290
+ val h = 20.0
291
+
292
+ ctx.save()
293
+ ctx.translate(x, y)
294
+ ctx.rotate(heading - Pi / 2)
295
+
296
+ ctx.beginPath()
297
+ ctx.moveTo(0, 0)
298
+ ctx.lineTo(-w / 2, h / 2)
299
+ ctx.lineTo(0, h)
300
+ ctx.lineTo(w / 2, h / 2)
301
+ ctx.closePath()
302
+
303
+ ctx.strokeStyle = "green"
304
+ ctx.lineWidth = 2
305
+ ctx.stroke()
306
+
307
+ ctx.restore()