If you've drawn on an HTML <canvas>, Codetoy will feel familiar. Its drawing API is a trimmed-down CanvasRenderingContext2D, and its loop is requestAnimationFrame with the boilerplate removed. There's no HTML, no bundler, no getContext, and no resize handling to write.
Use AssemblyScript
AssemblyScript is TypeScript syntax compiled to WebAssembly, so you can read it immediately. It's also the language behind Builds and Discord Activities. The snippets below follow the sidebar's language; leave it on AssemblyScript for the closest match.
#What you don't have to write
const canvas = document.querySelector("canvas")
const ctx = canvas.getContext("2d")
function resize() {
canvas.width = innerWidth * devicePixelRatio
canvas.height = innerHeight * devicePixelRatio
ctx.scale(devicePixelRatio, devicePixelRatio)
}
addEventListener("resize", resize)
resize()
let last = performance.now()
function frame(now) {
const dt = (now - last) / 1000
last = now
ctx.fillStyle = "#14141e"
ctx.fillRect(0, 0, innerWidth, innerHeight)
ctx.fillStyle = "#64b4ff"
ctx.beginPath()
ctx.arc(mouse.x, mouse.y, 24, 0, Math.PI * 2)
ctx.fill()
requestAnimationFrame(frame)
}
requestAnimationFrame(frame)Codetoy handles high-DPI scaling, resizing, the frame loop and timing for you.
#Canvas API mapping
CanvasRenderingContext2D |
Codetoy |
|---|---|
ctx.fillStyle = "…" |
fillStyle()Canvas.FillStyle()fillStyle(), or fill()Canvas.Fill()fill() with RGB(A) |
ctx.strokeStyle = "…" |
strokeStyle()Canvas.StrokeStyle()strokeStyle(), or stroke()Canvas.Stroke()stroke() |
ctx.fillRect / ctx.roundRect |
rect()Canvas.Rect()rect() (fifth argument is the corner radius) |
ctx.arc(…, 0, 2π) + fill() |
circle()Canvas.Circle()circle() |
ctx.ellipse |
ellipse()Canvas.Ellipse()ellipse() |
ctx.moveTo + lineTo + stroke() |
line()Canvas.Line()line() |
ctx.lineWidth = n |
lineWidth()Canvas.LineWidth()lineWidth() |
ctx.lineCap, lineJoin, miterLimit |
lineCap()Canvas.LineCap()lineCap(), lineJoin()Canvas.LineJoin()lineJoin(), lineMiterLimit()Canvas.LineMiterLimit()lineMiterLimit() |
ctx.save() / ctx.restore() |
push()Canvas.Push()push() / pop()Canvas.Pop()pop() |
ctx.translate / rotate / scale |
translate()Canvas.Translate()translate() / rotate()Canvas.Rotate()rotate() / scale()Canvas.Scale()scale() |
ctx.resetTransform() |
resetTransform()Canvas.ResetTransform()resetTransform() |
ctx.beginPath … fill() / stroke() |
beginPath()Canvas.BeginPath()beginPath() … fillPath()Canvas.FillPath()fillPath() / strokePath()Canvas.StrokePath()strokePath() |
ctx.font = "bold 24px Arial" |
font()Canvas.Font()font() with "Arial", 24, "bold" |
ctx.fillText |
text()Canvas.Text()text() (top-left aligned) |
ctx.measureText(s).width |
measureText()Canvas.MeasureText()measureText() |
ctx.reset() |
reset()Canvas.Reset()reset() |
Two differences to know:
- Shapes fill and stroke in one call.
rect()Canvas.Rect()rect(),circle()Canvas.Circle()circle()and friends use the current fill and stroke (a thin black outline by default). CallnoStroke()Canvas.NoStroke()noStroke()ornoFill()Canvas.NoFill()noFill()to turn either off. - Text is drawn from its top-left corner, as if
textBaseline = "top".
#Events
| DOM | Codetoy |
|---|---|
requestAnimationFrame loop |
export function update()Screen.OnUpdate()def update() |
keydown / keyup |
export function onKeyDown()Input.OnKeyDown()def keyDown() / export function onKeyUp()Input.OnKeyUp()def keyUp() (same event.key names) |
Tracking pressed keys in a Set |
input.isKeyDown()Input.IsKeyDown()isKeyDown() |
mousemove → clientX/Y |
input.mouseX()Input.MouseXmouseX(), input.mouseY()Input.MouseYmouseY() (already in canvas pixels) |
mousedown / mouseup |
export function onMouseDown()Input.OnMouseDown()def mouseDown() / export function onMouseUp()Input.OnMouseUp()def mouseUp() (same event.button numbers) |
touchstart / touchmove / touchend |
export function onTouchStart()Input.OnTouchStart()def touchStart() / export function onTouchMove()Input.OnTouchMove()def touchMove() / export function onTouchEnd()Input.OnTouchEnd()def touchEnd() |
resize |
export function resize()not in C#def resize() |
console.log |
console.log()Console.Log()print() |
#TypeScript vs. AssemblyScript
AssemblyScript looks like TypeScript but compiles ahead of time to WebAssembly, so it's stricter:
- Numbers are concrete types:
f64(like JSnumber),i32,u8and so on.let x = 0is ani32, so writelet x: f64 = 0. - No
any,undefined, or union types (except nullable classes likeEnemy | null). - Closures can't capture local variables; use top-level variables or class fields.
- No DOM,
fetchorsetTimeout: everything goes through Codetoy's API. - Bonus: operator overloading with
@operator("+").
More in Language Notes and the AssemblyScript docs.
#From prototype to published
Because AssemblyScript compiles to a compact .wasm, finished games can be captured as a build: a hosted, game-only page at https://<build-id>.codetoy.dev/. You can embed it on your own site or run it as a Discord Activity.