1
0
Fork 0

add canvas/draw.nas library

This commit is contained in:
Henning Stahlke 2020-03-08 21:02:44 +01:00
parent 8b0fa9a08d
commit f547786fe4
5 changed files with 878 additions and 0 deletions

26
Nasal/canvas/draw.nas Normal file
View file

@ -0,0 +1,26 @@
#
# canvas_draw loader
# 03/2020 by jsb
# if you add files to the draw subdirectory, add corresponding lines below in
# the main() function
#
var _canvas_draw_load = {
namespace: "canvas",
path: getprop("/sim/fg-root")~"/Nasal/canvas/draw/",
load: func(filename) {
io.load_nasal(me.path~filename, me.namespace);
},
main: func {
me.load("draw.nas");
me.load("transform.nas");
me.load("scales.nas");
me.load("compass.nas");
},
};
_canvas_draw_load.main();
_canvas_draw_load = nil;

View file

@ -0,0 +1,104 @@
#
# canvas.draw library - compass rose module
# created 12/2018 by jsb
# WARNING: this is still under development, interfaces may change
#
var CompassRose = {
VERSION: 1.0,
Style: {
new: func() {
var obj = {
parents: [CompassRose.Style, canvas.draw.marksStyle.new()],
mark_count: 36, # number of marks, count = 360 / interval
label_count: 12, # number of text labels (degrees), e.g. 12
label_div: 10, # >0 divide degrees by this number for text label, e.g. 10
circle_color: [255,255,255,1],
mark_color: [255,255,255,1],
label_color: [255,255,255,1],
center_mark: 0, # draw a mark in the center of the rose
fontsize: 0, # fontsize for labels
nesw: 1, # replace labels 0,90,180,270 by N,E,S,W
};
obj.setMarkLength(0.1);
obj.setSubdivisionLength(0.5);
return obj;
},
setMarkCount: func(value) {
me.mark_count = num(value) or 0;
return me;
},
setLabelCount: func(value) {
me.label_count = num(value) or 0;
return me;
},
# divide course by this value before creating text label. (try 10, 1 or 0)
setLabelDivisor: func(value) {
me.label_div = num(value) or 10;
return me;
},
setFontSize: func(value) {
me.fontsize = num(value) or 26;
return me;
},
},
};
# draw a compass rose
# cgroup canvas group, marks and lables will be created as children
CompassRose.draw = func(cgroup, radius, style=nil) {
if (style == nil) {
style = me.Style.new();
}
cgroup = cgroup.createChild("group", "compass-rose");
if (style.center_mark) {
var center = cgroup.createChild("path","center");
var l = radius * 0.1;
center.setStrokeLineWidth(1).setColor(style.circle_color)
.moveTo(-l,0).line(2*l,0).moveTo(0,-l).line(0,2*l);
}
if (style.baseline_width > 0) {
var c = cgroup.createChild("path", "circle");
c.circle(radius)
.setStrokeLineWidth(style.baseline_width)
.setColor(style.circle_color);
}
if (style.mark_count > 0) {
var marks = canvas.draw.marksCircular(cgroup, radius, 360 / style.mark_count, 0, 360, style);
marks.setStrokeLineWidth(style.mark_width);
}
var fontsize = style.fontsize;
if (style.fontsize == 0) {
fontsize = math.round(math.sqrt(radius) * 1.4);
}
if (style.label_count > 0) {
var labels = cgroup.createChild("group", "labels")
.createChildren("text", style.label_count);
var offset = (style.mark_offset < 0 ? -1 : 1) * style.mark_length * radius;
var rot = 2*math.pi/style.label_count;
forindex (i; labels) {
var t = n = int(i*360 / style.label_count);
if (style.label_div > 0) t = int(n / style.label_div);
var txt = sprintf("%d", t);
if (style.nesw) {
if (n == 0) txt = "N";
elsif (n == 90) txt = "E";
elsif (n == 180) txt = "S";
elsif (n == 270) txt = "W";
}
labels[i]
.setText(txt)
.setFontSize(fontsize)
.setColor(style.label_color)
.setAlignment("center-"~(style.mark_offset < 0 ? "top" : "bottom"))
.setTranslation(0,-radius-offset)
.setCenter(0,radius+offset)
.setRotation(i*rot);
}
}
return cgroup;
};

448
Nasal/canvas/draw/draw.nas Normal file
View file

@ -0,0 +1,448 @@
#
# canvas.draw library
# created 12/2018 by jsb
# based on plot2D.nas from the oscilloscope add-on by R. Leibner
#
# Contains functions to draw path elements on an existing canvas group.
# - basic shapes
# - grids
# - scale marks
#
# Basic shapes:
# These are macros calling existing API command for path elements.
# They return a path element, no styling done. You can easily do by passing
# the returned element to other existing functions like setColor, e.g.
# var myCircle = canvas.circle(myCanvasGroup, 10, 30, 30).setColor(myColor)
#
# Grids:
# draw horizontal and vertical lines
# from the Oscilloscope add-on by rleibner with a few modifications
#
# Scale marks:
# Draw equidistant lines ("marker", "ticks") perpendicular to a baseline.
# Baseline can be a horizontal or vertical line or a part of a circle.
# This is a building block for compass rose and tapes.
#
var draw = {
# draw line; from and to must be vectors [x,y]
line: func(cgroup, from, to) {
var path = cgroup.createChild("path", "line");
return path.moveTo(from[0], from[1]).lineTo(to[0], to[1]);
},
# draw horizontal line;
# from: optional, vector, defaults to [0,0]
# or scalar
hline: func(cgroup, length, from = nil) {
if (from == nil) {
to = [length, 0];
from = [0,0];
}
elsif (typeof(from) == "scalar") {
to = [num(from) + length, 0];
from = [num(from) ,0];
}
else to = [from[0] + length, from[1]];
me.line(cgroup, from, to);
},
# draw vertical line;
# from: optional, vector, defaults to [0,0]
# or scalar
vline: func(cgroup, length, from = nil) {
if (from == nil) {
to = [0, length];
from = [0,0];
}
elsif (typeof(from) == "scalar") {
to = [0, num(from) + length];
from = [0, num(from)];
}
else to = [from[0], from[1] + length];
me.line(cgroup, from, to);
},
# if center_x is given as vector, its first two elements define the center
# and center_y is ignored
circle: func(cgroup, radius, center_x = nil, center_y = nil) {
var path = cgroup.createChild("path", "circle");
return path.circle(radius, center_x, center_y);
},
# if center_x is given as vector, its first two elements define the center
# and center_y is ignored
ellipse: func(cgroup, radius_x, radius_y, center_x = nil, center_y = nil) {
var path = cgroup.createChild("path", "ellipse");
return path.ellipse(radius_x, radius_y, center_x, center_y);
},
# draw part of a circle
# radius as integer (for circle) or [rx,ry] (for ellipse) in pixels.
# center vector [x,y]
# from_deg begin of arc in degree (0 = north, increasing clockwise)
# to_deg end of arc
arc: func(cgroup, radius, center, from_deg = nil, to_deg = nil) {
if (from_deg == nil)
return me.circle(radius, center);
var path = cgroup.createChild("path", "arc");
from_deg *= D2R;
to_deg *= D2R;
var (rx, ry) = (typeof(radius) == "vector") ? [radius[0], radius[1]] : [radius, radius];
var (fs, fc) = [math.sin(from_deg), math.cos(from_deg)];
var dx = (math.sin(to_deg) - fs) * rx;
var dy = (math.cos(to_deg) - fc) * ry;
path.moveTo(center[0] + rx*fs, center[1] - ry*fc);
if(abs(to_deg - from_deg) > 180*D2R) {
path.arcLargeCW(rx, ry, 0, dx, -dy);
}
else {
path.arcSmallCW(rx, ry, 0, dx, -dy);
}
return path;
},
# x, y is top, left corner
rectangle: func(cgroup, width, height, x = 0, y = 0, rounded = nil) {
var path = cgroup.createChild("path", "rectangle");
return path.rect(x, y, width, height, {"border-radius": rounded});
},
# x, y is top, left corner
square: func(cgroup, length, center_x = 0, center_y = 0, cfg = nil) {
var path = cgroup.createChild("path", "square");
return path.square(center_x, center_y, length, cfg = nil);
},
# deltoid draws a kite (dy1 > 0 and dy2 > 0) or a arrow head (dy2 < 0)
# dx = width
# dy1 = height of "upper" triangle
# dy2 = height of "lower" triangle, < 0 draws an arrow head
# x, y = position of tip
deltoid: func (cgroup, dx, dy1, dy2, x = 0, y = 0) {
var path = cgroup.createChild("path", "deltoid");
path.moveTo(x, y)
.line(-dx/2, dy1)
.line(dx/2, dy2)
.line(dx/2, -dy2)
.close();
return path;
},
# draw a "diamond"
# dx: width
# dy: height
rhombus: func(cgroup, dx, dy, center_x = 0, center_y = 0) {
return draw.deltoid(cgroup, dx, dy/2, dy/2, center_x, center_y - dy/2);
},
};
#aliases
draw.diamond = draw.rhombus;
draw.colors = {
white: [1, 1, 1],
grey50: [0.5, 0.5, 0.5],
grey25: [0.25, 0.25, 0.25],
black: [0, 0, 0],
red: [1, 0, 0],
green: [0, 1, 0],
blue: [0, 0, 1],
cyan: [0, 1, 1],
magenta: [1, 0, 1],
yellow: [1, 1, 0],
orange: [1, 0.5, 0],
};
#base class for styles
draw.style = {
new: func() {
var obj = {
parents: [draw.style],
_color: [255, 255, 255, 1],
_color_fill: nil,
_stroke_width: 1,
};
return obj;
},
#set value of existing(!) key
set: func(key, value) {
if (contains(me, key)) {
me[key] = value;
return me;
}
return nil;
},
get: func(key) {
return me[key];
},
setColor: func() {
me._color = arg;
return me;
},
getColor: func() {
return me._color;
},
setColorFill: func() {
me._color_fill = arg;
return me;
},
setStrokeLineWidth: func() {
me._stroke_width = arg;
return me;
},
};
#
# marksStyle - parameter set for draw.marks*
# Interpretation depends on the draw function used. In general, marks are
# lines drawn perpendicular to a baseline in certain intervals. 'Big' and
# 'small' marks are supported by means of 'subdivisions'.
# Some values are expressed as percentage to allow easy scaling.
# Again: Interpretation depends on the draw function using this style.
#
draw.marksStyle = {
# constants to align marks relative to baseline
MARK_LEFT: -1,
MARK_UP: -1,
MARK_CENTER: 0,
MARK_RIGHT: 1,
MARK_DOWN: 1,
MARK_IN: -1, # from radius to center of circle
MARK_OUT: 1, # from radius to outside
new: func() {
var obj = {
parents: [draw.marksStyle, draw.style.new()],
baseline_width: 0, # stroke of baseline
mark_length: 0.8, # length of a division marker in %interval or %radius
mark_offset: 0, # in %mark_length, see setMarkLength below
mark_width: 1, # in pixel
subdivisions: 0, # number of smaller marks between to marks
subdiv_length: 0.5, # in %mark_length
};
return obj;
},
setBaselineWidth: func(value) {
me.baseline_width = num(value) or 0;
return me;
},
setMarkLength: func(value) {
me.mark_length = num(value) or 1;
return me;
},
# position of mark relative to baseline, call this with MARK_* defined above
# -1 = left, 0 = center, 1 = right
setMarkOffset: func(value) {
if (num(value) == nil) return nil;
me.mark_offset = value;
return me;
},
setMarkWidth: func(value) {
me.mark_width = num(value) or 1;
return me;
},
setSubdivisions: func(value) {
me.subdivisions = int(value) or 0;
return me;
},
setSubdivisionLength: func(value) {
me.subdiv_length = num(value) or 0.5;
return me;
},
};
# draw.marksLinear: draw marks for a linear scale on a canvas group, e.g. speed tape
# mark lines are draws perpendicular to baseline
# orientation of baseline; "up", "down", "left", "right"
# num_marks number of marks to draw
# interval distance between marks (pixel)
# style marksStyle hash with more parameters
draw.marksLinear = func(cgroup, orientation, num_marks, interval, style)
{
if (!isa(style, draw.marksStyle)) {
printlog("alert", "draw.marks: invalid style argument.");
return nil;
}
orientation = chr(string.tolower(orientation[0]));
if (orientation == "v") orientation = "d";
if (orientation == "h") orientation = "r";
var marks = cgroup.createChild("path", "marks");
if (style.baseline_width > 0) {
var length = interval * (num_marks - 1);
if (orientation == "d") {
marks.vert(length);
}
elsif(orientation == "u") {
marks.vert(-length);
}
elsif(orientation == "r") {
marks.horiz(length);
}
elsif(orientation == "l") {
marks.horiz(-length);
}
}
var mark_length = interval * style.mark_length;
if (style.subdivisions > 0) {
interval /= (style.subdivisions + 1);
var subdiv_length = mark_length * style.subdiv_length;
};
marks.setColor(style.getColor());
var offset0 = 0.5 * style.mark_offset - 0.5;
for (var i = 0; i < num_marks; i += 1) {
for (var j = 0; j <= style.subdivisions; j += 1) {
var length = (j == 0) ? mark_length : subdiv_length;
var translation = interval * (i*(style.subdivisions + 1) + j);
var offset = offset0 * length;
if (orientation == "d") {
marks.moveTo(offset, translation)
.horiz(length);
}
elsif (orientation == "u") {
marks.moveTo(offset, -translation)
.horiz(length);
}
elsif (orientation == "r") {
marks.moveTo(translation, offset)
.vert(length);
}
elsif (orientation == "l") {
marks.moveTo(-translation, offset)
.vert(length);
}
}
}
while (j) {
marks.pop_back();
j -= 1;
}
return marks;
}
# radius of baseline (circle)
# interval distance of marks in degree
# phi_start position of first mark in degree (default 0 = north)
# phi_stop position of last mark in degree (default 360)
draw.marksCircular = func(cgroup, radius, interval, phi_start = 0, phi_stop = 360, style = nil) {
if (style == nil) {
style = draw.marksStyle.new();
}
if (!isa(style, draw.marksStyle)) {
printlog("info", "draw.marksCircular: invalid style argument");
return nil;
}
# normalize
while (phi_start >= 360) { phi_start -= 360; }
while (phi_stop > 360) { phi_stop -= 360; }
if (phi_start > phi_stop) {
phi_stop += 360;
}
if (style.baseline_width > 0) {
var marks = draw.arc(cgroup, radius, [0,0], phi_start, phi_stop)
.set("id", "marksCircular")
.setColor(style.getColor());
}
else {
var marks = cgroup.createChild("path", "marksCircular").setColor(style.getColor());
}
var mark_length = style.mark_length * radius;
var subd_length = style.subdiv_length * mark_length;
interval *= D2R;
phi_start *= D2R;
phi_stop *= D2R;
var phi_s = interval / (style.subdivisions + 1);
var x = y = l = 0;
var offset0 = 0.5 * style.mark_offset - 0.5;
for (var phi = phi_start; phi <= phi_stop; phi += interval) {
for (var j = 0; j <= style.subdivisions; j += 1) {
var p = phi + j * phi_s;
#print(p*R2D);
x = math.sin(p);
y = -math.cos(p);
l = (j == 0) ? mark_length : subd_length;
r = radius + offset0 * l;
marks.moveTo(x * r, y * r)
.line(x * l, y * l);
}
}
while (j) {
marks.pop_back();
j -= 1;
}
return marks;
}
# draw.grid
# 1) (cgroup, [sizeX, sizeY], dx, dy, border = 1)
# 2) (cgroup, nx, ny, dx, dy, border = 1)
# size [width, height] in pixels.
# nx, ny number of lines in x/y direction
# dx tiles width in pixels.
# dy tiles height in pixels.
# border optional as boolean, True by default.
draw.grid = func(cgroup) {
var i = 0;
var (s, n) = ([0, 0], []);
if (typeof(arg[i]) == "vector") {
s = arg[i];
i += 1;
}
else {
var n = [arg[i], arg[i + 1]];
i += 2;
}
var dx = arg[i];
i += 1;
var dy = dx;
var border = 1;
if (size(arg) - 1 >= i) { dy = arg[i]; i += 1; }
if (size(arg) - 1 >= i) { border = arg[i]; }
if (size(n) == 2) {
s[0] = n[0] * dx - 1;
s[1] = n[1] * dy - 1;
}
#print("size: ", s[0], " ", s[1], " d:", dx, ", ", dy, " b:", border);
var grid = cgroup.createChild("path", "grid").setColor([255, 255, 255, 1]);
var (x0, y0) = border ? [0, 0] : [dx, dy];
for (var x = x0; x <= s[0]; x += dx) {
grid.moveTo(x, 0).vertTo(s[1]);
}
for (var y = y0; y <= s[1]; y += dy) {
grid.moveTo(0, y).horizTo(s[0]);
}
if (border) {
grid.moveTo(s[0], 0).vertTo(s[1]).horizTo(0);
}
return grid;
}
draw.arrow = func(cgroup, length, origin_center=0) {
#var path = cgroup.createChild("path", "arrow");
var tip_size = length * 0.1;
var offset = (origin_center) ? -length/2 : 0;
var arrow = draw.deltoid(cgroup, tip_size, tip_size, 0, 0, offset);
#if (offset) { arrow.moveTo(0, offset); }
arrow.line(0,length);
return arrow;
}

View file

@ -0,0 +1,129 @@
#
# canvas.draw library - scale module for speed tape etc.
# created 12/2018 by jsb
#
# The Scale class combines draw.marks* with text labels to create scales/gauges
# to build speed tape, compass etc.
var Scale = {
# see also canvas.draw.marksStyle
Style: {
new: func() {
var obj = {
parents: [Scale.Style, canvas.draw.marksStyle.new()],
orientation: "u",
spacing: 20, # spacing in pixel
label_interval: 1, # one txt every n marks
label_distance_abs: 0, # distance between ticks and text labels in px
label_distance_rel: 1.1, # distance in %mark_length
mark_color: [255,255,255,1],
label_color: [255,255,255,1],
fontsize: 16, # fontsize for labels
};
return obj;
},
# the 'draw direction' for a linear scale (up, down, left, right)
setOrientation: func(value) {
me.orientation = chr(string.tolower(value[0]));
if (me.orientation == "v") me.orientation = "d";
if (me.orientation == "h") me.orientation = "r";
return me;
},
setFontSize: func(value) {
me.fontsize = num(value) or 16;
return me;
},
setSpacing: func(value) {
me.spacing = num(value) or 10;
return me;
},
# draw one text label every <value> marks
setLabelInterval: func(value) {
me.label_interval = num(value) or 1;
return me;
},
# Distance of text label to mark in pixel (be careful when resizing).
# See also setLabelDistanceRel()
setLabelDistanceAbs: func(value) {
me.label_distance_abs = num(value) or 0;
return me;
},
# Distance of text label to mark in %mark_length (easy scaling).
setLabelDistanceRel: func(value) {
me.label_distance_rel = num(value) or 1.1;
return me;
},
# get alignment of text label for linear scales based on style params
getAlignmentString: func() {
if (me.orientation == "d") {
return (me.mark_offset < 0 ? "right" : "left")~"-center";
}
elsif (me.orientation == "u") {
return (me.mark_offset < 0 ? "right" : "left")~"-center";
}
elsif (me.orientation == "r") {
return "center-"~(me.mark_offset > 0 ? "top" : "bottom");
}
elsif (me.orientation == "l") {
return "center-"~(me.mark_offset > 0 ? "top" : "bottom");
}
else {
return "center-center";
}
},
},
};
# draw a scale on canvas group
# start first value of scale
# count number of values to draw
# increment value to add (can be negative)
Scale.draw = func(cgroup, start, count, increment, style=nil) {
if (style == nil) {
style = me.Style.new();
}
var label_count = count; #math.floor(math.abs(stop - start) / increment);
if (label_count < 2) {
return false;
}
var marks = canvas.draw.marksLinear(cgroup, style.orientation, label_count * style.label_interval, style.spacing, style);
marks.setStrokeLineWidth(style.mark_width);
var labels = cgroup.createChild("group", "labels")
.createChildren("text", label_count);
var length = style.spacing * style.mark_length;
var offset = style.mark_offset * length * style.label_distance_rel;
offset += (offset > 0) ? style.label_distance_abs : -style.label_distance_abs;
var alignment = style.getAlignmentString();
forindex (i; labels) {
var txt = sprintf("%d", start + i * increment);
var translation = i * style.spacing * style.label_interval;
labels[i]
.setText(txt)
.setAlignment(alignment)
.setFontSize(style.fontsize)
.setColor(style.label_color);
if (style.orientation == "d") {
labels[i].setTranslation(offset, translation);
}
elsif (style.orientation == "u") {
labels[i].setTranslation(offset, -translation);
}
elsif (style.orientation == "r") {
labels[i].setTranslation(translation, offset);
}
elsif (style.orientation == "l") {
labels[i].setTranslation(-translation, offset);
}
}
return cgroup;
};

View file

@ -0,0 +1,171 @@
#
# canvas.transform library
# created 12/2018 by jsb
# based on plot2D.nas from the oscilloscope add-on by R. Leibner
#
# Contains functions to transform existing canvas elements.
var transform = {
_xy: func(elem, uv){
# returns [x, y]: intrinsic coords of the absolute(u, v)
var (tx, ty) = elem.getTranslation();
var (sx, sy) = elem.getScale();
return [(uv[0] - tx)/sx, (uv[1] - ty)/sy];
},
move: func(elem, dx, dy){
# moves the element <dx, dy> pixels position.
var (tx, ty) = elem.getTranslation();
elem.setTranslation(tx + dx, ty + dy);
},
rotate: func(elem, deg, center){
# rotates the element <deg> degrees around <center>.
var c = me._xy(elem, center);
elem.setCenter(c).setRotation(-deg * D2R);
},
flipX: func(elem, xaxis = 0) {
elem.updateCenter();
var (sx, sy) = elem.getScale();
var (tx, ty) = elem.getTranslation();
var (xmin, ymin, xmax, ymax) = elem.getTightBoundingBox();
if (xaxis == 0) {
xaxis = tx + sx*(xmax + xmin)/2;
}
elem.setScale(-sx, sy);
elem.setTranslation(2*xaxis - tx, ty);
return elem;
},
flipY: func(elem, yaxis = 0) {
elem.updateCenter();
var (sx, sy) = elem.getScale();
var (tx, ty) = elem.getTranslation();
var (xmin, ymin, xmax, ymax) = elem.getTightBoundingBox();
if (yaxis == 0) {
yaxis = ty + sy*(ymax + ymin)/2;
}
elem.setScale(sx, -sy);
elem.setTranslation(tx, 2*yaxis - ty);
return elem;
},
# Aligns the element, moving it horizontaly to ref.
# params:
# elem element to be moved.
# ref reference may be an integer or another element.
# alignment as string: may be 'left-left', 'left-center', 'left-right',
# 'center-left', 'center-center', 'center-right',
# 'right-left', 'right-center', 'right-right'.
# If ref is a single number, the 2nd word is ignored.
alignX: func(elem, ref, alignment) {
elem.updateCenter();
var (sx, sy) = elem.getScale();
var (tx, ty) = elem.getTranslation();
var (xmin, ymin, xmax, ymax) = elem.getTightBoundingBox();
var a = split('-', alignment)[0];
var x = a == 'left' ? xmin : a == 'right' ? Xmax : (xmin + xmax)/2;
if(typeof(ref) == 'scalar') var uRef = ref;
else {
ref.updateCenter();
var (sRx, sRy) = ref.getScale();
var (tRx, tRy) = ref.getTranslation();
var (xmin, ymin, xmax, ymax) = ref.getTightBoundingBox();
var aR = split('-', alignment)[1];
var uRef = aR =='left' ? tRx+sRx*xmin : aR =='right' ? tRx+sRx*xmax : tRx+sRx*(xmin+xmax)/2;
}
elem.setTranslation(uRef-x*sx, ty);
return elem;
},
# Aligns the element, moving it vertically to ref.
# params:
# elem element to be moved.
# ref reference may be an integer or another element.
# alignment as string: may be 'top-top', 'top-center', 'top-bottom',
# 'center-top', 'center-center', 'center-bottom',
# 'bottom-top', 'bottom-center', 'bottom-bottom'.
# text elements also accept 'baseline' as reference.
# If ref is a single number, the 2nd word is ignored.
alignY: func(elem, ref, alignment) {
elem.updateCenter();
var (sx, sy) = elem.getScale();
var (tx, ty) = elem.getTranslation();
var (Xmin, Ymin, Xmax, Ymax) = elem.getTightBoundingBox();
var a = split('-', alignment)[0];
var y = a == 'top' ? Ymin : a == 'bottom' ? Ymax : (Ymin+Ymax)/2;
if(typeof(ref) =='scalar') var vRef = ref;
else {
ref.updateCenter();
var (sRx, sRy) = ref.getScale();
var (tRx, tRy) = ref.getTranslation();
var (Xmin, Ymin, Xmax, Ymax) = ref.getTightBoundingBox();
var aR = split('-', alignment)[1];
var vRef = aR =='top' ? tRy+sRy*Ymin : aR =='bottom' ? tRy+sRy*Ymax : tRy+sRy*(Ymin+Ymax)/2;
}
elem.setTranslation(tx, vRef-y*sy);
return elem;
},
# center as [x,y] in pixels, otherwise in place
rotate180: func(elem, center = nil) {
if(center == nil){
me.flipX(elem);
me.flipY(elem);
}
else {
me.flipX(elem, center[0]);
me.flipY(elem, center[1]);
}
return elem;
},
# Stretch element horizontally
# params:
# elem element to be stretched.
# factor the <new-width>/<old-width> ratio.
# ref the relative point to keep inplace. May be 'left', 'center' or 'right'.
scaleX: func(elem, factor, ref = 'left') {
elem.updateCenter();
var (sx, sy) = elem.getScale();
var (tx, ty) = elem.getTranslation();
var (xmin, ymin, xmax, ymax) = elem.getTightBoundingBox();
var x = (ref == 'left') ? xmin : (ref == 'right') ? xmax : (xmin + xmax)/2;
var u = tx + x*sx;
print("scaleX: "~factor~"; sx="~sx~" sy="~sy~" tx="~tx~" ty="~ty,
sprintf(" BB %1.3e, %1.3e, %1.3e, %1.3e, ", xmin, ymin, xmax ,ymax),
" u="~u);
elem.setScale(sx*factor, sy);
elem.setTranslation(u-x*sx*factor, ty);
return elem;
},
# strech element vertically
# params:
# elem element to be stretched.
# factor the <new-height>/<old-height> ratio.
# ref the relative point to keep inplace. May be 'top', 'center' or 'bottom'.
scaleY: func(elem, factor, ref = 'top') {
elem.updateCenter();
var (sx, sy) = elem.getScale();
var (tx, ty) = elem.getTranslation();
var (xmin, ymin, xmax, ymax) = elem.getTightBoundingBox();
var y = (ref =='top') ? ymin : (ref == 'bottom') ? ymax : (ymin + ymax)/2;
var v = ty + y*sy;
elem.setScale(sx, sy*factor);
elem.setTranslation(tx, v-y*sy*factor);
return elem;
},
# factors as [Xfactor, Yfactor] .
# ref the relative point to keep inplace:
# may be 'left-top', 'left-center', 'left-bottom',
# 'center-top', 'center-center', 'center-bottom',
# 'right-top', 'right-center', 'right-bottom'.
resize: func(elem, factors, ref = 'left-top') {
me.scaleX(elem, factors[0], split('-', ref)[0]);
me.scaleY(elem, factors[1], split('-', ref)[1]);
return elem;
},
};