racket / draw

Other
17 stars 23 forks source link

Rotation and draw-text. #38

Open soegaard opened 3 years ago

soegaard commented 3 years ago

This example shows a rotating text.

Zooming in on the text (on macOS use ctrl and swipe up with two fingers to zoom in) shows that the letters are "dancing". It looks as if each letter is rotated individually instead of the text being drawn as a whole.

The expected result can be seen here (using p5.js): https://processing.org/examples/textrotation.html

The example also shows that the thickness of the line varies. Zoom in on the line and it becomes apparent that the line at some angles shrink in width. In an animation this looks odd.

Turning on smoothing fixes the line thickness problem - but the reason for turning it off was for speed.

Screen recording:

https://user-images.githubusercontent.com/461765/124123958-28c71b00-da78-11eb-8ab9-f7c3563dcb23.mov

#lang racket/base
(require racket/gui)

(define width  640)
(define height 360)

(define angle 0.0) ; the rotation angle
(define (radians a) (* pi (/ a 180.)))

; A frame containing a single canvas with a timer that continously calls draw.
(define top-frame  #f)
(define top-canvas #f)
(define top-timer  #f)

(define dc #f) ; drawing context of the canvas

(define red-pen
  (new pen%              
       [color  "red"]                
       [width  4]
       [style  'solid]
       [cap    'round]
       [join   'round]
       [stipple #f]))

(define white-pen
  (new pen%              
       [color  "white"]              
       [width  1]
       [style  'solid]
       [cap    'round]
       [join   'round]
       [stipple #f]))

(define large-font
  (make-object font% 24 'modern))

(define (draw)
  (define old-transformation #f)
  (when dc
    (send dc set-background "black")
    (send dc clear)
    (send dc set-font large-font)
    ; (send dc set-smoothing 'smoothed)
    ; (send dc set-smoothing 'unsmoothed)    
    (send dc set-pen white-pen)
    (send dc set-text-foreground "white")

    (set! old-transformation (send dc get-transformation))
    (define angle1 (radians 45))
    (send dc translate 100.5 180.5)
    (send dc rotate angle1)
    (send dc draw-text "45 DEGREES" 0 0)
    (send dc draw-line 0 0 150 0)
    (send dc set-transformation old-transformation)

    (set! old-transformation (send dc get-transformation))
    (define angle2 (radians 270))
    (send dc translate 200.5 180.5)
    (send dc rotate angle2)
    (send dc draw-text "180 DEGREES" 0 0)
    (send dc draw-line 0 0 150 0)
    (send dc set-transformation old-transformation)

    (set! old-transformation (send dc get-transformation))
    (define angle3 (radians angle))
    (send dc translate 440.5 180.5)
    (send dc rotate angle3)
    (send dc draw-text (~a (modulo (inexact->exact (round angle)) 360) " DEGREES") 0 0)
    (send dc draw-line 0 0 150 0)
    (send dc set-transformation old-transformation)
    (set! angle (+ angle 0.25))

    (send dc set-pen red-pen)
    (send dc draw-point 100.5 180.5)
    (send dc draw-point 200.5 180.5)
    (send dc draw-point 440.5 180.5)))

(define my-frame%
  (class frame%
    (define/augment (on-close)
      (when top-timer
        (send top-timer stop)))
    (super-new)))

(define my-canvas%
  (class canvas%
    (define/override (on-paint)   ; repaint (exposed or resized)
      (define dc (send this get-dc))
      (send this suspend-flush)
      (handle-on-paint dc)
      (send this resume-flush))
    (super-new)))

(define (start-gui)
  (define frame  (new my-frame%
                      [label "sketch"]))
  (set! top-frame frame)  
  (define canvas (new my-canvas%
                      [parent     frame]
                      [min-width  width]
                      [min-height height]))
  (set! top-canvas canvas)
  (set! dc (send top-canvas get-dc))

  (define timer (new timer%
                     [notify-callback handle-on-timer]
                     [interval (inexact->exact (floor (/ 1000 30)))])) ; milliseconds
  (set! top-timer timer)

  (send frame show #t))

(define (handle-on-paint dc)  
  (when dc 
    (draw)))

(define (handle-on-timer)
  (send top-canvas on-paint))

(start-gui)
mflatt commented 3 years ago

FWIW, I thought the answer to the dancing text was going to be creating a font with 'unaligned hinting, but that does not solve the problem.

soegaard commented 3 years ago

An obversation - made after I wrote the guess below:

The rotate? flag in do-text is set to (and draw-mode (not (zero? angle))). But what if a rotation has been made with (send dc rotate angle) then the normal code path is taken. Is that intended?

The loop in do-text draws in one of the paths each character individually.

https://github.com/racket/draw/blob/master/draw-lib/racket/draw/private/dc.rkt#L1709

My intution - but I am far from sure - is that the alignments of each individual character gives a different result than using the line "top line" of the rectangular binding box of the string.

Let's say we want to draw the text hello. The bounding box is a rectangle ABCD with the line AB being at the top. Due to rotation the line AB might no be parallel to an axis.

If A is aligned to A1 and we set B1 to B+(A1-A), then the upper left corners of each character ought to be on the line from A1 to B1. However if we align each individual character, then I think there is a risk that two neighbour characters might move in different directions relative to the line when aligned. At least when rotation is involved.

I haven't grooked the Pango part of the draw-text code, but this aligns each individual character: https://github.com/racket/draw/blob/master/draw-lib/racket/draw/private/dc.rkt#L1709

mflatt commented 3 years ago

You can avoid the character-by-character loop by providing #t as the combine? argument to draw-text. But I think that doesn't solve the problem here.

Yes, as I remember/understand the code, the normal code path is meant to be taken when rotation is in the transformation.

soegaard commented 3 years ago

You are right, using #t as the combine? flag for draw-text didn't help.

soegaard commented 2 years ago

@lexi-lambda

You have looked at fonts and text rendering recently.

Have you spotted something that explains the behaviour in the video?

/Jens Axel

lexi-lambda commented 2 years ago

Hi, @soegaard—I just took a look at this. After some investigation, I suspect that pango_context_set_round_glyph_positions may be relevant. By default, Pango rounds all glyph positions to pixels, regardless of hinting settings, but that function can be used to disable that behavior.

Unfortunately, when I actually experimented with disabling rounding, I found that it didn’t seem to affect the rendering at all on my machine. While I’m not sure why that would be the case, after some further reading, I get the sense this may be because the version of Cairo I have on my machine (which is running Ubuntu 20.04) doesn’t support subpixel positioning (which is distinct from what’s normally called “subpixel rendering”!) for the necessary font backend. If you’re on macOS, you might want to give it a try yourself (and disable hinting) and see if that changes anything.

Having said all of this, I think you probably don’t want to be using draw-text if you’re animating text, anyway. Font rendering is really complicated, and there are so many different moving parts involved in a modern text rendering stack that it makes my head spin. Lots of those parts have to make tough choices about how to place glyphs in a readable fashion, and arranging for all those choices to be temporally consistent under transformation seems like probably a lost cause.

Instead, I think what you almost certainly want to do is convert the text to an outline first using the text-outline method of dc-path%, then fill the resulting path using the draw-path method of dc<%>. This will run through the whole text shaping and layout pipeline with the glyphs in a well-behaved configuration, namely placed in a straight line, then give you back a set of vector paths. Then, when you call draw-path, Cairo will render the path itself as an ordinary shape, skipping the OS- and backend-specific text rendering pathways altogether. Cairo certainly knows how to do antialiasing for arbitrary paths, so you’ll get a smooth animation.

There are two major downsides to rendering text this way:

  1. Since Cairo’s drawing the paths itself, all of the fancy logic in your operating system’s text layout and rendering stack will be skipped: you’ll get no font hinting or subpixel antialiasing/LCD smoothing, and any user-controlled, OS-wide font rendering options will be ignored.

    In your case, this is more of a feature than a bug, since you want to animate the paths yourself. But it does mean that you may need to take some extra care to avoid blurry edges resulting from misalignment—though, fortunately, dc<%>’s 'aligned drawing mode is probably sufficient to avoid that most of the time.

  2. Since you’ve converted the glyphs to a shape, their textual content is necessarily lost before they get to the backend. This means that if you’re using a pdf-dc%, for example, the rendered text won’t be selectable in a PDF viewer.

    This probably doesn’t matter to you, because if you’re rendering an animation, you aren’t going to be using the PDF backend, anyway. Moreover, I’m not sure racket/draw currently actually sets the relevant options to have the text stored in the PDF in the first place.

tl;dr: I’m not exactly sure what the problem is, but there are probably too many moving parts to make animated text look good in a backend-agnostic way without just drawing the glyphs yourself. Try that, instead.

soegaard commented 2 years ago

Hi @lexi-lambda

Thanks for the very thorough answer.

I am currently using draw-text to implement text [1] in Sketching. The text function optionally accepts a rectangle to draw the text within and supports left, center and right alignment of the text.

Since it is part of Sketching, I don't know whether the text will be used for animation or for static text.

I'll make an experiment with text-outline + draw-path instead of draw-text. If the results look identically I'll just use the new method.

I was hoping there were some low-level Cairo routines I could have used.

[1] https://github.com/soegaard/sketching/blob/3773cb790ac69201c967201a2fa1727d90a4ad62/sketching-lib/sketching/typography.rkt#L214