928 lines
24 KiB
Text
928 lines
24 KiB
Text
# Internal helper
|
|
var _getColor = func(color)
|
|
{
|
|
if( size(color) == 1 )
|
|
var color = color[0];
|
|
|
|
if( typeof(color) == 'scalar' )
|
|
return color;
|
|
if( typeof(color) != "vector" )
|
|
return debug.warn("Wrong type for color");
|
|
|
|
if( size(color) < 3 or size(color) > 4 )
|
|
return debug.warn("Color needs 3 or 4 values (RGB or RGBA)");
|
|
|
|
var str = 'rgb';
|
|
if( size(color) == 4 )
|
|
str ~= 'a';
|
|
str ~= '(';
|
|
|
|
# rgb = [0,255], a = [0,1]
|
|
for(var i = 0; i < size(color); i += 1)
|
|
str ~= (i > 0 ? ',' : '') ~ (i < 3 ? int(color[i] * 255) : color[i]);
|
|
|
|
return str ~ ')';
|
|
};
|
|
|
|
var _arg2valarray = func
|
|
{
|
|
var ret = arg;
|
|
while ( typeof(ret) == "vector"
|
|
and size(ret) == 1 and typeof(ret[0]) == "vector" )
|
|
ret = ret[0];
|
|
return ret;
|
|
}
|
|
|
|
# Transform
|
|
# ==============================================================================
|
|
# A transformation matrix which is used to transform an #Element on the canvas.
|
|
# The dimensions of the matrix are 3x3 where the last row is always 0 0 1:
|
|
#
|
|
# a c e
|
|
# b d f
|
|
# 0 0 1
|
|
#
|
|
# See http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined for details.
|
|
#
|
|
var Transform = {
|
|
new: func(node, vals = nil)
|
|
{
|
|
var m = {
|
|
parents: [Transform],
|
|
_node: node,
|
|
a: node.getNode("m[0]", 1),
|
|
b: node.getNode("m[1]", 1),
|
|
c: node.getNode("m[2]", 1),
|
|
d: node.getNode("m[3]", 1),
|
|
e: node.getNode("m[4]", 1),
|
|
f: node.getNode("m[5]", 1)
|
|
};
|
|
|
|
var use_vals = typeof(vals) == 'vector' and size(vals) == 6;
|
|
|
|
# initialize to identity matrix
|
|
m.a.setDoubleValue(use_vals ? vals[0] : 1);
|
|
m.b.setDoubleValue(use_vals ? vals[1] : 0);
|
|
m.c.setDoubleValue(use_vals ? vals[2] : 0);
|
|
m.d.setDoubleValue(use_vals ? vals[3] : 1);
|
|
m.e.setDoubleValue(use_vals ? vals[4] : 0);
|
|
m.f.setDoubleValue(use_vals ? vals[5] : 0);
|
|
|
|
return m;
|
|
},
|
|
setTranslation: func
|
|
{
|
|
var trans = _arg2valarray(arg);
|
|
|
|
me.e.setDoubleValue(trans[0]);
|
|
me.f.setDoubleValue(trans[1]);
|
|
|
|
return me;
|
|
},
|
|
# Set rotation (Optionally around a specified point instead of (0,0))
|
|
#
|
|
# setRotation(rot)
|
|
# setRotation(rot, cx, cy)
|
|
#
|
|
# @note If using with rotation center different to (0,0) don't use
|
|
# #setTranslation as it would interfere with the rotation.
|
|
setRotation: func(angle)
|
|
{
|
|
var center = _arg2valarray(arg);
|
|
|
|
var s = math.sin(angle);
|
|
var c = math.cos(angle);
|
|
|
|
me.a.setDoubleValue(c);
|
|
me.b.setDoubleValue(s);
|
|
me.c.setDoubleValue(-s);
|
|
me.d.setDoubleValue(c);
|
|
|
|
if( size(center) == 2 )
|
|
{
|
|
me.e.setDoubleValue( (-center[0] * c) + (center[1] * s) + center[0] );
|
|
me.f.setDoubleValue( (-center[0] * s) - (center[1] * c) + center[1] );
|
|
}
|
|
|
|
return me;
|
|
},
|
|
# Set scale (either as parameters or array)
|
|
#
|
|
# If only one parameter is given its value is used for both x and y
|
|
# setScale(x, y)
|
|
# setScale([x, y])
|
|
setScale: func
|
|
{
|
|
var scale = _arg2valarray(arg);
|
|
|
|
me.a.setDoubleValue(scale[0]);
|
|
me.d.setDoubleValue(size(scale) >= 2 ? scale[1] : scale[0]);
|
|
|
|
return me;
|
|
},
|
|
getScale: func()
|
|
{
|
|
# TODO handle rotation
|
|
return [me.a.getValue(), me.d.getValue()];
|
|
}
|
|
};
|
|
|
|
# Element
|
|
# ==============================================================================
|
|
# Baseclass for all elements on a canvas
|
|
#
|
|
var Element = {
|
|
# Constructor
|
|
#
|
|
# @param ghost Element ghost as retrieved from core methods
|
|
new: func(ghost)
|
|
{
|
|
return {
|
|
parents: [PropertyElement, Element, ghost],
|
|
_node: props.wrapNode(ghost._node_ghost)
|
|
};
|
|
},
|
|
# Trigger an update of the element
|
|
#
|
|
# Elements are automatically updated once a frame, with a delay of one frame.
|
|
# If you wan't to get an element updated in the current frame you have to use
|
|
# this method.
|
|
update: func()
|
|
{
|
|
me.setInt("update", 1);
|
|
},
|
|
# Hide/Show element
|
|
#
|
|
# @param visible Whether the element should be visible
|
|
setVisible: func(visible = 1)
|
|
{
|
|
me.setBool("visible", visible);
|
|
},
|
|
getVisible: func me.getBool("visible"),
|
|
# Hide element (Shortcut for setVisible(0))
|
|
hide: func me.setVisible(0),
|
|
# Show element (Shortcut for setVisible(1))
|
|
show: func me.setVisible(1),
|
|
# Toggle element visibility
|
|
toggleVisibility: func me.setVisible( !me.getVisible() ),
|
|
#
|
|
setGeoPosition: func(lat, lon)
|
|
{
|
|
me._getTf()._node.getNode("m-geo[4]", 1).setValue("N" ~ lat);
|
|
me._getTf()._node.getNode("m-geo[5]", 1).setValue("E" ~ lon);
|
|
return me;
|
|
},
|
|
# Create a new transformation matrix
|
|
#
|
|
# @param vals Default values (Vector of 6 elements)
|
|
createTransform: func(vals = nil)
|
|
{
|
|
var node = me._node.addChild("tf", 1); # tf[0] is reserved for
|
|
# setRotation
|
|
return Transform.new(node, vals);
|
|
},
|
|
# Shortcut for setting translation
|
|
setTranslation: func { me._getTf().setTranslation(arg); return me; },
|
|
# Set rotation around transformation center (see #setCenter).
|
|
#
|
|
# @note This replaces the the existing transformation. For additional scale or
|
|
# translation use additional transforms (see #createTransform).
|
|
setRotation: func(rot)
|
|
{
|
|
if( me['_tf_rot'] == nil )
|
|
# always use the first matrix slot to ensure correct rotation
|
|
# around transformation center.
|
|
me['_tf_rot'] = Transform.new(me._node.getNode("tf[0]", 1));
|
|
|
|
me._tf_rot.setRotation(rot, me.getCenter());
|
|
return me;
|
|
},
|
|
# Shortcut for setting scale
|
|
setScale: func { me._getTf().setScale(arg); return me; },
|
|
# Shortcut for getting scale
|
|
getScale: func me._getTf().getScale(),
|
|
# Set the fill/background/boundingbox color
|
|
#
|
|
# @param color Vector of 3 or 4 values in [0, 1]
|
|
setColorFill: func me.set('fill', _getColor(arg)),
|
|
#
|
|
getBoundingBox: func()
|
|
{
|
|
var bb = me._node.getNode("bounding-box");
|
|
if( bb != nil )
|
|
{
|
|
var min_x = bb.getNode("min-x").getValue();
|
|
|
|
if( min_x != nil )
|
|
return [ min_x,
|
|
bb.getNode("min-y").getValue(),
|
|
bb.getNode("max-x").getValue(),
|
|
bb.getNode("max-y").getValue() ];
|
|
}
|
|
|
|
return [0, 0, 0, 0];
|
|
},
|
|
# Calculate the transformation center based on bounding box and center-offset
|
|
updateCenter: func
|
|
{
|
|
me.update();
|
|
var bb = me.getTransformedBounds();
|
|
|
|
if( bb[0] > bb[2] or bb[1] > bb[3] )
|
|
return;
|
|
|
|
me._setupCenterNodes
|
|
(
|
|
(bb[0] + bb[2]) / 2 + (me.get("center-offset-x") or 0),
|
|
(bb[1] + bb[3]) / 2 + (me.get("center-offset-y") or 0)
|
|
);
|
|
return me;
|
|
},
|
|
# Set transformation center (currently only used for rotation)
|
|
setCenter: func()
|
|
{
|
|
var center = _arg2valarray(arg);
|
|
if( size(center) != 2 )
|
|
return debug.warn("invalid arg");
|
|
|
|
me._setupCenterNodes(center[0], center[1]);
|
|
return me;
|
|
},
|
|
# Get transformation center
|
|
getCenter: func()
|
|
{
|
|
var center = [0, 0];
|
|
me._setupCenterNodes();
|
|
|
|
if( me._center[0] != nil )
|
|
center[0] = me._center[0].getValue() or 0;
|
|
if( me._center[1] != nil )
|
|
center[1] = me._center[1].getValue() or 0;
|
|
|
|
return center;
|
|
},
|
|
# Internal Transform for convenience transform functions
|
|
_getTf: func
|
|
{
|
|
if( me['_tf'] == nil )
|
|
me['_tf'] = me.createTransform();
|
|
return me._tf;
|
|
},
|
|
_setupCenterNodes: func(cx = nil, cy = nil)
|
|
{
|
|
if( me["_center"] == nil )
|
|
me["_center"] = [
|
|
me._node.getNode("center[0]", cx != nil),
|
|
me._node.getNode("center[1]", cy != nil)
|
|
];
|
|
|
|
if( cx != nil )
|
|
me._center[0].setDoubleValue(cx);
|
|
if( cy != nil )
|
|
me._center[1].setDoubleValue(cy);
|
|
}
|
|
};
|
|
|
|
# Group
|
|
# ==============================================================================
|
|
# Class for a group element on a canvas
|
|
#
|
|
var Group = {
|
|
# public:
|
|
new: func(ghost)
|
|
{
|
|
return { parents: [Group, Element.new(ghost)] };
|
|
},
|
|
# Create a child of given type with specified id.
|
|
# type can be group, text
|
|
createChild: func(type, id = nil)
|
|
{
|
|
var ghost = me._createChild(type, id);
|
|
var factory = me._getFactory(type);
|
|
if( factory == nil )
|
|
return ghost;
|
|
|
|
return factory(ghost);
|
|
},
|
|
# Create multiple children of given type
|
|
createChildren: func(type, count)
|
|
{
|
|
var factory = me._getFactory(type);
|
|
if( factory == nil )
|
|
return [];
|
|
|
|
var nodes = props._addChildren(me._node._g, [type, count, 0, 0]);
|
|
for(var i = 0; i < count; i += 1)
|
|
nodes[i] = factory( me._getChild(nodes[i]) );
|
|
|
|
return nodes;
|
|
},
|
|
# Create a path child drawing a (rounded) rectangle
|
|
#
|
|
# @param x Position of left border
|
|
# @param y Position of top border
|
|
# @param w Width
|
|
# @param h Height
|
|
# @param cfg Optional settings (eg. {"border-top-radius": 5})
|
|
rect: func(x, y, w, h, cfg = nil)
|
|
{
|
|
return me.createChild("path").rect(x, y, w, h, cfg);
|
|
},
|
|
# Get a vector of all child elements
|
|
getChildren: func()
|
|
{
|
|
var children = [];
|
|
|
|
foreach(var c; me._node.getChildren())
|
|
if( me._isElementNode(c) )
|
|
append(children, me._wrapElement(c));
|
|
|
|
return children;
|
|
},
|
|
# Get first child with given id (breadth-first search)
|
|
#
|
|
# @note Use with care as it can take several miliseconds (for me eg. ~2ms).
|
|
# TODO check with new C++ implementation
|
|
getElementById: func(id)
|
|
{
|
|
var ghost = me._getElementById(id);
|
|
if( ghost == nil )
|
|
return nil;
|
|
|
|
var node = props.wrapNode(ghost._node_ghost);
|
|
var factory = me._getFactory( node.getName() );
|
|
if( factory == nil )
|
|
return ghost;
|
|
|
|
return factory(ghost);
|
|
},
|
|
# Remove all children
|
|
removeAllChildren: func()
|
|
{
|
|
foreach(var type; keys(me._element_factories))
|
|
me._node.removeChildren(type, 0);
|
|
return me;
|
|
},
|
|
# private:
|
|
_isElementNode: func(el)
|
|
{
|
|
# element nodes have type NONE and valid element names (those in the factory
|
|
# list)
|
|
return el.getType() == "NONE"
|
|
and me._element_factories[ el.getName() ] != nil;
|
|
},
|
|
_wrapElement: func(node)
|
|
{
|
|
# Create element from existing node
|
|
return me._element_factories[ node.getName() ]( me._getChild(node._g) );
|
|
},
|
|
_getFactory: func(type)
|
|
{
|
|
var factory = me._element_factories[type];
|
|
|
|
if( factory == nil )
|
|
debug.dump("canvas.Group.createChild(): unknown type (" ~ type ~ ")");
|
|
|
|
return factory;
|
|
}
|
|
};
|
|
|
|
# Map
|
|
# ==============================================================================
|
|
# Class for a group element on a canvas with possibly geopgraphic positions
|
|
# which automatically get projected according to the specified projection.
|
|
#
|
|
var Map = {
|
|
new: func(ghost)
|
|
{
|
|
return { parents: [Map, Group.new(ghost)] };
|
|
}
|
|
# TODO
|
|
};
|
|
|
|
# Text
|
|
# ==============================================================================
|
|
# Class for a text element on a canvas
|
|
#
|
|
var Text = {
|
|
new: func(ghost)
|
|
{
|
|
return { parents: [Text, Element.new(ghost)] };
|
|
},
|
|
# Set the text
|
|
setText: func(text)
|
|
{
|
|
me.set("text", typeof(text) == 'scalar' ? text : "");
|
|
},
|
|
# Set alignment
|
|
#
|
|
# @param algin String, one of:
|
|
# left-top
|
|
# left-center
|
|
# left-bottom
|
|
# center-top
|
|
# center-center
|
|
# center-bottom
|
|
# right-top
|
|
# right-center
|
|
# right-bottom
|
|
# left-baseline
|
|
# center-baseline
|
|
# right-baseline
|
|
# left-bottom-baseline
|
|
# center-bottom-baseline
|
|
# right-bottom-baseline
|
|
#
|
|
setAlignment: func(align)
|
|
{
|
|
me.set("alignment", align);
|
|
},
|
|
# Set the font size
|
|
setFontSize: func(size, aspect = 1)
|
|
{
|
|
me.setDouble("character-size", size);
|
|
me.setDouble("character-aspect-ratio", aspect);
|
|
},
|
|
# Set font (by name of font file)
|
|
setFont: func(name)
|
|
{
|
|
me.set("font", name);
|
|
},
|
|
# Enumeration of values for drawing mode:
|
|
TEXT: 1, # The text itself
|
|
BOUNDINGBOX: 2, # A bounding box (only lines)
|
|
FILLEDBOUNDINGBOX: 4, # A filled bounding box
|
|
ALIGNMENT: 8, # Draw a marker (cross) at the position of the text
|
|
# Set draw mode. Binary combination of the values above. Since I haven't found
|
|
# a bitwise or we have to use a + instead.
|
|
#
|
|
# eg. my_text.setDrawMode(Text.TEXT + Text.BOUNDINGBOX);
|
|
setDrawMode: func(mode)
|
|
{
|
|
me.setInt("draw-mode", mode);
|
|
},
|
|
# Set bounding box padding
|
|
setPadding: func(pad)
|
|
{
|
|
me.setDouble("padding", pad);
|
|
},
|
|
setMaxWidth: func(w)
|
|
{
|
|
me.setDouble("max-width", w);
|
|
},
|
|
setColor: func me.set('fill', _getColor(arg)),
|
|
setColorFill: func me.set('background', _getColor(arg))
|
|
};
|
|
|
|
# Path
|
|
# ==============================================================================
|
|
# Class for an (OpenVG) path element on a canvas
|
|
#
|
|
var Path = {
|
|
# Path segment commands (VGPathCommand)
|
|
VG_CLOSE_PATH: 0,
|
|
VG_MOVE_TO: 2,
|
|
VG_MOVE_TO_ABS: 2,
|
|
VG_MOVE_TO_REL: 3,
|
|
VG_LINE_TO: 4,
|
|
VG_LINE_TO_ABS: 4,
|
|
VG_LINE_TO_REL: 5,
|
|
VG_HLINE_TO: 6,
|
|
VG_HLINE_TO_ABS: 6,
|
|
VG_HLINE_TO_REL: 7,
|
|
VG_VLINE_TO: 8,
|
|
VG_VLINE_TO_ABS: 8,
|
|
VG_VLINE_TO_REL: 9,
|
|
VG_QUAD_TO: 10,
|
|
VG_QUAD_TO_ABS: 10,
|
|
VG_QUAD_TO_REL: 11,
|
|
VG_CUBIC_TO: 12,
|
|
VG_CUBIC_TO_ABS: 12,
|
|
VG_CUBIC_TO_REL: 13,
|
|
VG_SQUAD_TO: 14,
|
|
VG_SQUAD_TO_ABS: 14,
|
|
VG_SQUAD_TO_REL: 15,
|
|
VG_SCUBIC_TO: 16,
|
|
VG_SCUBIC_TO_ABS: 16,
|
|
VG_SCUBIC_TO_REL: 17,
|
|
VG_SCCWARC_TO: 20, # Note that CC and CCW commands are swapped. This is
|
|
VG_SCCWARC_TO_ABS:20, # needed due to the different coordinate systems used.
|
|
VG_SCCWARC_TO_REL:21, # In OpenVG values along the y-axis increase from bottom
|
|
VG_SCWARC_TO: 18, # to top, whereas in the Canvas system it is flipped.
|
|
VG_SCWARC_TO_ABS: 18,
|
|
VG_SCWARC_TO_REL: 19,
|
|
VG_LCCWARC_TO: 24,
|
|
VG_LCCWARC_TO_ABS:24,
|
|
VG_LCCWARC_TO_REL:25,
|
|
VG_LCWARC_TO: 22,
|
|
VG_LCWARC_TO_ABS: 22,
|
|
VG_LCWARC_TO_REL: 23,
|
|
|
|
# Number of coordinates per command
|
|
num_coords: [
|
|
0, 0, # VG_CLOSE_PATH
|
|
2, 2, # VG_MOVE_TO
|
|
2, 2, # VG_LINE_TO
|
|
1, 1, # VG_HLINE_TO
|
|
1, 1, # VG_VLINE_TO
|
|
4, 4, # VG_QUAD_TO
|
|
6, 6, # VG_CUBIC_TO
|
|
2, 2, # VG_SQUAD_TO
|
|
4, 4, # VG_SCUBIC_TO
|
|
5, 5, # VG_SCCWARC_TO
|
|
5, 5, # VG_SCWARC_TO
|
|
5, 5, # VG_LCCWARC_TO
|
|
5, 5 # VG_LCWARC_TO
|
|
],
|
|
|
|
#
|
|
new: func(ghost)
|
|
{
|
|
return {
|
|
parents: [Path, Element.new(ghost)],
|
|
_num_cmds: 0,
|
|
_num_coords: 0
|
|
};
|
|
},
|
|
# Remove all existing path data
|
|
reset: func
|
|
{
|
|
me._node.removeChildren('cmd', 0);
|
|
me._node.removeChildren('coord', 0);
|
|
me._node.removeChildren('coord-geo', 0);
|
|
me._num_cmds = 0;
|
|
me._num_coords = 0;
|
|
return me;
|
|
},
|
|
# Set the path data (commands and coordinates)
|
|
setData: func(cmds, coords)
|
|
{
|
|
me.reset();
|
|
me._node.setValues({cmd: cmds, coord: coords});
|
|
me._num_cmds = size(cmds);
|
|
me._num_coords = size(coords);
|
|
return me;
|
|
},
|
|
setDataGeo: func(cmds, coords)
|
|
{
|
|
me.reset();
|
|
me._node.setValues({cmd: cmds, 'coord-geo': coords});
|
|
me._num_cmds = size(cmds);
|
|
me._num_coords = size(coords);
|
|
return me;
|
|
},
|
|
# Add a path segment
|
|
addSegment: func(cmd, coords...)
|
|
{
|
|
var coords = _arg2valarray(coords);
|
|
var num_coords = me.num_coords[cmd];
|
|
if( size(coords) != num_coords )
|
|
debug.warn
|
|
(
|
|
"Invalid number of arguments (expected " ~ num_coords ~ ")"
|
|
);
|
|
else
|
|
{
|
|
me.setInt("cmd[" ~ (me._num_cmds += 1) ~ "]", cmd);
|
|
for(var i = 0; i < num_coords; i += 1)
|
|
me.setDouble("coord[" ~ (me._num_coords += 1) ~ "]", coords[i]);
|
|
}
|
|
|
|
return me;
|
|
},
|
|
# Move path cursor
|
|
moveTo: func me.addSegment(me.VG_MOVE_TO_ABS, arg),
|
|
move: func me.addSegment(me.VG_MOVE_TO_REL, arg),
|
|
# Add a line
|
|
lineTo: func me.addSegment(me.VG_LINE_TO_ABS, arg),
|
|
line: func me.addSegment(me.VG_LINE_TO_REL, arg),
|
|
# Add a horizontal line
|
|
horizTo: func me.addSegment(me.VG_HLINE_TO_ABS, arg),
|
|
horiz: func me.addSegment(me.VG_HLINE_TO_REL, arg),
|
|
# Add a vertical line
|
|
vertTo: func me.addSegment(me.VG_VLINE_TO_ABS, arg),
|
|
vert: func me.addSegment(me.VG_VLINE_TO_REL, arg),
|
|
# Add a quadratic Bézier curve
|
|
quadTo: func me.addSegment(me.VG_QUAD_TO_ABS, arg),
|
|
quad: func me.addSegment(me.VG_QUAD_TO_REL, arg),
|
|
# Add a cubic Bézier curve
|
|
cubicTo: func me.addSegment(me.VG_CUBIC_TO_ABS, arg),
|
|
cubic: func me.addSegment(me.VG_CUBIC_TO_REL, arg),
|
|
# Add a smooth quadratic Bézier curve
|
|
quadTo: func me.addSegment(me.VG_SQUAD_TO_ABS, arg),
|
|
quad: func me.addSegment(me.VG_SQUAD_TO_REL, arg),
|
|
# Add a smooth cubic Bézier curve
|
|
scubicTo: func me.addSegment(me.VG_SCUBIC_TO_ABS, arg),
|
|
scubic: func me.addSegment(me.VG_SCUBIC_TO_REL, arg),
|
|
# Draw an elliptical arc (shorter counter-clockwise arc)
|
|
arcSmallCCWTo: func me.addSegment(me.VG_SCCWARC_TO_ABS, arg),
|
|
arcSmallCCW: func me.addSegment(me.VG_SCCWARC_TO_REL, arg),
|
|
# Draw an elliptical arc (shorter clockwise arc)
|
|
arcSmallCWTo: func me.addSegment(me.VG_SCWARC_TO_ABS, arg),
|
|
arcSmallCW: func me.addSegment(me.VG_SCWARC_TO_REL, arg),
|
|
# Draw an elliptical arc (longer counter-clockwise arc)
|
|
arcLargeCCWTo: func me.addSegment(me.VG_LCCWARC_TO_ABS, arg),
|
|
arcLargeCCW: func me.addSegment(me.VG_LCCWARC_TO_REL, arg),
|
|
# Draw an elliptical arc (shorter clockwise arc)
|
|
arcLargeCWTo: func me.addSegment(me.VG_LCWARC_TO_ABS, arg),
|
|
arcLargeCW: func me.addSegment(me.VG_LCWARC_TO_REL, arg),
|
|
# Close the path (implicit lineTo to first point of path)
|
|
close: func me.addSegment(me.VG_CLOSE_PATH),
|
|
|
|
# Add a (rounded) rectangle to the path
|
|
#
|
|
# @param x Position of left border
|
|
# @param y Position of top border
|
|
# @param w Width
|
|
# @param h Height
|
|
# @param cfg Optional settings (eg. {"border-top-radius": 5})
|
|
rect: func(x, y, w, h, cfg = nil)
|
|
{
|
|
var opts = (cfg != nil) ? cfg : {};
|
|
|
|
# resolve border-[top-,bottom-][left-,right-]radius
|
|
var br = opts["border-radius"];
|
|
if( typeof(br) == 'scalar' )
|
|
br = [br, br];
|
|
|
|
var _parseRadius = func(id)
|
|
{
|
|
if( (var r = opts["border-" ~ id ~ "-radius"]) == nil )
|
|
{
|
|
# parse top, bottom, left, right separate if no value specified for
|
|
# single corner
|
|
foreach(var s; ["top", "bottom", "left", "right"])
|
|
{
|
|
if( id.starts_with(s ~ "-") )
|
|
{
|
|
r = opts["border-" ~ s ~ "-radius"];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if( r == nil )
|
|
return br;
|
|
else if( typeof(r) == 'scalar' )
|
|
return [r, r];
|
|
else
|
|
return r;
|
|
};
|
|
|
|
# top-left
|
|
if( (var r = _parseRadius("top-left")) != nil )
|
|
{
|
|
me.moveTo(x, y + r[1])
|
|
.arcSmallCWTo(r[0], r[1], 0, x + r[0], y);
|
|
}
|
|
else
|
|
me.moveTo(x, y);
|
|
|
|
# top-right
|
|
if( (r = _parseRadius("top-right")) != nil )
|
|
{
|
|
me.horizTo(x + w - r[0])
|
|
.arcSmallCWTo(r[0], r[1], 0, x + w, y + r[1]);
|
|
}
|
|
else
|
|
me.horizTo(x + w);
|
|
|
|
# bottom-right
|
|
if( (r = _parseRadius("bottom-right")) != nil )
|
|
{
|
|
me.vertTo(y + h - r[1])
|
|
.arcSmallCWTo(r[0], r[1], 0, x + w - r[0], y + h);
|
|
}
|
|
else
|
|
me.vertTo(y + h);
|
|
|
|
# bottom-left
|
|
if( (r = _parseRadius("bottom-left")) != nil )
|
|
{
|
|
me.horizTo(x + r[0])
|
|
.arcSmallCWTo(r[0], r[1], 0, x, y + h - r[1]);
|
|
}
|
|
else
|
|
me.horizTo(x);
|
|
|
|
return me.close();
|
|
},
|
|
|
|
setColor: func me.setStroke(_getColor(arg)),
|
|
setColorFill: func me.setFill(_getColor(arg)),
|
|
|
|
setFill: func(fill)
|
|
{
|
|
me.set('fill', fill);
|
|
},
|
|
setStroke: func(stroke)
|
|
{
|
|
me.set('stroke', stroke);
|
|
},
|
|
setStrokeLineWidth: func(width)
|
|
{
|
|
me.setDouble('stroke-width', width);
|
|
},
|
|
# Set stroke linecap
|
|
#
|
|
# @param linecap String, "butt", "round" or "square"
|
|
#
|
|
# See http://www.w3.org/TR/SVG/painting.html#StrokeLinecapProperty for details
|
|
setStrokeLineCap: func(linecap)
|
|
{
|
|
me.set('stroke-linecap', linecap);
|
|
},
|
|
# Set stroke dasharray
|
|
#
|
|
# @param pattern Vector, Vector of alternating dash and gap lengths
|
|
# [on1, off1, on2, ...]
|
|
setStrokeDashArray: func(pattern)
|
|
{
|
|
if( typeof(pattern) == 'vector' )
|
|
me.set('stroke-dasharray', string.join(',', pattern));
|
|
else
|
|
debug.warn("setStrokeDashArray: vector expected!");
|
|
|
|
return me;
|
|
}
|
|
};
|
|
|
|
# Image
|
|
# ==============================================================================
|
|
# Class for an image element on a canvas
|
|
#
|
|
var Image = {
|
|
new: func(ghost)
|
|
{
|
|
return {parents: [Image, Element.new(ghost)]};
|
|
},
|
|
# Set image file to be used
|
|
#
|
|
# @param file Path to file or canvas (Use canvas://... for canvas, eg.
|
|
# canvas://by-index/texture[0])
|
|
setFile: func(file)
|
|
{
|
|
me.set("file", file);
|
|
},
|
|
# Set rectangular region of source image to be used
|
|
#
|
|
# @param left Rectangle minimum x coordinate
|
|
# @param top Rectangle minimum y coordinate
|
|
# @param right Rectangle maximum x coordinate
|
|
# @param bottom Rectangle maximum y coordinate
|
|
# @param normalized Whether to use normalized ([0,1]) or image
|
|
# ([0, image_width]/[0, image_height]) coordinates
|
|
setSourceRect: func(left, top, right, bottom, normalized = 1)
|
|
{
|
|
me._node.getNode("source", 1).setValues({
|
|
left: left,
|
|
top: top,
|
|
right: right,
|
|
bottom: bottom,
|
|
normalized: normalized
|
|
});
|
|
return me;
|
|
},
|
|
# Set size of image element
|
|
setSize: func(width, height)
|
|
{
|
|
me._node.setValues({size: [width, height]});
|
|
return me;
|
|
}
|
|
};
|
|
|
|
# Element factories used by #Group elements to create children
|
|
Group._element_factories = {
|
|
"group": Group.new,
|
|
"map": Map.new,
|
|
"text": Text.new,
|
|
"path": Path.new,
|
|
"image": Image.new
|
|
};
|
|
|
|
# Canvas
|
|
# ==============================================================================
|
|
# Class for a canvas
|
|
#
|
|
var Canvas = {
|
|
# Place this canvas somewhere onto the object. Pass criterions for placement
|
|
# as a hash, eg:
|
|
#
|
|
# my_canvas.addPlacement({
|
|
# "texture": "EICAS.png",
|
|
# "node": "PFD-Screen",
|
|
# "parent": "Some parent name"
|
|
# });
|
|
#
|
|
# Note that we can choose whichever of the three filter criterions we use for
|
|
# matching the target object for our placement. If none of the three fields is
|
|
# given every texture of the model will be replaced.
|
|
addPlacement: func(vals)
|
|
{
|
|
var placement = me.texture.addChild("placement", 0, 0);
|
|
placement.setValues(vals);
|
|
return placement;
|
|
},
|
|
# Create a new group with the given name
|
|
#
|
|
# @param id Optional id/name for the group
|
|
createGroup: func(id = nil)
|
|
{
|
|
var ghost = me._createGroup();
|
|
return {
|
|
parents: [ Group.new(ghost) ]
|
|
};
|
|
},
|
|
# Set the background color
|
|
#
|
|
# @param color Vector of 3 or 4 values in [0, 1]
|
|
setColorBackground: func () { me.texture.getNode('background', 1).setValue(_getColor(arg)); me; },
|
|
# Get path of canvas to be used eg. in Image::setFile
|
|
getPath: func()
|
|
{
|
|
return "canvas://by-index/texture[" ~ me.texture.getIndex() ~ "]";
|
|
},
|
|
# Destructor
|
|
#
|
|
# releases associated canvas and makes this object unusable
|
|
del: func
|
|
{
|
|
me.texture.remove();
|
|
me.parents = nil; # ensure all ghosts get destroyed
|
|
}
|
|
};
|
|
|
|
var wrapCanvas = func(canvas_ghost)
|
|
{
|
|
return {
|
|
parents: [Canvas, canvas_ghost],
|
|
texture: props.wrapNode(canvas_ghost._node_ghost)
|
|
};
|
|
}
|
|
|
|
# Create a new canvas. Pass parameters as hash, eg:
|
|
#
|
|
# var my_canvas = canvas.new({
|
|
# "name": "PFD-Test",
|
|
# "size": [512, 512],
|
|
# "view": [768, 1024],
|
|
# "mipmapping": 1
|
|
# });
|
|
var new = func(vals)
|
|
{
|
|
var m = wrapCanvas(_newCanvasGhost());
|
|
m.texture.setValues(vals);
|
|
return m;
|
|
};
|
|
|
|
# Get the first existing canvas with the given name
|
|
#
|
|
# @param name Name of the canvas
|
|
# @return #Canvas, if canvas with #name exists
|
|
# nil, otherwise
|
|
var get = func(arg)
|
|
{
|
|
if( isa(arg, props.Node) )
|
|
var node = arg;
|
|
else if( typeof(arg) == "hash" )
|
|
var node = props.Node.new(arg);
|
|
else
|
|
die("canvas.new: Invalid argument.");
|
|
|
|
var canvas_ghost = _getCanvasGhost(node._g);
|
|
if( canvas_ghost == nil )
|
|
return nil;
|
|
|
|
return wrapCanvas(canvas_ghost);
|
|
};
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Show warnings if API used with too old version of FlightGear without Canvas
|
|
# support (Wrapped in anonymous function do not polute the canvas namespace)
|
|
|
|
(func {
|
|
var legacy_dir = getprop("/sim/fg-root") ~ "/Nasal/canvas";
|
|
var version_str = getprop("/sim/version/flightgear");
|
|
if( string.scanf(version_str, "%u.%u.%u", var fg_version = []) < 1 )
|
|
debug.warn("Canvas: Error parsing flightgear version (" ~ version_str ~ ")");
|
|
else
|
|
{
|
|
if( fg_version[0] < 2
|
|
or (fg_version[0] == 2 and fg_version[1] < 8) )
|
|
{
|
|
debug.warn("Canvas: FlightGear version too old (" ~ version_str ~ ")");
|
|
gui.popupTip
|
|
(
|
|
"FlightGear v2.8.0 or newer needed for Canvas support!",
|
|
600,
|
|
{button: {legend: "Ok", binding: {command: "dialog-close"}}}
|
|
);
|
|
}
|
|
|
|
# Load support for older versions of FlightGear (TODO generalize :) )
|
|
if( fg_version[0] == 2 and fg_version[1] == 8 )
|
|
io.load_nasal(legacy_dir ~ "/api.nas.2.8", "canvas");
|
|
}
|
|
|
|
Canvas.property_root = props.globals.getNode("canvas/by-index", 1);
|
|
})();
|