TimeUtility.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. using System;
  2. using System.Text.RegularExpressions;
  3. namespace UnityEngine.Timeline
  4. {
  5. // Sequence specific utilities for time manipulation
  6. static class TimeUtility
  7. {
  8. // chosen because it will cause no rounding errors between time/frames for frames values up to at least 10 million
  9. public static readonly double kTimeEpsilon = 1e-14;
  10. public static readonly double kFrameRateEpsilon = 1e-6;
  11. public static readonly double k_MaxTimelineDurationInSeconds = 9e6; //104 days of running time
  12. static void ValidateFrameRate(double frameRate)
  13. {
  14. if (frameRate <= kTimeEpsilon)
  15. throw new ArgumentException("frame rate cannot be 0 or negative");
  16. }
  17. public static int ToFrames(double time, double frameRate)
  18. {
  19. ValidateFrameRate(frameRate);
  20. time = Math.Min(Math.Max(time, -k_MaxTimelineDurationInSeconds), k_MaxTimelineDurationInSeconds);
  21. // this matches OnFrameBoundary
  22. double tolerance = GetEpsilon(time, frameRate) / 2.0;
  23. if (time < 0)
  24. {
  25. return (int)Math.Ceiling(time * frameRate - tolerance);
  26. }
  27. return (int)Math.Floor(time * frameRate + tolerance);
  28. }
  29. public static double ToExactFrames(double time, double frameRate)
  30. {
  31. ValidateFrameRate(frameRate);
  32. return time * frameRate;
  33. }
  34. public static double FromFrames(int frames, double frameRate)
  35. {
  36. ValidateFrameRate(frameRate);
  37. return (frames / frameRate);
  38. }
  39. public static double FromFrames(double frames, double frameRate)
  40. {
  41. ValidateFrameRate(frameRate);
  42. return frames / frameRate;
  43. }
  44. public static bool OnFrameBoundary(double time, double frameRate)
  45. {
  46. return OnFrameBoundary(time, frameRate, GetEpsilon(time, frameRate));
  47. }
  48. public static double GetEpsilon(double time, double frameRate)
  49. {
  50. return Math.Max(Math.Abs(time), 1) * frameRate * kTimeEpsilon;
  51. }
  52. public static bool OnFrameBoundary(double time, double frameRate, double epsilon)
  53. {
  54. ValidateFrameRate(frameRate);
  55. double exact = ToExactFrames(time, frameRate);
  56. double rounded = Math.Round(exact);
  57. return Math.Abs(exact - rounded) < epsilon;
  58. }
  59. public static double RoundToFrame(double time, double frameRate)
  60. {
  61. ValidateFrameRate(frameRate);
  62. var frameBefore = (int)Math.Floor(time * frameRate) / frameRate;
  63. var frameAfter = (int)Math.Ceiling(time * frameRate) / frameRate;
  64. return Math.Abs(time - frameBefore) < Math.Abs(time - frameAfter) ? frameBefore : frameAfter;
  65. }
  66. public static string TimeAsFrames(double timeValue, double frameRate, string format = "F2")
  67. {
  68. if (OnFrameBoundary(timeValue, frameRate)) // make integral values when on time borders
  69. return ToFrames(timeValue, frameRate).ToString();
  70. return ToExactFrames(timeValue, frameRate).ToString(format);
  71. }
  72. public static string TimeAsTimeCode(double timeValue, double frameRate, string format = "F2")
  73. {
  74. ValidateFrameRate(frameRate);
  75. int intTime = (int)Math.Abs(timeValue);
  76. int hours = intTime / 3600;
  77. int minutes = (intTime % 3600) / 60;
  78. int seconds = intTime % 60;
  79. string result;
  80. string sign = timeValue < 0 ? "-" : string.Empty;
  81. if (hours > 0)
  82. result = hours + ":" + minutes.ToString("D2") + ":" + seconds.ToString("D2");
  83. else if (minutes > 0)
  84. result = minutes + ":" + seconds.ToString("D2");
  85. else
  86. result = seconds.ToString();
  87. int frameDigits = (int)Math.Floor(Math.Log10(frameRate) + 1);
  88. // Add partial digits on the frame if needed.
  89. // we are testing the original value (not the truncated), because the truncation can cause rounding errors leading
  90. // to invalid strings for items on frame boundaries
  91. string frames = (ToFrames(timeValue, frameRate) - ToFrames(intTime, frameRate)).ToString().PadLeft(frameDigits, '0');
  92. if (!OnFrameBoundary(timeValue, frameRate))
  93. {
  94. string decimals = ToExactFrames(timeValue, frameRate).ToString(format);
  95. int decPlace = decimals.IndexOf('.');
  96. if (decPlace >= 0)
  97. frames += " [" + decimals.Substring(decPlace) + "]";
  98. }
  99. return sign + result + ":" + frames;
  100. }
  101. // Given a time code string, return the time in seconds
  102. // 1.5 -> 1.5 seconds
  103. // 1:1.5 -> 1 minute, 1.5 seconds
  104. // 1:1[.5] -> 1 second, 1.5 frames
  105. // 2:3:4 -> 2 minutes, 3 seconds, 4 frames
  106. // 1[.6] -> 1.6 frames
  107. public static double ParseTimeCode(string timeCode, double frameRate, double defaultValue)
  108. {
  109. timeCode = RemoveChar(timeCode, c => char.IsWhiteSpace(c));
  110. string[] sections = timeCode.Split(':');
  111. if (sections.Length == 0 || sections.Length > 4)
  112. return defaultValue;
  113. int hours = 0;
  114. int minutes = 0;
  115. double seconds = 0;
  116. double frames = 0;
  117. try
  118. {
  119. // depending on the format of the last numbers
  120. // seconds format
  121. string lastSection = sections[sections.Length - 1];
  122. if (Regex.Match(lastSection, @"^\d+\.\d+$").Success)
  123. {
  124. seconds = double.Parse(lastSection);
  125. if (sections.Length > 3) return defaultValue;
  126. if (sections.Length > 1) minutes = int.Parse(sections[sections.Length - 2]);
  127. if (sections.Length > 2) hours = int.Parse(sections[sections.Length - 3]);
  128. }
  129. // frame formats
  130. else
  131. {
  132. if (Regex.Match(lastSection, @"^\d+\[\.\d+\]$").Success)
  133. {
  134. string stripped = RemoveChar(lastSection, c => c == '[' || c == ']');
  135. frames = double.Parse(stripped);
  136. }
  137. else if (Regex.Match(lastSection, @"^\d*$").Success)
  138. {
  139. frames = int.Parse(lastSection);
  140. }
  141. else
  142. {
  143. return defaultValue;
  144. }
  145. if (sections.Length > 1) seconds = int.Parse(sections[sections.Length - 2]);
  146. if (sections.Length > 2) minutes = int.Parse(sections[sections.Length - 3]);
  147. if (sections.Length > 3) hours = int.Parse(sections[sections.Length - 4]);
  148. }
  149. }
  150. catch (FormatException)
  151. {
  152. return defaultValue;
  153. }
  154. return frames / frameRate + seconds + minutes * 60 + hours * 3600;
  155. }
  156. // fixes rounding errors from using single precision for length
  157. public static double GetAnimationClipLength(AnimationClip clip)
  158. {
  159. if (clip == null || clip.empty)
  160. return 0;
  161. double length = clip.length;
  162. if (clip.frameRate > 0)
  163. {
  164. double frames = Mathf.Round(clip.length * clip.frameRate);
  165. length = frames / clip.frameRate;
  166. }
  167. return length;
  168. }
  169. static string RemoveChar(string str, Func<char, bool> charToRemoveFunc)
  170. {
  171. var len = str.Length;
  172. var src = str.ToCharArray();
  173. var dstIdx = 0;
  174. for (var i = 0; i < len; i++)
  175. {
  176. if (!charToRemoveFunc(src[i]))
  177. src[dstIdx++] = src[i];
  178. }
  179. return new string(src, 0, dstIdx);
  180. }
  181. }
  182. }