Public Lab Research note

MapKnitter Annotations: Textbox Rotation using CSS Transforms

by justinmanley | July 02, 2014 01:16 02 Jul 01:16 | #10641 | #10641


I am working to add rich annotation functionality to MapKnitter as part of Google Summer of Code (read about my project here: This is my second week of coding. The purpose of this research note is to provide an update on my progress with implementing text annotation for Leaflet.

To trace the discussions that have led me up to this point, you can look back through the following research notes, listed in chronological order.

March 18, 2014 - MapKnitter Annotations Using Fabric.js (GSoC 2014 Proposal)

June 17, 2014 - MapKnitter Annotations Plugin: Preliminary Specification

June 25, 2014 - MapKnitter Annotations Update: L.Illustrate.Textbox


The first step in my project is implementing basic text annotation for Leaflet. My solution to text annotation for Leaflet uses a textbox-based UI for text annotation which should be familiar to users of Powerpoint, Photoshop, and many other image-editing programs. I am currently developing this functionality in a plugin for Leaflet, Leaflet.Illustrate which extends Leaflet.draw.

Mathew and I determined in discussion over my last research note that rotatable text was part of the core needs that this plugin aims to address. At the time, I thought that rotatable text would have to be implemented using SVG. After our discussion, I realized that another option could be to use CSS transforms on HTML text. I decided to go ahead with this, rather than jumping directly into an SVG implementation (which would likely have involved extending Leaflet's core for vector layers), since it seemed that HTML textboxes using CSS transforms would be both easier to implement and more user-friendly for application developers.

This week, I successfully implemented rotatable HTML textboxes using L.DivIcon and CSS transforms. You can view a demo of the functionality below:

The code that makes the rotation happen is copied below:

function _updateRotation() {
    var degrees = Math.round(this._rotation*(180/Math.PI)),
        rotationString = "rotate(" + degrees + "deg)",
        size = this.getSize(),
        translateString = "",

    if (this._map) {
        center = this._map.latLngToContainerPoint(this._latlng);
        translateString = "translate(" + center.x + "px, " + center.y + "px)";["-webkit-transform-origin"] = (center.x + Math.round(size.x/2)) + "px " + (center.y + Math.round(size.y/2)) + "px";
    }["-webkit-transform"] = rotationString + " " + translateString;["-o-transform"] = rotationString + " " + translateString;["-ms-transform"] = rotationString + " " + translateString;["-moz-transform"] = rotationString + " " + translateString; = rotationString + " " + translateString;

As the MDN entry notes, CSS transforms are an experimental technology, and that's why all of the vendor prefixes are required. But CSS transforms have been around for quite a while, are well-tested, and with the appropriate vendor prefixes, the basic 2D CSS transforms that I use above (rotate and translate), are supported by

  • Chrome (all versions)
  • Firefox 3.5+
  • Internet Explorer 9.0+
  • Opera 10.5+
  • Safari 3.1+

(click for a complete compatibility table). I think that this will probably be sufficient for compatibility. If we must make it compatible with IE 8 and below, that can be done using Microsoft's Matrix Filters.


I'm not really sure why I needed the translateString and transform-origin properties in the code above - the rotate transform should have been enough. I think that the CSS transforms are interacting in a strange way with the position: absolute property that the textbox inherits from L.Marker. The code above works, but I would like to understand better the way that the CSS transforms are interacting with the positioning.

Currently, the rotation is undone on zooming in or out. This is because Leaflet positions markers using the CSS transform translate function. Whenever the user zooms, the translate values have to be recalculated and the css-transform property is reset, which overwrites the rotation and translation put in by _updateRotation(). The easiest way to fix this is probably to store the rotation string and then reset the css-transform property once the zoom completes.

It's impossible to select text inside the textbox right now. I think that this has to do with the handlers that Leaflet sets up for drag events occurring anywhere over the map. Even when I call map.enableTextSelection(), the selectstart event is never fired on the <textarea>. This is going to take some looking in to.

Next Steps

  • Add editing handles for resizing and moving (the video above implements a handle for rotation).
  • Make textbox outline highlighting more natural
  • Correct text selection issues.
  • Fix rotation on zoom.
  • Remove default <textarea> resizing behavior so that there is a single tool for resizing the textbox.
  • Fix disturbing flickering of the rotation handle when the marker reaches an angle of -π/2.
  • Rotate handles along with the textbox


Woo!!! rotatable text!!! Wooo!! I'm doing a happy dance in Portland!

I'm glad you found a manageable way to implement rotational text. I don't think support for IE 8 is worth any trouble. People can upgrade, that's a 5+ year old browser 3 versions ago. It does sound like its worth doing some early-stage browser testing to verify that CSS Transforms works with the browsers before proceeding.

Reply to this comment...

NIce progress Justin. Thanks.

Reply to this comment...

Looking good! Did you want to share via the testing server? Hopefully we'll get you set up soon but since you should only be using client-side code so far, perhaps you could set up a Github page with your code if you don't want to wait?

Is this a question? Click here to post it to the Questions page.

Reply to this comment...

thanks for this work, rotatable text is pretty important and gives mapknitter an edge over other options i have as an extremely naive, novice graphics-maker.

Reply to this comment...

Login to comment.