Rendera text till Shape

Marcus visar hur man får fram ett Shape-objekt från en text.

För att rita text kan man använda metoder som drawText i Graphics-klassen. Men om man vill ha lite mer kontroll kan man manuellt ta fram en Shape för en text och manipulera den med samma verktyg som man använder för att manipulera andra former, så som rektanglar, ellipser etc.

Nyckeln till det hela är klassen TextLayout. Man skapar ett TextLayout-objekt från en sträng, ett typsnitt och ett FontRenderContext-objekt. Det senare får man enklast från det Graphics2D-objekt som man har tänkt att rita på. Enklast är att skriva och använda följande metod:

    private TextLayout getTextLayout(Graphics2D g2, String s) {
        return new TextLayout(
                s,
                g2.getFont(),
                g2.getFontRenderContext());
    }

OBS! Av någon anledning tycker inte TextLayout-konstruktorn om när man skickar in en sträng som inte "blir något", t ex tomma strängen eller en sträng med bara mellanslag, så detta måste man se till att undvika.

När man väl har ett TextLayout-objekt kan man enkelt använda metoden getOutline för att få fram ett Shape-objekt. Detta kan man sedan t ex manipulera med transformationer, kolla om en viss punkt ligger i det (metoden contains) eller framförallt rita med Graphics2D-metoderna draw och fill.

Låt oss ta ett exempel. Jag har skrivit en liten klass som tar en text, renderar en bokstav i taget och placerar dem i en halvcirkel. Här är hela klassen, men de intressanta bitarna är det som händer i paintComponent-metoden:

    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setPaint(new GradientPaint(
                0,
                0,
                Color.BLUE,
                0,
                getHeight() - 1,
                new Color(0, 128, 255)));
        g2.fillRect(0, 0, getWidth(), getHeight());
        g2.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 96));
        g2.setStroke(new BasicStroke(3));
        String text = "Testing Font Shapes";
        AffineTransform reset = g2.getTransform();
        g2.translate(getWidth() / 2.0, getHeight());
        for (int i = 0; i < text.length(); i++) {
            String letter = text.substring(i, i + 1).trim();
            if (letter.length() > 0) {
                AffineTransform localReset = g2.getTransform();
                Shape letterShape =
                        getTextShape(
                                g2,
                                letter,
                                SwingConstants.CENTER);
                double angle =
                        Math.PI
                                / text.length()
                                * (i + 0.5)
                                - Math.PI
                                / 2;
                g2.rotate(angle);
                g2.translate(
                        0,
                        -Math.min(getHeight(), getWidth() / 2.0)
                                + g2.getFont().getSize2D());
                g2.setPaint(new GradientPaint(
                        0,
                        -60,
                        Color.RED,
                        0,
                        0,
                        Color.YELLOW));
                g2.fill(letterShape);
                g2.setPaint(new GradientPaint(
                        0,
                        -60,
                        Color.BLACK,
                        0,
                        0,
                        Color.RED));
                g2.draw(letterShape);
                g2.setTransform(localReset);
            }
        }
        g2.setTransform(reset);
    }
    
    private Shape getTextShape(
            Graphics2D g2,
            String s,
            int justification) {
        TextLayout textLayout = getTextLayout(g2, s);
        AffineTransform transform;
        switch (justification) {
            case SwingConstants.CENTER:
                transform =
                        AffineTransform.getTranslateInstance(
                                -textLayout.getAdvance() / 2,
                                0);
                break;
            case SwingConstants.LEFT:
                transform = null;
                break;
            case SwingConstants.RIGHT:
                transform =
                        AffineTransform.getTranslateInstance(
                                -textLayout.getAdvance(),
                                0);
                break;
            default:
                throw new IllegalArgumentException(
                        "Invalid justification: " + justification);
        }
        return textLayout.getOutline(transform);
    }
    
    private TextLayout getTextLayout(Graphics2D g2, String s) {
        return new TextLayout(
                s,
                g2.getFont(),
                g2.getFontRenderContext());
    }

Såhär ser resultatet ut:

Jag har gjort det enkelt för mig genom att ge varje tecken lika mycket plats (rotera lika mycket). I verkligheten är det ju dock skillnad på hur breda tecknen är, vilket borde avspegla sig i hur mycket man roterar dem.

För att göra detta på ett bra sätt behöver vi veta hur brett respektive tecken är. Ett sätt är att titta på TextLayout-objektet. Där finns, bland mycket annat praktiskt, en metod som heter getAdvance som vi kan använda. Den returnerar bredden (egentligen bredden plus lite mellanrum, dvs den returnerar hur många pixlar längre till höger nästa tecken skulle hamna, om detta var i en text).

Vi skriver om koden till följande:

    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setPaint(new GradientPaint(
                0,
                0,
                Color.BLUE,
                0,
                getHeight() - 1,
                new Color(0, 128, 255)));
        g2.fillRect(0, 0, getWidth(), getHeight());
        g2.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 96));
        g2.setStroke(new BasicStroke(3));
        String text = "Testing Font Shapes";
        TextLayout fullTextLayout = getTextLayout(g2, text);
        double fullAdvance = fullTextLayout.getAdvance();
        AffineTransform reset = g2.getTransform();
        g2.translate(getWidth() / 2.0, getHeight());
        double advanceExcludingCurrentLetter = 0;
        for (int i = 0; i < text.length(); i++) {
            String textIncludingCurrentLetter =
                    text.substring(0, i + 1);
            double advanceIncludingCurrentLetter =
                    getTextLayout(g2, textIncludingCurrentLetter)
                            .getAdvance();
            AffineTransform localReset = g2.getTransform();
            String letter = text.substring(i, i + 1).trim();
            if (letter.length() > 0) {
                TextLayout letterLayout = getTextLayout(g2, letter);
                AffineTransform transform =
                        AffineTransform.getTranslateInstance(
                                -letterLayout.getAdvance() / 2,
                                0);
                Shape letterShape =
                        letterLayout.getOutline(transform);
                double angle =
                        Math.PI
                                * (advanceIncludingCurrentLetter + advanceExcludingCurrentLetter)
                                / 2
                                / fullAdvance
                                - Math.PI
                                / 2;
                g2.rotate(angle);
                g2.translate(
                        0,
                        -Math.min(getHeight(), getWidth() / 2.0)
                                + g2.getFont().getSize2D());
                g2.setPaint(new GradientPaint(
                        0,
                        -60,
                        Color.RED,
                        0,
                        0,
                        Color.YELLOW));
                g2.fill(letterShape);
                g2.setPaint(new GradientPaint(
                        0,
                        -60,
                        Color.BLACK,
                        0,
                        0,
                        Color.RED));
                g2.draw(letterShape);
                g2.setTransform(localReset);
            }
            advanceExcludingCurrentLetter =
                    advanceIncludingCurrentLetter;
        }
        g2.setTransform(reset);
    }
    
    private TextLayout getTextLayout(Graphics2D g2, String s) {
        return new TextLayout(
                s,
                g2.getFont(),
                g2.getFontRenderContext());
    }

Nu känns texten lite jämnare fördelad:

Och här är hela klassen.

Rendera text till Shape

Om bloggaren

Marcus Björkander

Marcus Björkander

Schlagernörd och småbarnspappa med tvångstankar som jobbar som utvecklare på Westbahr i Göteborg. Favoritspråket är Java, som han tidigare har undervisat i vid Chalmers i många år.

 

Nyckelord/tag moln