Index: remoting/ios/ui/scene_view.mm |
diff --git a/remoting/ios/ui/scene_view.mm b/remoting/ios/ui/scene_view.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..97a577df775849732402ac4809cc22b272e617d8 |
--- /dev/null |
+++ b/remoting/ios/ui/scene_view.mm |
@@ -0,0 +1,642 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#if !defined(__has_feature) || !__has_feature(objc_arc) |
+#error "This file requires ARC support." |
+#endif |
+ |
+#import "remoting/ios/ui/scene_view.h" |
+ |
+#import "remoting/ios/utility.h" |
+ |
+namespace { |
+ |
+// TODO (aboone) Some of the layout is not yet set in stone, so variables have |
+// been used to position and turn items on and off. Eventually these may be |
+// stabilized and removed. |
+ |
+// Scroll speed multiplier for swiping |
+const static int kMouseSensitivity = 2.5; |
+ |
+// Input Axis inversion |
+// 1 for standard, -1 for inverted |
+const static int kXAxisInversion = -1; |
+const static int kYAxisInversion = -1; |
+ |
+// Experimental value for bounding the maximum zoom ratio |
+const static int kMaxZoomSize = 3; |
+} // namespace |
+ |
+@interface SceneView (Private) |
+// Returns the number of pixels displayed per device pixel when the scaling is |
+// such that the entire frame would fit perfectly in content. Note the ratios |
+// are different for width and height, some people have multiple monitors, some |
+// have 16:9 or 4:3 while iPad is always single screen, but different iOS |
+// devices have different resolutions. |
+- (CGPoint)pixelRatio; |
+ |
+// Return the FrameSize in perspective of the CLIENT resolution |
+- (webrtc::DesktopSize)frameSizeToScale:(float)scale; |
+ |
+// When bounded on the top and right, this point is where the scene must be |
+// positioned given a scene size |
+- (webrtc::DesktopVector)getBoundsForSize:(const webrtc::DesktopSize&)size; |
+ |
+// Converts a point in the the CLIENT resolution to a similar point in the HOST |
+// resolution. Additionally, CLIENT resolution is expressed in float values |
+// while HOST operates in integer values. |
+- (BOOL)convertTouchPointToMousePoint:(CGPoint)touchPoint |
+ targetPoint:(webrtc::DesktopVector&)desktopPoint; |
+ |
+// Converts a point in the the HOST resolution to a similar point in the CLIENT |
+// resolution. Additionally, CLIENT resolution is expressed in float values |
+// while HOST operates in integer values. |
+- (BOOL)convertMousePointToTouchPoint:(const webrtc::DesktopVector&)mousePoint |
+ targetPoint:(CGPoint&)touchPoint; |
+@end |
+ |
+@implementation SceneView |
+ |
+- (id)init { |
+ self = [super init]; |
+ if (self) { |
+ |
+ _frameSize = webrtc::DesktopSize(1, 1); |
+ _contentSize = webrtc::DesktopSize(1, 1); |
+ _mousePosition = webrtc::DesktopVector(0, 0); |
+ |
+ _position = GLKVector3Make(0, 0, 1); |
+ _margin.left = 0; |
+ _margin.right = 0; |
+ _margin.top = 0; |
+ _margin.bottom = 0; |
+ _anchored.left = false; |
+ _anchored.right = false; |
+ _anchored.top = false; |
+ _anchored.bottom = false; |
+ } |
+ return self; |
+} |
+ |
+- (const GLKMatrix4&)projectionMatrix { |
+ return _projectionMatrix; |
+} |
+ |
+- (const GLKMatrix4&)modelViewMatrix { |
+ // Start by using the entire scene |
+ _modelViewMatrix = GLKMatrix4Identity; |
+ |
+ // Position scene according to any panning or bounds |
+ _modelViewMatrix = GLKMatrix4Translate(_modelViewMatrix, |
+ _position.x + _margin.left, |
+ _position.y + _margin.bottom, |
+ 0.0); |
+ |
+ // Apply zoom |
+ _modelViewMatrix = GLKMatrix4Scale(_modelViewMatrix, |
+ _position.z / self.pixelRatio.x, |
+ _position.z / self.pixelRatio.y, |
+ 1.0); |
+ |
+ // We are directly above the screen and looking down. |
+ static const GLKMatrix4 viewMatrix = GLKMatrix4MakeLookAt( |
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); // center view |
+ |
+ _modelViewMatrix = GLKMatrix4Multiply(viewMatrix, _modelViewMatrix); |
+ |
+ return _modelViewMatrix; |
+} |
+ |
+- (const webrtc::DesktopSize&)contentSize { |
+ return _contentSize; |
+} |
+ |
+- (void)setContentSize:(const CGSize&)size { |
+ |
+ _contentSize.set(size.width, size.height); |
+ |
+ _projectionMatrix = GLKMatrix4MakeOrtho( |
+ 0.0, _contentSize.width(), 0.0, _contentSize.height(), 1.0, -1.0); |
+ |
+ TexturedQuad newQuad; |
+ newQuad.bl.geometryVertex = CGPointMake(0.0, 0.0); |
+ newQuad.br.geometryVertex = CGPointMake(_contentSize.width(), 0.0); |
+ newQuad.tl.geometryVertex = CGPointMake(0.0, _contentSize.height()); |
+ newQuad.tr.geometryVertex = |
+ CGPointMake(_contentSize.width(), _contentSize.height()); |
+ |
+ newQuad.bl.textureVertex = CGPointMake(0.0, 1.0); |
+ newQuad.br.textureVertex = CGPointMake(1.0, 1.0); |
+ newQuad.tl.textureVertex = CGPointMake(0.0, 0.0); |
+ newQuad.tr.textureVertex = CGPointMake(1.0, 0.0); |
+ |
+ _glQuad = newQuad; |
+} |
+ |
+- (const webrtc::DesktopSize&)frameSize { |
+ return _frameSize; |
+} |
+ |
+- (void)setFrameSize:(const webrtc::DesktopSize&)size { |
+ DCHECK(size.width() > 0 && size.height() > 0); |
+ // Don't do anything if the size has not changed. |
+ if (_frameSize.equals(size)) |
+ return; |
+ |
+ _frameSize.set(size.width(), size.height()); |
+ |
+ _position.x = 0; |
+ _position.y = 0; |
+ |
+ float verticalPixelScaleRatio = |
+ (static_cast<float>(_contentSize.height() - _margin.top - |
+ _margin.bottom) / |
+ static_cast<float>(_frameSize.height())) / |
+ _position.z; |
+ |
+ // Anchored at the position (0,0) |
+ _anchored.left = YES; |
+ _anchored.right = NO; |
+ _anchored.top = NO; |
+ _anchored.bottom = YES; |
+ |
+ [self panAndZoom:CGPointMake(0.0, 0.0) scaleBy:verticalPixelScaleRatio]; |
+ |
+ // Center the mouse on the CLIENT screen |
+ webrtc::DesktopVector centerMouseLocation; |
+ if ([self convertTouchPointToMousePoint:CGPointMake(_contentSize.width() / 2, |
+ _contentSize.height() / 2) |
+ targetPoint:centerMouseLocation]) { |
+ _mousePosition.set(centerMouseLocation.x(), centerMouseLocation.y()); |
+ } |
+ |
+#if DEBUG |
+ NSLog(@"resized frame:%d:%d scale:%f", |
+ _frameSize.width(), |
+ _frameSize.height(), |
+ _position.z); |
+#endif // DEBUG |
+} |
+ |
+- (const webrtc::DesktopVector&)mousePosition { |
+ return _mousePosition; |
+} |
+ |
+- (void)setPanVelocity:(const CGPoint&)delta { |
+ _panVelocity.x = delta.x; |
+ _panVelocity.y = delta.y; |
+} |
+ |
+- (void)setMarginsFromLeft:(int)left |
+ right:(int)right |
+ top:(int)top |
+ bottom:(int)bottom { |
+ _margin.left = left; |
+ _margin.right = right; |
+ _margin.top = top; |
+ _margin.bottom = bottom; |
+} |
+ |
+- (void)draw { |
+ glEnableVertexAttribArray(GLKVertexAttribPosition); |
+ glEnableVertexAttribArray(GLKVertexAttribTexCoord0); |
+ glEnableVertexAttribArray(GLKVertexAttribTexCoord1); |
+ |
+ // Define our scene space |
+ glVertexAttribPointer(GLKVertexAttribPosition, |
+ 2, |
+ GL_FLOAT, |
+ GL_FALSE, |
+ sizeof(TexturedVertex), |
+ &(_glQuad.bl.geometryVertex)); |
+ // Define the desktop plane |
+ glVertexAttribPointer(GLKVertexAttribTexCoord0, |
+ 2, |
+ GL_FLOAT, |
+ GL_FALSE, |
+ sizeof(TexturedVertex), |
+ &(_glQuad.bl.textureVertex)); |
+ // Define the cursor plane |
+ glVertexAttribPointer(GLKVertexAttribTexCoord1, |
+ 2, |
+ GL_FLOAT, |
+ GL_FALSE, |
+ sizeof(TexturedVertex), |
+ &(_glQuad.bl.textureVertex)); |
+ |
+ // Draw! |
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); |
+ |
+ [Utility logGLErrorCode:@"SceneView draw"]; |
+} |
+ |
+- (CGPoint)pixelRatio { |
+ |
+ CGPoint r = CGPointMake(static_cast<float>(_contentSize.width()) / |
+ static_cast<float>(_frameSize.width()), |
+ static_cast<float>(_contentSize.height()) / |
+ static_cast<float>(_frameSize.height())); |
+ return r; |
+} |
+ |
+- (webrtc::DesktopSize)frameSizeToScale:(float)scale { |
+ return webrtc::DesktopSize(_frameSize.width() * scale, |
+ _frameSize.height() * scale); |
+} |
+ |
+- (webrtc::DesktopVector)getBoundsForSize:(const webrtc::DesktopSize&)size { |
+ webrtc::DesktopVector r( |
+ _contentSize.width() - _margin.left - _margin.right - size.width(), |
+ _contentSize.height() - _margin.bottom - _margin.top - size.height()); |
+ |
+ if (r.x() > 0) { |
+ r.set((_contentSize.width() - size.width()) / 2, r.y()); |
+ } |
+ |
+ if (r.y() > 0) { |
+ r.set(r.x(), (_contentSize.height() - size.height()) / 2); |
+ } |
+ |
+ return r; |
+} |
+ |
+- (BOOL)containsTouchPoint:(CGPoint)point { |
+ // Here frame is from the top-left corner, most other calculations are framed |
+ // from the bottom left. |
+ CGRect frame = |
+ CGRectMake(_margin.left, |
+ _margin.top, |
+ _contentSize.width() - _margin.left - _margin.right, |
+ _contentSize.height() - _margin.top - _margin.bottom); |
+ return CGRectContainsPoint(frame, point); |
+} |
+ |
+- (BOOL)convertTouchPointToMousePoint:(CGPoint)touchPoint |
+ targetPoint:(webrtc::DesktopVector&)mousePoint { |
+ if (![self containsTouchPoint:touchPoint]) { |
+ return NO; |
+ } |
+ // A touch location occurs in respect to the user's entire view surface. |
+ |
+ // The GL Context is upside down from the User's perspective so flip it. |
+ CGPoint glOrientedTouchPoint = |
+ CGPointMake(touchPoint.x, _contentSize.height() - touchPoint.y); |
+ |
+ // The GL surface generally is not at the same origination point as the touch, |
+ // so translate by the scene's position. |
+ CGPoint glOrientedPointInRespectToFrame = |
+ CGPointMake(glOrientedTouchPoint.x - _position.x, |
+ glOrientedTouchPoint.y - _position.y); |
+ |
+ // The perspective exists in relative to the CLIENT resolution at 1:1, zoom |
+ // our perspective so we are relative to the HOST at 1:1 |
+ CGPoint glOrientedPointInFrame = |
+ CGPointMake(glOrientedPointInRespectToFrame.x / _position.z, |
+ glOrientedPointInRespectToFrame.y / _position.z); |
+ |
+ // Finally, flip the perspective back over to the Users, but this time in |
+ // respect to the HOST desktop. Floor to ensure the result is always in |
+ // frame. |
+ CGPoint deskTopOrientedPointInFrame = |
+ CGPointMake(floorf(glOrientedPointInFrame.x), |
+ floorf(_frameSize.height() - glOrientedPointInFrame.y)); |
+ |
+ // Convert from float to integer |
+ mousePoint.set(deskTopOrientedPointInFrame.x, deskTopOrientedPointInFrame.y); |
+ |
+ return CGRectContainsPoint( |
+ CGRectMake(0, 0, _frameSize.width(), _frameSize.height()), |
+ deskTopOrientedPointInFrame); |
+} |
+ |
+- (BOOL)convertMousePointToTouchPoint:(const webrtc::DesktopVector&)mousePoint |
+ targetPoint:(CGPoint&)touchPoint { |
+ // A mouse point is in respect to the desktop frame. |
+ |
+ // Flip the perspective back over to the Users, in |
+ // respect to the HOST desktop. |
+ CGPoint deskTopOrientedPointInFrame = |
+ CGPointMake(mousePoint.x(), _frameSize.height() - mousePoint.y()); |
+ |
+ // The perspective exists in relative to the CLIENT resolution at 1:1, zoom |
+ // our perspective so we are relative to the HOST at 1:1 |
+ CGPoint glOrientedPointInFrame = |
+ CGPointMake(deskTopOrientedPointInFrame.x * _position.z, |
+ deskTopOrientedPointInFrame.y * _position.z); |
+ |
+ // The GL surface generally is not at the same origination point as the touch, |
+ // so translate by the scene's position. |
+ CGPoint glOrientedPointInRespectToFrame = |
+ CGPointMake(glOrientedPointInFrame.x + _position.x, |
+ glOrientedPointInFrame.y + _position.y); |
+ |
+ // Convert from float to integer |
+ touchPoint.x = floorf(glOrientedPointInRespectToFrame.x); |
+ touchPoint.y = floorf(glOrientedPointInRespectToFrame.y); |
+ |
+ return [self containsTouchPoint:touchPoint]; |
+} |
+ |
+- (void)panAndZoom:(CGPoint)translation scaleBy:(float)ratio { |
+ CGPoint ratios = [self pixelRatio]; |
+ |
+ // New Scaling factor bounded by a min and max |
+ float resultScale = _position.z * ratio; |
+ float scaleUpperBound = MAX(ratios.x, MAX(ratios.y, kMaxZoomSize)); |
+ float scaleLowerBound = MIN(ratios.x, ratios.y); |
+ |
+ if (resultScale < scaleLowerBound) { |
+ resultScale = scaleLowerBound; |
+ } else if (resultScale > scaleUpperBound) { |
+ resultScale = scaleUpperBound; |
+ } |
+ |
+ DCHECK(isnormal(resultScale) && resultScale > 0); |
+ |
+ // The GL perspective is upside down in relation to the User's view, so flip |
+ // the translation |
+ translation.y = -translation.y; |
+ |
+ // The constants here could be user options later. |
+ translation.x = |
+ translation.x * kXAxisInversion * (1 / (ratios.x * kMouseSensitivity)); |
+ translation.y = |
+ translation.y * kYAxisInversion * (1 / (ratios.y * kMouseSensitivity)); |
+ |
+ CGPoint delta = CGPointMake(0, 0); |
+ CGPoint scaleDelta = CGPointMake(0, 0); |
+ |
+ webrtc::DesktopSize currentSize = [self frameSizeToScale:_position.z]; |
+ |
+ { |
+ // Closure for this variable, so the variable is not available to the rest |
+ // of this function |
+ webrtc::DesktopVector currentBounds = [self getBoundsForSize:currentSize]; |
+ // There are rounding errors in the scope of this function, see the |
+ // butterfly effect. In successive calls, the resulting position isn't |
+ // always exactly the calculated position. If we know we are Anchored, then |
+ // go ahead and reposition it to the values above. |
+ if (_anchored.right) { |
+ _position.x = currentBounds.x(); |
+ } |
+ |
+ if (_anchored.top) { |
+ _position.y = currentBounds.y(); |
+ } |
+ } |
+ |
+ if (_position.z != resultScale) { |
+ // When scaling the scene, the origination of scaling is the mouse's |
+ // location. But when the frame is anchored, adjust the origination to the |
+ // anchor point. |
+ |
+ CGPoint mousePositionInClientResolution; |
+ [self convertMousePointToTouchPoint:_mousePosition |
+ targetPoint:mousePositionInClientResolution]; |
+ |
+ // Prefer to zoom based on the left anchor when there is a choice |
+ if (_anchored.left) { |
+ mousePositionInClientResolution.x = 0; |
+ } else if (_anchored.right) { |
+ mousePositionInClientResolution.x = _contentSize.width(); |
+ } |
+ |
+ // Prefer to zoom out from the top anchor when there is a choice |
+ if (_anchored.top) { |
+ mousePositionInClientResolution.y = _contentSize.height(); |
+ } else if (_anchored.bottom) { |
+ mousePositionInClientResolution.y = 0; |
+ } |
+ |
+ scaleDelta.x -= |
+ [SceneView positionDeltaFromScaling:ratio |
+ position:_position.x |
+ length:currentSize.width() |
+ anchor:mousePositionInClientResolution.x]; |
+ |
+ scaleDelta.y -= |
+ [SceneView positionDeltaFromScaling:ratio |
+ position:_position.y |
+ length:currentSize.height() |
+ anchor:mousePositionInClientResolution.y]; |
+ } |
+ |
+ delta.x = [SceneView |
+ positionDeltaFromTranslation:translation.x |
+ position:_position.x |
+ freeSpace:_contentSize.width() - currentSize.width() |
+ scaleingPositionDelta:scaleDelta.x |
+ isAnchoredLow:_anchored.left |
+ isAnchoredHigh:_anchored.right]; |
+ |
+ delta.y = [SceneView |
+ positionDeltaFromTranslation:translation.y |
+ position:_position.y |
+ freeSpace:_contentSize.height() - currentSize.height() |
+ scaleingPositionDelta:scaleDelta.y |
+ isAnchoredLow:_anchored.bottom |
+ isAnchoredHigh:_anchored.top]; |
+ { |
+ // Closure for this variable, so the variable is not available to the rest |
+ // of this function |
+ webrtc::DesktopVector bounds = |
+ [self getBoundsForSize:[self frameSizeToScale:resultScale]]; |
+ |
+ delta.x = [SceneView boundDeltaFromPosition:_position.x |
+ delta:delta.x |
+ lowerBound:bounds.x() |
+ upperBound:0]; |
+ |
+ delta.y = [SceneView boundDeltaFromPosition:_position.y |
+ delta:delta.y |
+ lowerBound:bounds.y() |
+ upperBound:0]; |
+ } |
+ |
+ BOOL isLeftAndRightAnchored = _anchored.left && _anchored.right; |
+ BOOL isTopAndBottomAnchored = _anchored.top && _anchored.bottom; |
+ |
+ [self updateMousePositionAndAnchorsWithTranslation:translation |
+ scale:resultScale]; |
+ |
+ // If both anchors were lost, then keep the one that is easier to predict |
+ if (isLeftAndRightAnchored && !_anchored.left && !_anchored.right) { |
+ delta.x = -_position.x; |
+ _anchored.left = YES; |
+ } |
+ |
+ // If both anchors were lost, then keep the one that is easier to predict |
+ if (isTopAndBottomAnchored && !_anchored.top && !_anchored.bottom) { |
+ delta.y = -_position.y; |
+ _anchored.bottom = YES; |
+ } |
+ |
+ // FINALLY, update the scene's position |
+ _position.x += delta.x; |
+ _position.y += delta.y; |
+ _position.z = resultScale; |
+} |
+ |
+- (void)updateMousePositionAndAnchorsWithTranslation:(CGPoint)translation |
+ scale:(float)scale { |
+ webrtc::DesktopVector centerMouseLocation; |
+ [self convertTouchPointToMousePoint:CGPointMake(_contentSize.width() / 2, |
+ _contentSize.height() / 2) |
+ targetPoint:centerMouseLocation]; |
+ |
+ webrtc::DesktopVector currentBounds = |
+ [self getBoundsForSize:[self frameSizeToScale:_position.z]]; |
+ webrtc::DesktopVector nextBounds = |
+ [self getBoundsForSize:[self frameSizeToScale:scale]]; |
+ |
+ webrtc::DesktopVector predictedMousePosition( |
+ _mousePosition.x() - translation.x, _mousePosition.y() + translation.y); |
+ |
+ _mousePosition.set( |
+ [SceneView boundMouseGivenNextPosition:predictedMousePosition.x() |
+ maxPosition:_frameSize.width() |
+ centerPosition:centerMouseLocation.x() |
+ isAnchoredLow:_anchored.left |
+ isAnchoredHigh:_anchored.right], |
+ [SceneView boundMouseGivenNextPosition:predictedMousePosition.y() |
+ maxPosition:_frameSize.height() |
+ centerPosition:centerMouseLocation.y() |
+ isAnchoredLow:_anchored.top |
+ isAnchoredHigh:_anchored.bottom]); |
+ |
+ _panVelocity.x = [SceneView boundVelocity:_panVelocity.x |
+ axisLength:_frameSize.width() |
+ mousePosition:_mousePosition.x()]; |
+ _panVelocity.y = [SceneView boundVelocity:_panVelocity.y |
+ axisLength:_frameSize.height() |
+ mousePosition:_mousePosition.y()]; |
+ |
+ _anchored.left = (nextBounds.x() >= 0) || |
+ (_position.x == 0 && |
+ predictedMousePosition.x() <= centerMouseLocation.x()); |
+ |
+ _anchored.right = |
+ (nextBounds.x() >= 0) || |
+ (_position.x == currentBounds.x() && |
+ predictedMousePosition.x() >= centerMouseLocation.x()) || |
+ (_mousePosition.x() == _frameSize.width() - 1 && !_anchored.left); |
+ |
+ _anchored.bottom = (nextBounds.y() >= 0) || |
+ (_position.y == 0 && |
+ predictedMousePosition.y() >= centerMouseLocation.y()); |
+ |
+ _anchored.top = |
+ (nextBounds.y() >= 0) || |
+ (_position.y == currentBounds.y() && |
+ predictedMousePosition.y() <= centerMouseLocation.y()) || |
+ (_mousePosition.y() == _frameSize.height() - 1 && !_anchored.bottom); |
+} |
+ |
++ (float)positionDeltaFromScaling:(float)ratio |
+ position:(float)position |
+ length:(float)length |
+ anchor:(float)anchor { |
+ float newSize = length * ratio; |
+ float scaleXBy = fabs(position - anchor) / length; |
+ float delta = (newSize - length) * scaleXBy; |
+ return delta; |
+} |
+ |
++ (int)positionDeltaFromTranslation:(int)translation |
+ position:(int)position |
+ freeSpace:(int)freeSpace |
+ scaleingPositionDelta:(int)scaleingPositionDelta |
+ isAnchoredLow:(BOOL)isAnchoredLow |
+ isAnchoredHigh:(BOOL)isAnchoredHigh { |
+ if (isAnchoredLow && isAnchoredHigh) { |
+ // center the view |
+ return (freeSpace / 2) - position; |
+ } else if (isAnchoredLow) { |
+ return 0; |
+ } else if (isAnchoredHigh) { |
+ return scaleingPositionDelta; |
+ } else { |
+ return translation + scaleingPositionDelta; |
+ } |
+} |
+ |
++ (int)boundDeltaFromPosition:(float)position |
+ delta:(int)delta |
+ lowerBound:(int)lowerBound |
+ upperBound:(int)upperBound { |
+ int result = position + delta; |
+ |
+ if (lowerBound < upperBound) { // the view is larger than the bounds |
+ if (result > upperBound) { |
+ result = upperBound; |
+ } else if (result < lowerBound) { |
+ result = lowerBound; |
+ } |
+ } else { |
+ // the view is smaller than the bounds so we'll always be at the lowerBound |
+ result = lowerBound; |
+ } |
+ return result - position; |
+} |
+ |
++ (int)boundMouseGivenNextPosition:(int)nextPosition |
+ maxPosition:(int)maxPosition |
+ centerPosition:(int)centerPosition |
+ isAnchoredLow:(BOOL)isAnchoredLow |
+ isAnchoredHigh:(BOOL)isAnchoredHigh { |
+ if (nextPosition < 0) { |
+ return 0; |
+ } |
+ if (nextPosition > maxPosition - 1) { |
+ return maxPosition - 1; |
+ } |
+ |
+ if ((isAnchoredLow && nextPosition <= centerPosition) || |
+ (isAnchoredHigh && nextPosition >= centerPosition)) { |
+ return nextPosition; |
+ } |
+ |
+ return centerPosition; |
+} |
+ |
++ (float)boundVelocity:(float)velocity |
+ axisLength:(int)axisLength |
+ mousePosition:(int)mousePosition { |
+ if (velocity != 0) { |
+ if (mousePosition <= 0 || mousePosition >= (axisLength - 1)) { |
+ return 0; |
+ } |
+ } |
+ |
+ return velocity; |
+} |
+ |
+- (BOOL)tickPanVelocity { |
+ BOOL inMotion = ((_panVelocity.x != 0.0) || (_panVelocity.y != 0.0)); |
+ |
+ if (inMotion) { |
+ |
+ uint32_t divisor = 50 / _position.z; |
+ float reducer = .95; |
+ |
+ if (_panVelocity.x != 0.0 && ABS(_panVelocity.x) < divisor) { |
+ _panVelocity = CGPointMake(0.0, _panVelocity.y); |
+ } |
+ |
+ if (_panVelocity.y != 0.0 && ABS(_panVelocity.y) < divisor) { |
+ _panVelocity = CGPointMake(_panVelocity.x, 0.0); |
+ } |
+ |
+ [self panAndZoom:CGPointMake(_panVelocity.x / divisor, |
+ _panVelocity.y / divisor) |
+ scaleBy:1.0]; |
+ |
+ _panVelocity.x *= reducer; |
+ _panVelocity.y *= reducer; |
+ } |
+ |
+ return inMotion; |
+} |
+ |
+@end |