// 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 { /// /// Samples and helpers for creating TMProLink animations /// namespace TMProLinkAnimEffects { /// /// Used by BaseEffect and child classes to configure how locations and pivots are calculated /// before being passed to internal functions. /// public enum TMPLinkAnimatorMode { PerCharacter, PerWord, PerSection, PerLine, } /// /// 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. /// 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 /// /// Helper for mesh vertex updating within a found link, adapted from TMPRo examples VertexJitter. /// /// /// /// /// /// /// static public void MeshVertUpdateLoop(TMProLinkAnimator context, int start, int length, System.Func transformFunc, System.Func 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; } } /// /// Same as calcing a character mid but averaging over all characters in the given character range /// /// /// /// /// 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); } /// /// Determine which TMPro World a given character index is within /// /// /// /// 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; } /// /// Determine which TMPro Line a given character index is within /// /// /// /// 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 } /// /// Shake the element, by moving centre slightly and randomly rolling each update. /// 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); } } /// /// Wiggle the position by over time using perlin noise to offset its centre. /// 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); } } /// /// Use a sine wave by time to offset the height of the element. /// 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); } } /// /// Use a sinewave to swing or pivot the element around its centre back n forth. /// 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); } } /// /// 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 /// 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); } } /// /// Bounce up and down on an endless parabolic curve. /// 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); } } /// /// Create a staircase effect of the the elements. /// 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); } } /// /// Cycle the colors of the elements by forcing color to a roling Hue and fixed SV color value. /// 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