@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/README.md +30 -0
- package/dist/main.js +71205 -0
- package/dist/main.js.map +8 -0
- package/package.json +8 -7
- package/src/Logo.d.ts +63 -0
- package/src/index.d.ts +1 -0
- package/src/main/scala/io/github/edadma/logo/LogoJS.scala +307 -0
- package/dist/logo.js +0 -48901
package/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edadma/logo",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Logo programming language interpreter with React component",
|
|
5
|
-
"main": "dist/
|
|
6
|
-
"module": "dist/
|
|
7
|
-
"types": "
|
|
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/
|
|
15
|
-
"types": "./
|
|
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/
|
|
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()
|