Howto:Using raster images and nested canvases

From FlightGear wiki
Jump to navigation Jump to search


Objective

Document how raster images can be used with the new Canvas system, including using other canvases as raster image sources.

Introduction

Raster images (png, jpeg etc) are supported by creating an "image" child using a createChild("image") method call. The file name must be set using the "setFile()" method which should point to a raster image in $FG_ROOT using a RELATIVE path.

You shouldn't use absolute paths and instead let the path be automatically resolved. Paths are searched in the following directories in the following order:

  • Root directory of current aircraft (defined by /sim/aircraft-dir)
  • All aircraft directories if branch starts with Aircraft/
  • $FG_ROOT directory

Please keep in mind that paths are case-sensitive!

Nested Canvases

In addition, the canvas system also supports using other canvas textures as input, so that nested canvases can be created, where multiple canvases can be cascaded and used as the input for another canvas. This is accomplished using a special syntax: canvas://by-index/texture[index]

For example, support for nested canvases makes it possible to also load canvas textures into other canvases, so that you can, for example, easily load an instrument into a GUI dialog, or even use GUI widgets in MFD instruments, which is a feature based on real avionics, i.e. as used in modern airliners like the A380 or the 787 - which are based on the ARINC661 standard.

Nested canvases also make it possible for people to easily implement GUI tools using the Canvas system, for example a GUI instrument or panel editor can be entirely implemented in scripting space now, without touching any C++ code. Similarly, GUI widgets could also be created in a WYSIWYG-fashion, too - so that even a dialog editor or full GUI "builders" can be built using the Canvas system.

To get a canvas path, just use the "getPath()" method.

Nested canvases using multiple windows are demonstrated in this video:

Texture Maps

To use Texture Maps, it is also possible to restrict the image to a certain region using the setSourceRect() method, which supports the following arguments:

  • left Rectangle minimum x coordinate
  • top Rectangle minimum y coordinate
  • right Rectangle maximum x coordinate
  • bottom Rectangle maximum y coordinate
  • normalized Whether to use normalized ([0,1]) or image ([0, image_width]/[0, image_height]) coordinates

Example: Loading a PNG image

root.createChild("image")
                                    .setFile("Textures/Splash1.png")
                                    .setSize(240,180)
                                    .setTranslation(1,20);

Example: Loading another canvas via a static path

root.createChild("image")
                                    .setFile("canvas://by-index/texture[0]")
                                    .setSize(240,180)
                                    .setTranslation(1,20);

This is just intended as an example - usually, you'll be using dynamic paths, because you never know in advance the index of a certain canvas texture, for a more feasible example, please see below:

Example: Loading a Canvas dynamically

Screen shot with a Canvas MFD dialog including an embedded Canvas (showing a splash screen here, which could just as well reference another Canvas/MFD)
root.createChild("image")
                                    .setFile( someCanvas.getPath() )
                                    .setSize(240,180)
                                    .setTranslation(1,20);

Example: Showing an existing canvas in a dialog

## (requires FlightGear >= 2.11+)
# x,y: dialog dimensions
# canvas: the canvas texture to show in the dialog
# width/height: the dimensions of the canvas image in the dialog (optional)
var show_canvas_in_dialog = func(x,y, the_canvas, width=x-10, height=y-10) {
var dlg = canvas.Window.new([x,y]);
var my_canvas = dlg.createCanvas()
                   .setColorBackground(1,1,1,1);
var root = my_canvas.createGroup();

root.createChild("image")
                                    .setFile( the_canvas.getPath() )
                                    .setSize(width,height)
                                    .setTranslation(1,20);


##
# return everything to the caller as a 3-element vector for customization purposes (to register event listeners, add buttons etc)
return [dlg, my_canvas, root];
}

This could be easily extended to show multiple canvases in a single dialog/window - you could even support drag/drop, to freely move different displays around and end up with a custom layout (very useful for creating an instructor station).

An even shorter version could look like this [1] This is a link to the FlightGear forum.:

# The optional second arguments enables creating a window decoration
var dlg = canvas.Window.new([x, y], "dialog");
dlg.setCanvas(my_existing_canvas);

Example: Showing a Splash Screen

var CanvasApplication = {
 ##
 # constructor
 new: func(x=300,y=200) {
  var m = { parents: [CanvasApplication] };
  m.dlg = canvas.Window.new([x,y],"dialog");
  m.canvas = m.dlg.createCanvas().setColorBackground(1,1,1,1);
  m.root = m.canvas.createGroup();
  m.init();
  return m;
 }, # new
 
 
init: func() {

 var filename = "Textures/Splash1.png";
 # create an image child for the texture
 var child=me.root.createChild("image")
			           .setFile( filename )
				   .setTranslation(25,25)
                                   .setSize(250,250);
}, #init

}; # end of CanvasApplication
 

var splash = CanvasApplication.new(x:300, y:300);
 
print("Splash Image Loaded...!");

Caching frequently used symbols

Note  Implemented by MapStructure/SymbolCache

If you'd like to draw a SVG symbol only once and then place multiple instances of it on a canvas and need them to be unique elements for independent animations, you can use the props.nas APIs to copy a previously created canvas/svg group: SVG files are just XML files that are read by Nasal and turned into canvas properties, so you can simply use the APIs in props.nas to clone/copy a canvas group (SVG image) and parametrize it afterwards for customization purposes (changing color etc):

https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Nasal/props.nas#l184

##
# Recursively copy property branch from source Node to
# destination Node. Doesn't copy aliases. Copies attributes
# if optional third argument is set and non-zero.
#

Copying the according branch of the property tree is probably the way to go. There is no special function in the Nasal API yet, but in the meantime you can just copy the property tree manually (using props.copy). For an example have a look at the SVG parser, as I have implemented the svg <use> tag using this method. Just be aware that no event handler is copied, and that completely independend objects are created, so every handle to an element has to be retrieved again for the new branch.

However, depending on the type of animations required, you can also set up all svg symbols as pre-created (separate) canvases and then just reference them as an image via canvas:// and independently transform/translate the corresponding raster images of each SVG.

If you have lots of symbols that only differ in color, orientation or position, that should be more efficient - even if it means having 10 different canvases as "templates". Instead of having dozens of independent canvas/svg groups in the tree that need to be separately updated and triangulated/rasterized. However, labels should then obviously kept distinct, i.e. in another canvas group.

I have never tried it, and I don't know if Tom has implemented the code for this (in the nested canvas mode) - but you should theoretically be able to use even a single canvas as the symbol cache by using texture mapping: http://wiki.flightgear.org/Howto:Using_raster_images_and_nested_canvases#Texture_Maps

Basically, you would set up all symbols once in a single new separate canvas (which serves as your symbol cache), just using different coordinates for each symbol/group - and then reference the right symbol by using canvas:// (with your symbol cache as source) and then using the setSourceRect() method to get the proper raster image out of the main texture. You can use the getBoundingBox() method to get each group's coordinates: http://wiki.flightgear.org/Canvas_Nasal_API#getBoundingBox

So assuming that the canvas code handles nested canvases just as conventional raster images, you can easily set up a simple caching scheme for frequently used symbols like this.