Classes & Objects

Preface

If we mix tables, metatables and namespaces and spice it with some creativity, we end up with "objects". That is to say: with the lua implementation of object orientation ( "oo"). This implementation however is not real oo at all, but just a combination of straight forward lua, a certain way of doing things and a bit of syntactic sugar to make that look "oo" like. We have already seen all of this sugar in the page on Functions.

A discussion about "oo" and what it is about is fár beyond the scope of these pages. But since it is no real "oo" at all, I don't have to go into it anyway. I wíll however go into lua's implementation of "objects" since they are a concept that could make scripting life a lot easier once you get a grasp of what they are about and how you can create and use them. Doing this however, I'll stay away from all formal "oo" theory and background and simply discuss "classes" and "objects" and how they work in lua/celx practice.

In short: "oo" is a programming technique that is all about the reduction of programming (scripting) efforts through reuse; reuse that is largely achieved through the encapsulation of data and functionality in reusable logical entities called objects and classes. Like my car is an implementation of the Opel car type or class "Zafira", an "oo" object is an implementation of a particular class. Where in the real world however classes are abstracts and do not exist other than through their object representatives, in the "oo" world classes are real things that have to be programmed. As a matter of fact, as you will see from the color object example below, classes are the things that - behind the scenes - do all the real work.

General

When scripting in celx we work with objects of types like: stars, planets, spacecrafts, observers, vectors and frames. What happens when we execute a statement like:

  • myObserver = celestia:getobserver()

is that the lua variable "myObserver" is assigned a reference to an object of the Celestia type (or class) "observer". Through this reference we now have access to all the functions and data of the observer object. We can ask it what it's position is, what it's speed is or it's orientation through statements like:

  • position = myObserver:getposition()
  • speed = myObserver:getspeed()
  • orientation = myObserver:getOrientation()

Or you can tell it to dó things with statements like:

  • myObserver:goto(target object / -position)
  • myObserver:center(target object)

All of this without any extra scripting because Celestia really ís object oriented and myObserver is a real object. OK, a réference to an object, but when scripting we use the reference to an object as if it were the object itself. Once we have a reference to an object we have access to all the data and functionality of that object.

Now wouldn't it be nice if we could make our own classes of objects and treat them like we do the Celestia objects? Classes that we could then put in lua and/or celx libraries, so that we can have them at our disposal whenever and whereever we need them? Well, the good news is that it can be done. Lua isn't "oo" but it get's close enough to give us some of the advantages.

Lua objects

In lua objects are made of table's:

    • 1 table per object for object specific data: the object table
    • 1 metatable for shared data and functions: the class table

There hardly is any work in creating the object table, other than deciding what data it is supposed to hold and so which fields it should have. All the real work is done designing and scripting out the data, structure and functions of the metatable. In the lua setup this metatable represents the class: a factory that can create and support objects but that also holds all the object functions since they are implemented as metamethods.

But instead of trying to describe it, let's just go and create an object and things will gradually become clear. We need a small object; not too complicated but still one that is usefull and that solves a real life "Celestia problem". So we'll go and create a Color object.

Celestia has this thing with colors; it uses the RGB color system but nót in the notation that most computer users are familiar with. Most desktop applications - at least the windows ones - use the "Digital 8-bit per channel" notation wherein "Red" is specified as (255, 0, 0). Like for instance in the screenprint to the left, that shows the "Google sites" color picker with the cursor on the red square (printscreen doesn't catch the cursor).Celestia however uses the arithmetic notation wherein red is defined as (1.0, 0.0, 0.0) and something like orange as (1.0, 0.6471, 0.0). It takes some doing (i.e. google searching) to find out that in order to arrive at the arithmetic notation for a certain color you take the 8-bit notation and devide the values for each of the three primary colors by 255. So working with Celestia colors means working with a calculator.

That is to say: "most of the time", since celx is not consistent in its application of color notations and uses the hexadecimal rgb notation for things like object markers. In hex notation red is defined as "#FF0000" ( the # character indentifies the string as a hex number). The hex notation is in fact a variant of the 8-bit notation; the first two positions define red, the middle two green and the last two blue. Per primary color you can specify the hex values 00 through FF which is equivalent to the decimal values 00 through 255. This notation is the standard notation for use in HTML. It is not so that in celx you can use either of the notations; you can only use one of them at a time. A function like setlabelcolor() only accepts arithmetic rgb whereas object:mark() only accepts hex rgb or predefined HTML color names (see also: W3schools html colornames).

So working with colors in celx is not realy simple or straightforward. Therefore we are going to create a class of color objects that will make scripting a little easier in that area. Something that will allow us to specify and retrieve colors in the more common notations of 8-bit and hex rgb and that will take care of all the necessary conversions for us.

Color object

For what I have in mind it is enough to store only the three primary colors per color. So a color object will be a table with three fields:

orange = { red   = 1.0,

green = 0.6471

           blue  = 0.0
         }

We will use aritmetic notation since that can define a larger range of individual colors than Digital 8-bit can and we have to be able to store every single color celestia can dream up. We won't do any direct scripting for the object table, only for the class table (the metatable). The first part of the script looks like this:

--[[  The color class metatable
------------------------------------------------------]]
color = {}                                                    -- create the metatable
color.__index = color                                         -- set the table as it's own extention table

--[[ class method: new( number, number, number ) -- creates a new color object

------------------------------------------------------
    Constructor:
    returns a new color object for the color specified
    by the parameters.
----------------------------------------------------]]
color.new = function( r, g, b )
        local object = {}                                    -- create the new object's table
        setmetatable(object, color)                          -- set color as the metatable for the object
                                                             -- check the r, g and b parameters
        if (r <= 1.0 and r>= 0.0) and (g <= 1.0 and g>= 0.0) and (b <= 1.0 and b>= 0.0) then
            object.red   = r
            object.green = g
            object.blue  = b
        else
            error("color.new(): unknown color format in rgb")
        end
        return object                                        -- return the table as the new color object
    end
--[[  object method: cel( self )
------------------------------------------------------
    returns r, g and b in celx rgb format (arithmetic)
    input:
        self: the color object ('s table)
----------------------------------------------------]]
color.cel = function(self)
        return self.red, self.green, self.blue
    end

This little bit of code represents a working class, that allows to store colors in color objects and use their values when and where needed. Ok, it doesn't do all that I would want it to do yet, but still. Before I'll demonstrate this minimal class, lets first add two lua metamethods for good measure and because I need one of them for the demo.

--[[  color metamethod: __eq
------------------------------------------------------

allows to compare two color objects for equality

and inequallity.

----------------------------------------------------]]
color.__eq = function(color1, color2)

return ( color1.red == color2.red and color1.green == color2.green and color1.blue == color2.blue )

    end
--[[  color metamethod: __tostring
------------------------------------------------------

converts the color object to a string representation.

----------------------------------------------------]]
color.__tostring = function(self)

return string.format("rgb(%f, %f, %f)", self.red, self.green, self.blue)

    end

The __eq() metamethod allows us to compare two color objects for (in)equality using default lua syntax like: if color1 == color2 then .... Lua's interpreter will translate that to a call to __eq() with both color objects as the parameters. It's then up to __eq() to decide if both objects are the same and return true or false depending on the result.

With the __tostring() metamethod we can influence what the lua tostring() function returns when we use it with a color object as the parameter. Without __tostring() it will return something in the order of: "table: 000000000062DA20", the id of the table that is used for the object. With the __tostring() as coded above it will return a string with a correct rgb(r, g, b) color indicator in arithmetic notation, like the demo will show.

If you store the class in it's own script file with a name of say: "color.celx" and then create and run a demo script with this code:

-- Demo script
dofile("--- your celestia scripts path ---/color.celx")         -- imports the color class

oldcolor = color.new(celestia:getlabelcolor("spacecraft")) -- creates a color object for the current labelcolor

                                                                -- notice: getlabelcolor() returns arithmetic r, g and b
orange = color.new(1.0, 0.6471, 0.0)                            -- creates a color object for orange
celestia:setlabelcolor("spacecraft", orange:cel())              -- sets labelcolor for spacecraft to orange
celestia:print("labelcolor = " ..tostring(orange), 5)           -- prints labelcolor text
wait(5)
celestia:setlabelcolor("spacecraft", oldcolor:cel())            -- resets labelcolor to original value
celestia:print("labelcolor = " ..tostring(oldcolor), 5)         -- prints labelcolor text

you will see the labelcolor for spacecrafts turn to orange and back again to the original color after 5 seconds. Don't forget to set the rendering of labels for spacecrafts to "on" before you run this demo.

As the example shows, to create a color object you call the class method new(), like this:

  • orange = color.new(1.0, 0.6471, 0.0)

To use the color with a function like setlabelcolor you use the object method cell(), like this:

  • celestia:setlabelcolor("spacecraft", orange:cel())

This method is called "cel" to distinguish it from methods that are still to come and that return rgb and hex instead of arithmetic. I named it "cel" and not "arithmetic" to make it easier to recognise which method returns the default celx notation. Oh and by the way; "method" is just another word for function.

Lua will first try to resolve orange:cel() before calling setlabelcolor(); a succesful return from orange:cel() will adjust the call to:

  • celestia:setlabelcolor("spacecraft", 1.0, 0.6471, 0.0)

Since there is a color cónstructor (the new() function) you might think that we would also have to create a color déstructor. This is however not necessary; lua's garbage collector does a good job at disposing of objects as soon as they finally go out of scope. It already does that for the default types like strings, numbers and tables so we can let it do the same thing for our color objects. It does not need any special coding.

OK, now we have a basic color class, but we want to be able to also specify colors in rgb and hex notation rather than just arithmetic. This means that at the very least we have to be able to convert from rgb and hex to arithmetic and vise versa, so we will have to create the correspoding functions. Since it would be good when the (scripting) user could also use these functions directly, lets implement them as class methods. Class methods are general purpose functions that are called with class.method(arguments) rather than object:method(arguments). Here we go:

--[[  class method: fromRgb()
------------------------------------------------------
    converts rgb to celx notation (arithmetic).
    input: r, g, b in digital 8-bit
    output: r, g, b in arithmetic
----------------------------------------------------]]
color.fromRgb = function(r, g, b)
        if not (type(r) == "number" and type(g) == "number" and type(b) == "number") then
            error("color.fromRgb(): scripting error; parameters should be numbers")
        elseif not (r <= 255 and r>= 0) and (g <= 255 and g>= 0) and (b <= 255 and b>= 0) then
            error(string.format("color.fromRgb(): incorrect rgb color code: (%f, %f, %f)", r, g, b))
        else
            return r/255, g/255, b/255
        end
    end
--[[  class method: toRgb()
------------------------------------------------------
    converts celx (arithmetic) to rgb notation
    input: r, g, b in arithmetic
    output: r, g, b in digital 8-bit
----------------------------------------------------]]
color.toRgb = function(r, g, b)
        if not (type(r) == "number" and type(g) == "number" and type(b) == "number") then
            error("color.fromRgb(): scripting error; parameters should be numbers")
        elseif not (r <= 1.0 and r>= 0.0) and (g <= 1.0 and g>= 0.0) and (b <= 1.0 and b>= 0.0) then
            error(string.format("color.toRgb(): incorrect rgb color code: (%f, %f, %f)", r, g, b))
        else
            -- multiply by 255 and round to nearest integer
            -- lua does not have a math.round() function so we will have to use math.floor(n + 0.5)
            return math.floor((r * 255) + 0.5), math.floor((g * 255) + 0.5), math.floor((b * 255) + 0.5)
        end
    end
--[[  class method: fromHex()
------------------------------------------------------
    converts hex to celx notation (arithmetic).
    input: hex number or HTML name
    output: r, g, b in arithmetic
----------------------------------------------------]]
color.fromHex = function(hex)
        if type(hex) ~= "string" then
            error("color.fromHex(): scripting error; parameter should be a string")
        else

hex = string.lower(string.match(hex, "^%s*(.-)%s*$" )) -- trim any whitespace and convert to lower case

if #hex == 0 then -- if trimmed string is empty

                error("color.fromHex(): hex parameter is empty")

elseif hex:sub(1, 1) ~= "#" then -- first character is nót "#", so is not hex

                error("color.fromHex(): parameter error: unknown hex color: "..hex)

elseif #hex ~= 7 or hex:match("[^#0-9a-f]") then -- if hex matches any non-hex character

                error("color.fromHex(): parameter error: incorrect hex color: "..hex)
            else
                return tonumber(hex:sub(2,3), 16)/255, tonumber(hex:sub(4,5), 16)/255, tonumber(hex:sub(6,7), 16)/255
                -- tonumber(nr, 16) converts to hex ( = base 16 )
            end
        end
    end
--[[  class method: toHex()
------------------------------------------------------
    converts celx (arithmetic) to hex notation.
    input: r, g, b in arithmetic
    output: hex number or HTML name
----------------------------------------------------]]
color.toHex = function(r, g, b)
        if not (type(r) == "number" and type(g) == "number" and type(b) == "number") then
            error("color.fromRgb(): scripting error; parameters should be numbers")
        elseif not (r <= 1.0 and r>= 0.0) and (g <= 1.0 and g>= 0.0) and (b <= 1.0 and b>= 0.0) then
            error(string.format("color.toHex(): unknown rgb color code: (%f, %f, %f)", r, g, b))
        else
            return string.format("#%02X%02X%02X", color.toRgb(r, g, b))
    end

Now we can convert, we can adjust the constructor to also accept rgb and hex.

--[[ class method: new() color constructor

      overload 1:    new( string )
  overload 2:    new( number, number, number )
------------------------------------------------------
    returns a new color object for the color specified
    call variants:
    1.  new(string = hex) hex variant
    2.  new(r = number, g = number, b = number) rgb variant

3. new(r = number, g = number, b = number) celx rgb variant

----------------------------------------------------]]
color.new = function( r, g, b )
        -- determine overload
        overload = 0
        if r == nil then
            error("color.new(): scripting error: missing first parameter")
        elseif g == nil and b == nil then
            -- there's only one parameter, so this should be overload 1: hex
            overload = 1
        elseif g ~= nil and b ~= nil then
            -- there are 3 parameters, so this should be overload 2: RGB.
            overload = 2
        else
            -- not 3 parameters, so unknown overload
            error("color.new(): scripting error: incorrect number of parameters")
        end


local object = {} -- create the new object's table

setmetatable(object, color) -- set color as the metatable for the object


        if overload == 1 then
            object.red, object.green, object.blue = color.fromHex(r)
        elseif overload == 2 then
            if (r <= 1.0 and r>= 0.0) and (g <= 1.0 and g>= 0.0) and (b <= 1.0 and b>= 0.0) then
                object.red = r object.green = g object.blue = b
            else
                object.red, object.green, object.blue = color.fromRgb(r, g, b)
            end
        else
            error("color.new(): scripting error: unknown call variant")
        end
        return object
    end

With this change there are now three possibilities to create an object for the color yellow and by the end of this page there will also be a fourth one:

  1. celx: color.new(1.0, 1.0, 0.0)
  2. rgb: color.new(255, 255, 0)
  3. hex: color.new("#FFFF00")
  4. name: color.new("yellow")

This is however only halve the change since the colors can still only be recalled in arithmetic rgb and we will also want to support printing activities or other use of colors as strings. So to finish of this change:

--[[  object method: rgb(self)
------------------------------------------------------
    returns r, g and b in digital 8-bit rgb format
    0 <= r <= 255
----------------------------------------------------]]
color.rgb = function( self )
        return math.floor((self.red * 255) + 0.5), math.floor((self.green * 255) + 0.5), math.floor((self.blue * 255) + 0.5)
    end
--[[  object method: hex( self )
------------------------------------------------------
    returns the color in hex format
    "#RRGGBB"
----------------------------------------------------]]
color.hex = function(self)
        return string.format("#%02X%02X%02X", color.rgb(self))
    end


--[[ The print sub table
------------------------------------------------------]]
color.print = {}
--[[  class method: print.cel( self )
------------------------------------------------------
    returns the color as a celx rgb string
----------------------------------------------------]]
color.print.cel = function(clr)
        return string.format("cel(%f, %f, %f)", color.cel(clr))
    end
--[[  class method: print.rgb( self )
------------------------------------------------------
    returns the color as a standard rgb string
----------------------------------------------------]]
color.print.rgb = function(clr)
        return string.format("rgb(%i, %i, %i)", color.rgb(clr))
    end
--[[  class method: print.hex( self )
------------------------------------------------------
    returns the color as a hexadecimal string
----------------------------------------------------]]
color.print.hex = function(clr)
        return string.format("hex %s", color.hex(clr))
    end

So now we have a color class, that acts as a celx color type: a variable that you can store colors in; colors that you either define yourself: yellow = color.new(255, 255, 0) or retrieve from Celestia: labelcolor = color.new(celestia:getlabelcolor("spacecraft")). A type that can reproduce it's color value in any of three color notations. The "yellow" object for instance can both be used to define a labelcolor in arithmetic rgb and an object marker color in hexadecimal rgb:

  • celestia:setlabelcolor("spacecraft", yellow:cel())
  • object:mark(yellow:hex())

For one-off things that you don't need objects for, you can use the class methods to specify colors like in:

  • celestia:setlabelcolor("spacecraft", color.fromHex("#EEEEEE"))

You can now use color examples from all sorts of desktop programs or that you retrieve by using a color picker utility or by visiting a website like W3schools without needing a calculator to communicate them to celestia. And it will be a lot easier to read from your script what colors you are using. All of that by writing a relatively small library script that can be reused for as many colors in as many scripts as you like. Simply put: it will make working with colors in celx scripts a lot easier.

You can go even further and allow color names as specifiers for the constructor; something that would only need a relatively small change to the color class script. It would require the implementation of a table like this one:

--[[  17 standard colors ( W3 )
------------------------------------------------------
    see also: 
        http://www.w3schools.com/HTML/html_colornames.asp
        http://www.w3schools.com/HTML/html_colorvalues.asp
    notice !
        use lower case only for color names !!
----------------------------------------------------]]
color.list = {}

color.list.aqua = "#0000FF" -- cyan

color.list.black = "#000000"

color.list.blue = "#0000FF"

color.list.fuchsia = "#FF00FF" -- magenta

color.list.gray = "#808080" -- or grey

color.list.grey = "#808080" -- or gray

color.list.green = "#008000"

color.list.lime = "#00FF00"

color.list.maroon = "#800000"

color.list.navy = "#000080"

color.list.olive = "#808000"

color.list.purple = "#800080"

color.list.red = "#FF0000"

color.list.silver = "#C0C0C0"

color.list.teal = "#008080"

color.list.white = "#FFFFFF"

color.list.yellow = "#FFFF00"

--[[ additional colors ( W3 )

----------------------------------------------------]]

color.list.darkorange = "#FF8C00"

color.list.gold = "#FFD700"

color.list.orange = "#FFA500"

--[[ additional colors (celestia)

----------------------------------------------------]]

color.list.labelwhite = "#EEEEEE"

and this small change to color.fromHex()

--[[  class method: fromHex()
------------------------------------------------------
    converts hex to celx notation (arithmetic).
    input: hex number or HTML name
    output: r, g, b in arithmetic
----------------------------------------------------]]
color.fromHex = function(hex)
        if type(hex) ~= "string" then
            error("color.fromHex(): scripting error; parameter should be a string")
        else
            -- first check if hex is a name; íf yes then retrieve hex from color.list

hex = string.lower(string.match(hex, "^%s*(.-)%s*$" )) -- trim any whitespace and convert to lower case

if #hex == 0 then -- if trimmed string is empty

                error("color.fromHex(): hex parameter is empty")

elseif hex:sub(1, 1) ~= "#" then -- first character is nót "#",

                                                                        -- so could be a color name from the list

if color.list[hex] ~= nil then -- it ís a named color

                    hex = string.lower(string.match(color.list[hex], "^%s*(.-)%s*$" ))
                else
                    error("color.fromHex(): parameter error: unknown hex color: "..hex)
                end
            end

if #hex ~= 7 or hex:match("[^#0-9a-f]") then -- if hex matches any non-hex character

                error("color.fromHex(): parameter error: incorrect hex color: "..hex)
            else
                return tonumber(hex:sub(2,3), 16)/255, tonumber(hex:sub(4,5), 16)/255, tonumber(hex:sub(6,7), 16)/255
                -- tonumber(nr, 16) converts to hex ( = base 16 )
            end
        end
    end

With this change you can define a color using a name. Don't put áll 147 named colors in the color.list table; limit the list to only those colors that you often use.

Notice that the names are only used to retrieve the hex color code and that they are not stored in the color object and therefore can not be recalled for use in print or other statements.

The color demo below shows examples of how the color class can be used; it is also available as download.

--[[  Load library
========================================================================

Load the color library, located in ....Celestia/scripts/celx_libs and

    called: color.celx
=======================================================================]]
dofile("scripts/celx_libs/color.celx")
--[[  The demo
========================================================================
    shows some usages of the color object
=======================================================================]]
function Demo()
    labelflags = celestia:getlabelflags()
    celestia:showlabel("planets")
    celestia:showlabel("spacecraft")
    oldplanet = color.new(celestia:getlabelcolor("planets"))
    oldspacecraft = color.new(celestia:getlabelcolor("spacecraft"))
    orange = color.new(1.0, 0.6471, 0.0)
    yellow = color.new(255, 255, 0)
    white = color.new("#FFFFFF")
    fuchsia = color.new("Fuchsia")
    celestia:setlabelcolor("planets", orange:cel())
    celestia:print("color planets = " ..color.print.cel(orange), 2)
    wait(2)
    celestia:setlabelcolor("spacecraft", yellow:cel())
    celestia:print("color spacecraft = " ..color.print.rgb(orange), 2)
    wait(2)
    celestia:setlabelcolor("planets", white:cel())
    celestia:print("color planets = " ..color.print.hex(white), 2)
    wait(2)
    celestia:setlabelcolor("spacecraft", fuchsia:cel())
    celestia:print("color spacecraft = " ..color.print.hex(fuchsia), 2)
    wait(2)
    celestia:setlabelcolor("planets", color.fromRgb(0, 255, 0))
    celestia:print("color planets = lime", 2)
    wait(2)
    celestia:setlabelcolor("spacecraft", color.fromHex("#0000FF"))
    celestia:print("color spacecraft = aqua", 2)
    wait(2)
    celestia:setlabelcolor("planets", oldplanet:cel())
    celestia:print("color planets = " ..color.print.cel(oldplanet), 2)
    wait(2)
    celestia:setlabelcolor("spacecraft", oldspacecraft:cel())
    celestia:print("color spacecraft = " ..color.print.cel(oldspacecraft), 2)
    wait(2)
    celestia:setlabelflags(labelflags)
    celestia:print("label colors back to normal", 4)
end
Demo()

Downloads

Installation and use

Installation

    1. download "color.celx" to the folder Celestia/scripts/celx libs/
    2. - or to your existing celx library folder if you already have one
    3. - in that case don't forget to adjust the name of the folder in the menu example (colorDemo)
    4. download "colorDemo.celx" to your Celestia/scripts/ folder.

Use

    1. Start Celestia and choose "File/Scripts/Color object demo" from the menubar. Or choose "File/Open Script...", navigate to your scripts folder, select "colorDemo.celx" and choose "Open". The script will start changing label colors on your screen.

History

Version 1.1, 17 apr 2011

Changed:

    • Corrected an error in color.celx that caused a '330: nesting of [[...]] is deprecated near "["' error message

Version 1.0, 16 mar 2011

Initial version