add canvas/draw.nas library
This commit is contained in:
parent
8b0fa9a08d
commit
f547786fe4
5 changed files with 878 additions and 0 deletions
26
Nasal/canvas/draw.nas
Normal file
26
Nasal/canvas/draw.nas
Normal 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;
|
104
Nasal/canvas/draw/compass.nas
Normal file
104
Nasal/canvas/draw/compass.nas
Normal 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
448
Nasal/canvas/draw/draw.nas
Normal 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;
|
||||
}
|
129
Nasal/canvas/draw/scales.nas
Normal file
129
Nasal/canvas/draw/scales.nas
Normal 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;
|
||||
};
|
171
Nasal/canvas/draw/transform.nas
Normal file
171
Nasal/canvas/draw/transform.nas
Normal 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;
|
||||
},
|
||||
};
|
Loading…
Add table
Reference in a new issue