JS

Nambafa

Graphics tutorials


Downloads: tut_01.zip

[ Nambafa's pixel graphics tutorial : Part 1 ]

Overview

This part of the tutorial will cover the basic elements that we will use to make pixel graphics:

  • a pixel buffer where the colour data is stored
  • a device where the pixel buffer can be displayed
  • an applet that runs any updating procedures (including thread and timer)

This is very similar to how the screen memory controlled the electron beam on CRT monitors back in the early days of computing, with an executable file updating the buffer between frames.

Since we will be using JavaScript and the web browser in this tutorial, our display device will be the html canvas instead of the monitor, which the browser/operating system then will use to update the physical display.

Key takeaways

After reading this tutorial, you should hopefully have learned:

  • how the pixel buffer is organised and that it holds the colors of all the display elements
  • how the function putpixel(x,y,rgbx) can be used to set the colour of individual pixels
  • that the html canvas element will be used as our output display device (the "screen")
  • that the running applet will continously update the display by calling the update() function

To start playing with some code, all you need to do is to:

  • download the (simple) code that takes care of the logistics
  • create a canvas element where the output will be displayed
  • make an applet and add code to the update() function to manipulate the pixels
  • initiate the applet to see the resulting graphics on the canvas

(Further down - in the "Command line interface" section - there is also a description of how to modify the pixels interactively from the console - which you already can do here by using the demo canvas towards the end of the page...)

The pixel buffer

Any computer display - such as a digital image or a part of the screen - is basically defined by a rectangular grid of pixels (picture elements). The display will have a discrete number of pixels in both the horizontal and vertical directions, which we will refer to as the width (w) and height (h), respectively. The (x,y) coordinates of the pixels are starting at (0,0) in the top-left corner and are increasing in the right/down direction, as shown in the below table.

Table-1-1: Pixel coordinates; (x,y)
(0,0) (1,0) ... ...
(0,1) (1,1) ... ...
... ... ... ...
... ... ... (w-1,h-1)

The appearance of the display is controlled by a pixel buffer, which is a data segment that holds the colours of the pixels in sequential order. The pixel indexing is started at 0 in the top-left corner and incremented towards the left and then downwards, as shown in the below two tables (the tables are equal; the only difference is the rectangular vs. linear formatting, where the former is directly comparable to the pixel coordinates of Table 1-1).

Table 1-2a: Pixel indices displayed in rectangular form; coordinates (x,y) maps to index x + y*w
0 1 ... ...
w w + 1 ... ...
... ... ... ...
... ... ... w*h - 1
Table 1-2b: Pixel indices displayed in linear (native) form; coordinates (x,y) maps to index x + y*w
0 1 ... ... w w + 1 ... ... ... ... ... w*h-1

Example: The index of the second pixel on the second row, which has coordinates (1,1), is w + 1.

The individual colours are then stored as numerical values in correspondence to the encoding scheme that is used. In this tutorial, we will use the common rgba encoding, where each colour is represented by four bytes; one for each of the following components:

  • red
  • green
  • blue
  • alpha

The three colour components (rgb) are mixed to represent any of the possible colours, while the alpha parameter (a) controls the pixel transparency (or rather opaqueness, as 0 is fully transparent). All four bytes are unsigned integers with values in the range 0-255, allowing for more than 16 million different colours (256*256*256) and 256 levels of transparency.

The pixel byte positions are thus ranging from 0-3 in the top-left corner of the screen and are incremented by 4 towards the left and then downwards, as shown in the below table.

Table 1-3: Pixel byte positions displayed in rectangular form; (x,y) maps to 4*(x + y*w) + {0,1,2,3}
{0,1,2,3} {4,5,6,7} ... ...
4*w + {0,1,2,3} 4*(w + 1) + {0,1,2,3} ... ...
... ... ... ...
... ... ... 4*(w*h - 1) + {0,1,2,3}

Example: To set the third pixel to opaque red or yellow, buffer bytes (8,9,10,11) should be set to (255,0,0,255) or (255,255,0,255), respectively.

The basic buffer functionality that we will use is implemented in the MyBuffer_basic class, as outlined below.

class MyBuffer_basic {
  
  properties: 
    width
    height    
    data
    idata
  
  methods:
    constructor(width,height)    
    clear(rgbx)  
    putpixel(x,y,rgbx)  
    getpixel(x,y)
    
}

The properties width and height defines the size of the buffer, while data holds all the raw pixel data (rgba values). The last property, idata, is simply a super-structure (ImageData) put on top of the pixel data to make it faster to send it to the canvas display.

The methods putpixel() and getpixel() will compute the byte position for any given (x,y) coordinates and set/get the associated pixel bytes (i.e., the four rgba bytes), while clear() flushes the whole buffer to a fixed colour.

The code for putpixel(), which is the key method, is given below.

 
putpixel(x,y,rgbx) {
  // Sets a pixel in the buffer to the given colour (input rgbx is either rgb or rgba)
  var i,outside;
  x               = Math.round(x);
  y               = Math.round(y);
  outside         = (x<0) || (x>=this.width) || (y<0) || (y>=this.height); if (outside){return;}
  i               = 4 * (x + y * this.width);
  this.data[i+0]  = rgbx[0];
  this.data[i+1]  = rgbx[1];
  this.data[i+2]  = rgbx[2];
  this.data[i+3]  = (rgbx.length>3) ? rgbx[3] : 255;
}

The logic is pretty straightforward: round (x,y) to the nearest integer; verify that point is inside the display; compute pixel byte position; set all bytes to the given color (with alpha component defaulting to fully solid).

We will want to add some more functionality to the buffer class later, so let's already now define the class MyBuffer as an extension of the MyBuffer_basic class.

 
class MyBuffer extends MyBuffer_basic {  
 
  methods:
    constructor(width,height)

}

For now, MyBuffer will simply call the super-class constructor and add nothing but the reference/name.

The display device

As mentioned above, we will use the html canvas element as our display device (~screen). A canvas element is typically added to the page with a simple html tag like

<canvas id="screen" width="480" height="300"></canvas> 

and by that we will have our output container readily defined. :)

To interact with the canvas, we will use the MyCanvas class outlined below.

class MyCanvas {

  properties: 
    el
    width
    height
    rc2d    
 
  methods:
    constructor(canvas)    
    publish(source)
    resize(width,height)    
    
}

The class constructor accepts a string with the id of the canvas to operate on (e.g., "screen"), or alternatively, the canvas element itself. The width and height properties are taken from the canvas element, which then the byte buffer also should match. The rc2d property is a rendering context for 2D displays that we will use to publish the pixel data (I could of course have named it "screen" instead of "rc2d" to keep it simpler, but I thought it could be useful to have some awareness of the JavaScript context we are operating in).

The method publish() flips the data for a given buffer onto the canvas, i.e., displays it. It accepts a buffer as argument, even though the required input is really the idata property of the buffer (which is the ImageData object mentioned above). The method resize() is simply a utility function to resize the canvas (just note that if it is ever used, any associated pixel buffers would also need to be redefined).

We have now defined the display data (pixel buffer) and the display device (canvas), and we will now connect the two with the so-called applet.

The applet runner (including thread and timer)

In order to create dynamic graphics, we will need a graphics routine that makes updates to the buffer/display. This will be handled by what we will refer to as the applet. The applet will run in the background and call an updating function at regular intervals, e.g., 60 times a second.

The term applet might have some historical bias, as it has been used most frequently to denote Java plug-ins - but I will use it here simply as a reference to the small programs that calls the graphics routines between each frame, and then ensures that the pixel buffer and then display device are updated.

The basic applet functionality is implemented in the MyApplet class, as outlined below.

class MyApplet {

  properties: 
    canvas
    buffer
    bgbuff
    thread    
    timer    
    param
    tag
    timeout
 
  methods:
    constructor(canvas)       
    isrunning()   
    start()     
    stop()
    toggle()
    restart()
    fps()
    wrk()
    showtag(str)
    init()   
    update()       
    tagstr()
    nextframe()
    bg2buff()
    setparam(key,val)
    param_ctrl()

}

The core process of the applet is to call the init() function initially (once), and then for each new frame call the function update() before publishing buffer onto canvas (repeatedly).

The property bgbuff is not used by default, but is included for applets that needs a background buffer.

The init() and update() methods are only abstractly defined, in the sense that they are empty in the class definition. They will still be called, but for something to actually happen, they need to be redefined by any sub-classes or object instances.

The applet also defines some basic functionality for applet parameterisation via the param property and the setparam() method, but these are also abstractly defined (empty by default).

The task of running the update function in the background is handled by the thread object, which is of the MyThread class outlined below.

class MyThread {  

  properties:   
    id
    fun
    fps
    meter
  
  methods:
    constructor(fun,fps)
    register()
    init()  
    isrunning()
    kill()
    start()  
    restart()

}

An instance of the MyThread class is responsible to call the input function fun at the targeted number of frames-per-second (fps), and to update the meter property with the actual/achieved frame-rate. In the MyApplet case, the function to be called is simply the nextframe() function (which again calls update() and publishes the buffer).

The applet class also has a timer property, which is an instance of the MyStopwatch class outlined below.

class MyStopwatch {
  
  properties:
    id
    t0
  
  methods:
    now()
    has(tag)
    nae(tag)
    rem(tag)    
    timedata(tag)    
    tic(tag)  
    toc(tag)  
    tac(tag)
    ons(tag)
    adjust(dt)

}

The timer object is mainly used to provide some basic synchronised time-keeping functionality to any associated objects, i.e., by adjusting any registered timers once the applet is restarted after a pause.

As a special service, the applet also displays the frame-rate in the top-left corner of the canvas by using a built-in JavaScript text function, which is called from showtag(). This is admittedly a cheat with respect to pure pixel graphics, but since it doesn't do much other than providing some useful performance info, I thought I'd include it anyways... :P

Today's applet

Today's applet is quite simple. It extends the MyApplet class by defining a few parameters and overloading the update() method with a funtion that basically draws a sine wave.


class MyApp_Sine extends MyApplet{
 
  properties:
    param : {amp,speed,n,dampdx,resx,baseline}
  
  methods:
    constructor(canvas)
    init()
    putsymb(x,y,rgb)
    update()      
    setparam(key,val)    
}

The class is relatively simple: The constructor simply calls the super-class constructor and sets some parameter defaults, while the update() draws the curve by computing the y values for each x location and then setting the corresponding buffer pixels (to red). The central line of the computation is

y = y0 + ce(x) * amp * Math.sin( t * ct + x * cx )

where ce is a damping factor (function of x) at the edges, while ct and cx controls the wave temporal frequency (~speed) and the spatial frequency (~#cycles), respectively.

In order to make it easier to see the individual points, I added a putsymb() function that optionally draws a small symbol at the curve locations instead of just a single pixel.

Applet controllers

I have added some html controllers, e.g., sliders, to play with the different applet parameters. Using these is exactly the same as modifying the parameters in the code itself, but using the controllers gives it a more interactive feeling. :)

To provide a standard for the controller specification, I have added a param_ctrl() method to the MyApplet class. The method is abstractly defined (empty by default), but contains the specification as text/comments to inform about the intended use.

Demo

Here comes the demo of today's applet - just press start to check it out! :)

Check it out: Keeping the sampling factor at 1, and playing with the number of cycles gives some nice aliasing effects, especially at low speeds (1-2)... :)

Command line interface

The applet can also be accessed directly via the brower's command line interface (console). As can be seen in the script in the html file, I have named the applet variable app, so in order to put a pixel in the buffer and send the response to the canvas, the following set of commands could be inserted in the console:

app.stop()
app.buffer.clear()
app.buffer.putpixel(100,20,[255,255,255])
app.canvas.publish(app.buffer)

(The first command is included to stop the applet - in case it is running - so that the pixel we set with the second command is not immediately overwritten by the updating function...)

(The second command is not really needed - it just clears the buffer to black, in case that would be useful...)

(Please note that you need both the putpixel() and the publish() to actually update the display...)

In closing

At this point I could really say "That's all, folks!", as this is really all you need to get started with pixel graphics. But since I have some more that I want to cover in this tutorial, I'll rather end by saying "That's all for now!" :)

-Nambafa-
nambafa.com

Downloads: tut_01.zip