package inf101.v18.gfx.textmode; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import javafx.geometry.Point2D; import javafx.scene.SnapshotParameters; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.image.Image; import javafx.scene.image.PixelReader; import javafx.scene.image.PixelWriter; import javafx.scene.image.WritableImage; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.shape.StrokeLineCap; import javafx.scene.text.Font; import javafx.scene.transform.Affine; import javafx.scene.transform.Transform; /** * TextFont – for grid-based text / character graphics *

* TextFonts are used for drawing text and character graphics. Each character is * assumed to be uniform in size, fitting within a square with sides of length * {@link #getSquareSize()}. The given font file should contain a monospaced * font of a type suitable for JavaFX, such as OpenType or TrueType. * *

* Additional horizontal and vertical positioning and scaling can be used to * make the font fit with the square-shaped concept. * *

* See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics * context for writing with the font, or * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly * horizontal scaling, e.g. to make half-width letters ("hires" mode). * */ public class TextFont { public static final int ATTR_INVERSE = 0x01; public static final int ATTR_ITALIC = 0x02; public static final int ATTR_BOLD = 0x04; public static final int ATTR_OUTLINE = 0x08; public static final int ATTR_UNDERLINE = 0x10; public static final int ATTR_OVERLINE = 0x20; public static final int ATTR_LINE_THROUGH = 0x40; public static final int ATTR_OVERSTRIKE = 0x80; public static final int ATTR_CLIP = 0x80; public static final int ATTR_NO_FAKE_CHARS = 0x100; public static final int ATTR_BLINK = 0x200; // NOT IMPLEMENTED public static final int ATTR_FAINT = 0x400; // NOT IMPLEMENTED public static final int ATTR_BRIGHT = 0x800; private static final String[] searchPath = { "", "../", "../fonts/" }; private static final Map loadedFonts = new HashMap<>(); private static final double thin = 2.0, thick = 4.0; private static final String[] boxDrawingShapes = { // lines "--..", "**..", "..--", "..**", // dashed lines "3--..", "3**..", "3..--", "3..**", "4--..", "4**..", "4..--", "4..**", // corners ".-.-", ".*.-", ".-.*", ".*.*", "-..-", "*..-", "-..*", "*..*", ".--.", ".*-.", ".-*.", ".**.", "-.-.", "*.-.", "-.*.", "*.*.", // |- ".---", ".*--", ".-*- ", ".--* ", ".-**", ".**-", ".*-*", ".***", "-.--", "*.--", "-.*-", "-.-*", "-.**", "*.*-", "*.-*", "*.**", // T "--.-", "*-.-", "-*.-", "**.-", "--.*", "*-.*", "-*.*", "**.*", "---.", "*--. ", "-*-.", "**-.", "--*.", "*-*.", "-**.", "***.", // + "----", "*---", "-*--", "**--", "--*-", "---*", "--**", "*-*-", "-**-", "*--*", "-*-*", "***-", "**-*", "*-**", "-***", "****", // dashes "2--..", "2**..", "2..--", "2..**", // double lines "==..", "..==", ".=.-", ".-.=", ".N.N", "=..-", "-..=", "M..M", ".=-.", ".-=.", ".MM.", "=.-.", "-.=.", "N.N.", ".=--", ".s==", ".zMN", "=.--", "s.==", "z.NM", "==.s", "--.=", "MN.z", "==s.", "--=.", "NMz.", "==--", "--==", "zzzz", // round corners "0.-.-", "0-..-", "0-.-.", "0.--.", // diagonals "////", "\\\\\\\\", "//\\\\\\", // short lines "-...", "..-.", ".-..", "...-", "*...", "..*.", ".*..", "...*", // thin/thick lines "-*..", "..-*", "*-..", "..*-" }; public static void drawMasked(GraphicsContext ctx, Image src, Image mask, double x, double y, boolean invert, boolean blend) { int w = (int) src.getWidth(); int h = (int) src.getHeight(); PixelReader pixelSrc = src.getPixelReader(); PixelReader pixelMask = mask.getPixelReader(); PixelWriter pixelWriter = ctx.getPixelWriter(); Affine transform = ctx.getTransform(); Point2D point = transform.transform(x, y); int dx = (int) point.getX(), dy = (int) point.getY(); for (int px = 0; px < w; px++) { for (int py = 0; py < h; py++) { int a = pixelMask.getArgb(px, py) >>> 24; int rgb = pixelSrc.getArgb(px, py); if (invert) a = ~a & 0xff; if (blend) a = ((rgb >>> 24) * a) >>> 8; pixelWriter.setArgb(px + dx, py + dy, (a << 24) | rgb); } } } public static void fillInverse(GraphicsContext ctx, Image img, double x, double y) { int w = (int) img.getWidth(); int h = (int) img.getHeight(); PixelReader pixelReader = img.getPixelReader(); PixelWriter pixelWriter = ctx.getPixelWriter(); Affine transform = ctx.getTransform(); Point2D point = transform.transform(x, y); int dx = (int) point.getX(), dy = (int) point.getY(); Color c = ctx.getFill() instanceof Color ? (Color) ctx.getFill() : Color.BLACK; int rgb = ((int) (c.getRed() * 255)) << 16 | ((int) (c.getGreen() * 255)) << 8 | ((int) (c.getBlue() * 255)); for (int px = 0; px < w; px++) { for (int py = 0; py < h; py++) { int a = (~pixelReader.getArgb(px, py) & 0xff000000); // if(a != 0) pixelWriter.setArgb(px + dx, py + dy, a | rgb); } } } /** * Load a font. * * Will first try to use the provided string as a file name, and load the font * from a font file (should be in a format supported by JavaFX). If loading from * file fails, we assume we have been given the name of a font which is passed * directly to JavaFX for loading. A fallback font is used if this fails. * * When looking for files, his method will search for the file in a search path * starting with the folder containing the {@link TextFont} class file (using * Java's standard {@link Class#getResourceAsStream(String)} system). The search * path also includes ".." (parent directory) and "../fonts". * * The loaded font will be cached, so that additional calls with the same file * name will not cause the file to be loaded again. * * If the font cannot be loaded, a default font will be substituted. You may * check which font you got using {@link Font#getName()} or * {@link Font#getFamily()}. * * @param name * Font name, or relative path to the file * @param size * Desired point size of the font * @return A JavaFX font */ public static final Font findFont(String name, double size) { return findFont(name, size, TextFont.class); } /** * Load a font. * * Will first try to use the provided string as a file name, and load the font * from a font file (should be in a format supported by JavaFX). If loading from * file fails, we assume we have been given the name of a font which is passed * directly to JavaFX for loading. A fallback font is used if this fails. * * When looking for files, this method will search for the file in a search path * starting with the folder containing the provided Java class (using Java's * standard {@link Class#getResourceAsStream(String)} system). The search path * also includes ".." (parent directory) and "../fonts". * * The loaded font will be cached, so that additional calls with the same file * name will not cause the file to be loaded again. * * If the font cannot be loaded, a default font will be substituted. You may * check which font you got using {@link Font#getName()} or * {@link Font#getFamily()}. * * @param name * Name of a font, or relative path to the font file * @param size * Desired point size of the font * @param clazz * A class for finding files relative to * @return A JavaFX font */ public static final Font findFont(String name, double size, Class clazz) { if (name == null || size < 0) throw new IllegalArgumentException(); if (loadedFonts.containsKey(name)) return Font.font(loadedFonts.get(name), size); for (String path : searchPath) { try (InputStream stream = clazz.getResourceAsStream(path + name)) { Font font = Font.loadFont(stream, size); if (font != null) { loadedFonts.put(name, font.getName()); // System.err.println("Found: " + font.getName()); return font; } } catch (FileNotFoundException e) { // we'll just try the next alternative in the search path } catch (IOException e) { e.printStackTrace(); } } Font font = Font.font(name, size); if (font == null) font = Font.font(size); if (font == null) throw new RuntimeException("Even the default font seems to be unavailable – this shouldn't happen! :("); if (font.getName().equals(Font.getDefault().getName())) { System.err.println("TextFont: Default font '" + font.getName() + "' substituted for '" + name + "'"); } // System.err.println("Found: " + name + "=" + font.getName()); loadedFonts.put(name, name); return font; } private Canvas tmpCanvas; private final WritableImage img; /** * The JavaFX font */ private Font font; /** Font size */ private final double size; /** * Horizontal positioning of letters. * * Each letter should be approximately centered within its available * square-shaped space. */ private final double xTranslate; /** * Vertical positioning of letters. * * Each letter should be positioned on the baseline so that ascenders and * descenders fall within its available square-shaped space. */ private final double yTranslate; /** * Horizontal scaling factor (1.0 means no scaling) * * Most fonts are relatively tall and narrow, and need horizontal scaling to fit * a square shape. */ private final double xScale; /** * Vertical scaling factor (1.0 means no scaling) */ private final double yScale; /** * Width and height of the square-shaped space each letter should fit within */ private double squareSize; private String fileName; SnapshotParameters snapshotParameters = new SnapshotParameters(); { snapshotParameters.setFill(Color.TRANSPARENT); } /** * Create a new TextFont. * *

* TextFonts are used for drawing text and character graphics. Each character is * assumed to be uniform in size, fitting within a square with sides of length * {@link #getSquareSize()}. The given font file should contain a monospaced * font of a type suitable for JavaFX, such as OpenType or TrueType. * *

* Additional horizontal and vertical positioning and scaling can be used to * make the font fit with the square-shaped concept. * *

* See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics * context for writing with the font, or * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly * horizontal scaling, e.g. to make half-width letters ("hires" mode). * * * @param font * Name of the font file. Will search for the file in the same folder * as the TextFont class, as well as ".." and "../fonts". * @param size * Point size of the font. * @param squareSize * The width and height of a square defining the bounds of letters * @param xTranslate * Horizontal positioning of letters * @param yTranslate * Vertical positioning of letters * @param xScale * Horizontal scaling factor * @param yScale * Vertical scaling factor */ public TextFont(Font font, double squareSize, double xTranslate, double yTranslate, double xScale, double yScale) { super(); this.fileName = font.getName(); this.font = font; this.size = font.getSize(); this.squareSize = squareSize; this.xTranslate = xTranslate; this.yTranslate = yTranslate; this.xScale = xScale; this.yScale = yScale; this.img = new WritableImage((int) squareSize, (int) squareSize); } /** * Create a new TextFont. * *

* TextFonts are used for drawing text and character graphics. Each character is * assumed to be uniform in size, fitting within a square with sides of length * {@link #getSquareSize()}. The given font file should contain a monospaced * font of a type suitable for JavaFX, such as OpenType or TrueType. * *

* Additional horizontal and vertical positioning and scaling can be used to * make the font fit with the square-shaped concept. * *

* See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics * context for writing with the font, or * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly * horizontal scaling, e.g. to make half-width letters ("hires" mode). * * * @param font * Name of the font file. Will search for the file in the same folder * as the TextFont class, as well as ".." and "../fonts". * @param size * Point size of the font. * @param squareSize * The width and height of a square defining the bounds of letters * @param xTranslate * Horizontal positioning of letters * @param yTranslate * Vertical positioning of letters * @param xScale * Horizontal scaling factor * @param yScale * Vertical scaling factor */ public TextFont(String fileName, double size, double squareSize, double xTranslate, double yTranslate, double xScale, double yScale) { super(); this.fileName = fileName; this.font = findFont(fileName, size); this.size = size; this.squareSize = squareSize; this.xTranslate = xTranslate; this.yTranslate = yTranslate; this.xScale = xScale; this.yScale = yScale; this.img = new WritableImage((int) squareSize, (int) squareSize); } /** * Create a new TextFont. * *

* TextFonts are used for drawing text and character graphics. Each character is * assumed to be uniform in size, fitting within a square with sides of length * {@link #getSquareSize()}. The given font file should contain a monospaced * font of a type suitable for JavaFX, such as OpenType or TrueType. * *

* Additional horizontal and vertical positioning and scaling can be used to * make the font fit with the square-shaped concept. * *

* See {@link #setGraphicsContext(GraphicsContext)} for setting up the graphics * context for writing with the font, or * {@link #setGraphicsContext(GraphicsContext, double)} for extra on-the-fly * horizontal scaling, e.g. to make half-width letters ("hires" mode). * * * @param font * Name of the font file. Will search for the file in the same folder * as the TextFont class, as well as ".." and "../fonts". * @param size * Point size of the font. * @param squareSize * The width and height of a square defining the bounds of letters * @param xTranslate * Horizontal positioning of letters * @param yTranslate * Vertical positioning of letters * @param xScale * Horizontal scaling factor * @param yScale * Vertical scaling factor * @param deferLoading * True if the font file shouldn't be loaded before the font is * actually used */ public TextFont(String fileName, double size, double squareSize, double xTranslate, double yTranslate, double xScale, double yScale, boolean deferLoading) { super(); this.fileName = fileName; this.font = deferLoading ? null : findFont(fileName, size); this.size = size; this.squareSize = squareSize; this.xTranslate = xTranslate; this.yTranslate = yTranslate; this.xScale = xScale; this.yScale = yScale; this.img = new WritableImage((int) squareSize, (int) squareSize); } /** * Create a copy of this font, with the given adjustments to the translation and * scaling. * * @param deltaXTranslate * @param deltaYTranslate * @param deltaXScale * @param deltaYScale * @return */ public TextFont adjust(double size, double deltaXTranslate, double deltaYTranslate, double deltaXScale, double deltaYScale) { if (size == 0.0) { return new TextFont(fileName, this.size, squareSize, xTranslate + deltaXTranslate, yTranslate + deltaYTranslate, xScale + deltaXScale, yScale + deltaYScale); } else { return new TextFont(fileName, this.size + size, squareSize, xTranslate + deltaXTranslate, yTranslate + deltaYTranslate, xScale + deltaXScale, yScale + deltaYScale); } } /** * Draw the given text at position (0,0). * * The ctx should normally be translated to the appropriate text * position before calling this method. * * Text will be clipped so each character fits its expected square-shaped area, * and the area will be cleared to transparency before drwaing. * * The graphics context's current path will be overwritten. * * @param ctx * a grapics context * @param text * string to be printed */ public void drawText(GraphicsContext ctx, String text) { textAt(ctx, 0.0, 0.0, text, 1.0, true, true, ctx.getFill(), null, 0, null); } /** * Draw the given text at position (0,0), with horizontal scaling. * * The ctx should normally be translated to the appropriate text * position before calling this method. * * Text will be clipped so each character fits its expected square-shaped area, * and the area will be cleared to transparency before drwaing. * * The graphics context's current path will be overwritten. * * @param ctx * a grapics context * @param text * string to be printed * @param xScaleFactor * a horizontal scaling factor */ public void drawText(GraphicsContext ctx, String text, double xScaleFactor) { textAt(ctx, 0.0, 0.0, text, xScaleFactor, true, false, ctx.getFill(), null, 0, null); } /** * Draw the given text at position (x,y). * * Text will be clipped so each character fits its expected square-shaped area, * and the area will be cleared to transparency before drwaing. * * The graphics context's current path will be overwritten. * * @param ctx * a grapics context * @param x * X-position of the lower left corner of the text * @param y * Y-position of the lower left corner of the text * @param text * string to be printed */ public void drawTextAt(GraphicsContext ctx, double x, double y, String text) { textAt(ctx, x, y, text, 1.0, true, false, ctx.getFill(), null, 0, null); } /** * Draw the given text at position (x, y), with horizontal scaling. * * Text will be clipped so each character fits its expected square-shaped area, * and the area will be cleared to transparency before drwaing. * * The graphics context's current path will be overwritten. * * @param ctx * a grapics context * @param x * X-position of the lower left corner of the text * @param y * Y-position of the lower left corner of the text * @param text * string to be printed * @param xScaleFactor * a horizontal scaling factor */ public void drawTextAt(GraphicsContext ctx, double x, double y, String text, double xScaleFactor, int mode, Paint bg) { textAt(ctx, x, y, text, xScaleFactor, true, false, ctx.getFill(), null, mode, bg); } private void fakeBlockElement(GraphicsContext ctx, String text, double xScaleFactor) { text.codePoints().forEach((int c) -> { // System.out.printf("%s %x%n", text, c); if (c >= 0x2596 && c <= 0x259f) { // quadrants int bits = BlocksAndBoxes.unicodeBlocksString.indexOf(c); if ((bits & 1) > 0) { // lower right ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize / 2, (squareSize / 2) * xScaleFactor, squareSize / 2); } if ((bits & 2) > 0) { // lower left ctx.fillRect(0, -squareSize / 2, (squareSize / 2) * xScaleFactor, squareSize / 2); } if ((bits & 4) > 0) { // upper right ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize, (squareSize / 2) * xScaleFactor, squareSize / 2); } if ((bits & 8) > 0) { // upper left ctx.fillRect(0, -squareSize, (squareSize / 2) * xScaleFactor, squareSize / 2); } } else if (c == 0x2580) { // upper half ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize / 2); } else if (c > 0x2580 && c <= 0x2588) { // x/8 block int height = c - 0x2580; ctx.fillRect(0, -(height * squareSize) / 8, (squareSize) * xScaleFactor, (height * squareSize) / 8); } else if (c >= 0x2589 && c <= 0x258f) { // x/8 block int width = 8 - (c - 0x2588); ctx.fillRect(0, -squareSize, ((width * squareSize) / 8) * xScaleFactor, squareSize); } else if (c == 0x2590) { // right half ctx.fillRect(squareSize * xScaleFactor / 2, -squareSize, (squareSize / 2) * xScaleFactor, squareSize); } else if (c == 0x2591) { // light shade ctx.save(); ctx.setGlobalAlpha(0.25); ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize); ctx.restore(); } else if (c == 0x2592) { // medium shade ctx.save(); ctx.setGlobalAlpha(0.5); ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize); ctx.restore(); } else if (c == 0x2593) { // dark shade ctx.save(); ctx.setGlobalAlpha(0.75); ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize); ctx.restore(); } else if (c == 0x2594) { // upper eighth ctx.fillRect(0, -squareSize, (squareSize) * xScaleFactor, squareSize / 8); } else if (c == 0x2595) { // right eighth ctx.fillRect(((7 * squareSize) / 8) * xScaleFactor, -squareSize, (squareSize / 8) * xScaleFactor, squareSize); } else if (c == 0x2571) { ctx.save(); ctx.setLineWidth(2.0); ctx.strokeLine(0, 0, squareSize * xScaleFactor, -squareSize); ctx.restore(); } else if (c == 0x2572) { ctx.save(); ctx.setLineWidth(2.0); ctx.strokeLine(0, -squareSize, squareSize * xScaleFactor, 0); ctx.restore(); } else if (c == 0x2573) { ctx.save(); ctx.setLineWidth(2.0); ctx.strokeLine(0, 0, squareSize * xScaleFactor, -squareSize); ctx.strokeLine(0, -squareSize, squareSize * xScaleFactor, 0); ctx.restore(); } else if (c >= 0x2500 && c <= 0x257f) { ctx.save(); ctx.setLineCap(StrokeLineCap.BUTT); String spec = boxDrawingShapes[c - 0x2500]; int i = 0; double extraThickness = 0.0; if (Character.isDigit(spec.charAt(i))) { extraThickness = setContextFromChar(ctx, spec.charAt(i++)); } char s; double hThickness = Math.max(setContextFromChar(ctx, spec.charAt(i)), setContextFromChar(ctx, spec.charAt(i + 1))) + extraThickness; double vThickness = Math.max(setContextFromChar(ctx, spec.charAt(i + 2)), setContextFromChar(ctx, spec.charAt(i + 3))) + extraThickness; if (c >= 0x2550 || spec.charAt(i) != spec.charAt(i + 1)) { s = spec.charAt(i++); setContextFromChar(ctx, s); strokeHalfLine(ctx, xScaleFactor, -1, 0, s, vThickness); s = spec.charAt(i++); setContextFromChar(ctx, s); strokeHalfLine(ctx, xScaleFactor, 1, 0, s, vThickness); } else { s = spec.charAt(i++); s = spec.charAt(i++); setContextFromChar(ctx, s); strokeFullLine(ctx, xScaleFactor, 1, 0, s); } if (c >= 0x2550 || spec.charAt(i) != spec.charAt(i + 1)) { s = spec.charAt(i++); setContextFromChar(ctx, s); strokeHalfLine(ctx, xScaleFactor, 0, -1, s, hThickness); s = spec.charAt(i++); setContextFromChar(ctx, s); strokeHalfLine(ctx, xScaleFactor, 0, 1, s, hThickness); } else { s = spec.charAt(i++); s = spec.charAt(i++); setContextFromChar(ctx, s); strokeFullLine(ctx, xScaleFactor, 0, 1, s); } ctx.restore(); } }); } /** * @return the font */ public Font getFont() { if (font == null) font = findFont(fileName, size); return font; } /** * @return the size */ public double getSize() { return size; } /** * Width and height of the square-shaped space each letter should fit within * * @return the squareSize */ public double getSquareSize() { return squareSize; } private Canvas getTmpCanvas() { if (tmpCanvas == null) { tmpCanvas = new Canvas(squareSize, squareSize); } else { tmpCanvas.getGraphicsContext2D().clearRect(0, 0, squareSize, squareSize); } return tmpCanvas; } /** * Horizontal scaling factor (1.0 means no scaling) * * Most fonts are relatively tall and narrow, and need horizontal scaling to fit * a square shape. * * @return the xScale */ public double getxScale() { return xScale; } /** * Horizontal positioning of letters. * * Each letter should be approximately centered within its available * square-shaped space. * * @return the xTranslate */ public double getxTranslate() { return xTranslate; } /** * Vertical scaling factor (1.0 means no scaling) * * @return the yScale */ public double getyScale() { return yScale; } /** * /** Vertical positioning of letters. * * Each letter should be positioned on the baseline so that ascenders and * descenders fall within its available square-shaped space. * * @return the yTranslate */ public double getyTranslate() { return yTranslate; } private double setContextFromChar(GraphicsContext ctx, char c) { switch (c) { case '0': return -2 * thin; case '2': ctx.setLineDashes(new double[] { 14.75, 2.5 }); break; case '3': ctx.setLineDashes(new double[] { 9, 2.5 }); break; case '4': ctx.setLineDashes(new double[] { 6.125, 2.5 }); break; case '.': return 0.0; case '-': case 's': ctx.setLineWidth(thin); return thin; case '*': ctx.setLineWidth(thick); return thick; case '=': case 'N': case 'M': case 'z': ctx.setLineWidth(thin); return thin; } return 0.0; } /** * Set up a graphics context for drawing with this font. * * Caller should call {@link GraphicsContext#save()} first, and then * {@link GraphicsContext#restore()} afterwards, to clean up adjustments to the * transformation matrix (i.e., translation, scaling). * * The GraphicsContext should be translated to the coordinates where the text * should appear before calling this method, since this method will * modify the coordinate system. * * @param ctx * A GraphicsContext */ public void setGraphicsContext(GraphicsContext ctx) { ctx.setFont(getFont()); ctx.translate(xTranslate, yTranslate); ctx.scale(xScale, yScale); } /** * Set up a graphics context for drawing with this font. * * Caller should call {@link GraphicsContext#save()} first, and then * {@link GraphicsContext#restore()} afterwards, to clean up adjustments to the * transformation matrix (i.e., translation, scaling). * * The GraphicsContext should be translated to the coordinates where the text * should appear before calling this method, since this method will * modify the coordinate system. * * @param ctx * A GraphicsContext * @param xScaleFactor * Additional horizontal scaling, normally 0.5 (for half-width * characters) */ public void setGraphicsContext(GraphicsContext ctx, double xScaleFactor) { ctx.setFont(getFont()); ctx.translate(xTranslate * xScaleFactor, yTranslate); ctx.scale(xScale * xScaleFactor, yScale); } private void strokeFullLine(GraphicsContext ctx, double xScaleFactor, double xDir, double yDir, char c) { double x = squareSize * xScaleFactor / 2, y = -squareSize / 2; if (c != '.') { // System.out.printf("(%4.1f %4.1f %4.1f %4.1f) w=%1.4f%n", x * yDir, y * xDir, // x * yDir + xDir * squareSize, // y * xDir + yDir * squareSize, ctx.getLineWidth()); if (c == '=') { ctx.setLineWidth(2.0); ctx.strokeLine((x - 2) * yDir, (y - 2) * xDir, (x - 2) * yDir + xDir * squareSize * xScaleFactor, (y - 2) * xDir + yDir * -squareSize); ctx.strokeLine((x + 2) * yDir, (y + 2) * xDir, (x + 2) * yDir + xDir * squareSize * xScaleFactor, (y + 2) * xDir + yDir * -squareSize); } else { ctx.strokeLine(x * yDir, y * xDir, x * yDir + xDir * squareSize * xScaleFactor, y * xDir + yDir * -squareSize); } } } private void strokeHalfLine(GraphicsContext ctx, double xScaleFactor, double xDir, double yDir, char c, double otherWidth) { if (c != '.') { double factor = 1.0, dblFactor = 0.0; if (c == 's' || c == 'z') factor = -1; if (c == 'N') dblFactor = -2; if (c == 'M') dblFactor = 2; double x = squareSize * xScaleFactor / 2, y = -squareSize / 2; x -= xDir * factor * (otherWidth / 2); y -= yDir * factor * (otherWidth / 2); if (c == '=' || c == 'z' || c == 'N' || c == 'M') { ctx.setLineWidth(2.0); double x0 = x - 2 * yDir; double y0 = y - 2 * xDir; x0 += dblFactor * xDir * (otherWidth / 2); y0 += dblFactor * yDir * (otherWidth / 2); ctx.strokeLine(x0, y0, x0 + xDir * squareSize * xScaleFactor, y0 + yDir * squareSize); double x1 = x + 2 * yDir; double y1 = y + 2 * xDir; x1 -= dblFactor * xDir * (otherWidth / 2); y1 -= dblFactor * yDir * (otherWidth / 2); ctx.strokeLine(x1, y1, x1 + xDir * squareSize * xScaleFactor, y1 + yDir * squareSize); } else { ctx.strokeLine(x, y, x + xDir * squareSize * xScaleFactor, y + yDir * squareSize); } } } /** * Draw text at the given position. * * For most cases, the simpler {@link #drawText(GraphicsContext, String)} or * {@link #drawText(GraphicsContext, String, double)} will be easier to use. * * If clip is true, the graphics context's current path will be * overwritten. * * @param ctx * A GraphicsContext * @param x * X-position of the lower left corner of the text * @param y * Y-position of the lower left corner of the text * @param text * The text to be printed * @param xScaleFactor * Horizontal scaling factor, normally 1.0 (full width) or 0.5 (half * width) * @param clear * True if the area should be cleared (to transparency) before * drawing; normally true. * @param clip * True if the text drawing should be clipped to fit the expected * printing area; normally true. * @param fill * True if the letter shapes should be filled; normally true. * @param stroke * True if the letter shapes should be stroked (outlined); normally * false. */ public void textAt(GraphicsContext ctx, double x, double y, String text, double xScaleFactor, boolean clear, boolean clip, Paint fill, Paint stroke, int mode, Paint bg) { if ((mode & ATTR_BRIGHT) != 0) { fill = fill instanceof Color ? ((Color) fill).deriveColor(0.0, 1.0, 3.0, 1.0) : fill; stroke = stroke instanceof Color ? ((Color) stroke).deriveColor(0.0, 1.0, 3.0, 1.0) : stroke; } if ((mode & ATTR_FAINT) != 0) { fill = fill instanceof Color ? ((Color) fill).deriveColor(0.0, 1.0, 1.0, .5) : fill; stroke = stroke instanceof Color ? ((Color) stroke).deriveColor(0.0, 1.0, 1.0, .5) : stroke; } GraphicsContext target = ctx; int width = text.codePointCount(0, text.length()); ctx.save(); // save 1 ctx.setFill(fill); ctx.setStroke(stroke); ctx.translate(x, y); if (clear && (mode & ATTR_INVERSE) == 0 && (mode & ATTR_OVERSTRIKE) == 0) { ctx.clearRect(0 + 0.5, -squareSize + 0.5, squareSize * width * xScaleFactor - 1, squareSize - 1); } if (bg != null && bg != Color.TRANSPARENT) { ctx.save(); // save 2 ctx.setFill(bg); ctx.fillRect(0, -squareSize, squareSize * width * xScaleFactor, squareSize); ctx.restore(); // restore 2 } boolean drawIndirect = clip || (mode & (ATTR_INVERSE | ATTR_CLIP)) != 0; Canvas tmpCanvas = getTmpCanvas(); if (drawIndirect) { target = tmpCanvas.getGraphicsContext2D(); target.save(); // save 2 if drawIndirect target.translate(0, squareSize); target.setFill((mode & ATTR_INVERSE) != 0 ? Color.BLACK : fill); target.setStroke((mode & ATTR_INVERSE) != 0 ? Color.BLACK : stroke); } if (text.length() > 0 && text.charAt(0) >= 0x2500 && text.charAt(0) <= 0x259f && (mode & ATTR_NO_FAKE_CHARS) == 0) { target.save(); // save 3 target.setStroke(target.getFill()); fakeBlockElement(target, text, xScaleFactor); target.restore(); // restore 3 } else { target.save(); // save 3 if ((mode & ATTR_ITALIC) != 0) { target.translate(-0.2, 0); target.transform(new Affine(Transform.shear(-0.2, 0))); } setGraphicsContext(target, xScaleFactor); if (fill != null) target.fillText(text, 0.0, 0.0); if ((mode & ATTR_BOLD) != 0) { // System.err.println("stroke2"); target.save(); // save 4 target.setLineWidth(thin); target.setStroke(target.getFill()); target.strokeText(text, 0.0, 0.0); target.restore(); // restore 4 } if (stroke != null || (mode & ATTR_OUTLINE) != 0) { // System.err.println("stroke2"); target.setLineWidth(((mode & ATTR_BOLD) != 0) ? thin : thin / 2); target.strokeText(text, 0.0, 0.0); } target.restore(); // restore 3 } if ((mode & ATTR_UNDERLINE) != 0) { target.fillRect(0, yTranslate - 2, width * squareSize * xScaleFactor, thin); } if ((mode & ATTR_OVERLINE) != 0) { target.fillRect(0, -squareSize + 2, width * squareSize * xScaleFactor, thin); } if ((mode & ATTR_LINE_THROUGH) != 0) { target.fillRect(0, -squareSize / 2 + 2, width * squareSize * xScaleFactor, thin); } if (drawIndirect) { target.restore(); // restore 2 if drawIndirect tmpCanvas.snapshot(snapshotParameters, img); if ((mode & ATTR_INVERSE) != 0) { fillInverse(ctx, img, 0, -squareSize); } else ctx.drawImage(img, 0, -squareSize); } ctx.restore(); // restore 1 } }