Skip to content

Instantly share code, notes, and snippets.

@jpsarda
Created September 28, 2013 23:59
Show Gist options
  • Save jpsarda/6747929 to your computer and use it in GitHub Desktop.
Save jpsarda/6747929 to your computer and use it in GitHub Desktop.
FSpeechBubble for Futile. Uses latest version of GoKit (which renamed almost all classes this way : Tween > GoTween) Demo http://bonuslevel.org/experiments/futiletests/futiletests.html You need to include the following classes too : Drawing polygons https://gist.github.com/wtrebella/5444072 Drawing lines https://gist.github.com/jpsarda/4573831 D…
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
using System.Linq;
using System.Text;
using System.Reflection;
public class FSpeechBubble : FContainer
{
protected float _width,_height,_cornerRadius=10f;
protected int _cornerSegmentsCount;
protected float _pointerBaseSize=10f;
protected float _minSize,_bigCornerRadius;
protected Vector2 _point;
protected float _pointerMargin=10f;
//draw
//protected Color _borderColor=Color.white; //TODO
protected WTPolygonSprite _polygonSprite;
protected FDrawingSprite _borderSprite=null;
public FSpeechBubble ()
{
UpdateIntermediateValues();
_polygonSprite=new WTPolygonSprite(null);
AddChild(_polygonSprite);
_borderSprite=new FDrawingSprite("Futile_White");
AddChild(_borderSprite);
}
public void SetSizeAndPointer(float width,float height,Vector2 point,float pointerMargin) {
_width=width;
_height=height;
if (_width<_minSize) _width=_minSize;
if (_height<_minSize) _height=_minSize;
_point=point;
_pointerMargin=pointerMargin;
Update();
}
public Color backgroundColor {
get { return _polygonSprite.color;}
set {
if (value!=_polygonSprite.color) {
_polygonSprite.color=value;
}
}
}
public float width { get { return _width; } }
public float height { get { return _height; } }
protected void Update() {
float halfWidth=_width*0.5f;
float halfHeight=_height*0.5f;
//Is point outside of the rectangle?
bool outside=true;
if ((_point.x-_pointerMargin<=halfWidth)&&(_point.x+_pointerMargin>=-halfWidth)) {
if ((_point.y-_pointerMargin<=halfHeight)&&(_point.y+_pointerMargin>=-halfHeight)) {
Debug.LogWarning("FSpeechBubble, the point is inisde the bubble.");
outside=false;
//return;
}
}
//All clockwise
Vector2[] segmentTop = new Vector2[] {new Vector2(-halfWidth+_cornerRadius, halfHeight),new Vector2(halfWidth-_cornerRadius, halfHeight)};
Vector2[] segmentBottom = new Vector2[] {new Vector2(halfWidth-_cornerRadius, -halfHeight),new Vector2(-halfWidth+_cornerRadius, -halfHeight)};
Vector2[] segmentLeft = new Vector2[] {new Vector2(-halfWidth, -halfHeight+_cornerRadius),new Vector2(-halfWidth, halfHeight-_cornerRadius)};
Vector2[] segmentRight = new Vector2[] {new Vector2(halfWidth, halfHeight-_cornerRadius),new Vector2(halfWidth, -halfHeight+_cornerRadius)};
Vector2[] cornerTopLeft=new Vector2[_cornerSegmentsCount+1],cornerTopRight=new Vector2[_cornerSegmentsCount+1],cornerBottomRight=new Vector2[_cornerSegmentsCount+1],cornerBottomLeft=new Vector2[_cornerSegmentsCount+1];
for (int i=0;i<=_cornerSegmentsCount;i++) {
float angle=(float)Math.PI*0.5f*(float)i/(float)_cornerSegmentsCount;
cornerTopLeft[i]=new Vector2(-_cornerRadius*(float)Math.Cos(angle),_cornerRadius*(float)Math.Sin(angle));
cornerBottomRight[i]=cornerTopLeft[i]*(-1);
}
for (int i=0;i<=_cornerSegmentsCount;i++) {
cornerTopRight[i]=new Vector2(-cornerTopLeft[_cornerSegmentsCount-i].x,cornerTopLeft[_cornerSegmentsCount-i].y);
cornerBottomLeft[i]=cornerTopRight[i]*(-1);
}
Vector2 centerCornerTopLeft=new Vector2(-halfWidth+_cornerRadius,halfHeight-_cornerRadius);
Vector2 centerCornerTopRight=new Vector2(halfWidth-_cornerRadius,halfHeight-_cornerRadius);
Vector2 centerCornerBottomRight=new Vector2(halfWidth-_cornerRadius,-halfHeight+_cornerRadius);
Vector2 centerCornerBottomLeft=new Vector2(-halfWidth+_cornerRadius,-halfHeight+_cornerRadius);
for (int i=0;i<=_cornerSegmentsCount;i++) {
cornerTopLeft[i]+=centerCornerTopLeft;
cornerTopRight[i]+=centerCornerTopRight;
cornerBottomRight[i]+=centerCornerBottomRight;
cornerBottomLeft[i]+=centerCornerBottomLeft;
}
if (outside) {
//Which part of the rectangle is the point? (8 zones)
if ((_point.x>=-halfWidth+_bigCornerRadius) && (_point.x<=halfWidth-_bigCornerRadius)) {
//top and bottom zones
if (_point.y<0) {
//bottom
segmentBottom=new Vector2[] {segmentBottom[0],new Vector2(_point.x+_pointerBaseSize*0.5f,segmentBottom[0].y),_point+new Vector2(0,_pointerMargin),new Vector2(_point.x-_pointerBaseSize*0.5f,segmentBottom[0].y),segmentBottom[1]};
} else {
//top
segmentTop=new Vector2[] {segmentTop[0],new Vector2(_point.x-_pointerBaseSize*0.5f,segmentTop[0].y),_point-new Vector2(0,_pointerMargin),new Vector2(_point.x+_pointerBaseSize*0.5f,segmentTop[0].y),segmentTop[1]};
}
} else if ((_point.y>=-halfHeight+_bigCornerRadius) && (_point.y<=halfHeight-_bigCornerRadius)) {
//left and right zones
if (_point.x<0) {
//left
segmentLeft=new Vector2[] {segmentLeft[0],new Vector2(segmentLeft[0].x,_point.y-_pointerBaseSize*0.5f),_point+new Vector2(_pointerMargin,0),new Vector2(segmentLeft[0].x,_point.y+_pointerBaseSize*0.5f),segmentLeft[1]};
} else {
//right
segmentRight=new Vector2[] {segmentRight[0],new Vector2(segmentRight[0].x,_point.y+_pointerBaseSize*0.5f),_point-new Vector2(_pointerMargin,0),new Vector2(segmentRight[0].x,_point.y-_pointerBaseSize*0.5f),segmentRight[1]};
}
} else {
//corners
Vector2 aim;
if (_point.x<0) {
if (_point.y<0) {
//bottom-left corner
aim=new Vector2(-halfWidth+_bigCornerRadius,-halfHeight+_bigCornerRadius);
InsertPointer(aim,ref segmentBottom,ref cornerBottomLeft,ref segmentLeft);
} else {
//top-left corner
aim=new Vector2(-halfWidth+_bigCornerRadius,halfHeight-_bigCornerRadius);
InsertPointer(aim,ref segmentLeft,ref cornerTopLeft,ref segmentTop);
}
} else {
if (_point.y<0) {
//bottom-right corner
aim=new Vector2(halfWidth-_bigCornerRadius,-halfHeight+_bigCornerRadius);
InsertPointer(aim,ref segmentRight,ref cornerBottomRight,ref segmentBottom);
} else {
//top-right corner
aim=new Vector2(halfWidth-_bigCornerRadius,halfHeight-_bigCornerRadius);
InsertPointer(aim,ref segmentTop,ref cornerTopRight,ref segmentRight);
}
}
//find intersection point-aim with the bubble
}
}
//Link all paths together
Vector2[][] paths=new Vector2[][] { segmentTop,cornerTopRight,segmentRight,cornerBottomRight,segmentBottom,cornerBottomLeft,segmentLeft,cornerTopLeft };
int verticesCount=0;
foreach (Vector2[] path in paths) {
verticesCount+=path.Length-1;
}
Vector2[] vertices = new Vector2[verticesCount];
int j=0;
//int l=0;
foreach (Vector2[] path in paths) {
for (int k=1;k<path.Length;k++) { //skip first point that is redundant with last point of previous path (could also skip last point, same result)
vertices[j]=path[k]; j++;
}
/*
Debug.Log("Path >>> "+l); l++;
for (int i = 0; i < path.Length; i++) {
Vector2 v = path[i];
Debug.Log("Vertex " + i + ": " + v);
}
*/
}
_polygonSprite.UpdateWithData(new WTPolygonData(vertices));
//log
/*
Debug.Log("Total path >>> "+l);
Vector2[] originalVertices = _polygonSprite.polygonData.polygonVertices;
for (int i = 0; i < originalVertices.Length; i++) {
Vector2 v = originalVertices[i];
Debug.Log("Vertex " + i + ": " + v);
}
*/
_borderSprite.Clear();
_borderSprite.SetLineJointStyle(FTDrawingJointStyle.BEVEL);
_borderSprite.SetLineThickness(1.5f);
_borderSprite.SetLineColor(new Color(0,0,0,1.0f));
_borderSprite.PushTopBorder(1.5f,new Color(1.0f,1.0f,1.0f,0.0f),true);
_borderSprite.PushBottomBorder(1.5f,new Color(0.0f,0.0f,0.0f,0.0f),true);
Vector2[] originalVertices = _polygonSprite.polygonData.polygonVertices;
_borderSprite.MoveTo(originalVertices[0].x,originalVertices[0].y);
for (int i = 1; i < originalVertices.Length; i++) {
Vector2 v = originalVertices[i];
_borderSprite.LineTo(v.x,v.y);
}
_borderSprite.Loop();
}
protected bool FindClosestIntersection(Vector2 aim, Vector2[] path, out Vector2 intersection, out int lowerIdx) {
Vector2 diff=aim-_point;
intersection=Vector2.zero;
lowerIdx=-1;
float minDist=-1f;
for (int i=1;i<path.Length;i++) {
Vector2 seg0=path[i-1];
Vector2 seg1=path[i];
float S, T;
if ( VectorUtils.LinesIntersect(seg0, seg1, _point, aim, out S, out T ) ) {
if (S >= 0.0f && S <= 1.0f && T >= 0.0f /*&& T <= 1.0f*/) { //don't check T<=1 because the intersection go on after aim
Vector2 foundIntersection=_point+diff*T;
float dist=(foundIntersection-_point).magnitude;
if ((lowerIdx<0)||(dist<minDist)) {
minDist=dist;
lowerIdx=i-1;
intersection=foundIntersection;
}
}
}
}
return (lowerIdx>=0);
}
protected bool FindClosestIntersection(Vector2 aim, Vector2[][] paths, out Vector2 intersection, out int pathIdx, out int segmentLowerIdx) {
//Vector2 diff=aim-_point;
intersection=Vector2.zero;
pathIdx=-1;
segmentLowerIdx=-1;
float minDist=-1f;
for (int i=0;i<paths.Length;i++) {
Vector2[] path = paths[i];
Vector2 foundIntersection;
int foundSegmentLowerIdx;
bool ret=FindClosestIntersection(aim,path,out foundIntersection,out foundSegmentLowerIdx);
if (ret) {
float dist=(foundIntersection-_point).magnitude;
if ((pathIdx<0)||(dist<minDist)) {
minDist=dist;
pathIdx=i;
segmentLowerIdx=foundSegmentLowerIdx;
intersection=foundIntersection;
}
}
}
return (pathIdx>=0);
}
protected bool InsertPointer(Vector2 aim, ref Vector2[] path0, ref Vector2[] path1, ref Vector2[] path2) {
Vector2 diff=aim-_point;
Vector2[][] paths=new Vector2[][] {path0,path1,path2};
Vector2 foundIntersection;
int foundPathIdx;
int foundSegmentLowerIdx;
bool ret=FindClosestIntersection(aim,paths,out foundIntersection,out foundPathIdx, out foundSegmentLowerIdx);
if (ret) {
Vector2 dir=diff.normalized;
Vector2 orth=new Vector2(-dir.y,dir.x);
Vector2 point0=foundIntersection-orth*_pointerBaseSize*0.5f;
Vector2 point1=foundIntersection+orth*_pointerBaseSize*0.5f;
//Take into acount the pointerMargin
Vector2 correctedPoint=_point+dir*_pointerMargin;
//find intersections with point0 and point1
Vector2 foundIntersection0;
int foundPathIdx0;
int foundSegmentLowerIdx0;
bool ret0=FindClosestIntersection(point0,paths,out foundIntersection0,out foundPathIdx0, out foundSegmentLowerIdx0);
if (ret0) {
Vector2 foundIntersection1;
int foundPathIdx1;
int foundSegmentLowerIdx1;
bool ret1=FindClosestIntersection(point1,paths,out foundIntersection1,out foundPathIdx1, out foundSegmentLowerIdx1);
if (ret1) {
if (foundPathIdx0<foundPathIdx1) {
int i;
Vector2[] path;
path=paths[foundPathIdx0];
int pathPointsCount0=foundSegmentLowerIdx0+3;
Vector2[] newPath0=new Vector2[pathPointsCount0];
i=0;
for (;i<pathPointsCount0-2;i++) {
newPath0[i]=path[i];
}
newPath0[i]=foundIntersection0; i++;
newPath0[i]=correctedPoint; i++;
path=paths[foundPathIdx1];
int pathPointsCount1=path.Length-foundSegmentLowerIdx1+1;
Vector2[] newPath1=new Vector2[pathPointsCount1];
i=0;
newPath1[i]=correctedPoint; i++;
newPath1[i]=foundIntersection1; i++;
for (int j=foundSegmentLowerIdx1+1;j<path.Length;j++) {
newPath1[i]=path[j]; i++;
}
if (foundPathIdx0==0) {
path0=newPath0;
path1=newPath1;
} else { //foundPathIdx0==1
path1=newPath0;
path2=newPath1;
}
return true;
} else if (foundPathIdx0==foundPathIdx1) {
if (foundSegmentLowerIdx0<=foundSegmentLowerIdx1) {
int i;
Vector2[] path=paths[foundPathIdx0];
int pathPointsCount=path.Length-(foundSegmentLowerIdx1-foundSegmentLowerIdx0)+3;
Vector2[] newPath=new Vector2[pathPointsCount];
i=0;
for (int j=0;j<=foundSegmentLowerIdx0;j++) {
newPath[i]=path[j]; i++;
}
newPath[i]=foundIntersection0; i++;
newPath[i]=correctedPoint; i++;
newPath[i]=foundIntersection1; i++;
for (int j=foundSegmentLowerIdx1+1;j<path.Length;j++) {
newPath[i]=path[j]; i++;
}
if (foundPathIdx0==0) {
path0=newPath;
} else if (foundPathIdx0==1) {
path1=newPath;
} else { //foundPathIdx0==2
path2=newPath;
}
}
//else if (foundSegmentLowerIdx0==foundSegmentLowerIdx1) {
//}
}
}
}
}
return false;
}
//To call when _cornerRadius, or _pointerBaseSize changed
protected void UpdateIntermediateValues() {
_minSize=2*_cornerRadius+_pointerBaseSize;
_bigCornerRadius=_cornerRadius+_pointerBaseSize*0.5f;
_cornerSegmentsCount=(int)(_cornerRadius/2); if (_cornerSegmentsCount<4) _cornerSegmentsCount=4;
}
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
using System.Linq;
using System.Text;
using System.Reflection;
/*
Usage :
FPseudoHtmlText text=new FPseudoHtmlText(Config.fontFile,"<style text-scale='1.25'>FSpeechBubble</style><br/><style text-scale='0.5'>Combined with a FPseudoHtmlText. How cool is that? <style text-color='#FF99FF'><br/><fsprite width='50' src='diamant'/>. With FSprites and FButtons inside, that's pretty cool! <fbutton src='yes' label='FButtons' scale='0.5' label-scale='0.5' color-down='#FF0000' action='MyMethodNameWithData' data='mybutttonid'/></style>",Config.textParams,200f,PseudoHtmlTextAlign.left,1f,this);
FSpeechBubbleManager.TransitionPop(FSpeechBubbleManager.Instance.Show(text,touch,10f));
*/
public class FSpeechBubbleManager
{
static readonly FSpeechBubbleManager instance=new FSpeechBubbleManager();
static FSpeechBubbleManager () {}
FSpeechBubbleManager() {}
public static FSpeechBubbleManager Instance { get { instance.Initialize(); return instance; } }
protected bool _initialized=false;
protected void Initialize() {
if (!_initialized) {
_initialized=true;
HandleResize(false);
Futile.screen.SignalResize += HandleResize;
}
}
protected void HandleResize(bool wasOrientationChange)
{
_defaultContainer=Futile.stage;
float screenMargin=5f;
_defaultVisibleArea=new Rect(-Futile.screen.halfWidth+screenMargin,-Futile.screen.halfHeight+screenMargin,Futile.screen.width-screenMargin*2,Futile.screen.height-screenMargin*2);
}
protected Rect _defaultVisibleArea;
protected FContainer _defaultContainer=null;
protected float _defaultContentMarginX=5f,_defaultContentMarginY=5f;
protected float _defaultPointerLength=10f;
protected Color _defaultBackgroundColor=Color.white;
public Rect defaultVisibleArea { get { return _defaultVisibleArea; } }
public FContainer defaultContainer { get { return _defaultContainer; } }
public float defaultContentMarginX { get { return _defaultContentMarginX; } }
public float defaultContentMarginY { get { return _defaultContentMarginY; } }
public float defaultPointerLength { get { return _defaultPointerLength; } }
public Color defaultBackgroundColor { get { return _defaultBackgroundColor; } }
public FSpeechBubble Show(FPseudoHtmlText text,Vector2 point,float pointerMargin) {
return Show(text,text.width,text.height,point,pointerMargin);
}
public FSpeechBubble Show(FNode node, float width, float height, Vector2 point, float pointerMargin) {
return Show(node,width,height,_defaultContentMarginX,_defaultContentMarginY,point,_defaultPointerLength,pointerMargin,_defaultBackgroundColor,_defaultContainer,_defaultVisibleArea);
}
public FSpeechBubble Show(FNode node, float width, float height, float contentMarginX, float contentMarginY, Vector2 point, float pointerLength, float pointerMargin, Color backgroundColor, FContainer container, Rect visibleArea) {
FSpeechBubble bubble=new FSpeechBubble();
bubble.backgroundColor=backgroundColor;
if (node!=null) {
if (node.container!=null) node.RemoveFromContainer();
bubble.AddChild(node);
}
container.AddChild(bubble);
//size fo the bubble with content margin taken into account
float totalWidth=width+contentMarginX*2;
float totalHeight=height+contentMarginY*2;
float totalPointerLength=pointerLength+pointerMargin;
float verticalFreeWidth=0,verticalFreeHeight=0;
float horizontalFreeWidth=0,horizontalFreeHeight=0;
//Try first the space on top/bottom or left/right
//Determine which space is best suited
if (point.x<visibleArea.center.x) {
//more space on the right
verticalFreeWidth=visibleArea.xMax-point.x-totalPointerLength;
verticalFreeHeight=visibleArea.height;
} else {
//more space on the left
verticalFreeWidth=point.x-visibleArea.xMin-totalPointerLength;
verticalFreeHeight=visibleArea.height;
}
if (point.y<visibleArea.center.y) {
//more space on the top
horizontalFreeWidth=visibleArea.width;
horizontalFreeHeight=visibleArea.yMax-point.y-totalPointerLength;
} else {
//more space on the bottom
horizontalFreeWidth=visibleArea.width;
horizontalFreeHeight=point.y-visibleArea.yMin-totalPointerLength;
}
float verticalRemainingSurface=-1,horizontalRemainingSurface=-1;
float verticalHiddenSurface=0,horizontalHiddenSurface=0;
bool verticalFit=false,horizontalFit=false;
if ((totalWidth<=verticalFreeWidth)&&(totalHeight<=verticalFreeHeight)) {
verticalFit=true;
} else {
if (totalWidth>verticalFreeWidth) {
verticalHiddenSurface+=(totalWidth-verticalFreeWidth)*totalHeight;
}
if (totalHeight>verticalFreeHeight) {
verticalHiddenSurface+=(totalHeight-verticalFreeHeight)*totalWidth;
}
}
verticalRemainingSurface=verticalFreeWidth*verticalFreeHeight-totalWidth*totalHeight;
if ((totalWidth<=horizontalFreeWidth)&&(totalHeight<=horizontalFreeHeight)) {
horizontalFit=true;
} else {
if (totalWidth>horizontalFreeWidth) {
horizontalHiddenSurface+=(totalWidth-horizontalFreeWidth)*totalHeight;
}
if (totalHeight>horizontalFreeHeight) {
horizontalHiddenSurface+=(totalHeight-horizontalFreeHeight)*totalWidth;
}
}
horizontalRemainingSurface=horizontalFreeWidth*horizontalFreeHeight-totalWidth*totalHeight;
bool chooseHorizontal;
if (verticalFit && horizontalFit) {
if (verticalRemainingSurface>horizontalRemainingSurface) {
//choose vertical
chooseHorizontal=false;
} else {
//choose horizontal
chooseHorizontal=true;
}
} else if (horizontalFit) {
//choose horizontal
chooseHorizontal=true;
} else if (verticalFit) {
//choose vertical
chooseHorizontal=false;
} else {
if (verticalHiddenSurface<horizontalHiddenSurface) {
//choose vertical
chooseHorizontal=false;
} else {
//choose horizontal
chooseHorizontal=true;
}
}
if (chooseHorizontal) {
//horizontal
if (point.y<visibleArea.center.y) {
//more space on the top
bubble.y=point.y+totalPointerLength+totalHeight*0.5f;
} else {
//more space on the bottom
bubble.y=point.y-totalPointerLength-totalHeight*0.5f;
}
bubble.x=point.x;
if (bubble.x+totalWidth*0.5f>visibleArea.xMax) {
bubble.x=visibleArea.xMax-totalWidth*0.5f;
} else if (bubble.x-totalWidth*0.5f<visibleArea.xMin) {
bubble.x=visibleArea.xMin+totalWidth*0.5f;
}
} else {
//vertical
if (point.x<visibleArea.center.x) {
//more space on the right
bubble.x=point.x+totalPointerLength+totalWidth*0.5f;
} else {
//more space on the left
bubble.x=point.x-totalPointerLength-totalWidth*0.5f;
}
bubble.y=point.y;
if (bubble.y+totalHeight*0.5f>visibleArea.yMax) {
bubble.y=visibleArea.yMax-totalHeight*0.5f;
} else if (bubble.y-totalHeight*0.5f<visibleArea.yMin) {
bubble.y=visibleArea.yMin+totalHeight*0.5f;
}
}
bubble.SetSizeAndPointer(totalWidth,totalHeight,point-bubble.GetPosition(),pointerMargin);
return bubble;
}
public void RemoveFromContainer(AbstractGoTween tween) {
FNode node = ((tween as GoTween).target) as FNode;
if (node.container!=null) {
node.RemoveFromContainer();
}
}
static public void TransitionPop(FNode node) {
node.scaleX=0;
node.scaleY=0.1f;
GoTweenConfig config0=new GoTweenConfig().floatProp("scaleX",1f);
config0.easeType=GoEaseType.ElasticOut;
Go.to(node,0.2f,config0);
GoTweenConfig config1=new GoTweenConfig().floatProp("scaleY",1f);
config1.easeType=GoEaseType.ElasticOut;
//config1.delay=0.15f;
Go.to(node,0.4f,config1);
}
static public void TransitionFadeOut(FNode node,float delay) {
GoTweenConfig config=new GoTweenConfig().floatProp("alpha",0f).onComplete(FSpeechBubbleManager.Instance.RemoveFromContainer);
config.easeType=GoEaseType.ExpoOut;
config.delay=delay;
Go.to (node,0.5f,config);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment