
com.threerings.chat.ComicChatOverlay Maven / Gradle / Ivy
//
// 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.util.Iterator;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.RoundRectangle2D;
import javax.swing.JScrollBar;
import com.google.common.collect.Lists;
import com.samskivert.swing.Label;
import com.samskivert.swing.util.SwingUtil;
import com.threerings.util.MessageBundle;
import com.threerings.util.Name;
import com.threerings.media.image.ColorUtil;
import com.threerings.crowd.chat.data.ChatCodes;
import com.threerings.crowd.chat.data.ChatMessage;
import com.threerings.crowd.chat.data.UserMessage;
import com.threerings.crowd.util.CrowdContext;
import static com.threerings.NenyaLog.log;
/**
* Implements comic chat in the yohoho client.
*/
public class ComicChatOverlay extends SubtitleChatOverlay
{
/**
* Construct a comic chat overlay.
*
* @param subtitleHeight the amount of vertical space to use for subtitles.
*/
public ComicChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar historyBar,
int subtitleHeight)
{
super(ctx, logic, historyBar, subtitleHeight);
}
@Override
public void newPlaceEntered (InfoProvider provider)
{
_newPlacePoint = _ctx.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 dx, int dy)
{
super.viewDidScroll(dx, dy);
viewDidScroll(_bubbles, dx, dy);
}
@Override
public void setDimmed (boolean dimmed)
{
super.setDimmed(dimmed);
updateDimmed(_bubbles);
}
@Override
public void speakerDeparted (Name speaker)
{
for (Iterator iter = _bubbles.iterator(); iter.hasNext();) {
BubbleGlyph rec = iter.next();
if (rec.isSpeaker(speaker)) {
_target.abortAnimation(rec);
iter.remove();
}
}
}
@Override
public void historyUpdated (int adjustment)
{
_newPlacePoint -= adjustment;
super.historyUpdated(adjustment);
}
/**
* Clear chat bubbles, either all of them or just the place-oriented ones.
*/
protected void clearBubbles (boolean all)
{
for (Iterator iter = _bubbles.iterator(); iter.hasNext();) {
ChatGlyph rec = iter.next();
if (all || isPlaceOrientedType(rec.getType())) {
_target.abortAnimation(rec);
iter.remove();
}
}
}
@Override
protected boolean shouldShowFromHistory (ChatMessage msg, int index)
{
// only show if the message was received since we last entered
// a new place, or if it's place-less chat.
return ((index >= _newPlacePoint) ||
(! isPlaceOrientedType(getType(msg, false))));
}
@Override
protected boolean isApprovedLocalType (String localtype)
{
if (ChatCodes.PLACE_CHAT_TYPE.equals(localtype) ||
ChatCodes.USER_CHAT_TYPE.equals(localtype)) {
return true;
}
log.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)) == ChatLogic.PLACE;
}
@Override
protected void displayMessage (ChatMessage message, int type, Graphics2D 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 ChatLogic.INFO:
case ChatLogic.ATTENTION:
if (createBubble(layoutGfx, type, message.timestamp, text, null, null)) {
return; // EXIT;
}
// if the bubble didn't fit (unlikely), make it a subtitle
break;
case ChatLogic.PLACE: {
UserMessage msg = (UserMessage) message;
Point speakerloc = _provider.getSpeaker(msg.speaker);
if (speakerloc == null) {
log.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) == ChatLogic.EMOTE) {
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(text, text.length() / ii);
if (createBubble(layoutGfx, type, msg.timestamp,
bubtext + ((ii > 1) ? "..." : ""), msg.speaker, speakerloc)) {
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, ChatLogic.CONTINUATION,
message.timestamp, null, 0, ltext, true));
}
return; // EXIT
}
}
super.displayMessage(message, type, layoutGfx);
}
/**
* Split the text at the space nearest the specified location.
*/
protected String splitNear (String text, int 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.
*
* @param speakerloc if non-null, specifies that a tail should be added which points to that
* location.
* @return true if we successfully laid out the bubble
*/
protected boolean createBubble (
Graphics2D gfx, int type, long timestamp, String text, Name speaker, Point speakerloc)
{
Label label = layoutText(gfx, _logic.getFont(type), text);
label.setAlignment(Label.CENTER);
gfx.dispose();
// get the size of the new bubble
Rectangle r = getBubbleSize(type, label.getSize());
// get the user's old bubbles.
List oldbubs = getAndExpireBubbles(speaker);
int numold = oldbubs.size();
Rectangle placer, bigR = null;
if (numold == 0) {
placer = new Rectangle(r);
positionRectIdeally(placer, type, speakerloc);
} else {
// get a big rectangle encompassing the old and new
bigR = getRectWithOlds(r, oldbubs);
placer = new Rectangle(bigR);
positionRectIdeally(placer, type, speakerloc);
// 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(_target.getViewBounds());
vbounds.height -= _subtitleHeight;
if (!SwingUtil.positionRect(placer, vbounds, getAvoidList(speaker))) {
// we couldn't fit the bubble!
return false;
}
// now 'placer' is positioned reasonably.
if (0 == numold) {
r.setLocation(placer.x, placer.y);
} else {
int dx = placer.x - bigR.x;
int dy = placer.y - bigR.y;
for (int ii=0; ii < numold; ii++) {
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(xadjust, dy);
}
// 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(type, r);
Shape full = shape;
// if we have a tail, the full area should include that.
if (speakerloc != null) {
Area area = new Area(getTail(type, r, speakerloc));
area.add(new Area(shape));
full = area;
}
// finally, add the bubble
long lifetime = getChatExpire(timestamp, label.getText())-timestamp;
BubbleGlyph newbub = new BubbleGlyph(
this, type, lifetime, full, label, adjustLabel(type, r.getLocation()), shape,
speaker, _logic.getOutlineColor(type));
newbub.setDim(_dimmed);
_bubbles.add(newbub);
_target.addAnimation(newbub);
// and we need to dirty all the bubbles because they'll all be painted in slightly
// different colors
int numbubs = _bubbles.size();
for (int ii=0; ii < numbubs; ii++) {
_bubbles.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 type, Dimension d)
{
switch (ChatLogic.modeOf(type)) {
case ChatLogic.SHOUT:
case ChatLogic.THINK:
case ChatLogic.EMOTE:
// extra room for these two monsters
return new Rectangle(d.width + (PAD * 4), d.height + (PAD * 4));
default:
return new Rectangle(d.width + (PAD * 2), d.height + (PAD * 2));
}
}
/**
* Position the label based on the type.
*/
protected Point adjustLabel (int type, Point labelpos)
{
switch (ChatLogic.modeOf(type)) {
case ChatLogic.SHOUT:
case ChatLogic.EMOTE:
case ChatLogic.THINK:
labelpos.translate(PAD * 2, PAD * 2);
break;
default:
labelpos.translate(PAD, PAD);
break;
}
return labelpos;
}
/**
* Position the rectangle in its ideal location given the type and speaker positon (which may
* be null).
*/
protected void positionRectIdeally (Rectangle r, int type, Point 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 = _target.getViewBounds();
switch (ChatLogic.placeOf(type)) {
case ChatLogic.INFO:
case ChatLogic.ATTENTION:
// upper left
r.setLocation(vbounds.x + BUBBLE_SPACING,
vbounds.y + BUBBLE_SPACING);
return;
case ChatLogic.PLACE:
log.warning("Got to a place where I shouldn't get!");
break; // fall through
}
// put it in the center..
log.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 r, List oldbubs)
{
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 < n; ii++) {
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.width, r.width);
bigR.height += r.height;
return bigR;
}
/**
* Get the appropriate shape for the specified type of chat.
*/
protected Shape getBubbleShape (int type, Rectangle r)
{
switch (ChatLogic.placeOf(type)) {
case ChatLogic.INFO:
case ChatLogic.ATTENTION:
// boring rectangle wrapped in an Area for translation
return new Area(r);
}
switch (ChatLogic.modeOf(type)) {
case ChatLogic.SPEAK:
// a rounded rectangle balloon, put in an Area so that it's
// translatable
return new Area(new RoundRectangle2D.Float(
r.x, r.y, r.width, r.height, PAD * 4, PAD * 4));
case ChatLogic.SHOUT: {
// spikey balloon
Polygon left = new Polygon(), right = new Polygon();
Polygon top = new Polygon(), bot = new Polygon();
int x = r.x + PAD;
int y = r.y + PAD;
int wid = r.width - PAD * 2;
int hei = r.height - PAD * 2;
Area a = new Area(new Rectangle(x, y, wid, hei));
int spikebase = 10;
int cornbase = spikebase*3/4;
// configure spikes to the left and right sides
left.addPoint(x, y);
left.addPoint(x - PAD, y + spikebase/2);
left.addPoint(x, y + spikebase);
right.addPoint(x + wid, y);
right.addPoint(x + wid + PAD, y + spikebase/2);
right.addPoint(x + wid, y + 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 / ((float) spikebase));
for (int ii=0; ii < numvert; ii++) {
int newpos = cornbase/2 +
Math.min((ahei * ii) / numvert, maxpos);
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(x, y);
top.addPoint(x + spikebase/2, y - PAD);
top.addPoint(x + spikebase, y);
bot.addPoint(x, y + hei);
bot.addPoint(x + spikebase/2, y + hei + PAD);
bot.addPoint(x + spikebase, y + hei);
// add top and bottom spikes
int xpos = 0;
int awid = wid - cornbase;
maxpos = awid - spikebase + 1;
int numhorz = (int) Math.ceil(awid / ((float) spikebase));
for (int ii=0; ii < numhorz; ii++) {
int newpos = cornbase/2 +
Math.min((awid * ii) / numhorz, maxpos);
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(x, y + cornbase);
corner.addPoint(x - PAD + 2, y - PAD + 2);
corner.addPoint(x + cornbase, y);
a.add(new Area(corner));
corner.reset();
corner.addPoint(x + wid - cornbase, y);
corner.addPoint(x + wid + PAD - 2, y - PAD + 2);
corner.addPoint(x + wid, y + cornbase);
a.add(new Area(corner));
corner.reset();
corner.addPoint(x + wid, y + hei - cornbase);
corner.addPoint(x + wid + PAD - 2, y + hei + PAD - 2);
corner.addPoint(x + wid - cornbase, y + hei);
a.add(new Area(corner));
corner.reset();
corner.addPoint(x + cornbase, y + hei);
corner.addPoint(x - PAD + 2, y + hei + PAD - 2);
corner.addPoint(x, y + hei - cornbase);
a.add(new Area(corner));
// grunt work!
return a;
}
case ChatLogic.EMOTE: {
// a box that curves inward on all sides
Area a = new Area(r);
a.subtract(new Area(new Ellipse2D.Float(r.x, r.y - PAD, r.width, PAD * 2)));
a.subtract(new Area(new Ellipse2D.Float(r.x, r.y + r.height - PAD, r.width, PAD * 2)));
a.subtract(new Area(new Ellipse2D.Float(r.x - PAD, r.y, PAD * 2, r.height)));
a.subtract(new Area(new Ellipse2D.Float(r.x + r.width - PAD, r.y, PAD * 2, r.height)));
return a;
}
case ChatLogic.THINK: {
// cloudy balloon!
int x = r.x + PAD;
int y = r.y + PAD;
int wid = r.width - PAD * 2;
int hei = r.height - PAD * 2;
Area a = new Area(new Rectangle(x, y, wid, hei));
// small circles on the left and right
int dia = 12;
int numvert = (int) Math.ceil(hei / ((float) dia));
int leftside = x - dia/2;
int rightside = x + wid - (dia/2) - 1;
int maxh = hei - dia;
for (int ii=0; ii < numvert; ii++) {
int ypos = y + Math.min((hei * ii) / numvert, maxh);
a.add(new Area(new Ellipse2D.Float(leftside, ypos, dia, dia)));
a.add(new Area(new Ellipse2D.Float(rightside, ypos, dia, dia)));
}
// larger ovals on the top and bottom
dia = 16;
int numhorz = (int) Math.ceil(wid / ((float) dia));
int topside = y - dia/3;
int botside = y + hei - (dia/3) - 1;
int maxw = wid - dia;
for (int ii=0; ii < numhorz; ii++) {
int xpos = x + Math.min((wid * ii) / numhorz, maxw);
a.add(new Area(new Ellipse2D.Float(xpos, topside, dia, dia*2/3)));
a.add(new Area(new Ellipse2D.Float(xpos, botside, dia, dia*2/3)));
}
return a;
}
}
// fall back to subtitle shape
return _logic.getSubtitleShape(type, r, r);
}
/**
* Create a tail to the specified rectangular area from the speaker point.
*/
protected Shape getTail (int type, Rectangle r, Point speaker)
{
// emotes don't actually have tails
if (ChatLogic.modeOf(type) == ChatLogic.EMOTE) {
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 - SPEAKER_DISTANCE) / dist;
if (ChatLogic.modeOf(type) == ChatLogic.THINK) {
int steps = Math.max((int) (dist / SPEAKER_DISTANCE), 2);
float step = perc / steps;
Area a = new Area();
for (int ii=0; ii < steps; ii++, perc -= step) {
int radius = Math.min(SPEAKER_DISTANCE / 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 + PAD;
} else {
x = r.x + r.width - PAD;
}
p.addPoint(x, midy - (TAIL_WIDTH / 2));
p.addPoint(x, midy + (TAIL_WIDTH / 2));
} else {
int y;
if (midy > speaker.y) {
y = r.y + PAD;
} else {
y = r.y + r.height - PAD;
}
p.addPoint(midx - (TAIL_WIDTH / 2), y);
p.addPoint(midx + (TAIL_WIDTH / 2), y);
}
return p;
}
/**
* Expire a bubble, if necessary, and return the old bubbles for the specified speaker.
*/
protected List getAndExpireBubbles (Name speaker)
{
int num = _bubbles.size();
// first, get all the old bubbles belonging to the user
List oldbubs = Lists.newArrayList();
if (speaker != null) {
for (int ii=0; ii < num; ii++) {
BubbleGlyph bub = _bubbles.get(ii);
if (bub.isSpeaker(speaker)) {
oldbubs.add(bub);
}
}
}
// see if we need to expire this user's oldest bubble
if (oldbubs.size() >= MAX_BUBBLES_PER_USER) {
BubbleGlyph bub = oldbubs.remove(0);
_bubbles.remove(bub);
_target.abortAnimation(bub);
// or some other old bubble
} else if (num >= MAX_BUBBLES) {
_target.abortAnimation(_bubbles.remove(0));
}
// return the speaker's old bubbles
return oldbubs;
}
@Override
protected void glyphExpired (ChatGlyph glyph)
{
super.glyphExpired(glyph);
_bubbles.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 gfx, Font font, String text)
{
Label label = _logic.createLabel(text);
label.setFont(font);
// layout in one line
Rectangle vbounds = _target.getViewBounds();
label.setTargetWidth(vbounds.width - PAD * 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 > MINIMUM_SPLIT_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 = ((double) d.width + (PAD * 2)) /
((double) d.height + (PAD * 2));
// now try increasing the # of lines and seeing if we get closer to the golden ratio
for (int lines=2; true; lines++) {
double ratio = ((double) (d.width / lines) + (PAD * 2)) /
((double) (d.height * lines) + (PAD * 2));
if (Math.abs(ratio - GOLDEN) < Math.abs(lastratio - GOLDEN)) {
// 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 getAvoidList (Name speaker)
{
List avoid = Lists.newArrayList();
if (_provider == null) {
return avoid;
}
// for now we don't accept low-priority avoids
_provider.getAvoidables(speaker, avoid, null);
// add the existing chatbub non-tail areas from other speakers
for (BubbleGlyph bub : _bubbles) {
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.
*
* @param sansTail the chat bubble shape without the tail.
*/
public BubbleGlyph (
SubtitleChatOverlay owner, int type, long lifetime, Shape shape, Label label,
Point labelpos, Shape sansTail, Name speaker, Color outline) {
super(owner, type, lifetime, shape.getBounds(), shape, null, null,
label, labelpos, outline);
_sansTail = sansTail;
_speaker = speaker;
}
public void setAgeLevel (int agelevel) {
_agelevel = agelevel;
invalidate();
}
@Override
public void viewDidScroll (int dx, int dy) {
// only system info and attention messages remain fixed, all others scroll
if ((_type == ChatLogic.INFO) || (_type == ChatLogic.ATTENTION)) {
translate(dx, dy);
}
}
@Override
protected Color getBackground () {
if (_background == Color.WHITE) {
return BACKGROUNDS[_agelevel];
} else {
return _background;
}
}
/**
* 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(BUBBLE_SPACING, BUBBLE_SPACING);
return bounds;
}
/**
* Get the bounds of this bubble, sans tail space.
*/
public Rectangle getBubbleBounds () {
return _sansTail.getBounds();
}
/**
* Is the specified player the speaker of this bubble?
*/
public boolean isSpeaker (Name player) {
return (_speaker != null) && _speaker.equals(player);
}
/**
* Remove the tail on this bubble, if any.
*/
public void removeTail () {
invalidate();
_shape = _sansTail;
_bounds = _shape.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 _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[MAX_BUBBLES];
static {
Color yellowy = new Color(0xdd, 0xdd, 0x6a);
Color blackish = new Color(0xcccccc);
float steps = (MAX_BUBBLES - 1) / 2;
for (int ii=0; ii < MAX_BUBBLES / 2; ii++) {
BACKGROUNDS[ii] = ColorUtil.blend(Color.white, yellowy, (steps - ii) / steps);
}
for (int ii= MAX_BUBBLES / 2; ii < MAX_BUBBLES; ii++) {
BACKGROUNDS[ii] = ColorUtil.blend(blackish, yellowy, (ii - steps) / steps);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy