#------------------------------------------------------------------------------- # canvas.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) { var obj = { parents: [Path, Element.new(ghost)], _first_cmd: 0, _first_coord: 0, _last_cmd: -1, _last_coord: -1 }; return obj; }, # Remove all existing path data reset: func { me._node.removeChildren("cmd", 0); me._node.removeChildren("coord", 0); me._node.removeChildren("coord-geo", 0); me._first_cmd = 0; me._first_coord = 0; me._last_cmd = -1; me._last_coord = -1; return me; }, # Set the path data (commands and coordinates) setData: func(cmds, coords) { me.reset(); me._node.setValues({cmd: cmds, coord: coords}); me._last_cmd = size(cmds) - 1; me._last_coord = size(coords) - 1; return me; }, setDataGeo: func(cmds, coords) { me.reset(); me._node.setValues({cmd: cmds, "coord-geo": coords}); me._last_cmd = size(cmds) - 1; me._last_coord = size(coords) - 1; 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._last_cmd += 1)~"]", cmd); for (var i = 0; i < num_coords; i += 1) me.setDouble("coord["~(me._last_coord += 1)~"]", coords[i]); } return me; }, addSegmentGeo: 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._last_cmd += 1)~"]", cmd); for (var i = 0; i < num_coords; i += 1) me.set("coord-geo["~(me._last_coord += 1)~"]", coords[i]); } return me; }, # Remove first segment pop_front: func { me._removeSegment(1); }, # Remove last segment pop_back: func { me._removeSegment(0); }, # Get the number of segments getNumSegments: func() { return me._last_cmd - me._first_cmd + 1; }, # Get the number of coordinates (each command has 0..n coords) getNumCoords: func() { return me._last_coord - me._first_coord + 1; }, # 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 squadTo: func me.addSegment(me.VG_SQUAD_TO_ABS, arg), squad: 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(); }, # Add a (rounded) square to the path # # @param x Position of left border # @param y Position of top border # @param l length # @param cfg Optional settings (eg. {"border-top-radius": 5}) square: func(x, y, l, cfg = nil) { return me.rect(x, y, l, l, cfg); }, # Add an ellipse to the path # # @param rx radius x # @param ry radius y # @param cx (optional) center x coordinate or vector [cx, cy] # @param cy (optional) center y coordinate ellipse: func(rx, ry, cx = nil, cy = nil) { if (typeof(cx) == "vector") { cy = cx[1]; cx = cx[0]; } else { cx = num(cx) or 0; cy = num(cy) or 0; } me.moveTo(cx - rx, cy) .arcSmallCW(rx, ry, 0, 2*rx, 0) .arcSmallCW(rx, ry, 0, -2*rx, 0); return me; }, # Add a circle to the path # # @param r radius # @param cx (optional) center x coordinate or vector [cx, cy] # @param cy (optional) center y coordinate circle: func(r, cx = nil, cy = nil) { return me.ellipse(r, r, cx, cy); }, setColor: func { me.setStroke(_getColor(arg)); }, getColor: func { me.getStroke(); }, setColorFill: func { me.setFill(_getColor(arg)); }, getColorFill: func { me.getColorFill(); }, setFill: func(fill) { me.set("fill", fill); }, setStroke: func(stroke) { me.set("stroke", stroke); }, getStroke: func { me.get("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 linejoin # # @param linejoin String, "miter", "round" or "bevel" # # See http://www.w3.org/TR/SVG/painting.html#StrokeLinejoinProperty for details setStrokeLineJoin: func(linejoin) { me.set("stroke-linejoin", linejoin); }, # 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; }, # private: _removeSegment: func(front) { if (me.getNumSegments() < 1) { debug.warn("No segment available"); return me; } var cmd = front ? me._first_cmd : me._last_cmd; var num_coords = me.num_coords[me.get("cmd["~cmd~"]")]; if (me.getNumCoords() < num_coords) { debug.warn("To few coords available"); } me._node.removeChild("cmd", cmd); var first_coord = front ? me._first_coord : me._last_coord - num_coords + 1; for (var i = 0; i < num_coords; i += 1) { me._node.removeChild("coord", first_coord + i); } if (front) { me._first_cmd += 1; me._first_coord += num_coords; } else { me._last_cmd -= 1; me._last_coord -= num_coords; } return me; }, };