Howto:Using raster images and nested canvases
The FlightGear forum has a subforum related to: Canvas |
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
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] :
# 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.