• Docs
  • Install
  • Follow @cosplayengine
  • v.0.9.5
  • GitHub
  • Watch
  • Examples
  1. Home
  2. Canvas Drawing

Canvas Drawing

  • Introduction
  • Install
  • ASCII Games
  • Demos
  • Examples
  • Macarena
  • Pong
  • Snake
  • Bird
  • Developer Guide
  • Quick Game 🏃
  • Key Concepts
  • Game Structure
  • Scenes & Objects
  • Log & Debugging
  • Pixels & Colors
  • Images
  • Keyboard Input
  • Sprite Animation
  • Shaders
  • Particle Effects
  • Fonts
  • Canvas Drawing
  • Text Input
  • Camera Tracking
  • Tile Mapping
  • Audio
  • Video
  • UI Toolkit
  • Build & Run

Canvas Drawing

In CosPlay, class CPCanvas defines a 2D rendering pane. To display something on the terminal - it needs to be rendered on the canvas.

For each game frame, the game engine creates a new empty canvas for all scene objects to draw on. Note that in most cases, the canvas dimension matches the scene dimension. This canvas is available to scene objects via CPSceneObjectContext.getCanvas method. Game engine then compares previous canvas and this one to determine which areas of the terminal need redrawing. You can also create the canvas object outside game loop using companion object methods.

Canvas (0,0) coordinate point is located in the top left corner. X-axis goes to the right and Y-axis points down. Every drawn pixel on the canvas also has a Z-index or depth. Pixel with larger or equal Z-index visually overrides the pixel with the smaller Z-index, i.e. Z-axis is pointing away from the screen towards the observer:

         ,*-- -- -- -- -- X
        / | (0,0)
      .'  |
     /    |
   .'     |
  / Z     | Y
        

ASCII-based graphics are vastly different from the traditional raster-based graphics. Since ASCII-based drawing uses printable characters from ASCII character set that are displayed on basic text terminal most differences are obvious but some are more subtle and unexpected. For example, while straight vertical and horizontal lines are easy to implement many curved lines are much trickier to draw. Specifically, circles and ellipses, for example, are hard to do properly in automated way. Moreover, many complex curves need to be done manually, especially if they are dynamically redrawn on each frame update.

For example, consider the following subtle curvatures (borrowed from the excellent ASCII art tutorial by Richard Crawford):

                     /               /             /              /              /               /
                ____/            _.-~          _,-~           _.-'           _.-'            _.-'
               /               ,^            ,^             ,"             ,`              ,'
              /               /             /              /              /               /
             /               /             /              /              /               /
        ____/           __.-~         __,-~          __.-"          __.-'           __.-`
        

First line is a default naive drawing. Second and third curves differ only in use of ',' vs. '.' (line 2 and line 6). This is just one of several stylistic modifications one could make - in this particular case, use of ',' make the curvature appear more angular at the connection point. Later options make the curve even smoother by utilising ''', '"' and '`' characters. Last line looks particular smooth.

Class CPCanvas provides many methods for basic line and rectangular drawing, circle and polylines, anti-aliasing, "color" fill in, and much more. Most functions have multiple overridden variants with different parameters so that they can be easily used in different contexts. Note also that this class deals primarily with line ASCII art and has only few functions like antialiasing for the solid ASCII art.

Here's the code snippet from the built-in Canvas Example that demonstrates some of the CPCanvas API:

        // Color shortcuts.
        val c1 = C_STEEL_BLUE1
        val c2 = C_INDIAN_RED
        val c3 = C_LIGHT_STEEL_BLUE

        val spr = new CPCanvasSprite():
            override def render(ctx: CPSceneObjectContext): Unit =
                val canv = ctx.getCanvas

                // Draw image.
                canv.drawImage(alienImg, 2, 2, 0)
                canv.drawString(5, alienImg.getHeight + 4, 0, "Image", c1)

                // Draw a basic polyline shape.
                canv.drawPolyline(Seq(
                    15 -> 2,
                    25 -> 3,
                    30 -> 15,
                    22 -> 17,
                    18 -> 5,
                    15 -> 2,
                ), 100, '*'&c3)

                // Fill this shape with random color on each frame.
                canv.fill(17, 3, _.char == '*', (_, _) => CPRand.rand(pxs))
                canv.drawString(18, 19, 0, "Filled shape", c1)

                // Draw ASCII-art style polyline shape.
                canv.drawArtPolyline(Seq(
                    32 -> 2,
                    42 -> 3,
                    47 -> 15,
                    39 -> 17,
                    35 -> 5,
                    32 -> 2,
                ), 100, _.px.withFg(c3), ART_SMOOTH)

                // Fill this ASCII-art shape with dark grey 'x'.
                canv.fill(35, 4, _.px != bgPx, (_, _) => 'x'&C_GRAY3)
                canv.drawString(34, 19, 0, "Filled smooth shape", c1)

                // Draw antialias circle.
                val circRect = canv.drawCircle(75, 12, 10, z = 0, 2f, 1f, 0.5f, true, 'x'&c2)
                canv.antialias(circRect, _.char != 'x')

                // Draw a panel with titled border.
                canv.fillRect(3, 27, 40, 37, 0, Seq(' '&C_BLACK))
                canv.drawSimpleBorder(
                    2, 26, 41, 38, 0,
                    '|'&c3, '-'&c3, '+'&c1,
                    styleStr("/ ", c3) ++ styleStr("Titled Panel", C_INDIAN_RED) ++ styleStr(" /", c3), 5, 26
                )
        

NOTES:

  • Note that we use CPCanvasSprite which is a convenient scene object implementation when you just need to draw something on the screen.
  • On line 8 we obtain the current instance of the canvas, i.e. the canvas on which all scene objects of the current scene will draw themselves.

Canvas Animation

Note that you can implement simple animation by re-drawing the canvas differently on each frame update. In the above example we draw a polyline shape on the line 15 and then fill it in with a random color on each frame (line 25). This results in a nice flickering effect - the sort of animation that is ideally suited for this type of implementation, a literally one-line animation implementation.

Here is the result animation from the built-in Canvas Example:

Drawn Images

One of the useful techniques of obtaining a complex "drawn-on-canvas" images is to create a new canvas object at the scene's start (see CPLifecycle.onStart() method), draw all necessary ASCII art on it and then capture the image using one of the capture() methods in CPCanvas class. Once image is obtained you can use it with any image-based sprites to display it.

ASCII Art Drawing & Resources

There are many online tutorials for ASCII art that are highly recommended, including from:

  • Joan Stark
  • Daniel Au
  • Susie Oviatt
  • Rowan Crawford
  • Normand Veilleux
  • Targon
  • Hayley Wakenshaw

See also the following resources for general ASCII art collections:

  • http://www.asciiworld.com/
  • https://www.asciiart.eu/
  • https://asciiart.website
  • https://www.incredibleart.org/links/ascii.html
  • http://blocktronics.org/
  • http://ansiart.com/
  • http://www.ascii-art.de/
  • https://llizardakaejm.wordpress.com/ascii-animations/
  • On This Page
  • Canvas Drawing
  • Canvas Animation
  • Drawn Images
  • Resources
  • Example
  • Canvas Example
  • Quick Links
  • Discord
  • Stack Overflow
  • GitHub
  • @cosplayengine
  • YouTube
  • API
Copyright © 2023 Rowan Games, Inc. Privacy • Docs release: 0.9.5 Latest: