Skip to content

Instantly share code, notes, and snippets.

@Malkyne
Last active June 2, 2025 13:18
Show Gist options
  • Save Malkyne/d3ea1ae1d0ebd9418c58cc858892242c to your computer and use it in GitHub Desktop.
Save Malkyne/d3ea1ae1d0ebd9418c58cc858892242c to your computer and use it in GitHub Desktop.
Unity Canvas Scaler for splash screens
// Copyright (c) 2025 Hidden Achievement, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
// Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
using UnityEngine;
using UnityEngine.EventSystems;
/// <summary>
/// This is a Canvas scaler that is designed for screens such as start menus, with large splash art, which should be
/// cropped to fit the screen area. These can behave very badly on ultra wide screens, causing important titles and/or
/// controls to get crapped. The intent of this scaler is to keep the important area in-frame, no matter what -- even
/// if we eventually have to fall back on letter/pillar boxing.
///
/// It is intended to behave much like the "Shrink" option on the stock CanvasScaler, where it expands the contents of
/// the canvas until they fill up the screen. However, it allows the user to specify a safe area, which should not be
/// cropped. If there's a risk of cropping, the scaler will first shift the content, to hold it in the frame, and then,
/// if that is insufficient to the task, it will letter/pillar box the content.
/// </summary>
[RequireComponent(typeof(Canvas))]
[ExecuteAlways]
[DisallowMultipleComponent]
public class SafeFillScaler : UIBehaviour
{
[Tooltip("If a sprite has this 'Pixels Per Unit' setting, then one pixel in the sprite will cover one unit in the UI.")]
[SerializeField] protected float _referencePixelsPerUnit = 100;
/// <summary>
/// If a sprite has this 'Pixels Per Unit' setting, then one pixel in the sprite will cover one unit in the UI.
/// </summary>
public float ReferencePixelsPerUnit { get => _referencePixelsPerUnit; set => _referencePixelsPerUnit = value; }
[Tooltip("The resolution the UI layout is designed for. If the screen resolution is larger, the UI will be scaled up, and if it's smaller, the UI will be scaled down.")]
[SerializeField] protected Vector2 _referenceResolution = new(800, 600);
/// <summary>
/// The resolution the UI layout is designed for.
/// </summary>
/// <remarks>
/// If the screen resolution is larger, the UI will be scaled up, and if it's smaller, the UI will be scaled down.
/// </remarks>
public Vector2 referenceResolution
{
get => _referenceResolution;
set
{
_referenceResolution = value;
const float k_MinimumResolution = 0.00001f;
if (_referenceResolution.x is > -k_MinimumResolution and < k_MinimumResolution)
{
_referenceResolution.x = k_MinimumResolution * Mathf.Sign(_referenceResolution.x);
}
if (-referenceResolution.y > -k_MinimumResolution && _referenceResolution.y < k_MinimumResolution)
{
_referenceResolution.y = k_MinimumResolution * Mathf.Sign(_referenceResolution.y);
}
_referenceAspectRatio = _referenceResolution.x / _referenceResolution.y;
}
}
[SerializeField]
[Tooltip("An area that must be protected from being cropped, when scaling. Should include titling/controls.")]
private RectTransform _safeArea;
[SerializeField]
[Tooltip("This is a parent object to our safe area that will shift its position to keep the safe area within the frame of the screen.")]
private RectTransform _shuttle;
private Canvas _canvas;
private float _prevScaleFactor = 1;
private float _prevReferencePixelsPerUnit = 100;
private float _referenceAspectRatio = 1;
protected override void OnEnable()
{
base.OnEnable();
_canvas = GetComponent<Canvas>();
_referenceAspectRatio = _referenceResolution.x / _referenceResolution.y;
Handle();
Canvas.preWillRenderCanvases += CanvasPreWillRenderCanvases;
}
private void CanvasPreWillRenderCanvases()
{
Handle();
}
protected override void OnDisable()
{
SetScaleFactor(1);
SetReferencePixelsPerUnit(100);
Canvas.preWillRenderCanvases -= CanvasPreWillRenderCanvases;
base.OnDisable();
}
///<summary>
///Method that handles calculations of canvas scaling.
///</summary>
protected virtual void Handle()
{
if (_canvas == null || !_canvas.isRootCanvas)
return;
Vector2 screenSize = _canvas.renderingDisplaySize;
int displayIndex = _canvas.targetDisplay;
if (displayIndex > 0 && displayIndex < Display.displays.Length)
{
Display disp = Display.displays[displayIndex];
screenSize = new Vector2(disp.renderingWidth, disp.renderingHeight);
}
float scaleFactor = Mathf.Max(screenSize.x / _referenceResolution.x, screenSize.y / _referenceResolution.y);
float screenAspectRatio = screenSize.x / screenSize.y;
float canvasHeight = _referenceResolution.x / screenAspectRatio;
float canvasWidth = _referenceResolution.y * screenAspectRatio;
if (screenAspectRatio > _referenceAspectRatio)
{
if (canvasHeight < _safeArea.sizeDelta.y)
{
scaleFactor = screenSize.y / _safeArea.sizeDelta.y;
canvasHeight = _safeArea.sizeDelta.y;
}
}
else
{
if (canvasWidth < _safeArea.sizeDelta.x)
{
scaleFactor = screenSize.x / _safeArea.sizeDelta.x;
canvasWidth = _safeArea.sizeDelta.x;
}
}
float horizMargin = (canvasWidth - _safeArea.sizeDelta.x) * 0.5f;
float vertMargin = (canvasHeight - _safeArea.sizeDelta.y) * 0.5f;
Vector2 newPosition = Vector2.zero;
if (_safeArea.anchoredPosition.x > horizMargin) // Right is cropped
{
newPosition.x = -(_safeArea.anchoredPosition.x - horizMargin);
}
else if (-_safeArea.anchoredPosition.x > horizMargin) // Left side is cropped
{
newPosition.x = _safeArea.anchoredPosition.x + horizMargin;
}
if (_safeArea.anchoredPosition.y > vertMargin) // Bottom is cropped
{
newPosition.y = -(_safeArea.anchoredPosition.y - vertMargin);
}
else if (-_safeArea.anchoredPosition.y > vertMargin) // Top is cropped
{
newPosition.y = _safeArea.anchoredPosition.y + vertMargin;
}
_shuttle.anchoredPosition = newPosition;
SetScaleFactor(scaleFactor);
SetReferencePixelsPerUnit(_referencePixelsPerUnit);
}
/// <summary>
/// Sets the scale factor on the canvas.
/// </summary>
/// <param name="scaleFactor">The scale factor to use.</param>
protected void SetScaleFactor(float scaleFactor)
{
if (scaleFactor == _prevScaleFactor)
return;
_canvas.scaleFactor = scaleFactor;
_prevScaleFactor = scaleFactor;
}
/// <summary>
/// Sets the referencePixelsPerUnit on the Canvas.
/// </summary>
/// <param name="referencePixelsPerUnit">The new reference pixels per Unity value</param>
protected void SetReferencePixelsPerUnit(float referencePixelsPerUnit)
{
if (referencePixelsPerUnit == _prevReferencePixelsPerUnit)
return;
_canvas.referencePixelsPerUnit = referencePixelsPerUnit;
_prevReferencePixelsPerUnit = referencePixelsPerUnit;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment