Start line:  
End line:  

Snippet Preview

Snippet HTML Code

Stack Overflow Questions
  //
  // Nenya library - tools for developing networked games
  // Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
  // https://github.com/threerings/nenya
  //
  // This library is free software; you can redistribute it and/or modify it
  // under the terms of the GNU Lesser General Public License as published
  // by the Free Software Foundation; either version 2.1 of the License, or
  // (at your option) any later version.
 //
 // This library is distributed in the hope that it will be useful,
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 // Lesser General Public License for more details.
 //
 // You should have received a copy of the GNU Lesser General Public
 // License along with this library; if not, write to the Free Software
 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
 package com.threerings.chat;
 
 import java.util.List;
 
 import java.awt.Color;
 import java.awt.Font;
 import java.awt.Point;
 import java.awt.Shape;
 
 
 
 
 
 
 
 import static com.threerings.NenyaLog.log;

Implements comic chat in the yohoho client.
 
 public class ComicChatOverlay extends SubtitleChatOverlay
 {
    
Construct a comic chat overlay.

Parameters:
subtitleHeight the amount of vertical space to use for subtitles.
 
     public ComicChatOverlay (CrowdContext ctxChatLogic logicJScrollBar historyBar,
                              int subtitleHeight)
     {
         super(ctxlogichistoryBarsubtitleHeight);
     }
 
     @Override
     public void newPlaceEntered (InfoProvider provider)
     {
          = .getChatDirector().getHistory().size();
         super.newPlaceEntered(provider);
 
         // and clear place-oriented bubbles
         clearBubbles(false);
     }
 
     @Override
     public void layout ()
     {
         clearBubbles(true); // these will get repopulated from the history
         super.layout();
     }
 
     @Override
     public void removed ()
     {
         // we do this before calling super because we want our target to
         // be around for the bubble clearing
         clearBubbles(true);
 
         super.removed();
     }
 
     @Override
    public void clear ()
    {
        super.clear();
        clearBubbles(true);
    }
    @Override
    public void viewDidScroll (int dxint dy)
    {
        super.viewDidScroll(dxdy);
        viewDidScroll(dxdy);
    }
    @Override
    public void setDimmed (boolean dimmed)
    {
        super.setDimmed(dimmed);
        updateDimmed();
    }
    @Override
    public void speakerDeparted (Name speaker)
    {
        for (Iterator<BubbleGlyphiter = .iterator(); iter.hasNext();) {
            BubbleGlyph rec = iter.next();
            if (rec.isSpeaker(speaker)) {
                .abortAnimation(rec);
                iter.remove();
            }
        }
    }
    @Override
    public void historyUpdated (int adjustment)
    {
         -= adjustment;
        super.historyUpdated(adjustment);
    }

    
Clear chat bubbles, either all of them or just the place-oriented ones.
    protected void clearBubbles (boolean all)
    {
        for (Iterator<BubbleGlyphiter = .iterator(); iter.hasNext();) {
            ChatGlyph rec = iter.next();
            if (all || isPlaceOrientedType(rec.getType())) {
                .abortAnimation(rec);
                iter.remove();
            }
        }
    }
    @Override
    protected boolean shouldShowFromHistory (ChatMessage msgint index)
    {
        // only show if the message was received since we last entered
        // a new place, or if it's place-less chat.
        return ((index >= ) ||
                (! isPlaceOrientedType(getType(msgfalse))));
    }
    @Override
    protected boolean isApprovedLocalType (String localtype)
    {
        if (..equals(localtype) ||
            ..equals(localtype)) {
            return true;
        }
        .debug("Ignoring non-standard system/feedback chat""localtype"localtype);
        return false;
    }

    
Is the type of chat place-oriented.
    protected boolean isPlaceOrientedType (int type)
    {
        return (ChatLogic.placeOf(type)) == .;
    }
    @Override
    protected void displayMessage (ChatMessage messageint typeGraphics2D layoutGfx)
    {
        // we might need to modify the textual part with translations,
        // but we can't do that to the message object, since other chatdisplays also get it.
        String text = message.message;
        switch (ChatLogic.placeOf(type)) {
        case .:
        case .:
            if (createBubble(layoutGfxtypemessage.timestamptextnullnull)) {
                return// EXIT;
            }
            // if the bubble didn't fit (unlikely), make it a subtitle
            break;
        case .: {
            UserMessage msg = (UserMessagemessage;
            Point speakerloc = .getSpeaker(msg.speaker);
            if (speakerloc == null) {
                .warning("ChatOverlay.InfoProvider doesn't know the speaker!",
                    "speaker"msg.speaker"type"type);
                return;
            }
            // emotes won't actually have tails, but we do want them to appear near the pirate
            if (ChatLogic.modeOf(type) == .) {
                text = xlate(
                    MessageBundle.tcompose("m.emote_format"msg.getSpeakerDisplayName())) +
                    " " + text;
            }
            // try to add all the text as a bubble, but if it doesn't
            // fit, add some of it and 'continue' the rest in a subtitle.
            String leftover = text;
            for (int ii = 1; ii < 7; ii++) {
                String bubtext = splitNear(texttext.length() / ii);
                if (createBubble(layoutGfxtypemsg.timestamp,
                    bubtext + ((ii > 1) ? "..." : ""), msg.speakerspeakerloc)) {
                    leftover = text.substring(bubtext.length());
                    break;
                }
            }
            if (leftover.length() > 0 && !isHistoryMode()) {
                String ltext = MessageBundle.tcompose("m.continue_format"msg.speaker);
                ltext = xlate(ltext) + " \"" + leftover + "\"";
                addSubtitle(createSubtitle(layoutGfx.,
                    message.timestampnull, 0, ltexttrue));
            }
            return// EXIT
        }
        }
        super.displayMessage(messagetypelayoutGfx);
    }

    
Split the text at the space nearest the specified location.
    protected String splitNear (String textint pos)
    {
        if (pos >= text.length()) {
            return text;
        }
        int forward = text.indexOf(' 'pos);
        int backward = text.lastIndexOf(' 'pos);
        int newpos = (Math.abs(pos - forward) < Math.abs(pos - backward)) ? forward : backward;
        // if we couldn't find a decent place to split, just do it wherever
        if (newpos == -1) {
            newpos = pos;
        } else {
            // actually split the space onto the first part
            newpos++;
        }
        return text.substring(0, newpos);
    }

    
Create a chat bubble with the specified type and text.

Parameters:
speakerloc if non-null, specifies that a tail should be added which points to that location.
Returns:
true if we successfully laid out the bubble
    protected boolean createBubble (
        Graphics2D gfxint typelong timestampString textName speakerPoint speakerloc)
    {
        Label label = layoutText(gfx.getFont(type), text);
        label.setAlignment(.);
        gfx.dispose();
        // get the size of the new bubble
        Rectangle r = getBubbleSize(typelabel.getSize());
        // get the user's old bubbles.
        List<BubbleGlypholdbubs = getAndExpireBubbles(speaker);
        int numold = oldbubs.size();
        Rectangle placerbigR = null;
        if (numold == 0) {
            placer = new Rectangle(r);
            positionRectIdeally(placertypespeakerloc);
        } else {
            // get a big rectangle encompassing the old and new
            bigR = getRectWithOlds(roldbubs);
            placer = new Rectangle(bigR);
            positionRectIdeally(placertypespeakerloc);
            // we actually try to place midway between ideal and old
            // and adjust up half the height of the new boy
            placer.setLocation((placer.x + bigR.x) / 2,
                               (placer.y + (bigR.y - (r.height / 2))) / 2);
        }
        // then look for a place nearby where it will fit
        // (making sure we only put it in the area above the subtitles)
        Rectangle vbounds = new Rectangle(.getViewBounds());
        vbounds.height -= ;
        if (!SwingUtil.positionRect(placervboundsgetAvoidList(speaker))) {
            // we couldn't fit the bubble!
            return false;
        }
        // now 'placer' is positioned reasonably.
        if (0 == numold) {
            r.setLocation(placer.xplacer.y);
        } else {
            int dx = placer.x - bigR.x;
            int dy = placer.y - bigR.y;
            for (int ii=0; ii < numoldii++) {
                BubbleGlyph bub = oldbubs.get(ii);
                bub.removeTail();
                Rectangle ob = bub.getBubbleBounds();
                // recenter the translated bub within placer's width..
                int xadjust = dx - (ob.x - bigR.x) +
                    (placer.width - ob.width) / 2;
                bub.translate(xadjustdy);
            }
            // and position 'r' in the right place relative to 'placer'
            r.setLocation(placer.x + (placer.width - r.width) / 2,
                          placer.y + placer.height - r.height);
        }
        Shape shape = getBubbleShape(typer);
        Shape full = shape;
        // if we have a tail, the full area should include that.
        if (speakerloc != null) {
            Area area = new Area(getTail(typerspeakerloc));
            area.add(new Area(shape));
            full = area;
        }
        // finally, add the bubble
        long lifetime = getChatExpire(timestamplabel.getText())-timestamp;
        BubbleGlyph newbub = new BubbleGlyph(
            thistypelifetimefulllabeladjustLabel(typer.getLocation()), shape,
            speaker.getOutlineColor(type));
        newbub.setDim();
        .add(newbub);
        .addAnimation(newbub);
        // and we need to dirty all the bubbles because they'll all be painted in slightly
        // different colors
        int numbubs = .size();
        for (int ii=0; ii < numbubsii++) {
            .get(ii).setAgeLevel(numbubs - ii - 1);
        }
        return true// success!
    }

    
Calculate the size of the chat bubble based on the dimensions of the label and the type of chat. It will be turned into a shape later, but we manipulate it for a while as just a rectangle (which are easier to move about and do intersection tests with, and besides the Shape interface has no way to translate).
    protected Rectangle getBubbleSize (int typeDimension d)
    {
        switch (ChatLogic.modeOf(type)) {
        case .:
        case .:
        case .:
            // extra room for these two monsters
            return new Rectangle(d.width + ( * 4), d.height + ( * 4));
        default:
            return new Rectangle(d.width + ( * 2), d.height + ( * 2));
        }
    }

    
Position the label based on the type.
    protected Point adjustLabel (int typePoint labelpos)
    {
        switch (ChatLogic.modeOf(type)) {
        case .:
        case .:
        case .:
            labelpos.translate( * 2,  * 2);
            break;
        default:
            labelpos.translate();
            break;
        }
        return labelpos;
    }

    
Position the rectangle in its ideal location given the type and speaker positon (which may be null).
    protected void positionRectIdeally (Rectangle rint typePoint speaker)
    {
        if (speaker != null) {
            // center it on top of the speaker (it'll be moved..)
            r.setLocation(speaker.x - (r.width / 2),
                          speaker.y - (r.height / 2));
            return;
        }
        // otherwise we have different areas for different types
        Rectangle vbounds = .getViewBounds();
        switch (ChatLogic.placeOf(type)) {
        case .:
        case .:
            // upper left
            r.setLocation(vbounds.x + ,
                          vbounds.y + );
            return;
        case .:
            .warning("Got to a place where I shouldn't get!");
            break// fall through
        }
        // put it in the center..
        .debug("Unhandled chat type in getLocation()""type"type);
        r.setLocation((vbounds.width - r.width) / 2,
                      (vbounds.height - r.height) / 2);
    }

    
Get a rectangle based on the old bubbles, but with room for the new one.
    protected Rectangle getRectWithOlds (Rectangle rList<BubbleGlypholdbubs)
    {
        int n = oldbubs.size();
        // if no old bubs, just return the new one.
        if (n == 0) {
            return r;
        }
        // otherwise, encompass all the oldies
        Rectangle bigR = null;
        for (int ii=0; ii < nii++) {
            BubbleGlyph bub = oldbubs.get(ii);
            if (ii == 0) {
                bigR = bub.getBubbleBounds();
            } else {
                bigR = bigR.union(bub.getBubbleBounds());
            }
        }
        // and add space for the new boy
        bigR.width = Math.max(bigR.widthr.width);
        bigR.height += r.height;
        return bigR;
    }

    
Get the appropriate shape for the specified type of chat.
    protected Shape getBubbleShape (int typeRectangle r)
    {
        switch (ChatLogic.placeOf(type)) {
        case .:
        case .:
            // boring rectangle wrapped in an Area for translation
            return new Area(r);
        }
        switch (ChatLogic.modeOf(type)) {
        case .:
            // a rounded rectangle balloon, put in an Area so that it's
            // translatable
            return new Area(new RoundRectangle2D.Float(
                r.xr.yr.widthr.height * 4,  * 4));
        case .: {
            // spikey balloon
            Polygon left = new Polygon(), right = new Polygon();
            Polygon top = new Polygon(), bot = new Polygon();
            int x = r.x + ;
            int y = r.y + ;
            int wid = r.width -  * 2;
            int hei = r.height -  * 2;
            Area a = new Area(new Rectangle(xywidhei));
            int spikebase = 10;
            int cornbase = spikebase*3/4;
            // configure spikes to the left and right sides
            left.addPoint(xy);
            left.addPoint(x - y + spikebase/2);
            left.addPoint(xy + spikebase);
            right.addPoint(x + widy);
            right.addPoint(x + wid + y + spikebase/2);
            right.addPoint(x + widy + spikebase);
            // add the left and right side spikes
            int ypos = 0;
            int ahei = hei - cornbase;
            int maxpos = ahei - spikebase + 1;
            int numvert = (int) Math.ceil(ahei / ((floatspikebase));
            for (int ii=0; ii < numvertii++) {
                int newpos = cornbase/2 +
                    Math.min((ahei * ii) / numvertmaxpos);
                left.translate(0, newpos - ypos);
                right.translate(0, newpos - ypos);
                a.add(new Area(left));
                a.add(new Area(right));
                ypos = newpos;
            }
            // configure spikes for the top and bottom
            top.addPoint(xy);
            top.addPoint(x + spikebase/2, y - );
            top.addPoint(x + spikebasey);
            bot.addPoint(xy + hei);
            bot.addPoint(x + spikebase/2, y + hei + );
            bot.addPoint(x + spikebasey + hei);
            // add top and bottom spikes
            int xpos = 0;
            int awid = wid - cornbase;
            maxpos = awid - spikebase + 1;
            int numhorz = (int) Math.ceil(awid / ((floatspikebase));
            for (int ii=0; ii < numhorzii++) {
                int newpos = cornbase/2 +
                    Math.min((awid * ii) / numhorzmaxpos);
                top.translate(newpos - xpos, 0);
                bot.translate(newpos - xpos, 0);
                a.add(new Area(top));
                a.add(new Area(bot));
                xpos = newpos;
            }
            // and lets also add corner spikes
            Polygon corner = new Polygon();
            corner.addPoint(xy + cornbase);
            corner.addPoint(x -  + 2, y -  + 2);
            corner.addPoint(x + cornbasey);
            a.add(new Area(corner));
            corner.reset();
            corner.addPoint(x + wid - cornbasey);
            corner.addPoint(x + wid +  - 2, y -  + 2);
            corner.addPoint(x + widy + cornbase);
            a.add(new Area(corner));
            corner.reset();
            corner.addPoint(x + widy + hei - cornbase);
            corner.addPoint(x + wid +  - 2, y + hei +  - 2);
            corner.addPoint(x + wid - cornbasey + hei);
            a.add(new Area(corner));
            corner.reset();
            corner.addPoint(x + cornbasey + hei);
            corner.addPoint(x -  + 2, y + hei +  - 2);
            corner.addPoint(xy + hei - cornbase);
            a.add(new Area(corner));
            // grunt work!
            return a;
        }
        case .: {
            // a box that curves inward on all sides
            Area a = new Area(r);
            a.subtract(new Area(new Ellipse2D.Float(r.xr.y - r.width * 2)));
            a.subtract(new Area(new Ellipse2D.Float(r.xr.y + r.height - r.width * 2)));
            a.subtract(new Area(new Ellipse2D.Float(r.x - r.y * 2, r.height)));
            a.subtract(new Area(new Ellipse2D.Float(r.x + r.width - r.y * 2, r.height)));
            return a;
        }
        case .: {
            // cloudy balloon!
            int x = r.x + ;
            int y = r.y + ;
            int wid = r.width -  * 2;
            int hei = r.height -  * 2;
            Area a = new Area(new Rectangle(xywidhei));
            // small circles on the left and right
            int dia = 12;
            int numvert = (int) Math.ceil(hei / ((floatdia));
            int leftside = x - dia/2;
            int rightside =  x + wid - (dia/2) - 1;
            int maxh = hei - dia;
            for (int ii=0; ii < numvertii++) {
                int ypos = y + Math.min((hei * ii) / numvertmaxh);
                a.add(new Area(new Ellipse2D.Float(leftsideyposdiadia)));
                a.add(new Area(new Ellipse2D.Float(rightsideyposdiadia)));
            }
            // larger ovals on the top and bottom
            dia = 16;
            int numhorz = (int) Math.ceil(wid / ((floatdia));
            int topside = y - dia/3;
            int botside = y + hei - (dia/3) - 1;
            int maxw = wid - dia;
            for (int ii=0; ii < numhorzii++) {
                int xpos = x + Math.min((wid * ii) / numhorzmaxw);
                a.add(new Area(new Ellipse2D.Float(xpostopsidediadia*2/3)));
                a.add(new Area(new Ellipse2D.Float(xposbotsidediadia*2/3)));
            }
            return a;
        }
        }
        // fall back to subtitle shape
        return .getSubtitleShape(typerr);
    }

    
Create a tail to the specified rectangular area from the speaker point.
    protected Shape getTail (int typeRectangle rPoint speaker)
    {
        // emotes don't actually have tails
        if (ChatLogic.modeOf(type) == .) {
            return new Area(); // empty shape
        }
        int midx = r.x + (r.width / 2);
        int midy = r.y + (r.height / 2);
        // we actually want to start about SPEAKER_DISTANCE away from the
        // speaker
        int xx = speaker.x - midx;
        int yy = speaker.y - midy;
        float dist = (float) Math.sqrt(xx * xx + yy * yy);
        float perc = (dist - ) / dist;
        if (ChatLogic.modeOf(type) == .) {
            int steps = Math.max((int) (dist / ), 2);
            float step = perc / steps;
            Area a = new Area();
            for (int ii=0; ii < stepsii++, perc -= step) {
                int radius = Math.min( / 2 - 1, ii + 2);
                a.add(new Area(new Ellipse2D.Float(
                  (int) ((1 - perc) * midx + perc * speaker.x) + perc * radius,
                  (int) ((1 - perc) * midy + perc * speaker.y) + perc * radius,
                  radius * 2, radius * 2)));
            }
            return a;
        }
        // ELSE draw a triangular tail shape
        Polygon p = new Polygon();
        p.addPoint((int) ((1 - perc) * midx + perc * speaker.x),
                   (int) ((1 - perc) * midy + perc * speaker.y));
        if (Math.abs(speaker.x - midx) > Math.abs(speaker.y - midy)) {
            int x;
            if (midx > speaker.x) {
                x = r.x + ;
            } else {
                x = r.x + r.width - ;
            }
            p.addPoint(xmidy - ( / 2));
            p.addPoint(xmidy + ( / 2));
        } else {
            int y;
            if (midy > speaker.y) {
                y = r.y + ;
            } else {
                y = r.y + r.height - ;
            }
            p.addPoint(midx - ( / 2), y);
            p.addPoint(midx + ( / 2), y);
        }
        return p;
    }

    
Expire a bubble, if necessary, and return the old bubbles for the specified speaker.
    protected List<BubbleGlyphgetAndExpireBubbles (Name speaker)
    {
        int num = .size();
        // first, get all the old bubbles belonging to the user
        List<BubbleGlypholdbubs = Lists.newArrayList();
        if (speaker != null) {
            for (int ii=0; ii < numii++) {
                BubbleGlyph bub = .get(ii);
                if (bub.isSpeaker(speaker)) {
                    oldbubs.add(bub);
                }
            }
        }
        // see if we need to expire this user's oldest bubble
        if (oldbubs.size() >= ) {
            BubbleGlyph bub = oldbubs.remove(0);
            .remove(bub);
            .abortAnimation(bub);
            // or some other old bubble
        } else if (num >= ) {
            .abortAnimation(.remove(0));
        }
        // return the speaker's old bubbles
        return oldbubs;
    }
    @Override
    protected void glyphExpired (ChatGlyph glyph)
    {
        super.glyphExpired(glyph);
        .remove(glyph);
    }

    
Get a label formatted as close to the golden ratio as possible for the specified text and given the standard padding we use on all bubbles.
    protected Label layoutText (Graphics2D gfxFont fontString text)
    {
        Label label = .createLabel(text);
        label.setFont(font);
        // layout in one line
        Rectangle vbounds = .getViewBounds();
        label.setTargetWidth(vbounds.width -  * 2);
        label.layout(gfx);
        Dimension d = label.getSize();
        // if the label is wide enough, try to split the text into multiple
        // lines
        if (d.width > ) {
            int targetheight = getGoldenLabelHeight(d);
            if (targetheight > 1) {
                label.setTargetHeight(targetheight * d.height);
                label.layout(gfx);
            }
        }
        return label;
    }

    
Given the specified label dimensions, attempt to find the height that will give us the width/height ratio that is closest to the golden ratio.
    protected int getGoldenLabelHeight (Dimension d)
    {
        // compute the ratio of the one line (addin' the paddin')
        double lastratio = ((doubled.width + ( * 2)) /
                           ((doubled.height + ( * 2));
        // now try increasing the # of lines and seeing if we get closer to the golden ratio
        for (int lines=2; truelines++) {
            double ratio = ((double) (d.width / lines) + ( * 2)) /
                               ((double) (d.height * lines) + ( * 2));
            if (Math.abs(ratio - ) < Math.abs(lastratio - )) {
                // we're getting closer
                lastratio = ratio;
            } else {
                // we're getting further away, the last one was the one we want
                return lines - 1;
            }
        }
    }

    
Return a list of rectangular areas that we should avoid while laying out a bubble for the specified speaker.
    protected List<ShapegetAvoidList (Name speaker)
    {
        List<Shapeavoid = Lists.newArrayList();
        if ( == null) {
            return avoid;
        }
        // for now we don't accept low-priority avoids
        .getAvoidables(speakeravoidnull);
        // add the existing chatbub non-tail areas from other speakers
        for (BubbleGlyph bub : ) {
            if (!bub.isSpeaker(speaker)) {
                avoid.add(bub.getBubbleTerritory());
            }
        }
        return avoid;
    }
    @Override
    protected int getDisplayDurationOffset ()
    {
        return 0; // we don't do any funny hackery, unlike our super class
    }

    
A glyph of a particlar chat bubble
    protected static class BubbleGlyph extends ChatGlyph
    {
        
Construct a chat bubble glyph.

Parameters:
sansTail the chat bubble shape without the tail.
        public BubbleGlyph (
            SubtitleChatOverlay ownerint typelong lifetimeShape shapeLabel label,
            Point labelposShape sansTailName speakerColor outline) {
            super(ownertypelifetimeshape.getBounds(), shapenullnull,
                labellabelposoutline);
             = sansTail;
             = speaker;
        }
        public void setAgeLevel (int agelevel) {
             = agelevel;
            invalidate();
        }
        @Override
        public void viewDidScroll (int dxint dy) {
            // only system info and attention messages remain fixed, all others scroll
            if (( == .) || ( == .)) {
                translate(dxdy);
            }
        }
        @Override
        protected Color getBackground () {
            if ( == .) {
                return [];
            } else {
                return ;
            }
        }

        
Get the screen real estate that this bubble has reserved and doesn't want to let any other bubbles take.
        public Shape getBubbleTerritory () {
            Rectangle bounds = getBubbleBounds();
            bounds.grow();
            return bounds;
        }

        
Get the bounds of this bubble, sans tail space.
        public Rectangle getBubbleBounds () {
            return .getBounds();
        }

        
Is the specified player the speaker of this bubble?
        public boolean isSpeaker (Name player) {
            return ( != null) && .equals(player);
        }

        
Remove the tail on this bubble, if any.
        public void removeTail () {
            invalidate();
             = ;
             = .getBounds();
            jiggleBounds();
            invalidate();
        }

        
The shape of this chat bubble, without the tail.
        protected Shape _sansTail;

        
The name of the speaker.
        protected Name _speaker;

        
The age level of the bubble, used to pick the background color.
        protected int _agelevel = 0;
    }

    
The place in our history at which we last entered a new place.
    protected int _newPlacePoint = 0;

    
The currently displayed bubble areas.
    protected List<BubbleGlyph_bubbles = Lists.newArrayList();

    
The minimum width of a bubble's label before we consider splitting lines.
    protected static final int MINIMUM_SPLIT_WIDTH = 90;

    
The golden ratio.
    protected static final double GOLDEN = (1.0d + Math.sqrt(5.0d)) / 2.0d;

    
The space we force between adjacent bubbles.
    protected static final int BUBBLE_SPACING = 15;

    
The distance to stay from the speaker.
    protected static final int SPEAKER_DISTANCE = 20;

    
The width of the end of the tail.
    protected static final int TAIL_WIDTH = 12;

    
The maximum number of bubbles to show.
    protected static final int MAX_BUBBLES = 8;

    
The maximum number of bubbles to show per user.
    protected static final int MAX_BUBBLES_PER_USER = 3;

    
The background colors to use when drawing bubbles.
    protected static final Color[] BACKGROUNDS = new Color[];
    static {
        Color yellowy = new Color(0xdd, 0xdd, 0x6a);
        Color blackish = new Color(0xcccccc);
        float steps = ( - 1) / 2;
        for (int ii=0; ii <  / 2; ii++) {
            [ii] = ColorUtil.blend(.yellowy, (steps - ii) / steps);
        }
        for (int ii / 2; ii < ii++) {
            [ii] = ColorUtil.blend(blackishyellowy, (ii - steps) / steps);
        }
    }