// Copyright © 2016-2020 Andy Goryachev <[email protected]> package goryachev.fx; import goryachev.common.util.CList; import java.io.ByteArrayInputStream; import javafx.collections.ObservableList; import javafx.geometry.Bounds; import javafx.geometry.Point2D; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.effect.ColorAdjust; import javafx.scene.effect.Effect; import javafx.scene.effect.GaussianBlur; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.shape.ArcTo; import javafx.scene.shape.Circle; import javafx.scene.shape.CubicCurveTo; import javafx.scene.shape.FillRule; import javafx.scene.shape.LineTo; import javafx.scene.shape.MoveTo; import javafx.scene.shape.Path; import javafx.scene.shape.PathElement; import javafx.scene.shape.QuadCurveTo; import javafx.scene.shape.SVGPath; import javafx.scene.shape.Shape; import javafx.scene.shape.StrokeLineCap; import javafx.scene.shape.StrokeLineJoin; import javafx.scene.shape.StrokeType; /** * Fx Icon Builder. * * Enables programmatic creation of icons. * * Coordinate system is similar to SVG one, its (0,0) origin is located at the upper left corner, * unless modified by setOrigin(). * * Angles are measured in radians. * For rotations, positive angle corresponds to clockwise direction, negative - to counter-clockwise. * * Stroke and fill parameters are set at the moment of creation of a stroke or a shape. Subsequent setting * of those parameters has no effect on existing elements. */ public class FxIconBuilder { private final double width; private final double height; private final CList<Node> elements; private double xorigin; private double yorigin; private double scale = 1.0; private double rotate; private double opacity = 1.0; private double xtranslate; private double ytranslate; private double strokeWidth = 1.0; private Paint fill = Color.BLACK; private Paint strokeColor = Color.BLACK; private StrokeType strokeType = StrokeType.CENTERED; private StrokeLineCap lineCap = StrokeLineCap.ROUND; private StrokeLineJoin lineJoin = StrokeLineJoin.ROUND; private double miterLimit; private double dashOffset; private FillRule fillRule; private Effect effect; private Path path; public FxIconBuilder(double width, double height, double xcenter, double ycenter) { this(width, height); setOrigin(xcenter, ycenter); } public FxIconBuilder(double size, double xcenter, double ycenter) { this(size); setOrigin(xcenter, ycenter); } public FxIconBuilder(double width, double height) { this.width = width; this.height = height; this.elements = new CList<>(); } public FxIconBuilder(double size) { this(size, size); } /** sets the builder's origin point (normally, the origin is located at the upper left corner). */ public void setOrigin(double xcenter, double ycenter) { this.xorigin = xcenter; this.yorigin = ycenter; } /** sets fill color */ public void setFill(Paint c) { fill = c; } /** creates a full size rectangle filled with the current fill color */ public void fill() { fill(-xorigin, -yorigin, width, height); } /** creates a rectangle filled with the current fill color */ public void fill(double x, double y, double w, double h) { Region r = new Region(); r.setManaged(false); r.resizeRelocate(x + xorigin, y + yorigin, w, h); r.setBackground(FX.background(fill)); elements.add(r); } public void circle(double x, double y, double radius) { Circle c = new Circle(x, y, radius); applyShapeProperties(c); elements.add(c); } protected Path createPath() { Path p = new Path(); applyNodeProperties(p); applyShapeProperties(p); p.setFillRule(fillRule); return p; } protected void applyNodeProperties(Node n) { n.setOpacity(opacity); n.setScaleX(scale); n.setScaleY(scale); n.setTranslateX(xtranslate); n.setTranslateY(ytranslate); n.setEffect(effect); n.setRotate(rotate); } protected void applyShapeProperties(Shape p) { p.setFill(fill); p.setStroke(strokeColor); p.setStrokeDashOffset(dashOffset); p.setStrokeLineCap(lineCap); p.setStrokeLineJoin(lineJoin); p.setStrokeMiterLimit(miterLimit); p.setStrokeType(strokeType); p.setStrokeWidth(strokeWidth); } protected SVGPath createSVGPath() { SVGPath p = new SVGPath(); applyNodeProperties(p); applyShapeProperties(p); p.setFillRule(fillRule); return p; } /** creates new path segment */ public Path newPath() { path = createPath(); elements.add(path); return path; } /** add autoscaling SVG path */ public SVGPath svgPath(String svg) { return svgPath(svg, true); } /** add SVG path */ public SVGPath svgPath(String svg, boolean autoScale) { SVGPath p = createSVGPath(); p.setContent(svg); // Label r = new Label(); // r.setGraphic(p); elements.add(p); if(autoScale) { autoFitLastElement(); } return p; } /** add image */ public void image(byte[] bytes) { ImageView v = new ImageView(); v.setImage(new Image(new ByteArrayInputStream(bytes))); applyNodeProperties(v); v.setTranslateX(xtranslate + xorigin); v.setTranslateY(ytranslate + yorigin); elements.add(v); } public void setScale(double x) { scale = x; } public void setOpacity(double x) { opacity = x; } public void setTranslate(double dx, double dy) { xtranslate = dx; ytranslate = dy; } public void setRotate(double angleInRadians) { rotate = FX.toDegrees(angleInRadians); } public void setRotateDegrees(double angleInDegrees) { rotate = angleInDegrees; } public void setStrokeWidth(double w) { strokeWidth = w; } public void setStrokeColor(Paint x) { strokeColor = x; } public void setStrokeLineCap(StrokeLineCap x) { lineCap = x; } public void setStrokeLineJoin(StrokeLineJoin x) { lineJoin = x; } public void setStrokeMiterLimit(double x) { miterLimit = x; } public void setEffect(Effect x) { effect = x; } public void addEffect(Effect x) { if(effect == null) { effect = x; } else { effect = setInputEffect(effect, x); } } protected Effect setInputEffect(Effect a, Effect b) { // I don't know a better way to chain effects, it's missing in FX // https://bugs.openjdk.java.net/browse/JDK-8091895 // perhaps try Blend: // https://community.oracle.com/thread/2337194?tstart=0 if(b instanceof GaussianBlur) { ((GaussianBlur)b).setInput(a); } else if(b instanceof ColorAdjust) { ((ColorAdjust)b).setInput(a); } else { throw new Error("todo: does " + b + " have setInput()?"); } return b; } protected void add(PathElement em) { if(path == null) { path = newPath(); } path.getElements().add(em); } protected Point2D currentPos() { if(path == null) { return new Point2D(xorigin, yorigin); } ObservableList<PathElement> es = path.getElements(); int sz = es.size(); if(sz == 0) { return new Point2D(xorigin, yorigin); } PathElement em = es.get(sz - 1); if(em instanceof LineTo) { LineTo p = (LineTo)em; return new Point2D(p.getX(), p.getY()); } else if(em instanceof MoveTo) { MoveTo p = (MoveTo)em; return new Point2D(p.getX(), p.getY()); } else if(em instanceof ArcTo) { ArcTo p = (ArcTo)em; return new Point2D(p.getX(), p.getY()); } else if(em instanceof CubicCurveTo) { CubicCurveTo p = (CubicCurveTo)em; return new Point2D(p.getX(), p.getY()); } else if(em instanceof QuadCurveTo) { QuadCurveTo p = (QuadCurveTo)em; return new Point2D(p.getX(), p.getY()); } else { throw new Error("?" + em); } } /** move to absolute coordinates */ public void moveTo(double x, double y) { add(new MoveTo(x + xorigin, y + yorigin)); } /** move to absolute coordinates */ public void moveRel(double dx, double dy) { Point2D p = currentPos(); add(new MoveTo(dx + p.getX(), dy + p.getY())); } /** line to absolute coordinates */ public void lineTo(double x, double y) { add(new LineTo(x + xorigin, y + yorigin)); } /** line to absolute coordinates */ public void lineRel(double dx, double dy) { Point2D p = currentPos(); add(new LineTo(dx + p.getX(), dy + p.getY())); } /** arc from current position, using the specified center coordinates, radius, and angle */ public void arcRel(double xc, double yc, double radius, double angle) { // arcTo seems to fail if sweep angle is greater than 360 if(angle >= FX.TWO_PI) { angle = FX.TWO_PI - 0.0000001; } else if(angle <= -FX.TWO_PI) { angle = - FX.TWO_PI + 0.0000001; } Point2D p = currentPos(); double a = Math.atan2(yc + yorigin - p.getY(), p.getX() - xc - xorigin); double b = a - angle; double xe = xorigin + xc + radius * Math.cos(b); double ye = yorigin - yc - radius * Math.sin(b); // arcTo sweep is explained here: // https://docs.oracle.com/javase/8/javafx/api/javafx/scene/shape/ArcTo.html boolean large = (angle >= Math.PI); boolean sweep = (angle > 0); add(new ArcTo(radius, radius, 0, xe, ye, large, sweep)); } public Node last() { return elements.get(elements.size() - 1); } /** auto fit last node, useful for svg paths */ public void autoFitLastElement() { Node n = last(); double w = n.prefHeight(width); double h = n.prefWidth(height); double sx = width / w; double sy = height / h; double sc = Math.min(sx, sy); n.setScaleX(sc); n.setScaleY(sc); Bounds b = n.getBoundsInLocal(); double dx = (width / 2.0) - b.getMinX() - (b.getWidth() / 2.0); double dy = (height / 2.0) - b.getMinY() - (b.getHeight() / 2.0); n.setTranslateX(dx); n.setTranslateY(dy); } /** returns a new instance of the generated icon */ public IconBase getIcon() { IconBase ic = new IconBase(width, height); ic.addAll(elements); return ic; } /** returns a new instance of the generated icon in a centered box */ public Pane getIconBox() { HBox b = new HBox(getIcon()); b.setAlignment(Pos.CENTER); return b; } }