
com.threerings.chat.ComicChatOverlay Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nenya Show documentation
Show all versions of nenya Show documentation
Facilities for making networked multiplayer games.
The newest version!
//
// 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