1142 lines
34 KiB
C#
1142 lines
34 KiB
C#
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using ImageCropperNamespace;
|
|
using System.Collections;
|
|
|
|
// ===
|
|
// Auto-zoom feature inspired from: Android Image Cropper https://github.com/ArthurHub/Android-Image-Cropper
|
|
// Copyright 2016, Arthur Teplitzki, 2013, Edmodo, Inc.
|
|
// License (Apache License 2.0) https://github.com/ArthurHub/Android-Image-Cropper/blob/master/LICENSE.txt
|
|
// ===
|
|
|
|
public class ImageCropper : MonoBehaviour
|
|
{
|
|
public class Settings
|
|
{
|
|
public bool autoZoomEnabled = true;
|
|
public bool pixelPerfectSelection = false;
|
|
public bool ovalSelection = false;
|
|
public bool markTextureNonReadable = true;
|
|
|
|
public Color imageBackground = Color.black;
|
|
|
|
public Button visibleButtons = ~Button.None;
|
|
public Visibility guidelinesVisibility = Visibility.AlwaysVisible;
|
|
public Orientation initialOrientation = Orientation.Normal;
|
|
|
|
public Vector2 selectionMinSize = Vector2.zero;
|
|
public Vector2 selectionMaxSize = Vector2.zero;
|
|
|
|
public float selectionMinAspectRatio = 0f;
|
|
public float selectionMaxAspectRatio = 0f;
|
|
|
|
public float selectionInitialPaddingLeft = 0.1f;
|
|
public float selectionInitialPaddingTop = 0.1f;
|
|
public float selectionInitialPaddingRight = 0.1f;
|
|
public float selectionInitialPaddingBottom = 0.1f;
|
|
}
|
|
|
|
[System.Flags]
|
|
public enum Direction { None = 0, Left = 1, Top = 2, Right = 4, Bottom = 8 };
|
|
|
|
[System.Flags]
|
|
public enum Button { None = 0, FlipHorizontal = 1, FlipVertical = 2, Rotate90Degrees = 4 };
|
|
|
|
public enum Visibility { Hidden = 0, OnDrag = 1, AlwaysVisible = 2 };
|
|
|
|
// EXIF orientation: http://sylvana.net/jpegcrop/exif_orientation.html (indices are reordered)
|
|
public enum Orientation { Normal = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, FlipHorizontal = 4, Transpose = 5, FlipVertical = 6, Transverse = 7 };
|
|
|
|
public delegate void CropResult( bool result, Texture originalImage, Texture2D croppedImage );
|
|
public delegate void ImageResizePolicy( ref int width, ref int height );
|
|
|
|
private static ImageCropper m_instance = null;
|
|
public static ImageCropper Instance
|
|
{
|
|
get
|
|
{
|
|
if( m_instance == null )
|
|
m_instance = Instantiate( Resources.Load<ImageCropper>( "ImageCropper" ) );
|
|
|
|
return m_instance;
|
|
}
|
|
}
|
|
|
|
[Header( "Properties" )]
|
|
[SerializeField]
|
|
private float autoZoomInThreshold = 0.5f;
|
|
|
|
[SerializeField]
|
|
private float autoZoomOutThreshold = 0.65f;
|
|
|
|
[SerializeField]
|
|
private float autoZoomInFillAmount = 0.64f;
|
|
|
|
[SerializeField]
|
|
private float autoZoomOutFillAmount = 0.51f;
|
|
|
|
[SerializeField]
|
|
private AnimationCurve autoZoomCurve;
|
|
|
|
[SerializeField]
|
|
private float m_selectionSnapToEdgeThreshold = 5f;
|
|
public float SelectionSnapToEdgeThreshold { get { return m_selectionSnapToEdgeThreshold; } }
|
|
|
|
[SerializeField]
|
|
private float viewportScrollSpeed = 512f;
|
|
|
|
[Header( "Internal Variables" )]
|
|
[SerializeField]
|
|
private Canvas canvas;
|
|
|
|
[SerializeField]
|
|
private RectTransform m_viewport;
|
|
public RectTransform Viewport { get { return m_viewport; } }
|
|
|
|
[SerializeField]
|
|
private RectTransform m_imageHolder;
|
|
public RectTransform ImageHolder { get { return m_imageHolder; } }
|
|
|
|
[SerializeField]
|
|
private RawImage m_orientedImage;
|
|
public RawImage OrientedImage { get { return m_orientedImage; } }
|
|
|
|
[SerializeField]
|
|
private RectTransform m_selection;
|
|
public RectTransform Selection { get { return m_selection; } }
|
|
|
|
[SerializeField]
|
|
private RectTransform m_selectionGraphics;
|
|
public RectTransform SelectionGraphics { get { return m_selectionGraphics; } }
|
|
|
|
[SerializeField]
|
|
private SizeChangeListener viewportSizeChangeListener;
|
|
|
|
[SerializeField]
|
|
private SelectionGraphicsSynchronizer selectionGraphicsSynchronizer;
|
|
|
|
[SerializeField]
|
|
private Behaviour[] ovalMaskElements;
|
|
|
|
[SerializeField]
|
|
private Behaviour[] guidelines;
|
|
|
|
[Header( "User Interface" )]
|
|
[SerializeField]
|
|
private GameObject flipHorizontalButton;
|
|
|
|
[SerializeField]
|
|
private GameObject flipVerticalButton;
|
|
|
|
[SerializeField]
|
|
private GameObject rotate90DegreesButton;
|
|
|
|
[SerializeField]
|
|
private FontSizeSynchronizer textsSynchronizer;
|
|
|
|
[Header( "Crop Helpers" )]
|
|
[SerializeField]
|
|
private Canvas cropRenderCanvas;
|
|
|
|
[SerializeField]
|
|
private RawImage cropRenderImage;
|
|
|
|
[SerializeField]
|
|
private RectTransform cropRenderSelection;
|
|
|
|
[SerializeField]
|
|
private Camera cropRenderCamera;
|
|
|
|
public bool IsOpen { get { return gameObject.activeSelf; } }
|
|
|
|
private Settings m_defaultSettings;
|
|
private Settings DefaultSettings
|
|
{
|
|
get
|
|
{
|
|
if( m_defaultSettings == null )
|
|
m_defaultSettings = new Settings();
|
|
|
|
return m_defaultSettings;
|
|
}
|
|
}
|
|
|
|
private bool m_autoZoomEnabled;
|
|
public bool AutoZoomEnabled
|
|
{
|
|
get { return m_autoZoomEnabled; }
|
|
set
|
|
{
|
|
m_autoZoomEnabled = value;
|
|
if( m_autoZoomEnabled )
|
|
StartAutoZoom( false );
|
|
}
|
|
}
|
|
|
|
private bool m_pixelPerfectSelection;
|
|
public bool PixelPerfectSelection
|
|
{
|
|
get { return m_pixelPerfectSelection; }
|
|
set
|
|
{
|
|
m_pixelPerfectSelection = value;
|
|
if( m_pixelPerfectSelection )
|
|
MakePixelPerfectSelection();
|
|
}
|
|
}
|
|
|
|
private bool m_ovalSelection;
|
|
public bool OvalSelection
|
|
{
|
|
get { return m_ovalSelection; }
|
|
set
|
|
{
|
|
m_ovalSelection = value;
|
|
for( int i = 0; i < ovalMaskElements.Length; i++ )
|
|
ovalMaskElements[i].enabled = m_ovalSelection;
|
|
}
|
|
}
|
|
|
|
private Visibility m_guidelinesVisibility;
|
|
public Visibility GuidelinesVisibility
|
|
{
|
|
get { return m_guidelinesVisibility; }
|
|
set
|
|
{
|
|
m_guidelinesVisibility = value;
|
|
|
|
bool visible = m_guidelinesVisibility == Visibility.AlwaysVisible;
|
|
for( int i = 0; i < guidelines.Length; i++ )
|
|
guidelines[i].enabled = visible;
|
|
}
|
|
}
|
|
|
|
public bool MarkTextureNonReadable { get; set; }
|
|
|
|
public Color ImageBackground { get; set; }
|
|
|
|
private Vector2 m_viewportSize;
|
|
public Vector2 ViewportSize { get { return m_viewportSize; } }
|
|
|
|
private Vector2 m_originalImageSize;
|
|
public Vector2 OriginalImageSize { get { return m_originalImageSize; } }
|
|
|
|
private Vector2 m_orientedImageSize;
|
|
public Vector2 OrientedImageSize { get { return m_orientedImageSize; } }
|
|
|
|
public Vector2 SelectionSize { get { return m_selection.sizeDelta; } }
|
|
|
|
private RectTransform orientedImageTransform;
|
|
|
|
private IEnumerator autoZoomCoroutine;
|
|
private ISelectionHandler currentSelectionHandler;
|
|
|
|
private Orientation currentOrientation;
|
|
|
|
private CropResult cropCallback;
|
|
private ImageResizePolicy imageResizePolicy;
|
|
|
|
private bool shouldRefreshViewport;
|
|
|
|
private Vector2 minSize, maxSize;
|
|
private Vector2 currMinSize, currMaxSize;
|
|
|
|
private float minAspectRatio, maxAspectRatio;
|
|
private float minImageScale;
|
|
|
|
private void Awake()
|
|
{
|
|
if( m_instance == null )
|
|
m_instance = this;
|
|
else if( this != m_instance )
|
|
{
|
|
Destroy( gameObject );
|
|
return;
|
|
}
|
|
|
|
orientedImageTransform = (RectTransform) m_orientedImage.transform;
|
|
viewportSizeChangeListener.onSizeChanged = OnViewportDimensionsChange;
|
|
|
|
cropRenderCanvas.gameObject.layer = 22; // some random layer that is hopefully not used by any other object
|
|
cropRenderImage.gameObject.layer = 22;
|
|
cropRenderCamera.cullingMask = 1 << 22;
|
|
cropRenderCanvas.gameObject.SetActive( false );
|
|
|
|
gameObject.SetActive( false );
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
autoZoomCoroutine = null;
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
if( gameObject.activeInHierarchy )
|
|
{
|
|
if( currentSelectionHandler != null && m_imageHolder.localScale.z > minImageScale + 0.01f )
|
|
currentSelectionHandler.OnUpdate();
|
|
|
|
if( shouldRefreshViewport )
|
|
{
|
|
textsSynchronizer.Synchronize();
|
|
ResetView( true );
|
|
|
|
shouldRefreshViewport = false;
|
|
}
|
|
|
|
selectionGraphicsSynchronizer.Synchronize();
|
|
}
|
|
}
|
|
|
|
private void OnViewportDimensionsChange( Vector2 size )
|
|
{
|
|
m_viewportSize = size;
|
|
shouldRefreshViewport = true;
|
|
}
|
|
|
|
public void Show( Texture image, Settings settings = null )
|
|
{
|
|
Show( image, null, settings, null );
|
|
}
|
|
|
|
public void Show( Texture image, CropResult onCrop, Settings settings = null, ImageResizePolicy croppedImageResizePolicy = null )
|
|
{
|
|
if( image == null )
|
|
{
|
|
Debug.LogError( "Image is null!" );
|
|
return;
|
|
}
|
|
|
|
if( !gameObject.activeSelf )
|
|
gameObject.SetActive( true );
|
|
|
|
cropCallback = onCrop;
|
|
imageResizePolicy = croppedImageResizePolicy;
|
|
|
|
if( settings == null )
|
|
settings = DefaultSettings;
|
|
|
|
m_orientedImage.texture = image;
|
|
|
|
m_originalImageSize = new Vector2( image.width, image.height );
|
|
orientedImageTransform.sizeDelta = m_originalImageSize;
|
|
|
|
MarkTextureNonReadable = settings.markTextureNonReadable;
|
|
OvalSelection = settings.ovalSelection;
|
|
GuidelinesVisibility = settings.guidelinesVisibility;
|
|
ImageBackground = settings.imageBackground;
|
|
|
|
minAspectRatio = settings.selectionMinAspectRatio;
|
|
maxAspectRatio = settings.selectionMaxAspectRatio;
|
|
|
|
if( minAspectRatio <= 0f )
|
|
minAspectRatio = 1E-6f;
|
|
if( maxAspectRatio <= 0f )
|
|
maxAspectRatio = 1E6f;
|
|
|
|
if( minAspectRatio > maxAspectRatio )
|
|
{
|
|
float temp = minAspectRatio;
|
|
minAspectRatio = maxAspectRatio;
|
|
maxAspectRatio = temp;
|
|
}
|
|
|
|
minSize = settings.selectionMinSize;
|
|
maxSize = settings.selectionMaxSize;
|
|
|
|
Vector2 maxSizeDefault = new Vector2( 2f, 2f ) * Mathf.Max( m_originalImageSize.x, m_originalImageSize.y );
|
|
if( minSize.x < 1f || minSize.y < 1f )
|
|
minSize = new Vector2( 0.1f, 0.1f ) * Mathf.Min( m_originalImageSize.x, m_originalImageSize.y );
|
|
if( maxSize.x < 1f || maxSize.y < 1f )
|
|
maxSize = maxSizeDefault;
|
|
|
|
minSize = minSize.ClampBetween( Vector2.one, Vector2.one * Mathf.Max( m_originalImageSize.x, m_originalImageSize.y ) );
|
|
maxSize = maxSize.ClampBetween( minSize, maxSizeDefault );
|
|
|
|
m_autoZoomEnabled = false;
|
|
SetOrientation( settings.initialOrientation );
|
|
|
|
m_autoZoomEnabled = settings.autoZoomEnabled;
|
|
m_pixelPerfectSelection = settings.pixelPerfectSelection;
|
|
|
|
flipHorizontalButton.SetActive( ( settings.visibleButtons & Button.FlipHorizontal ) == Button.FlipHorizontal );
|
|
flipVerticalButton.SetActive( ( settings.visibleButtons & Button.FlipVertical ) == Button.FlipVertical );
|
|
rotate90DegreesButton.SetActive( ( settings.visibleButtons & Button.Rotate90Degrees ) == Button.Rotate90Degrees );
|
|
|
|
Vector2 paddingMax = new Vector2( 1f - Mathf.Clamp01( settings.selectionInitialPaddingRight ), 1f - Mathf.Clamp01( settings.selectionInitialPaddingTop ) );
|
|
Vector2 paddingMin = new Vector2( Mathf.Clamp( settings.selectionInitialPaddingLeft, 0f, paddingMax.x ), Mathf.Clamp( settings.selectionInitialPaddingBottom, 0f, paddingMax.y ) );
|
|
|
|
UpdateSelection( Vector2.zero, m_orientedImageSize.ScaleWith( paddingMax - paddingMin ) );
|
|
m_selection.anchoredPosition = ( m_orientedImageSize - m_selection.sizeDelta ) * 0.5f;
|
|
if( m_pixelPerfectSelection )
|
|
MakePixelPerfectSelection();
|
|
|
|
ResetView( false );
|
|
}
|
|
|
|
public void Hide()
|
|
{
|
|
gameObject.SetActive( false );
|
|
|
|
m_orientedImage.texture = null;
|
|
cropCallback = null;
|
|
imageResizePolicy = null;
|
|
}
|
|
|
|
public void ResetView( bool frameSelection )
|
|
{
|
|
if( m_orientedImageSize.x <= 0f || m_orientedImageSize.y <= 0f )
|
|
return;
|
|
|
|
StopAutoZoom();
|
|
|
|
minImageScale = Mathf.Min( m_viewportSize.x / m_orientedImageSize.x, m_viewportSize.y / m_orientedImageSize.y );
|
|
|
|
m_imageHolder.anchoredPosition = RestrictImageToViewport( m_imageHolder.anchoredPosition, minImageScale );
|
|
m_imageHolder.localScale = new Vector3( minImageScale, minImageScale, minImageScale );
|
|
|
|
if( frameSelection && m_autoZoomEnabled )
|
|
StartAutoZoom( true );
|
|
}
|
|
|
|
public void Cancel()
|
|
{
|
|
if( cropCallback != null )
|
|
cropCallback( false, m_orientedImage.texture, null );
|
|
|
|
Hide();
|
|
}
|
|
|
|
public void Crop()
|
|
{
|
|
if( cropCallback != null )
|
|
{
|
|
Texture2D result = CropSelection();
|
|
cropCallback( result != null, m_orientedImage.texture, result );
|
|
}
|
|
|
|
Hide();
|
|
}
|
|
|
|
public Texture2D CropSelection()
|
|
{
|
|
Vector2 selectionSize = m_selection.sizeDelta;
|
|
int width = Mathf.Clamp( (int) selectionSize.x, 1, (int) m_orientedImageSize.x );
|
|
int height = Mathf.Clamp( (int) selectionSize.y, 1, (int) m_orientedImageSize.y );
|
|
if( imageResizePolicy != null )
|
|
imageResizePolicy( ref width, ref height );
|
|
|
|
if( width < 0 )
|
|
width = 1;
|
|
if( height < 0 )
|
|
height = 1;
|
|
|
|
return CropSelection( width, height );
|
|
}
|
|
|
|
public Texture2D CropSelection( int width, int height )
|
|
{
|
|
if( !gameObject.activeInHierarchy )
|
|
{
|
|
Debug.LogError( "Cropper is not visible!" );
|
|
return null;
|
|
}
|
|
|
|
if( m_orientedImage.texture == null )
|
|
{
|
|
Debug.LogError( "Cropper is not initialized!" );
|
|
return null;
|
|
}
|
|
|
|
// Make sure that cropped image dimensions do not exceed maximum supported texture size
|
|
int maxTextureSize = SystemInfo.maxTextureSize;
|
|
if( width > maxTextureSize || height > maxTextureSize )
|
|
{
|
|
int preferredWidth = (int) ( maxTextureSize * width / (float) height );
|
|
int preferredHeight = (int) ( maxTextureSize * height / (float) width );
|
|
|
|
if( preferredWidth <= maxTextureSize )
|
|
preferredHeight = maxTextureSize;
|
|
else
|
|
preferredWidth = maxTextureSize;
|
|
|
|
Debug.Log( string.Concat( width, "x", height, " is too large, using ", preferredWidth, "x", preferredHeight, " instead..." ) );
|
|
|
|
width = preferredWidth;
|
|
height = preferredHeight;
|
|
}
|
|
|
|
RectTransform cropRenderCanvasTR = (RectTransform) cropRenderCanvas.transform;
|
|
RectTransform cropRenderImageTR = (RectTransform) cropRenderImage.transform;
|
|
Transform cropRenderCameraTR = cropRenderCamera.transform;
|
|
|
|
cropRenderImage.texture = m_orientedImage.texture;
|
|
|
|
Vector2 selectionSize = m_selection.sizeDelta;
|
|
Vector2 selectionCenter = m_selection.anchoredPosition + selectionSize * 0.5f;
|
|
|
|
cropRenderCanvasTR.sizeDelta = m_orientedImageSize;
|
|
|
|
cropRenderSelection.anchoredPosition = m_selection.anchoredPosition;
|
|
cropRenderSelection.sizeDelta = selectionSize;
|
|
|
|
cropRenderImageTR.position = cropRenderCanvasTR.position;
|
|
cropRenderImageTR.sizeDelta = m_originalImageSize;
|
|
cropRenderImageTR.localScale = orientedImageTransform.localScale;
|
|
cropRenderImageTR.localEulerAngles = orientedImageTransform.localEulerAngles;
|
|
|
|
cropRenderCameraTR.eulerAngles = cropRenderCanvasTR.eulerAngles;
|
|
cropRenderCameraTR.position = cropRenderCanvasTR.TransformPoint( new Vector3( selectionCenter.x - m_orientedImageSize.x * 0.5f, selectionCenter.y - m_orientedImageSize.y * 0.5f, -5f ) );
|
|
|
|
cropRenderCamera.aspect = selectionSize.x / selectionSize.y;
|
|
cropRenderCamera.orthographicSize = selectionSize.y * 0.5f;
|
|
|
|
Texture2D result = null;
|
|
RenderTexture temp = RenderTexture.active;
|
|
RenderTexture renderTex = RenderTexture.GetTemporary( width, height, 24 );
|
|
try
|
|
{
|
|
cropRenderCanvas.gameObject.SetActive( true );
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate( cropRenderCanvasTR );
|
|
|
|
RenderTexture.active = renderTex;
|
|
|
|
bool transparentBackground = ImageBackground.a < 1f;
|
|
if( transparentBackground )
|
|
{
|
|
cropRenderCamera.clearFlags = CameraClearFlags.Depth;
|
|
GL.Clear( false, true, ImageBackground );
|
|
}
|
|
else
|
|
cropRenderCamera.clearFlags = CameraClearFlags.Color;
|
|
|
|
cropRenderCamera.backgroundColor = ImageBackground;
|
|
cropRenderCamera.targetTexture = renderTex;
|
|
cropRenderCamera.Render();
|
|
|
|
result = new Texture2D( width, height, transparentBackground ? TextureFormat.RGBA32 : TextureFormat.RGB24, false );
|
|
result.ReadPixels( new Rect( 0, 0, width, height ), 0, 0, false );
|
|
result.Apply( false, MarkTextureNonReadable );
|
|
}
|
|
catch( System.Exception e )
|
|
{
|
|
Debug.LogException( e );
|
|
|
|
if( result != null )
|
|
{
|
|
DestroyImmediate( result );
|
|
result = null;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
RenderTexture.active = temp;
|
|
RenderTexture.ReleaseTemporary( renderTex );
|
|
|
|
cropRenderCanvas.gameObject.SetActive( false );
|
|
cropRenderImage.texture = null;
|
|
cropRenderCamera.targetTexture = null;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public void Rotate90Clockwise()
|
|
{
|
|
RotateClockwise( 1 );
|
|
}
|
|
|
|
public void Rotate180Clockwise()
|
|
{
|
|
RotateClockwise( 2 );
|
|
}
|
|
|
|
public void Rotate270Clockwise()
|
|
{
|
|
RotateClockwise( 3 );
|
|
}
|
|
|
|
private void RotateClockwise( int amount )
|
|
{
|
|
while( amount < 0 )
|
|
amount += 4;
|
|
while( amount > 3 )
|
|
amount -= 4;
|
|
|
|
if( amount == 0 )
|
|
return;
|
|
|
|
int orientationInt = (int) currentOrientation;
|
|
if( orientationInt < 4 )
|
|
{
|
|
orientationInt -= amount;
|
|
if( orientationInt < 0 )
|
|
orientationInt += 4;
|
|
}
|
|
else
|
|
{
|
|
orientationInt -= amount;
|
|
if( orientationInt < 4 )
|
|
orientationInt += 4;
|
|
}
|
|
|
|
SetOrientation( (Orientation) orientationInt );
|
|
}
|
|
|
|
public void FlipHorizontal()
|
|
{
|
|
if( currentOrientation == Orientation.Normal )
|
|
SetOrientation( Orientation.FlipHorizontal );
|
|
else if( currentOrientation == Orientation.FlipHorizontal )
|
|
SetOrientation( Orientation.Normal );
|
|
else if( currentOrientation == Orientation.Rotate90 )
|
|
SetOrientation( Orientation.Transverse );
|
|
else if( currentOrientation == Orientation.Transverse )
|
|
SetOrientation( Orientation.Rotate90 );
|
|
else if( currentOrientation == Orientation.Rotate180 )
|
|
SetOrientation( Orientation.FlipVertical );
|
|
else if( currentOrientation == Orientation.FlipVertical )
|
|
SetOrientation( Orientation.Rotate180 );
|
|
else if( currentOrientation == Orientation.Rotate270 )
|
|
SetOrientation( Orientation.Transpose );
|
|
else
|
|
SetOrientation( Orientation.Rotate270 );
|
|
}
|
|
|
|
public void FlipVertical()
|
|
{
|
|
if( currentOrientation == Orientation.Normal )
|
|
SetOrientation( Orientation.FlipVertical );
|
|
else if( currentOrientation == Orientation.FlipVertical )
|
|
SetOrientation( Orientation.Normal );
|
|
else if( currentOrientation == Orientation.Rotate90 )
|
|
SetOrientation( Orientation.Transpose );
|
|
else if( currentOrientation == Orientation.Transpose )
|
|
SetOrientation( Orientation.Rotate90 );
|
|
else if( currentOrientation == Orientation.Rotate180 )
|
|
SetOrientation( Orientation.FlipHorizontal );
|
|
else if( currentOrientation == Orientation.FlipHorizontal )
|
|
SetOrientation( Orientation.Rotate180 );
|
|
else if( currentOrientation == Orientation.Rotate270 )
|
|
SetOrientation( Orientation.Transverse );
|
|
else
|
|
SetOrientation( Orientation.Rotate270 );
|
|
}
|
|
|
|
public void SetOrientation( Orientation orientation )
|
|
{
|
|
Vector2 selectionPosition = m_selection.anchoredPosition;
|
|
Vector2 selectionSize = m_selection.sizeDelta;
|
|
|
|
if( currentOrientation == Orientation.Normal )
|
|
{
|
|
}
|
|
else if( currentOrientation == Orientation.Rotate90 )
|
|
{
|
|
selectionPosition = new Vector2( selectionPosition.y, m_orientedImageSize.x - selectionPosition.x - selectionSize.x );
|
|
selectionSize = new Vector2( selectionSize.y, selectionSize.x );
|
|
}
|
|
else if( currentOrientation == Orientation.Rotate180 )
|
|
selectionPosition = new Vector2( m_orientedImageSize.x - selectionPosition.x - selectionSize.x, m_orientedImageSize.y - selectionPosition.y - selectionSize.y );
|
|
else if( currentOrientation == Orientation.Rotate270 )
|
|
{
|
|
selectionPosition = new Vector2( m_orientedImageSize.y - selectionPosition.y - selectionSize.y, selectionPosition.x );
|
|
selectionSize = new Vector2( selectionSize.y, selectionSize.x );
|
|
}
|
|
else if( currentOrientation == Orientation.FlipHorizontal )
|
|
selectionPosition = new Vector2( m_orientedImageSize.x - selectionPosition.x - selectionSize.x, selectionPosition.y );
|
|
else if( currentOrientation == Orientation.Transpose )
|
|
{
|
|
selectionPosition = new Vector2( m_orientedImageSize.y - selectionPosition.y - selectionSize.y, m_orientedImageSize.x - selectionPosition.x - selectionSize.x );
|
|
selectionSize = new Vector2( selectionSize.y, selectionSize.x );
|
|
}
|
|
else if( currentOrientation == Orientation.FlipVertical )
|
|
selectionPosition = new Vector2( selectionPosition.x, m_orientedImageSize.y - selectionPosition.y - selectionSize.y );
|
|
else
|
|
{
|
|
selectionPosition = new Vector2( selectionPosition.y, selectionPosition.x );
|
|
selectionSize = new Vector2( selectionSize.y, selectionSize.x );
|
|
}
|
|
|
|
if( orientation == Orientation.Normal )
|
|
{
|
|
m_orientedImageSize = m_originalImageSize;
|
|
|
|
orientedImageTransform.localScale = Vector3.one;
|
|
orientedImageTransform.localEulerAngles = Vector3.zero;
|
|
}
|
|
else if( orientation == Orientation.Rotate90 )
|
|
{
|
|
m_orientedImageSize = new Vector2( m_originalImageSize.y, m_originalImageSize.x );
|
|
|
|
orientedImageTransform.localScale = Vector3.one;
|
|
orientedImageTransform.localEulerAngles = new Vector3( 0f, 0f, 90f );
|
|
|
|
selectionPosition = new Vector2( m_originalImageSize.y - selectionPosition.y - selectionSize.y, selectionPosition.x );
|
|
selectionSize = new Vector2( selectionSize.y, selectionSize.x );
|
|
}
|
|
else if( orientation == Orientation.Rotate180 )
|
|
{
|
|
m_orientedImageSize = m_originalImageSize;
|
|
|
|
orientedImageTransform.localScale = Vector3.one;
|
|
orientedImageTransform.localEulerAngles = new Vector3( 0f, 0f, 180f );
|
|
|
|
selectionPosition = new Vector2( m_originalImageSize.x - selectionPosition.x - selectionSize.x, m_originalImageSize.y - selectionPosition.y - selectionSize.y );
|
|
}
|
|
else if( orientation == Orientation.Rotate270 )
|
|
{
|
|
m_orientedImageSize = new Vector2( m_originalImageSize.y, m_originalImageSize.x );
|
|
|
|
orientedImageTransform.localScale = Vector3.one;
|
|
orientedImageTransform.localEulerAngles = new Vector3( 0f, 0f, 270f );
|
|
|
|
selectionPosition = new Vector2( selectionPosition.y, m_originalImageSize.x - selectionPosition.x - selectionSize.x );
|
|
selectionSize = new Vector2( selectionSize.y, selectionSize.x );
|
|
}
|
|
else if( orientation == Orientation.FlipHorizontal )
|
|
{
|
|
m_orientedImageSize = m_originalImageSize;
|
|
|
|
orientedImageTransform.localScale = new Vector3( -1f, 1f, 1f );
|
|
orientedImageTransform.localEulerAngles = Vector3.zero;
|
|
|
|
selectionPosition = new Vector2( m_originalImageSize.x - selectionPosition.x - selectionSize.x, selectionPosition.y );
|
|
}
|
|
else if( orientation == Orientation.Transpose )
|
|
{
|
|
m_orientedImageSize = new Vector2( m_originalImageSize.y, m_originalImageSize.x );
|
|
|
|
orientedImageTransform.localScale = new Vector3( -1f, 1f, 1f );
|
|
orientedImageTransform.localEulerAngles = new Vector3( 0f, 0f, 90f );
|
|
|
|
selectionPosition = new Vector2( m_originalImageSize.y - selectionPosition.y - selectionSize.y, m_originalImageSize.x - selectionPosition.x - selectionSize.x );
|
|
selectionSize = new Vector2( selectionSize.y, selectionSize.x );
|
|
}
|
|
else if( orientation == Orientation.FlipVertical )
|
|
{
|
|
m_orientedImageSize = m_originalImageSize;
|
|
|
|
orientedImageTransform.localScale = new Vector3( -1f, 1f, 1f );
|
|
orientedImageTransform.localEulerAngles = new Vector3( 0f, 0f, 180f );
|
|
|
|
selectionPosition = new Vector2( selectionPosition.x, m_originalImageSize.y - selectionPosition.y - selectionSize.y );
|
|
}
|
|
else
|
|
{
|
|
m_orientedImageSize = new Vector2( m_originalImageSize.y, m_originalImageSize.x );
|
|
|
|
orientedImageTransform.localScale = new Vector3( -1f, 1f, 1f );
|
|
orientedImageTransform.localEulerAngles = new Vector3( 0f, 0f, 270f );
|
|
|
|
selectionPosition = new Vector2( selectionPosition.y, selectionPosition.x );
|
|
selectionSize = new Vector2( selectionSize.y, selectionSize.x );
|
|
}
|
|
|
|
m_imageHolder.sizeDelta = m_orientedImageSize;
|
|
|
|
currMinSize = minSize.ClampBetween( Vector2.one, m_orientedImageSize );
|
|
currMaxSize = maxSize.ClampBetween( currMinSize, m_orientedImageSize );
|
|
|
|
// If new selection doesn't comply with limiting aspect ratios, keep original selection
|
|
float aspectRatio = selectionSize.x / selectionSize.y;
|
|
if( aspectRatio < minAspectRatio - 1E-4f ) // consider floating-point precision
|
|
{
|
|
// Pixel-perfect selection may not always comply with limiting aspect ratios
|
|
if( !m_pixelPerfectSelection || ( selectionSize.x + 1 ) / ( selectionSize.y - 1 ) < minAspectRatio - 1E-4f )
|
|
selectionSize = m_selection.sizeDelta;
|
|
}
|
|
else if( aspectRatio > maxAspectRatio + 1E-4f )
|
|
{
|
|
if( !m_pixelPerfectSelection || ( selectionSize.x - 1 ) / ( selectionSize.y + 1 ) > maxAspectRatio + 1E-4f )
|
|
selectionSize = m_selection.sizeDelta;
|
|
}
|
|
|
|
UpdateSelection( selectionPosition, selectionSize );
|
|
if( m_pixelPerfectSelection )
|
|
MakePixelPerfectSelection();
|
|
|
|
currentOrientation = orientation;
|
|
ResetView( true );
|
|
}
|
|
|
|
public void StartAutoZoom( bool instantZoom )
|
|
{
|
|
if( !gameObject.activeInHierarchy )
|
|
return;
|
|
|
|
StopAutoZoom();
|
|
|
|
Vector2 selectionSize = m_selection.sizeDelta;
|
|
Vector2 selectionSizeScaled = selectionSize * m_imageHolder.localScale.z;
|
|
|
|
float zoomAmount = -1f;
|
|
float scale = m_imageHolder.localScale.z;
|
|
float fillRate = Mathf.Max( selectionSizeScaled.x / m_viewportSize.x, selectionSizeScaled.y / m_viewportSize.y );
|
|
if( fillRate <= autoZoomInThreshold )
|
|
{
|
|
float scaleX = m_viewportSize.x * autoZoomInFillAmount / selectionSize.x;
|
|
float scaleY = m_viewportSize.y * autoZoomInFillAmount / selectionSize.y;
|
|
|
|
zoomAmount = Mathf.Min( scaleX, scaleY );
|
|
}
|
|
else if( fillRate >= autoZoomOutThreshold )
|
|
{
|
|
float scaleX = m_viewportSize.x * autoZoomOutFillAmount / selectionSize.x;
|
|
float scaleY = m_viewportSize.y * autoZoomOutFillAmount / selectionSize.y;
|
|
|
|
zoomAmount = Mathf.Min( scaleX, scaleY );
|
|
}
|
|
else
|
|
{
|
|
Vector2 selectionBottomLeft = m_imageHolder.anchoredPosition + m_selection.anchoredPosition * scale;
|
|
Vector2 selectionTopRight = selectionBottomLeft + m_selection.sizeDelta * scale;
|
|
|
|
// If part of the selection rests outside of the viewport, engage auto-zoom
|
|
if( selectionBottomLeft.x < -1E-4f || selectionBottomLeft.y < -1E-4f || selectionTopRight.x > m_viewportSize.x + 1E-4f || selectionTopRight.y > m_viewportSize.y + 1E-4f )
|
|
zoomAmount = scale;
|
|
}
|
|
|
|
if( zoomAmount < 0f )
|
|
return;
|
|
|
|
if( zoomAmount < minImageScale )
|
|
zoomAmount = minImageScale;
|
|
|
|
if( Mathf.Abs( zoomAmount - scale ) < 0.001f )
|
|
instantZoom = true;
|
|
|
|
autoZoomCoroutine = AutoZoom( zoomAmount, instantZoom );
|
|
StartCoroutine( autoZoomCoroutine );
|
|
}
|
|
|
|
private IEnumerator AutoZoom( float zoomAmount, bool instantZoom )
|
|
{
|
|
float elapsed = 0f;
|
|
float length = autoZoomCurve.length == 0 ? 0f : autoZoomCurve[autoZoomCurve.length - 1].time;
|
|
|
|
Vector3 initialScale = m_imageHolder.localScale;
|
|
Vector2 initialPosition = m_imageHolder.anchoredPosition;
|
|
|
|
Vector3 finalScale = new Vector3( zoomAmount, zoomAmount, zoomAmount );
|
|
Vector2 finalPosition = m_viewportSize * 0.5f - ( m_selection.anchoredPosition + m_selection.sizeDelta * 0.5f ) * finalScale.z;
|
|
|
|
finalPosition = RestrictImageToViewport( finalPosition, zoomAmount );
|
|
|
|
if( !instantZoom && elapsed < length )
|
|
{
|
|
Vector2 deltaPosition = finalPosition - initialPosition;
|
|
Vector3 deltaScale = finalScale - initialScale;
|
|
|
|
while( elapsed < length )
|
|
{
|
|
yield return null;
|
|
elapsed += Time.unscaledDeltaTime;
|
|
if( elapsed >= length )
|
|
break;
|
|
|
|
float modifier = autoZoomCurve.Evaluate( elapsed );
|
|
|
|
m_imageHolder.anchoredPosition = initialPosition + deltaPosition * modifier;
|
|
m_imageHolder.localScale = initialScale + deltaScale * modifier;
|
|
}
|
|
}
|
|
|
|
m_imageHolder.anchoredPosition = finalPosition;
|
|
m_imageHolder.localScale = finalScale;
|
|
|
|
autoZoomCoroutine = null;
|
|
}
|
|
|
|
private void StopAutoZoom()
|
|
{
|
|
if( autoZoomCoroutine != null )
|
|
{
|
|
StopCoroutine( autoZoomCoroutine );
|
|
autoZoomCoroutine = null;
|
|
}
|
|
|
|
if( currentSelectionHandler != null )
|
|
{
|
|
currentSelectionHandler.Stop();
|
|
currentSelectionHandler = null;
|
|
}
|
|
}
|
|
|
|
public bool CanModifySelectionWith( ISelectionHandler handler )
|
|
{
|
|
if( autoZoomCoroutine != null )
|
|
return false;
|
|
|
|
if( handler != currentSelectionHandler )
|
|
{
|
|
if( currentSelectionHandler != null )
|
|
currentSelectionHandler.Stop();
|
|
|
|
currentSelectionHandler = handler;
|
|
}
|
|
|
|
if( m_guidelinesVisibility == Visibility.OnDrag )
|
|
{
|
|
for( int i = 0; i < guidelines.Length; i++ )
|
|
guidelines[i].enabled = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void StopModifySelectionWith( ISelectionHandler handler )
|
|
{
|
|
if( currentSelectionHandler == handler )
|
|
{
|
|
currentSelectionHandler = null;
|
|
|
|
if( m_guidelinesVisibility == Visibility.OnDrag )
|
|
{
|
|
for( int i = 0; i < guidelines.Length; i++ )
|
|
guidelines[i].enabled = false;
|
|
}
|
|
|
|
if( m_pixelPerfectSelection )
|
|
MakePixelPerfectSelection();
|
|
|
|
if( m_autoZoomEnabled )
|
|
StartAutoZoom( false );
|
|
}
|
|
}
|
|
|
|
public void MakePixelPerfectSelection()
|
|
{
|
|
Vector2 size = m_selection.sizeDelta.RoundToInt().ClampBetween( currMinSize.CeilToInt(), m_orientedImageSize );
|
|
float aspectRatio = size.x / size.y;
|
|
if( aspectRatio < minAspectRatio )
|
|
{
|
|
bool foundMatchingSize = false;
|
|
bool canExpandWidth = size.x < m_orientedImageSize.x;
|
|
bool canShrinkHeight = size.y > Mathf.CeilToInt( currMinSize.y );
|
|
|
|
if( canExpandWidth )
|
|
{
|
|
aspectRatio = ( size.x + 1 ) / size.y;
|
|
if( aspectRatio >= minAspectRatio && aspectRatio <= maxAspectRatio )
|
|
{
|
|
size.x = size.x + 1;
|
|
foundMatchingSize = true;
|
|
}
|
|
}
|
|
|
|
if( !foundMatchingSize && canShrinkHeight )
|
|
{
|
|
aspectRatio = size.x / ( size.y - 1 );
|
|
if( aspectRatio >= minAspectRatio && aspectRatio <= maxAspectRatio )
|
|
{
|
|
size.y = size.y - 1;
|
|
foundMatchingSize = true;
|
|
}
|
|
}
|
|
|
|
if( !foundMatchingSize && canExpandWidth && canShrinkHeight )
|
|
{
|
|
aspectRatio = ( size.x + 1 ) / ( size.y - 1 );
|
|
if( aspectRatio >= minAspectRatio && aspectRatio <= maxAspectRatio )
|
|
{
|
|
size.x = size.x + 1;
|
|
size.y = size.y - 1;
|
|
}
|
|
}
|
|
}
|
|
else if( aspectRatio > maxAspectRatio )
|
|
{
|
|
bool foundMatchingSize = false;
|
|
bool canShrinkWidth = size.x > Mathf.CeilToInt( currMinSize.x );
|
|
bool canExpandHeight = size.y < m_orientedImageSize.y;
|
|
|
|
if( !foundMatchingSize && canShrinkWidth )
|
|
{
|
|
aspectRatio = ( size.x - 1 ) / size.y;
|
|
if( aspectRatio >= minAspectRatio && aspectRatio <= maxAspectRatio )
|
|
{
|
|
size.x = size.x - 1;
|
|
foundMatchingSize = true;
|
|
}
|
|
}
|
|
|
|
if( canExpandHeight )
|
|
{
|
|
aspectRatio = size.x / ( size.y + 1 );
|
|
if( aspectRatio >= minAspectRatio && aspectRatio <= maxAspectRatio )
|
|
{
|
|
size.y = size.y + 1;
|
|
foundMatchingSize = true;
|
|
}
|
|
}
|
|
|
|
if( !foundMatchingSize && canShrinkWidth && canExpandHeight )
|
|
{
|
|
aspectRatio = ( size.x - 1 ) / ( size.y + 1 );
|
|
if( aspectRatio >= minAspectRatio && aspectRatio <= maxAspectRatio )
|
|
{
|
|
size.x = size.x - 1;
|
|
size.y = size.y + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
m_selection.anchoredPosition = m_selection.anchoredPosition.RoundToInt().ClampBetween( Vector2.zero, m_orientedImageSize - size );
|
|
m_selection.sizeDelta = size;
|
|
}
|
|
|
|
public void UpdateSelection( Vector2 position )
|
|
{
|
|
m_selection.anchoredPosition = position.ClampBetween( Vector2.zero, m_orientedImageSize - m_selection.sizeDelta );
|
|
}
|
|
|
|
public void UpdateSelection( Vector2 position, Vector2 size, Direction pivot = Direction.None, bool shrinkToFit = true )
|
|
{
|
|
Vector2 newSize = size.ClampBetween( currMinSize, currMaxSize );
|
|
|
|
float aspectRatio = newSize.x / newSize.y;
|
|
if( aspectRatio < minAspectRatio )
|
|
{
|
|
if( shrinkToFit )
|
|
{
|
|
float requiredHeight = newSize.x / minAspectRatio;
|
|
if( requiredHeight >= currMinSize.y )
|
|
newSize.y = requiredHeight;
|
|
else
|
|
{
|
|
float requiredWidth = currMinSize.y * minAspectRatio;
|
|
if( requiredWidth <= currMaxSize.x )
|
|
newSize = new Vector2( requiredWidth, currMinSize.y );
|
|
else
|
|
newSize = new Vector2( currMaxSize.x, currMinSize.y );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float requiredWidth = newSize.y * minAspectRatio;
|
|
if( requiredWidth <= currMaxSize.x )
|
|
newSize.x = requiredWidth;
|
|
else
|
|
{
|
|
float requiredHeight = currMaxSize.x / minAspectRatio;
|
|
if( requiredHeight <= currMaxSize.y )
|
|
newSize = new Vector2( currMaxSize.x, requiredHeight );
|
|
else
|
|
newSize = currMaxSize;
|
|
}
|
|
}
|
|
}
|
|
else if( aspectRatio > maxAspectRatio )
|
|
{
|
|
if( shrinkToFit )
|
|
{
|
|
float requiredWidth = newSize.y * maxAspectRatio;
|
|
if( requiredWidth >= currMinSize.x )
|
|
newSize.x = requiredWidth;
|
|
else
|
|
{
|
|
float requiredHeight = currMinSize.x / maxAspectRatio;
|
|
if( requiredHeight <= currMaxSize.y )
|
|
newSize = new Vector2( currMinSize.x, requiredHeight );
|
|
else
|
|
newSize = new Vector2( currMinSize.x, currMaxSize.y );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float requiredHeight = newSize.x / maxAspectRatio;
|
|
if( requiredHeight <= currMaxSize.y )
|
|
newSize.y = requiredHeight;
|
|
else
|
|
{
|
|
float requiredWidth = currMaxSize.y * maxAspectRatio;
|
|
if( requiredWidth <= currMaxSize.x )
|
|
newSize = new Vector2( requiredWidth, currMaxSize.y );
|
|
else
|
|
newSize = currMaxSize;
|
|
}
|
|
}
|
|
}
|
|
|
|
if( size.x != newSize.x )
|
|
{
|
|
if( ( pivot & Direction.Right ) == Direction.Right )
|
|
position.x -= newSize.x - size.x;
|
|
else if( ( pivot & Direction.Left ) != Direction.Left )
|
|
position.x -= ( newSize.x - size.x ) * 0.5f;
|
|
|
|
size.x = newSize.x;
|
|
}
|
|
if( size.y != newSize.y )
|
|
{
|
|
if( ( pivot & Direction.Top ) == Direction.Top )
|
|
position.y -= newSize.y - size.y;
|
|
else if( ( pivot & Direction.Bottom ) != Direction.Bottom )
|
|
position.y -= ( newSize.y - size.y ) * 0.5f;
|
|
|
|
size.y = newSize.y;
|
|
}
|
|
|
|
m_selection.anchoredPosition = position.ClampBetween( Vector2.zero, m_orientedImageSize - size );
|
|
m_selection.sizeDelta = size;
|
|
}
|
|
|
|
private Vector2 RestrictImageToViewport( Vector2 position, float scale )
|
|
{
|
|
Vector2 sizeScaled = m_imageHolder.sizeDelta * scale;
|
|
|
|
if( sizeScaled.x < m_viewportSize.x )
|
|
position.x = ( m_viewportSize.x - sizeScaled.x ) * 0.5f;
|
|
else
|
|
position.x = Mathf.Clamp( position.x, m_viewportSize.x - sizeScaled.x, 0f );
|
|
|
|
if( sizeScaled.y < m_viewportSize.y )
|
|
position.y = ( m_viewportSize.y - sizeScaled.y ) * 0.5f;
|
|
else
|
|
position.y = Mathf.Clamp( position.y, m_viewportSize.y - sizeScaled.y, 0f );
|
|
|
|
return position;
|
|
}
|
|
|
|
public Vector2 ScrollImage( Vector2 imagePosition, Direction direction )
|
|
{
|
|
if( direction == Direction.Left )
|
|
{
|
|
imagePosition.x += viewportScrollSpeed * Time.unscaledDeltaTime;
|
|
if( imagePosition.x > 0f )
|
|
imagePosition.x = 0f;
|
|
}
|
|
else if( direction == Direction.Top )
|
|
{
|
|
float imageHeight = m_imageHolder.sizeDelta.y * m_imageHolder.localScale.z;
|
|
imagePosition.y -= viewportScrollSpeed * Time.unscaledDeltaTime;
|
|
if( imagePosition.y + imageHeight < m_viewportSize.y )
|
|
imagePosition.y = m_viewportSize.y - imageHeight;
|
|
}
|
|
else if( direction == Direction.Right )
|
|
{
|
|
float imageWidth = m_imageHolder.sizeDelta.x * m_imageHolder.localScale.z;
|
|
imagePosition.x -= viewportScrollSpeed * Time.unscaledDeltaTime;
|
|
if( imagePosition.x + imageWidth < m_viewportSize.x )
|
|
imagePosition.x = m_viewportSize.x - imageWidth;
|
|
}
|
|
else
|
|
{
|
|
imagePosition.y += viewportScrollSpeed * Time.unscaledDeltaTime;
|
|
if( imagePosition.y > 0f )
|
|
imagePosition.y = 0f;
|
|
}
|
|
|
|
return imagePosition;
|
|
}
|
|
|
|
public Vector2 GetTouchPosition( Vector2 screenPos, Camera cam )
|
|
{
|
|
Vector2 localPos;
|
|
RectTransformUtility.ScreenPointToLocalPointInRectangle( m_imageHolder, screenPos, cam, out localPos );
|
|
|
|
return localPos;
|
|
}
|
|
} |