434 lines
No EOL
13 KiB
Text
434 lines
No EOL
13 KiB
Text
#
|
|
# 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;
|
|
|
|
#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)) {
|
|
logprint(DEV_WARN, "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)) {
|
|
logprint(DEV_WARN, "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;
|
|
} |