Showing posts with label animation. Show all posts
Showing posts with label animation. Show all posts

Sunday, March 18, 2012

Text: Swivel Animations

I just wrapped another great year at SXSW. As expected: after 5 days of back-to-back sessions I'm exhausted, my brain is full, my feet are sore, and all the wonderful innovative ideas I have bubbling up inside won't really see the light of day at work because, well, pesky reality keeps getting in the way.

They always encourage you to tweet during SXSW sessions. Sometimes moderators actually refer to the tweets during each session to redirect the conversation or answer questions. During one session instead of presentating slides or a close up of the speaker: the projectors simply showed live tweets. But they were all at right angles to each other, and the camera swiveled around to focus on each one. I wish I knew exactly what program did that (can anyone tell me?), but I decided I wanted to achieve the same effect.

The Plan

What I want to focus on right now is the core of the animation: manipulating AffineTransforms to zoom in, out, and swivel around a large static canvas. To achieve this I started with a minimal interface:
public interface TransformAnimation {
/**
*
* @param progress a float within [0, 1].
* @return the AffineTransform for the argument.
*/
public AffineTransform getTransform(float progress,int viewWidth,int viewHeight);
}

The piece that followed was also pretty straight-forward: the SwivelPathAnimation is the animation I was looking for. The class itself is very small, but it took a few days to develop because I was trying to develop the UI at the same time. (What good is the animation without a way to test it?)

You construct the animation with a series of Highlight objects. These highlights have a center, a width and height, and an angle of rotation. So if you think of this animation as moving a camera around a page: this controls the placement, zoom and rotation of the camera. Each highlight also keeps track of its duration. When you construct a SwivelPathAnimation you also define the default swivel/transition duration: so each highlight can have its own unique duration, but it's assumed all your transitions should be the same duration.

The path the camera takes is automatic. It is guaranteed to cross the center of each highlight, and it my sincerest hope that it will do so gracefully (without wild curves or excessive detours). This draws on several other articles relating to working with bezier shapes, including this one, possibly this one, and most definitely this one.

Deep inside the animation are controls that ration out the timing as needed. The animation doesn't stop moving when it's displaying a highlight: it just slows down to a very slow speed. Then when it's time for a transition the speed picks back up again.

Here is the applet demo of the finished product:

You can download the applet (source included) here. If the applet doesn't load (or isn't responsive enough), you can also download a sample movie file here. On some browsers/platforms the flicker/animation might be so bad you're better off running the jar as an executable instead of an applet.

The UI

This might (?) be the most complicated UI I've put together for a proof-of-concept applet on my blog so far, so I want to explain it some.

First the easy part: the animation is on the right. Once everything is loaded, just press the play button or drag the slider to see your animation in action.

The left side is the interesting part. The left panel is the overview of the entire canvas. And you can interact with it in a few different ways.

It is populated with the RSS feed from one of my favorite sites: fmylife.com. (As a result: some of the content is for mature audiences. I assume most of my readers are not especially young, though...). So when you see a spinny widget when it starts up: that is what you're waiting on. If you try to view this applet/app without internet connectivity a pre-saved copy of a feed from earlier this week is used instead.

At the bottom of the left panel are a set of controls. I recommend turning down the number of blurbs to about 5 if you want to dabble with the highlights: it makes the UI much more manageable. Now in the overview you can:

  • Click once to create a new highlight.
  • Click and drag an existing highlight to a new location.
  • Click and drag the corner of a highlight to resize it.
  • Shift+click and drag the corner of a highlight to rotate it.
  • Also in the controls below: you can use the "Clear" button at any time to start over.

    The Canvas

    Converting an RSS feed into the layout in the overview took a lot of experimenting. I ended up with a tidy class (only about 100 lines of actual code): the TextClusterPaintable.

    There are a few essential building blocks I had to piece together first, though: the TextBoxPaintable breaks a java.lang.String up into a graphic representation, and the CompositePaintable is the abstract parent of the TextClusterPaintable that lets you compose different Paintables together as part of a bigger canvas. But the TextClusterPaintable is the entity that actually decides where each block of text goes, and at what angle. There's probably room for improvement in this class (it uses a kind of brute-force approach to resolve visual collisions), but it gets the job done for now.

    Extra Effects

    After I had that much working: I went back and looked for ways to improve the animation. The first oddity I noticed was that as you pan around the edges of the canvas: you see a lot of blank space. That is: you can clearly see where there is content and where there is not. This animation is supposed to evoke the feeling of lightly grazing over an unending stream of ideas, so seeing large blank areas off to the side is not the right model.

    So to fix this I decided to tile the canvas. In the CompositePaintable I added an enum titled Tiling:

    The text cluster uses the Tiling.OFFSET model to help reduce the obvious visual effect of a repeating pattern. There is still some white space (depending on how many blurbs we grabbed and how we arranged them), but the overall effect is much better.

    This accentuated a growing performance problem: the text boxes are stored as giant complex java.awt.Shapes. (Because treating them as java.lang.Strings would eventually result in a non-continuous animation: the whole point of rendering Strings vs Shapes is that they are abstract and the toolkit can (and will) optimize how they are rendered. We have to use Shapes to guarantee a continuous animation as we rotate and zoom.) These are not trivial to render, and when you invoke 7 calls to paint(..) instead of 1: a minor annoyance becomes a major obstacle. To address this the CompositePaintable now checks the rotated bounds of each child Paintable against the current clipping.

    Also at this point the animation was black-text-on-white-background. There's a time and place to embrace the "less is more" philosophy, but this was a little extreme. I decided to add a splash of color to the background. Also I decided the background should be at a different depth than the text itself. (That is: imagine the text is 6 inches below the camera, then the background is 4 inches below that.) This contrast of motion -- which hopefully will be so subtle most users won't notice it -- will add to the overall effect of depth and motion.

    I gravitated towards my own collection of tiled images, and eventually settled on one of the more modest (and non-animated) options. The colors were a little too bold (they competed with the text), so I softened them a little. Also at first I applied it as-is -- in a vertical orientation -- but the contrast between vertical and horizontal as the animation swiveled around was too weird. Using 45 degrees looked much better. And lastly I realized: there's no reason the background can't also move. When this particular pattern slides up or down the effect is similar to the Barberpole illusion, so that was a nice touch too.

    The original animation I saw at SXSW had its own special look: only the focused blurb was fully opaque -- all others were translucent. Also the background color would change every time the camera panned around. These are also nice effects, but in this iteration of the project I wanted to work with a static foreground layer. (I'd love to branch out into lots of text effects some day, and then that can be combined with this project for an even nicer effect).

    Conclusion

    There is a slight visual aliasing issue when the text (which is stored as a giant Shape) very slowly moves at an angled direction, but I don't know if that's something we can ever fix.

    Overall this worked out as I'd hoped, and I'd like to keep dabbling with similar effects in the future. Also I'd like to point out that everything in this project (except a few generics and enums) is compatible with Java 1.4 which was written just over a decade ago (in February of 2002). My point is: why didn't we do this kind of thing 10 years ago? The technology is there: we just need to take advantage of it. (Can you tell I just came from SXSW?)

    And lastly: I had to make up a lot of names to complete this project, including "SwivelPathAnimation" and "JFancyBox" and "TextClusterPaintable". If there exist more standardized names for these ideas please let me know and I'll happily refactor my code accordingly.

    Tuesday, March 1, 2011

    Text: Effects and Block Shadows

    This weekend I revisited an old project: the calligraphy stroke.

    I rewrote it using a handy unassuming class called the CalligraphyPathWriter. This is fewer lines of code than the previous model, pipes the data more efficiently, and includes a couple of extra functions.

    It no longer only makes strokes. The same basic notion of tracing a shape to apply a calligraphy-like stroke can be applied to creating drop shadows. The only difference is a stroke straddles the path, and a drop shadow lies entirely to the side of the original shape.

    The CalligraphyPathWriter constructor includes two floats to define these offsets: if you use 0 and 30: then you're effectively adding a drop shadow to your shape. If you use -5 and 5: then you're effectively creating a stroked outline of your shape.

    You can also create a drop-shadow-like effect with code like this:

    Graphics2D g;
    Shape myShape;
    ...
    double dx = Math.cos(angle);
    double dy = Math.sin(angle);
    for(int height = 0; height < 30; height++) {
      g.translate(height*dx, height*dy);
      g.fill(myShape);
      g.translate(-height*dx, -height*dy);
    }

    ... but if your shape or java.awt.Paint are complex: this might be expensive. (Also two of the four effects shown below can't be reproduced this way.)

    The CalligraphyPathWriter can reduce this down to:

    GeneralPath blockShadow = new GeneralPath();
    CalligraphyPathWriter writer =
        new CalligraphyPathWriter(angle, 0, height, blockShadow);
    writer.write(myShape);
    g.fill(blockShadow);

    The call write(myShape) iterates over the input shape and populates the blockShadow path with the silhouette of the block shadow.

    Applying To Text


    Here is a simple applet that creates animated text effects using these new block shadows:

    You can download the applet (source included) here.

    Type any text in the text field provided. (Any text is acceptable, but it should probably be 20 characters or less?) Then select any one of the buttons:

    • The "Outline" button uses the Scribbler class to randomize the texture of the text, fill and shadow. A little random vertical offset is added, and lastly a light drop shadow is traced (but not filled) around each letter.
    • The "Punch" button separately punches each word forward with an obvious drop shadow. (Also try writing a string with no spaces: then each character is punched.)
    • The "Wave" button fills and traces the block shadow to reinforce the 3D-look.
    • The "Explode" uses another obvious drop shadow, but each character is assigned an angle based on how far away it is from the center. (Then the height has to be divided by the sine of that angle so the baseline is constant.)

    My immediate goal this weekend was just to use block shadows for something fun. In the long term I want to define an architecture similar to what I did for simple 2D-geometric transitions. This demo applet uses a very loosely defined TextEffect interface, but it needs a lot more work to support a variety of effects for different export formats.

    Also in the future it would be nice to apply gradients to these block shadows. A height-based gradient effect will be very easy. An angle-based effect (which I think would be more popular) will be a little trickier, but is still manageable.

    Saturday, November 28, 2009

    Gradients: a Halftone Gradient

    So much to do: so little time! I have 3 other fun subjects to blog about... all of them in varying states of completion. But this is the most finished, so it's the first to write up.

    Halftone Gradients


    I don't know why, but I've always wanted a gradient that looked like halftoning.

    But for some reason whenever I thought of tackling it: I always envisioned myself writing a PaintContext and coming up with some ugly code that involved lots of varying radii, and antialiasing things by hand, and doing clever things with AffineTransforms and dot products that I'm not really well-versed in. And maybe that is the better/more efficient way to approach this subject... but there's a much easier way.

    My last blog entry used transformed tiles to achieve a multiple-color gradient. If you look at a halftone gradient as a tiling pattern, then suddenly it becomes a piece of cake. I just need to create a tall, skinny tile that transitions from one color to another.

    If I were a better blog author, I might try inserting a diagram here. I'd demonstrate exactly what the tile looks like, and maybe discuss the concept of halftoning a little bit more.

    However I'm not a better author. I'm much too lazy. Plus (and I'm not *entirely* bs-ing here): I really do believe a hands-on interactive model will explain this project more thoroughly than diagrams ever will. So... here's a demo applet showing off the HalftoneGradient class:



    The jar (source included) is available here.

    Your Basic Options


    Although I started with circles, it's even easier to make a tiled pattern using diamonds or isosceles triangles. The size of each shape scales in proportion to the width of the tile, and where in the gradient that shape is drawn.

    I'm unsure, though, if the circles are done correctly. I didn't find an exact algorithm for describing how halftoning should work -- but I admit I didn't look too closely. If a gradient is expressed in terms of t=[0,1]: does the gradient at t = .5 really look correct? Is that a half-way point between the two colors? For the circles I made the radius related to t^.8 to try to improve the balance. There is surely a more scientific way to approach this subject, but I didn't have the interest or patience. Does anyone have any experience with this subject? Basically the proportion of color1 to color2 should increase linearly with t.

    Oh, and as an afterthought I added the "offset" property, and the "animate" checkbox. How could I resist? (Have you seen my art? I love animating things.)

    Making a Tiled Pattern Not Tile


    This paint delegates all the hard work to a transformed TexturePaint, so it should be tiling like mad when the "Cycle" checkbox is unselected, right? This was probably the hardest part of the project: if a point lies outside of the strip created between P1 and P2: how do I make it a solid color?

    Originally I went to Sun's website and downloaded the source for the TexturePaintContext. I was determined to roll up my sleeves and make an adaptation that solved this. It didn't take me long to realize I was waaaaaaay over my head, though, as I looked through their source code. Aside from subtle problems (like referencing non-public classes to manipulate rasters), they somehow eliminated floats/doubles. Everything was int-based, on a scale of [0, Integer.MAX_VALUE]. While probably more efficient: this hurt my head. If you locked me in a dark prison cell with just bagels and a laptop: maybe in a few days I could figure this out. But I wanted to figure this out in a few hours, so I moved on to plan B:

    Instead I just rewrote the raster the TexturePaintContext returns. Is it the most efficient response? No. Does it work? Yes. The class is called CoveredContext. (It's non-public, although it's included in the jar mentioned above.) In its original draft the hard work looked like this:

    public Raster getRaster(int x, int y, int w, int h) {
    WritableRaster raster = (WritableRaster)context.getRaster(x, y, w, h);
    ...
    double[] matrix = new double[6];
    transform.getMatrix(matrix);
    for(int y2 = 0; y2 < h; y2++) {
    raster.getDataElements(0, y2, w, 1, data);

    for(int x2 = 0; x2 < data.length; x2++) {
    double x3 = x2+x;
    double y3 = y2+y;

    // apply the transform manually:
    y3 = matrix[1]*x3+matrix[3]*y3+matrix[5];

    if(y3 < 1) {
    data[x2] = color1;
    } else if(y3 >= distance-1) {
    data[x2] = color2;
    }
    }

    raster.setDataElements(0, y2, w, 1, data);
    }
    return raster;
    }

    The final draft, though, is much more mathematically clever. (It calculates the two x-points in a row of pixels that mark the beginning and end of the gradient, and flood fills everything beyond those points.) But it's basically the same approach in spirit. It involves many more special cases, though.

    Conclusion


    So there you have it. Very painless. The code, including comments and javadoc, probably comes to less than 500 lines of code. Eventually I plan to integrate this into some other projects I'm working on, but that will have to wait.

    I haven't tried to break this down and scrutinize performance yet. This class first needs to demonstrate that it, in fact, useful...