Browse Source

TMPro animations (#788)

Add TextMeshPro animation support via Link text
- TMProLinkAnimator, auto adds to components
- TMProLinkAnimLookup, stores all TMPro animation functions by link text hash
- TMProLinkAnimEffects, default set of configurable TMPro animation functions built around color and Matrix transformations
- Add TMPro Link Effect Demo scene
- Adds defaults for shake, wiggle, wave, pivot, rainbow, ascend, pulse
master
Steve Halliwell 5 years ago committed by GitHub
parent
commit
68a83d10a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 92
      Assets/Fungus/Scripts/Components/Writer.cs
  2. 436
      Assets/Fungus/Scripts/Utils/TMProLinkAnimEffects.cs
  3. 11
      Assets/Fungus/Scripts/Utils/TMProLinkAnimEffects.cs.meta
  4. 104
      Assets/Fungus/Scripts/Utils/TMProLinkAnimLookup.cs
  5. 11
      Assets/Fungus/Scripts/Utils/TMProLinkAnimLookup.cs.meta
  6. 206
      Assets/Fungus/Scripts/Utils/TMProLinkAnimator.cs
  7. 11
      Assets/Fungus/Scripts/Utils/TMProLinkAnimator.cs.meta
  8. 52
      Assets/Fungus/Scripts/Utils/TextAdapter.cs
  9. 9
      Assets/Fungus/Scripts/Utils/TextTagParser.cs
  10. 4
      Assets/Fungus/Scripts/Utils/TextTagToken.cs
  11. 1712
      Assets/FungusExamples/TextMeshPro/TMPro Link Default Effects Demo.unity
  12. 7
      Assets/FungusExamples/TextMeshPro/TMPro Link Default Effects Demo.unity.meta
  13. 1872
      Assets/FungusExamples/TextMeshPro/TMPro Link Effects Custom.unity
  14. 7
      Assets/FungusExamples/TextMeshPro/TMPro Link Effects Custom.unity.meta
  15. 72
      Assets/FungusExamples/TextMeshPro/TMProLinkStylingDemo.cs
  16. 11
      Assets/FungusExamples/TextMeshPro/TMProLinkStylingDemo.cs.meta
  17. 2064
      Assets/Tests/WritingSpeedTest.unity

92
Assets/Fungus/Scripts/Components/Writer.cs

@ -57,6 +57,8 @@ namespace Fungus
[Tooltip("Click while text is writing to finish writing immediately")] [Tooltip("Click while text is writing to finish writing immediately")]
[SerializeField] protected bool instantComplete = true; [SerializeField] protected bool instantComplete = true;
[SerializeField] protected bool doReadAheadText = true;
// This property is true when the writer is waiting for user input to continue // This property is true when the writer is waiting for user input to continue
protected bool isWaitingForInput; protected bool isWaitingForInput;
@ -71,6 +73,8 @@ namespace Fungus
protected bool italicActive = false; protected bool italicActive = false;
protected bool colorActive = false; protected bool colorActive = false;
protected string colorText = ""; protected string colorText = "";
protected bool linkActive = false;
protected string linkText = string.Empty;
protected bool sizeActive = false; protected bool sizeActive = false;
protected float sizeValue = 16f; protected float sizeValue = 16f;
protected bool inputFlag; protected bool inputFlag;
@ -89,6 +93,7 @@ namespace Fungus
protected string hiddenColorClose = ""; protected string hiddenColorClose = "";
protected int visibleCharacterCount = 0; protected int visibleCharacterCount = 0;
protected int readAheadStartIndex = 0;
public WriterAudio AttachedWriterAudio { get; set; } public WriterAudio AttachedWriterAudio { get; set; }
protected virtual void Awake() protected virtual void Awake()
@ -150,6 +155,12 @@ namespace Fungus
openString.Append(colorText); openString.Append(colorText);
openString.Append(">"); openString.Append(">");
} }
if (linkActive)
{
openString.Append("<link=");
openString.Append(linkText);
openString.Append(">");
}
if (boldActive) if (boldActive)
{ {
openString.Append("<b>"); openString.Append("<b>");
@ -179,6 +190,10 @@ namespace Fungus
{ {
closeString.Append("</color>"); closeString.Append("</color>");
} }
if (linkActive)
{
closeString.Append("</link>");
}
if (sizeActive) if (sizeActive)
{ {
closeString.Append("</size>"); closeString.Append("</size>");
@ -244,6 +259,8 @@ namespace Fungus
// Update the read ahead string buffer. This contains the text for any // Update the read ahead string buffer. This contains the text for any
// Word tags which are further ahead in the list. // Word tags which are further ahead in the list.
if (doReadAheadText)
{
readAheadString.Length = 0; readAheadString.Length = 0;
for (int j = i + 1; j < tokens.Count; ++j) for (int j = i + 1; j < tokens.Count; ++j)
{ {
@ -259,6 +276,7 @@ namespace Fungus
break; break;
} }
} }
}
switch (token.type) switch (token.type)
{ {
@ -294,6 +312,18 @@ namespace Fungus
colorActive = false; colorActive = false;
break; break;
case TokenType.LinkStart:
if (CheckParamCount(token.paramList, 1))
{
linkActive = true;
linkText = token.paramList[0];
}
break;
case TokenType.LinkEnd:
linkActive = false;
break;
case TokenType.SizeStart: case TokenType.SizeStart:
if (TryGetSingleParam(token.paramList, 0, 16f, out sizeValue)) if (TryGetSingleParam(token.paramList, 0, 16f, out sizeValue))
{ {
@ -495,10 +525,51 @@ namespace Fungus
UpdateCloseMarkup(); UpdateCloseMarkup();
float timeAccumulator = Time.deltaTime; float timeAccumulator = Time.deltaTime;
float invWritingSpeed = 1f / currentWritingSpeed;
//refactor this, its mostly the same 30 lines of code
if (textAdapter.SupportsHiddenCharacters())
{
//pausing for 1 frame means we can get better first data, but is conflicting with animation ?
// or is it something else inserting the color alpha invis
yield return null;
//this works for first thing being shown but then no subsequent, as the char counts have not been update
// by tmpro after the set to ""
var startingReveal = Mathf.Min(readAheadStartIndex, textAdapter.CharactersToReveal);
PartitionString(writeWholeWords, param, param.Length + 1);
ConcatenateString(startText);
textAdapter.Text = outputString.ToString();
NotifyGlyph();
textAdapter.RevealedCharacters = startingReveal;
yield return null;
while (textAdapter.RevealedCharacters < Mathf.Min(readAheadStartIndex, textAdapter.CharactersToReveal))
{
// No delay if user has clicked and Instant Complete is enabled
if (instantComplete && inputFlag)
{
textAdapter.RevealedCharacters = textAdapter.CharactersToReveal;
}
if (currentWritingSpeed > 0f)
{
textAdapter.RevealedCharacters++;
timeAccumulator -= invWritingSpeed;
if (timeAccumulator <= 0f)
{
var waitTime = Mathf.Max(invWritingSpeed, Time.deltaTime);
yield return new WaitForSeconds(waitTime);
timeAccumulator += waitTime;
}
}
}
}
else
{
for (int i = 0; i < param.Length + 1; ++i) for (int i = 0; i < param.Length + 1; ++i)
{ {
// Exit immediately if the exit flag has been set
if (exitFlag) if (exitFlag)
{ {
break; break;
@ -510,7 +581,6 @@ namespace Fungus
yield return null; yield return null;
} }
//actually grab the next chars
PartitionString(writeWholeWords, param, i); PartitionString(writeWholeWords, param, i);
ConcatenateString(startText); ConcatenateString(startText);
textAdapter.Text = outputString.ToString(); textAdapter.Text = outputString.ToString();
@ -528,26 +598,19 @@ namespace Fungus
rightString.Length > 0 && rightString.Length > 0 &&
IsPunctuation(leftString.ToString(leftString.Length - 1, 1)[0])) IsPunctuation(leftString.ToString(leftString.Length - 1, 1)[0]))
{ {
//timeAccumulator -= currentPunctuationPause; ???
yield return StartCoroutine(DoWait(currentPunctuationPause)); yield return StartCoroutine(DoWait(currentPunctuationPause));
} }
// Delay between characters // Delay between characters
if (currentWritingSpeed > 0f) if (currentWritingSpeed > 0f)
{ {
float invWritingSpeed = 1f / currentWritingSpeed;
timeAccumulator -= invWritingSpeed; timeAccumulator -= invWritingSpeed;
if (timeAccumulator <= 0f) if (timeAccumulator <= 0f)
{ {
if (invWritingSpeed > Time.deltaTime) var waitTime = Mathf.Max(invWritingSpeed, Time.deltaTime);
{ yield return new WaitForSeconds(waitTime);
yield return new WaitForSeconds(invWritingSpeed); timeAccumulator += waitTime;
timeAccumulator += invWritingSpeed;
}
else
{
yield return null;
timeAccumulator += Time.deltaTime;
} }
} }
} }
@ -591,6 +654,7 @@ namespace Fungus
protected virtual void ConcatenateString(string startText) protected virtual void ConcatenateString(string startText)
{ {
outputString.Length = 0; outputString.Length = 0;
readAheadStartIndex = int.MaxValue;
// string tempText = startText + openText + leftText + closeText; // string tempText = startText + openText + leftText + closeText;
outputString.Append(startText); outputString.Append(startText);
@ -612,6 +676,8 @@ namespace Fungus
CacheHiddenColorStrings(); CacheHiddenColorStrings();
} }
readAheadStartIndex = outputString.Length;
outputString.Append(hiddenColorOpen); outputString.Append(hiddenColorOpen);
outputString.Append(rightString); outputString.Append(rightString);
outputString.Append(readAheadString); outputString.Append(readAheadString);

436
Assets/Fungus/Scripts/Utils/TMProLinkAnimEffects.cs

@ -0,0 +1,436 @@
// This code is part of the Fungus library (https://github.com/snozbot/fungus)
// It is released for free under the MIT open source license (https://github.com/snozbot/fungus/blob/master/LICENSE)
using UnityEngine;
#if UNITY_2018_1_OR_NEWER
namespace Fungus
{
/// <summary>
/// Samples and helpers for creating TMProLink animations
/// </summary>
namespace TMProLinkAnimEffects
{
/// <summary>
/// Used by BaseEffect and child classes to configure how locations and pivots are calculated
/// before being passed to internal functions.
/// </summary>
public enum TMPLinkAnimatorMode
{
PerCharacter,
PerWord,
PerSection,
PerLine,
}
/// <summary>
/// Use of this is not required, all that the TMProLinkAnimLookup requires is a matching signature of
/// void delegate(TMProLinkAnimator context, int start, int length). The base class however is used to
/// create all the sample effects as they all operate in a similar underlying fashion, with a custom mode
/// but ultimately on a character by character basis, doing a relative translation and color modifcation.
///
/// Much of this class and its sample child effects are static functions to more easily allow reuse by
/// custom effects you may wish to make.
/// </summary>
public abstract class BaseEffect
{
public TMPLinkAnimatorMode mode;
protected TMProLinkAnimator CurrentContext { get; set; }
protected int CurrentStart { get; set; }
protected int CurrentLength { get; set; }
public virtual void DoEffect(TMProLinkAnimator context, int start, int length)
{
CurrentContext = context;
CurrentStart = start;
CurrentLength = length;
MeshVertUpdateLoop(context, start, length, TransFunc, ColorFunc, mode);
}
public virtual Matrix4x4 TransFunc(int index)
{
return Matrix4x4.identity;
}
public virtual Color32 ColorFunc(int index, Color32 col)
{
return col;
}
#region static helpers
/// <summary>
/// Helper for mesh vertex updating within a found link, adapted from TMPRo examples VertexJitter.
/// </summary>
/// <param name="context"></param>
/// <param name="start"></param>
/// <param name="length"></param>
/// <param name="transformFunc"></param>
/// <param name="colorFunc"></param>
/// <param name="mode"></param>
static public void MeshVertUpdateLoop(TMProLinkAnimator context, int start, int length, System.Func<int, Matrix4x4> transformFunc, System.Func<int, Color32, Color32> colorFunc, TMPLinkAnimatorMode mode)
{
var tmproComponent = context.TMProComponent;
var textInfo = tmproComponent.textInfo;
var end = start + length;
var cachedMeshInfo = context.CachedMeshInfo;
Matrix4x4 matrix = Matrix4x4.identity;
Vector2 middle = Vector2.zero;
Color32 col = Color.white;
int wordIndex = -1;
int lineIndex = -1;
for (int i = start; i < end; i++)
{
//required as TMPro is putting non visible invalid elements in the charinfo array assuming I will follow this rule
// if we don't character 0 ends up getting an effect applied to it even though it shouldn't
if (!textInfo.characterInfo[i].isVisible) continue;
// Get the index of the material used by the current character.
int materialIndex = textInfo.characterInfo[i].materialReferenceIndex;
// Get the index of the first vertex used by this text element.
int vertexIndex = textInfo.characterInfo[i].vertexIndex;
// Get the cached vertices of the mesh used by this text element (character or sprite).
Vector3[] sourceVertices = cachedMeshInfo[materialIndex].vertices;
Color32[] vertexColors = cachedMeshInfo[materialIndex].colors32;
if (i == start && mode == TMPLinkAnimatorMode.PerSection)
{
matrix = transformFunc(start);
middle = CalcMidFromChars(context, start, end);
col = colorFunc(start, vertexColors[vertexIndex]);
}
// Determine the center point of each character at the baseline.
//Vector2 charMidBasline = new Vector2((sourceVertices[vertexIndex + 0].x + sourceVertices[vertexIndex + 2].x) / 2, charInfo.baseLine);
// Determine the center point of each character.
if (mode == TMPLinkAnimatorMode.PerCharacter)
{
middle = (sourceVertices[vertexIndex + 0] + sourceVertices[vertexIndex + 2]) / 2;
matrix = transformFunc(i);
col = colorFunc(i, vertexColors[vertexIndex]);
}
else if (mode == TMPLinkAnimatorMode.PerWord)
{
var newWordIndex = CalcWordFromChar(i, textInfo.wordInfo);
if (newWordIndex != -1 && wordIndex != newWordIndex)
{
wordIndex = newWordIndex;
middle = CalcMidFromChars(context, Mathf.Max(start, textInfo.wordInfo[wordIndex].firstCharacterIndex), Mathf.Min(end, textInfo.wordInfo[wordIndex].lastCharacterIndex));
matrix = transformFunc(i);
col = colorFunc(i, vertexColors[vertexIndex]);
}
}
else if (mode == TMPLinkAnimatorMode.PerLine)
{
var newLineIndex = textInfo.characterInfo[i].lineNumber;
if (newLineIndex != -1 && lineIndex != newLineIndex)
{
lineIndex = newLineIndex;
middle = CalcMidFromChars(context, Mathf.Max(start, textInfo.lineInfo[lineIndex].firstCharacterIndex), Mathf.Min(end, textInfo.lineInfo[lineIndex].lastCharacterIndex));
matrix = transformFunc(i);
col = colorFunc(i, vertexColors[vertexIndex]);
}
}
// Need to translate all 4 vertices of each quad to aligned with middle of character / baseline.
// This is needed so the matrix TRS is applied at the origin for each character.
Vector3 offset = middle;
Vector3[] destinationVertices = textInfo.meshInfo[materialIndex].vertices;
Color32[] destinationVertColors = textInfo.meshInfo[materialIndex].colors32;
destinationVertices[vertexIndex + 0] = sourceVertices[vertexIndex + 0] - offset;
destinationVertices[vertexIndex + 1] = sourceVertices[vertexIndex + 1] - offset;
destinationVertices[vertexIndex + 2] = sourceVertices[vertexIndex + 2] - offset;
destinationVertices[vertexIndex + 3] = sourceVertices[vertexIndex + 3] - offset;
destinationVertices[vertexIndex + 0] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 0]);
destinationVertices[vertexIndex + 1] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 1]);
destinationVertices[vertexIndex + 2] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 2]);
destinationVertices[vertexIndex + 3] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 3]);
destinationVertices[vertexIndex + 0] += offset;
destinationVertices[vertexIndex + 1] += offset;
destinationVertices[vertexIndex + 2] += offset;
destinationVertices[vertexIndex + 3] += offset;
destinationVertColors[vertexIndex + 0] = col;
destinationVertColors[vertexIndex + 1] = col;
destinationVertColors[vertexIndex + 2] = col;
destinationVertColors[vertexIndex + 3] = col;
}
}
/// <summary>
/// Same as calcing a character mid but averaging over all characters in the given character range
/// </summary>
/// <param name="context"></param>
/// <param name="start"></param>
/// <param name="end"></param>
/// <returns></returns>
static public Vector2 CalcMidFromChars(TMProLinkAnimator context, int start, int end)
{
Vector3 middle = Vector3.zero;
var chInfo = context.TMProComponent.textInfo.characterInfo;
for (int i = start; i < end; i++)
{
int materialIndex = chInfo[i].materialReferenceIndex;
int vertexIndex = chInfo[i].vertexIndex;
Vector3[] sourceVertices = context.CachedMeshInfo[materialIndex].vertices;
middle += (sourceVertices[vertexIndex + 0] + sourceVertices[vertexIndex + 2]) / 2;
}
return middle / (end - start);
}
/// <summary>
/// Determine which TMPro World a given character index is within
/// </summary>
/// <param name="charIndex"></param>
/// <param name="wordInfo"></param>
/// <returns></returns>
static public int CalcWordFromChar(int charIndex, TMPro.TMP_WordInfo[] wordInfo)
{
for (int i = 0; i < wordInfo.Length; i++)
{
//enforcing start letter results in punctuation attaching to the word to its left rather than its right.
// which is more desirable for english at least
if (charIndex >= wordInfo[i].firstCharacterIndex && wordInfo[i].lastCharacterIndex >= charIndex)
{
return i;
}
}
return -1;
}
/// <summary>
/// Determine which TMPro Line a given character index is within
/// </summary>
/// <param name="charIndex"></param>
/// <param name="wordInfo"></param>
/// <returns></returns>
static public int CalcLineFromChar(int charIndex, TMPro.TMP_WordInfo[] wordInfo)
{
for (int i = 0; i < wordInfo.Length; i++)
{
//enforcing start letter results in punctuation attaching to the word to its left rather than its right.
// which is more desirable for english at least
if (charIndex >= wordInfo[i].firstCharacterIndex && wordInfo[i].lastCharacterIndex >= charIndex)
{
return i;
}
}
return -1;
}
#endregion static helpers
}
/// <summary>
/// Shake the element, by moving centre slightly and randomly rolling each update.
/// </summary>
public class ShakeEffect : BaseEffect
{
public float rotScale;
public Vector2 offsetScale = Vector2.one;
public override Matrix4x4 TransFunc(int index)
{
return ShakeTransformFunc(index, offsetScale, rotScale);
}
static public Matrix4x4 ShakeTransformFunc(int index, Vector2 positionOffsetScale, float rotDegScale)
{
return Matrix4x4.TRS(new Vector3(Random.Range(-.25f, .25f) * positionOffsetScale.x, Random.Range(-.25f, .25f), 0) * positionOffsetScale.y,
Quaternion.Euler(0, 0, Random.Range(-1f, 1f) * rotDegScale),
Vector3.one);
}
}
/// <summary>
/// Wiggle the position by over time using perlin noise to offset its centre.
/// </summary>
public class WiggleEffect : BaseEffect
{
public float speed = 1;
public Vector2 offsetScale = Vector2.one;
public override Matrix4x4 TransFunc(int index)
{
return WiggleTransformFunc(index, speed, offsetScale);
}
static public Matrix4x4 WiggleTransformFunc(int index, float speed, Vector2 wiggleScale)
{
const int SAFE_PRIME_A = 11;
const int SAFE_PRIME_B = 59;
//add a pingpong
var jitterOffset = new Vector3(Mathf.PerlinNoise(Time.time * speed + index * SAFE_PRIME_A, index * SAFE_PRIME_B),
Mathf.PerlinNoise(Time.time * speed + index * SAFE_PRIME_B, index * SAFE_PRIME_A),
0);
jitterOffset *= 2;
jitterOffset -= new Vector3(1, 1, 0);
return Matrix4x4.TRS(jitterOffset * wiggleScale,
Quaternion.identity,
Vector3.one);
}
}
/// <summary>
/// Use a sine wave by time to offset the height of the element.
/// </summary>
public class WaveEffect : BaseEffect
{
public float speed, indexStep, scale;
public override Matrix4x4 TransFunc(int index)
{
return WaveTransformFunc(index, speed, indexStep, scale);
}
static public Matrix4x4 WaveTransformFunc(int index, float waveSpeed, float waveIndexStep, float waveScale)
{
return Matrix4x4.TRS(new Vector3(0, Mathf.Sin(Time.time * waveSpeed + index * waveIndexStep) * waveScale, 0),
Quaternion.identity,
Vector3.one);
}
}
/// <summary>
/// Use a sinewave to swing or pivot the element around its centre back n forth.
/// </summary>
public class PivotEffect : BaseEffect
{
public float speed, degScale;
public override Matrix4x4 TransFunc(int index)
{
return PivotTransformFunc(index, speed, degScale);
}
static public Matrix4x4 PivotTransformFunc(int index, float pivotSpeed, float pivotDegScale)
{
return Matrix4x4.TRS(Vector3.zero,
Quaternion.Euler(0, 0, Mathf.Sin(Time.time * pivotSpeed + index) * pivotDegScale),
Vector3.one);
}
}
/// <summary>
/// Use a sine wave to animate the H,S,V elements individually, modifying them from their starting color.
/// Use a sine wave to scale the element
/// </summary>
public class PulseEffect : BaseEffect
{
public float speed = 1, HSVIntensityScale = 1, indexStep = 1, hueScale = 1, saturationScale = 1, valueScale = 1;
public Vector3 scale = Vector2.zero;
public override Color32 ColorFunc(int index, Color32 col)
{
return HSVPulse(index, indexStep, speed, HSVIntensityScale, col, hueScale, saturationScale, valueScale);
}
public override Matrix4x4 TransFunc(int index)
{
return ScalePulse(index, indexStep, speed, scale);
}
static public Color32 HSVPulse(int index, float indexStep, float speed, float colScale, Color32 startingColor, float hueScale, float saturationScale, float valueScale)
{
float t = Mathf.Sin(Time.time * speed + index * indexStep) * colScale;
Color.RGBToHSV(startingColor, out float h, out float s, out float v);
var col = Color.HSVToRGB(Mathf.Repeat(h + t * hueScale, 1),
Mathf.Clamp01(s + t * saturationScale),
Mathf.Clamp01(v + t * valueScale));
return (Color32)col;
}
static public Matrix4x4 ScalePulse(int index, float indexStep, float speed, Vector3 scale)
{
float t = Mathf.Sin(Time.time * speed + index * indexStep);
return Matrix4x4.TRS(Vector3.zero,
Quaternion.identity,
Vector3.one + scale * t);
}
}
/// <summary>
/// Bounce up and down on an endless parabolic curve.
/// </summary>
public class BounceEffect : BaseEffect
{
public float indexStep = 1, speed = 1, scale = 1, fixedOffsetScale = 0.5f;
public override Matrix4x4 TransFunc(int index)
{
return Bounce(index, indexStep, speed, scale, fixedOffsetScale);
}
static public Matrix4x4 Bounce(int index, float indexStep, float speed, float scale, float fixedOffsetScale)
{
float t = (Time.time * speed + index * indexStep) % 2.0f;
t = -t * t + 2 * t;
return Matrix4x4.TRS(Vector3.up * t * scale + Vector3.down * fixedOffsetScale * scale,
Quaternion.identity,
Vector3.one);
}
}
/// <summary>
/// Create a staircase effect of the the elements.
/// </summary>
public class AscendEffect : BaseEffect
{
public float totalStep;
public override Matrix4x4 TransFunc(int index)
{
return StepTransformFunc(index, index - CurrentStart, totalStep / CurrentLength);
}
static public Matrix4x4 StepTransformFunc(int index, int stepNum, float stepHeight)
{
return Matrix4x4.TRS(Vector3.up * stepNum * stepHeight,
Quaternion.identity,
Vector3.one);
}
}
/// <summary>
/// Cycle the colors of the elements by forcing color to a roling Hue and fixed SV color value.
/// </summary>
public class RainbowEffect : BaseEffect
{
public float speed, indexStep, s, v;
public override Color32 ColorFunc(int index, Color32 col)
{
return CycleColor(index, speed, indexStep, s, v);
}
static public Color32 CycleColor(int index, float speed, float indexStep, float s, float v)
{
float h = Time.time * speed + index * indexStep;
var col = Color.HSVToRGB(h % 1.0f, s, v);
return (Color32)col;
}
}
}
}
#endif

11
Assets/Fungus/Scripts/Utils/TMProLinkAnimEffects.cs.meta

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a2f0563cd4feb744975e63982dc480c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

104
Assets/Fungus/Scripts/Utils/TMProLinkAnimLookup.cs

@ -0,0 +1,104 @@
// This code is part of the Fungus library (https://github.com/snozbot/fungus)
// It is released for free under the MIT open source license (https://github.com/snozbot/fungus/blob/master/LICENSE)
using System.Collections.Generic;
#if UNITY_2018_1_OR_NEWER
namespace Fungus
{
/// <summary>
/// Static lookup for Text Mesh Pro Link animations. TMPro tracks and holds information about
/// link tags in its text body and is recommended as one of the ways to achieve effects within
/// a body of text. Giving you the text within the link and the name/hash of the link id itself.
///
/// Such that {link="shake"}this text will be marked up as within shake link{/link}.
///
/// By assigning to the LinkHashToEffect dictionary with a key of TMPro.TMP_TextUtilities.GetSimpleHashCode("shake")
/// and a matching function signature that can then be used the the TMProLinkAnimator.
///
/// See TMProLinkAnimEffects for sample basis for creating effects.
/// </summary>
public static class TMProLinkAnimLookup
{
//required signature for all TMProAnim functions for use in the lookup
public delegate void TMProAnimFunc(TMProLinkAnimator beh, int start, int length);
//static lookup for all tmpro link style lookups
//this is where additional effects would be added
static public Dictionary<int, TMProAnimFunc> LinkHashToEffect = new Dictionary<int, TMProAnimFunc>()
{
//comments left here for the effects that are added in the demo scene
/*
{TMPro.TMP_TextUtilities.GetSimpleHashCode("shake"),
new TMProLinkAnimEffects.ShakeEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerLine,
offsetScale = 2,
rotScale = 15
}.DoEffect },
{TMPro.TMP_TextUtilities.GetSimpleHashCode("wiggle"),
new TMProLinkAnimEffects.WiggleEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerSection,
scale = 5
}.DoEffect },
{TMPro.TMP_TextUtilities.GetSimpleHashCode("wave"),
new TMProLinkAnimEffects.WaveEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerCharacter,
speed = 10,
indexStep = 0.3f,
scale = 2
}.DoEffect },
{TMPro.TMP_TextUtilities.GetSimpleHashCode("pivot"),
new TMProLinkAnimEffects.PivotEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerWord,
speed = 10,
degScale = 15
}.DoEffect
},
{TMPro.TMP_TextUtilities.GetSimpleHashCode("rainbow"),
new TMProLinkAnimEffects.RainbowEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerCharacter,
speed = 2,
indexStep = 0.1f,
s = 0.8f,
v = 0.8f
}.DoEffect
},
{TMPro.TMP_TextUtilities.GetSimpleHashCode("ascend"),
new TMProLinkAnimEffects.AscendEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerCharacter,
totalStep = 10
}.DoEffect
},
*/
};
static public void AddHelper(string linkIdText, TMProAnimFunc func)
{
LinkHashToEffect.Add(TMPro.TMP_TextUtilities.GetSimpleHashCode(linkIdText), func);
}
static public void AddHelper(string linkIdText, TMProLinkAnimEffects.BaseEffect baseEffect)
{
LinkHashToEffect.Add(TMPro.TMP_TextUtilities.GetSimpleHashCode(linkIdText), baseEffect.DoEffect);
}
static public void Remove(string linkIdText)
{
LinkHashToEffect.Remove(TMPro.TMP_TextUtilities.GetSimpleHashCode(linkIdText));
}
static public void RemoveAll()
{
LinkHashToEffect.Clear();
}
}
}
#endif

11
Assets/Fungus/Scripts/Utils/TMProLinkAnimLookup.cs.meta

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a3990bfc87d31047a6cc1e300dff1d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

206
Assets/Fungus/Scripts/Utils/TMProLinkAnimator.cs

@ -0,0 +1,206 @@
// This code is part of the Fungus library (https://github.com/snozbot/fungus)
// It is released for free under the MIT open source license (https://github.com/snozbot/fungus/blob/master/LICENSE)
using UnityEngine;
#if UNITY_2018_1_OR_NEWER
namespace Fungus
{
/// <summary>
/// Component that is automatically added to all tmpro texts that contain links. Caches
/// local data for that TMProText and uses the TMProLinkAnimLookup to the actual animation.
/// </summary>
[DisallowMultipleComponent]
public class TMProLinkAnimator : MonoBehaviour
{
#region Auto Add Component
/// <summary>
/// Ensure we are being notified of TMPro changes.
/// </summary>
[RuntimeInitializeOnLoadMethod]
public static void RegisterAutoAddTMPLinkAnim()
{
TMPro.TMPro_EventManager.TEXT_CHANGED_EVENT.Add(AutoAddTMPLinkAnim);
}
/// <summary>
/// Adds a suite of default link text animations. These can be removed via the
/// TMProLinkAnimLookup if desired.
/// </summary>
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void RegisterDefaultTMPLinkAnims()
{
TMProLinkAnimLookup.AddHelper("jitter", new TMProLinkAnimEffects.ShakeEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerCharacter,
offsetScale = new Vector2(1, 4),
rotScale = 10
});
TMProLinkAnimLookup.AddHelper("angry", new TMProLinkAnimEffects.ShakeEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerWord,
offsetScale = new Vector2(1, 8),
rotScale = 4
});
TMProLinkAnimLookup.AddHelper("spooky", new TMProLinkAnimEffects.WiggleEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerSection,
offsetScale = new Vector2(6, 10),
speed = 1.5f,
});
TMProLinkAnimLookup.AddHelper("unknowable", new TMProLinkAnimEffects.WiggleEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerCharacter,
offsetScale = new Vector2(4, 8),
speed = 1f,
});
TMProLinkAnimLookup.AddHelper("wave", new TMProLinkAnimEffects.WaveEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerCharacter,
speed = 10,
indexStep = 0.3f,
scale = 2
});
TMProLinkAnimLookup.AddHelper("swing", new TMProLinkAnimEffects.PivotEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerWord,
speed = 10,
degScale = 15
});
TMProLinkAnimLookup.AddHelper("bounce", new TMProLinkAnimEffects.BounceEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerWord,
speed = 4,
scale = 5,
});
TMProLinkAnimLookup.AddHelper("excited", new TMProLinkAnimEffects.BounceEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerCharacter,
speed = 7,
scale = 2,
indexStep = 11.0f / 3.0f,
});
TMProLinkAnimLookup.AddHelper("glow", new TMProLinkAnimEffects.PulseEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerWord,
speed = 4,
HSVIntensityScale = 0.15f,
hueScale = 0,
saturationScale = 0.1f,
scale = new Vector3(0.06f, 0.06f, 0),
});
}
/// <summary>
/// Called by TMPro when a text is changed, ensuring link animator is there and
/// that data is ready for it to use.
/// </summary>
/// <param name="obj"></param>
public static void AutoAddTMPLinkAnim(object obj)
{
if (Application.isPlaying)
{
var tmp = (obj as TMPro.TMP_Text);
if (forcedUpdater == null && tmp.textInfo.linkCount > 0)
{
var tmpa = tmp.GetComponent<TMProLinkAnimator>();
if (tmpa == null)
{
tmpa = tmp.gameObject.AddComponent<TMProLinkAnimator>();
tmpa.TMProComponent = tmp;
}
tmpa.SetDirty();
tmpa.UpdateAnimation();
}
}
}
/// <summary>
/// Cache of the TMProLinkAnimator that just forced an update of the TMProText, used to
/// prevent cyclic updates of TMPro mesh content.
/// </summary>
protected static TMProLinkAnimator forcedUpdater;
#endregion Auto Add Component
public TMPro.TMP_Text TMProComponent { get; protected set; }
public bool dirty = true;
protected bool needsToForceMeshUpdate = true;
public TMPro.TMP_MeshInfo[] CachedMeshInfo { get; protected set; }
public void SetDirty()
{
dirty = true;
}
protected void Awake()
{
if (TMProComponent == null)
{
TMProComponent = GetComponent<TMPro.TMP_Text>();
}
}
protected void Update()
{
UpdateAnimation();
}
/// <summary>
/// If there is TMPro and a link to potentially animate then ask the AnimLookup for it
/// </summary>
protected void UpdateAnimation()
{
//could we anim
if (TMProComponent != null && enabled)
{
bool requiresVertexDataUpdate = false;
//for all found links
for (int i = 0; i < TMProComponent.textInfo.linkCount; i++)
{
var curLink = TMProComponent.textInfo.linkInfo[i];
//if a static lookup exists, ask it to run its animation with us as the context
if (TMProLinkAnimLookup.LinkHashToEffect.TryGetValue(curLink.hashCode, out TMProLinkAnimLookup.TMProAnimFunc animFunc))
{
//only update caches if we actually need it
HandleDirty();
animFunc(this, curLink.linkTextfirstCharacterIndex, curLink.linkTextLength);
requiresVertexDataUpdate = true;
}
}
// Push changes if we actually found a matching effect
if (requiresVertexDataUpdate)
{
TMProComponent.UpdateVertexData();
}
}
}
protected void HandleDirty()
{
//update internal cache if underlying data has changed
if (dirty)
{
if (needsToForceMeshUpdate)
{
forcedUpdater = this;
TMProComponent.ForceMeshUpdate();
forcedUpdater = null;
}
CachedMeshInfo = TMProComponent.textInfo.CopyMeshInfoVertexData();
dirty = false;
}
}
}
}
#endif

11
Assets/Fungus/Scripts/Utils/TMProLinkAnimator.cs.meta

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: beb7176ffd78d904497350708b9f8134
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

52
Assets/Fungus/Scripts/Utils/TextAdapter.cs

@ -87,7 +87,7 @@ namespace Fungus
} }
#if UNITY_2018_1_OR_NEWER #if UNITY_2018_1_OR_NEWER
if(tmpro != null) if (tmpro != null)
{ {
tmpro.richText = true; tmpro.richText = true;
} }
@ -167,7 +167,7 @@ namespace Fungus
{ {
return (textUI != null || inputField != null || textMesh != null || textComponent != null || return (textUI != null || inputField != null || textMesh != null || textComponent != null ||
#if UNITY_2018_1_OR_NEWER #if UNITY_2018_1_OR_NEWER
tmpro !=null || tmpro != null ||
#endif #endif
writerTextDestination != null); writerTextDestination != null);
} }
@ -199,6 +199,54 @@ namespace Fungus
return false; return false;
} }
public bool SupportsHiddenCharacters()
{
#if UNITY_2018_1_OR_NEWER
if (tmpro != null)
{
return true;
}
#endif
return false;
}
public int RevealedCharacters
{
get
{
#if UNITY_2018_1_OR_NEWER
if (tmpro != null)
{
return tmpro.maxVisibleCharacters;
}
#endif
return 0;
}
set
{
#if UNITY_2018_1_OR_NEWER
if (tmpro != null)
{
tmpro.maxVisibleCharacters = value;
}
#endif
}
}
public int CharactersToReveal
{
get
{
#if UNITY_2018_1_OR_NEWER
if (tmpro != null)
{
return tmpro.textInfo.characterCount;
}
#endif
return 0;
}
}
public virtual string Text public virtual string Text
{ {
get get

9
Assets/Fungus/Scripts/Utils/TextTagParser.cs

@ -161,6 +161,14 @@ namespace Fungus
{ {
type = TokenType.AudioStop; type = TokenType.AudioStop;
} }
else if (tag.StartsWith("link="))
{
type = TokenType.LinkStart;
}
else if (tag.StartsWith("/link"))
{
type = TokenType.LinkEnd;
}
if (type != TokenType.Invalid) if (type != TokenType.Invalid)
{ {
@ -215,6 +223,7 @@ namespace Fungus
"\t{wp}, {wp=0.5} Wait on punctuation (seconds){/wp}\n" + "\t{wp}, {wp=0.5} Wait on punctuation (seconds){/wp}\n" +
"\t{c} Clear\n" + "\t{c} Clear\n" +
"\t{x} Exit, advance to the next command without waiting for input\n" + "\t{x} Exit, advance to the next command without waiting for input\n" +
"\t{link=id}link text{/link} <link=id>link text</link>\n" +
"\n" + "\n" +
"\t{vpunch=10,0.5} Vertically punch screen (intensity,time)\n" + "\t{vpunch=10,0.5} Vertically punch screen (intensity,time)\n" +
"\t{hpunch=10,0.5} Horizontally punch screen (intensity,time)\n" + "\t{hpunch=10,0.5} Horizontally punch screen (intensity,time)\n" +

4
Assets/Fungus/Scripts/Utils/TextTagToken.cs

@ -68,6 +68,10 @@ namespace Fungus
AudioStop, AudioStop,
/// <summary> wvo </summary> /// <summary> wvo </summary>
WaitForVoiceOver, WaitForVoiceOver,
/// <summary> link start </summary>
LinkStart,
/// <summary> link end </summary>
LinkEnd,
} }
/// <summary> /// <summary>

1712
Assets/FungusExamples/TextMeshPro/TMPro Link Default Effects Demo.unity

File diff suppressed because it is too large Load Diff

7
Assets/FungusExamples/TextMeshPro/TMPro Link Default Effects Demo.unity.meta

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: cffaf714f3026ad4db01e751fd5fd2a4
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

1872
Assets/FungusExamples/TextMeshPro/TMPro Link Effects Custom.unity

File diff suppressed because it is too large Load Diff

7
Assets/FungusExamples/TextMeshPro/TMPro Link Effects Custom.unity.meta

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: df0f727d3fe53fe4e9e9ec53f00cdfe1
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

72
Assets/FungusExamples/TextMeshPro/TMProLinkStylingDemo.cs

@ -0,0 +1,72 @@
// This code is part of the Fungus library (https://github.com/snozbot/fungus)
// It is released for free under the MIT open source license (https://github.com/snozbot/fungus/blob/master/LICENSE)
using UnityEngine;
#if UNITY_2018_1_OR_NEWER
namespace Fungus.Examples
{
/// <summary>
/// Used in TMPro Link Anim Demo, adds a number of sample animation styles. Serves as
/// an example of how you might configure these effects and variations of them in
/// your projects
/// </summary>
public class TMProLinkStylingDemo : MonoBehaviour
{
private void Awake()
{
//force clearing and adding our own effects here
TMProLinkAnimLookup.RemoveAll();
TMProLinkAnimLookup.AddHelper("shake", new TMProLinkAnimEffects.ShakeEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerCharacter,
offsetScale = Vector2.one * 2,
rotScale = 15
});
TMProLinkAnimLookup.AddHelper("wiggle", new TMProLinkAnimEffects.WiggleEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerSection,
offsetScale = Vector2.one * 5
});
TMProLinkAnimLookup.AddHelper("wave", new TMProLinkAnimEffects.WaveEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerCharacter,
speed = 10,
indexStep = 0.3f,
scale = 2
});
TMProLinkAnimLookup.AddHelper("pivot", new TMProLinkAnimEffects.PivotEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerWord,
speed = 10,
degScale = 15
});
TMProLinkAnimLookup.AddHelper("rainbow", new TMProLinkAnimEffects.RainbowEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerCharacter,
speed = 2,
indexStep = 0.1f,
s = 0.8f,
v = 0.8f
});
TMProLinkAnimLookup.AddHelper("ascend", new TMProLinkAnimEffects.AscendEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerCharacter,
totalStep = 10,
});
TMProLinkAnimLookup.AddHelper("pulse", new TMProLinkAnimEffects.PulseEffect()
{
mode = TMProLinkAnimEffects.TMPLinkAnimatorMode.PerWord,
speed = 3,
HSVIntensityScale = 0.15f,
hueScale = 0,
saturationScale = 0,
scale = new Vector3(0.05f, 0.05f, 0),
});
}
}
}
#endif

11
Assets/FungusExamples/TextMeshPro/TMProLinkStylingDemo.cs.meta

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1d3b019791b0794419d9937f571dd578
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

2064
Assets/Tests/WritingSpeedTest.unity

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save