Skip to content

Commit 146ea59

Browse files
committed
Add script for letterboxing
1 parent f29c2df commit 146ea59

File tree

2 files changed

+283
-0
lines changed

2 files changed

+283
-0
lines changed

Runtime/Letterboxing.cs

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
using System.Collections;
2+
using UnityEngine;
3+
using UnityEngine.UI;
4+
5+
namespace Zigurous.UI
6+
{
7+
/// <summary>
8+
/// Displays mattes on the top and bottom of the screen to crop the screen
9+
/// to a specified aspect ratio. This is also referred to as cinematic black
10+
/// bars and is useful for cutscenes in games.
11+
/// </summary>
12+
[RequireComponent(typeof(RectTransform))]
13+
public sealed class Letterboxing : MonoBehaviour
14+
{
15+
/// <summary>
16+
/// The color of the mattes.
17+
/// </summary>
18+
[Tooltip("The color of the mattes.")]
19+
[SerializeField]
20+
private Color _color = Color.black;
21+
22+
/// <summary>
23+
/// The material of the mattes.
24+
/// </summary>
25+
[Tooltip("The material of the mattes.")]
26+
[SerializeField]
27+
private Material _material = null;
28+
29+
/// <summary>
30+
/// The aspect ratio of the mattes.
31+
/// </summary>
32+
[Tooltip("The aspect ratio of the mattes.")]
33+
[SerializeField]
34+
private float _aspectRatio = 2.35f;
35+
36+
/// <summary>
37+
/// The amount of seconds it takes to animate the mattes.
38+
/// </summary>
39+
[Tooltip("The amount of seconds it takes to animate the mattes.")]
40+
public float animationDuration = 0.5f;
41+
42+
/// <summary>
43+
/// The top letterbox matte.
44+
/// </summary>
45+
public RectTransform matteTop { get; private set; }
46+
47+
/// <summary>
48+
/// The bottom letterbox matte.
49+
/// </summary>
50+
public RectTransform matteBottom { get; private set; }
51+
52+
/// <summary>
53+
/// The color of the mattes.
54+
/// </summary>
55+
public Color color
56+
{
57+
get { return _color; }
58+
set { _color = value; UpdateStyles(); }
59+
}
60+
61+
/// <summary>
62+
/// The material of the mattes.
63+
/// </summary>
64+
public Material material
65+
{
66+
get { return _material; }
67+
set { _material = value; UpdateStyles(); }
68+
}
69+
70+
/// <summary>
71+
/// The aspect ratio of the mattes.
72+
/// </summary>
73+
public float aspectRatio
74+
{
75+
get { return _aspectRatio; }
76+
set { _aspectRatio = value; UpdateMattes(); }
77+
}
78+
79+
/// <summary>
80+
/// The current height of the letterbox mattes.
81+
/// </summary>
82+
public float matteHeight { get; private set; }
83+
84+
/// <summary>
85+
/// The coroutine that animates the mattes.
86+
/// </summary>
87+
private Coroutine _animation;
88+
89+
#if UNITY_EDITOR
90+
/// <summary>
91+
/// Whether the current settings have been invalidated.
92+
/// </summary>
93+
private bool _invalidated;
94+
#endif
95+
96+
private void Awake()
97+
{
98+
StretchToScreenSize stretch = this.gameObject.AddComponent<StretchToScreenSize>();
99+
stretch.hideFlags = HideFlags.HideInInspector;
100+
stretch.stretchWidth = true;
101+
stretch.stretchHeight = true;
102+
103+
this.matteBottom = CreateMatte();
104+
this.matteTop = CreateMatte();
105+
this.matteTop.localScale = new Vector3(1.0f, -1.0f, 1.0f);
106+
}
107+
108+
private RectTransform CreateMatte()
109+
{
110+
GameObject matte = new GameObject("Matte");
111+
matte.transform.parent = this.transform;
112+
113+
RectTransform matteRect = matte.AddComponent<RectTransform>();
114+
matteRect.SetHeight(0.0f);
115+
116+
matte.AddComponent<CanvasRenderer>();
117+
118+
Image graphic = matte.AddComponent<Image>();
119+
graphic.color = this.color;
120+
graphic.material = this.material;
121+
122+
StretchToScreenSize stretch = matte.AddComponent<StretchToScreenSize>();
123+
stretch.stretchWidth = true;
124+
stretch.stretchHeight = false;
125+
126+
return matteRect;
127+
}
128+
129+
private void Start()
130+
{
131+
ScreenSizeListener.Instance.resized += OnScreenResize;
132+
}
133+
134+
private void OnDestroy()
135+
{
136+
if (ScreenSizeListener.HasInstance) {
137+
ScreenSizeListener.Instance.resized -= OnScreenResize;
138+
}
139+
}
140+
141+
private void OnValidate()
142+
{
143+
_invalidated = true;
144+
}
145+
146+
private void OnEnable()
147+
{
148+
UpdateMattes();
149+
}
150+
151+
private void OnDisable()
152+
{
153+
UpdateMattes();
154+
}
155+
156+
private void OnScreenResize(int width, int height)
157+
{
158+
UpdateMattes(animated: false);
159+
}
160+
161+
private void Update()
162+
{
163+
if (_invalidated)
164+
{
165+
UpdateStyles();
166+
UpdateMattes(animated: false);
167+
168+
_invalidated = false;
169+
}
170+
}
171+
172+
private void UpdateMattes()
173+
{
174+
UpdateMattes(animated: this.animationDuration > 0.0f);
175+
}
176+
177+
private void UpdateMattes(bool animated)
178+
{
179+
if (!this.gameObject.activeInHierarchy)
180+
{
181+
SizeAndPositionMattes(0.0f);
182+
return;
183+
}
184+
185+
float desiredHeight = CalculateMatteHeight();
186+
187+
if (animated)
188+
{
189+
if (_animation != null) {
190+
StopCoroutine(_animation);
191+
}
192+
193+
_animation = StartCoroutine(Animate(this.matteHeight, desiredHeight));
194+
}
195+
else
196+
{
197+
SizeAndPositionMattes(desiredHeight);
198+
}
199+
}
200+
201+
private IEnumerator Animate(float currentHeight, float desiredHeight)
202+
{
203+
float elapsed = 0.0f;
204+
205+
while (elapsed < this.animationDuration)
206+
{
207+
float percent = Mathf.Clamp01(elapsed / this.animationDuration);
208+
float height = Mathf.SmoothStep(currentHeight, desiredHeight, percent);
209+
210+
SizeAndPositionMattes(height);
211+
212+
elapsed += Time.deltaTime;
213+
yield return null;
214+
}
215+
216+
SizeAndPositionMattes(desiredHeight);
217+
}
218+
219+
private float CalculateMatteHeight()
220+
{
221+
if (!this.enabled) {
222+
return 0.0f;
223+
}
224+
225+
float screenWidth = Screen.width;
226+
float screenHeight = Screen.height;
227+
228+
// Screen.width and Screen.height oddly does not always give the
229+
// correct values so try to use ScreenSizeListener if available
230+
if (ScreenSizeListener.HasInstance)
231+
{
232+
screenWidth = ScreenSizeListener.Instance.width;
233+
screenHeight = ScreenSizeListener.Instance.height;
234+
}
235+
236+
float letterbox = screenWidth / this.aspectRatio;
237+
return (screenHeight - letterbox) * 0.5f;
238+
}
239+
240+
private void SizeAndPositionMattes(float height)
241+
{
242+
this.matteHeight = height;
243+
244+
if (this.matteTop != null) {
245+
this.matteTop.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0.0f, height);
246+
}
247+
248+
if (this.matteBottom != null) {
249+
this.matteBottom.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Bottom, 0.0f, height);
250+
}
251+
}
252+
253+
private void UpdateStyles()
254+
{
255+
if (this.matteTop != null)
256+
{
257+
Graphic graphic = this.matteTop.GetComponent<Graphic>();
258+
graphic.color = this.color;
259+
graphic.material = this.material;
260+
}
261+
262+
if (this.matteBottom != null)
263+
{
264+
Graphic graphic = this.matteBottom.GetComponent<Graphic>();
265+
graphic.color = this.color;
266+
graphic.material = this.material;
267+
}
268+
}
269+
270+
}
271+
272+
}

Runtime/Letterboxing.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)