Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added 360° Support (#4) #5

Merged
merged 15 commits into from
Jan 24, 2018
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Change log

## [Version 1.1.0](https://github.com/efremidze/Shiny/releases/tag/1.1.0)
Released on 2017-01-23

- Gradient Repeats

## [Version 1.0.1](https://github.com/efremidze/Shiny/releases/tag/1.0.1)
Released on 2017-12-22

Expand Down
5 changes: 2 additions & 3 deletions Example/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ class ViewController: UIViewController {
}
@IBOutlet weak var shinyView: ShinyView! {
didSet {
shinyView.colors = [UIColor.red, UIColor.orange, UIColor.green, UIColor.blue, UIColor.purple, UIColor.pink, UIColor.gray].map { $0.withAlphaComponent(0.5) }
shinyView.locations = [0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 1]
shinyView.startUpdates()
shinyView.layer.cornerRadius = 20
shinyView.layer.masksToBounds = true
shinyView.colors = [UIColor.gray, UIColor.gray, UIColor.red, UIColor.orange, UIColor.green, UIColor.blue, UIColor.purple, UIColor.pink, UIColor.gray, UIColor.gray].map { $0.withAlphaComponent(0.9) }
shinyView.startUpdates()
}
}
@IBOutlet weak var imageView: UIImageView! {
Expand Down
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ Adding `ShinyView` programmatically (supports storyboard/xib too):
import Shiny

let shinyView = ShinyView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
shinyView.colors = [UIColor.red, UIColor.green, UIColor.blue, UIColor.gray]
shinyView.locations = [0, 0.1, 0.2, 0.3, 1]
shinyView.colors = [UIColor.gray, UIColor.gray, UIColor.red, UIColor.orange, UIColor.green, UIColor.blue, UIColor.purple, UIColor.pink, UIColor.gray, UIColor.gray]
shinyView.startUpdates() // necessary
view.addSubview(shinyView)
```
Expand All @@ -46,9 +45,7 @@ The `ShinyView` exposes several properties to customize the radial gradient used
```swift
var colors: [UIColor] // The color of each gradient stop.
var locations: [CGFloat]? // The location of each gradient stop. The default is `nil`.
var spread: CGFloat // The distance between colors on the gradient. The default is `0.8`.
var padding: CGFloat // The padding on the edges of the gradient. The default is `0.1`.
var sensitivity: CGFloat // The sensitivity of the gyroscopic motion. The default is `0.2`.
var scale: CGFloat // The scale factor of the gradient. The default is `2.0`.
```

You can start/stop observing motion updates:
Expand Down
2 changes: 1 addition & 1 deletion Shiny.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'Shiny'
s.version = '1.0.1'
s.version = '1.1.0'
s.summary = 'Iridescent Effect View (inspired by Apple Pay Cash)'
s.homepage = 'https://github.com/efremidze/Shiny'
s.license = { :type => 'MIT', :file => 'LICENSE' }
Expand Down
14 changes: 9 additions & 5 deletions Shiny.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
71463DCC1FEBB00F00BCC7B9 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71463DCA1FEBB00F00BCC7B9 /* Main.storyboard */; };
71463DCE1FEBB00F00BCC7B9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 71463DCD1FEBB00F00BCC7B9 /* Assets.xcassets */; };
71463DD11FEBB00F00BCC7B9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71463DCF1FEBB00F00BCC7B9 /* LaunchScreen.storyboard */; };
716B1867200FCE9500F09032 /* Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 716B1866200FCE9500F09032 /* Scene.swift */; };
71C96DB81FEBB7CF00C9CC58 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71C96DB41FEBB7CF00C9CC58 /* Extensions.swift */; };
71C96DB91FEBB7CF00C9CC58 /* Gyro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71C96DB51FEBB7CF00C9CC58 /* Gyro.swift */; };
71C96DBA1FEBB7CF00C9CC58 /* Gradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71C96DB61FEBB7CF00C9CC58 /* Gradient.swift */; };
Expand Down Expand Up @@ -61,6 +62,7 @@
71463DCD1FEBB00F00BCC7B9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
71463DD01FEBB00F00BCC7B9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
71463DD21FEBB00F00BCC7B9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
716B1866200FCE9500F09032 /* Scene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Scene.swift; sourceTree = "<group>"; };
71C96DB41FEBB7CF00C9CC58 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
71C96DB51FEBB7CF00C9CC58 /* Gyro.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Gyro.swift; sourceTree = "<group>"; };
71C96DB61FEBB7CF00C9CC58 /* Gradient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Gradient.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -115,12 +117,13 @@
71463DB71FEBB00300BCC7B9 /* Shiny */ = {
isa = PBXGroup;
children = (
71C96DB41FEBB7CF00C9CC58 /* Extensions.swift */,
71C96DB61FEBB7CF00C9CC58 /* Gradient.swift */,
71C96DB51FEBB7CF00C9CC58 /* Gyro.swift */,
71C96DB71FEBB7CF00C9CC58 /* Shiny.swift */,
71463DB81FEBB00300BCC7B9 /* Shiny.h */,
71463DB91FEBB00300BCC7B9 /* Info.plist */,
71463DB81FEBB00300BCC7B9 /* Shiny.h */,
71C96DB71FEBB7CF00C9CC58 /* Shiny.swift */,
716B1866200FCE9500F09032 /* Scene.swift */,
71C96DB51FEBB7CF00C9CC58 /* Gyro.swift */,
71C96DB61FEBB7CF00C9CC58 /* Gradient.swift */,
71C96DB41FEBB7CF00C9CC58 /* Extensions.swift */,
);
name = Shiny;
path = Sources;
Expand Down Expand Up @@ -262,6 +265,7 @@
files = (
71C96DB91FEBB7CF00C9CC58 /* Gyro.swift in Sources */,
71C96DBB1FEBB7CF00C9CC58 /* Shiny.swift in Sources */,
716B1867200FCE9500F09032 /* Scene.swift in Sources */,
71C96DB81FEBB7CF00C9CC58 /* Extensions.swift in Sources */,
71C96DBA1FEBB7CF00C9CC58 /* Gradient.swift in Sources */,
);
Expand Down
33 changes: 24 additions & 9 deletions Sources/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,34 @@
// Copyright © 2017 Lasha Efremidze. All rights reserved.
//

import Foundation
import UIKit

extension Comparable {
func clamped(to limits: ClosedRange<Self>) -> Self {
return min(max(self, limits.lowerBound), limits.upperBound)
infix operator %: MultiplicationPrecedence
func % (left: CGFloat, right: CGFloat) -> CGFloat {
let v = left.truncatingRemainder(dividingBy: right)
return v >= 0 ? v : v + right
}

extension UIImage {
convenience init(from view: UIView) {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, 0)
view.layer.render(in: UIGraphicsGetCurrentContext()!)
self.init(cgImage:(UIGraphicsGetImageFromCurrentImageContext()?.cgImage!)!)
UIGraphicsEndImageContext()
}
}

extension CGFloat {
func center() -> CGFloat {
return 0.5 - (self / 0.5)
extension CALayer {
var radius: CGFloat {
return sqrt(pow(bounds.width / 2, 2) + pow(bounds.height / 2, 2))
}
func add(_ padding: CGFloat) -> CGFloat {
return clamped(to: padding...(1 - padding))
var size: CGSize {
return frame.size
}
}

//extension UIView {
// var snapshot: UIImage {
// return UIImage(from: self)
// }
//}
78 changes: 69 additions & 9 deletions Sources/Gradient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,42 @@

import UIKit

class ReplicatorLayer<T: CALayer>: CALayer {
lazy var horizontalLayer: CAReplicatorLayer = {
let replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame.size = size
replicatorLayer.masksToBounds = true
return replicatorLayer
}()
lazy var verticalLayer: CAReplicatorLayer = {
let replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame.size = size
replicatorLayer.masksToBounds = true
return replicatorLayer
}()
lazy var instanceLayer: T = {
let layer = T()
layer.backgroundColor = UIColor.clear.cgColor
self.insertSublayer(verticalLayer, at: 0)
verticalLayer.addSublayer(horizontalLayer)
horizontalLayer.addSublayer(layer)
return layer
}()
var instanceSize: CGSize?
override func layoutSublayers() {
super.layoutSublayers()

guard let instanceSize = instanceSize else { return }
horizontalLayer.instanceCount = Int(ceil(size.width / instanceSize.width))
horizontalLayer.instanceTransform = CATransform3DMakeTranslation(instanceSize.width, 0, 0)
verticalLayer.instanceCount = Int(ceil(size.height / instanceSize.height))
verticalLayer.instanceTransform = CATransform3DMakeTranslation(0, instanceSize.height, 0)
instanceLayer.frame.size = instanceSize
}
}

class RadialGradientLayer: CALayer {
var colors = [CGColor]()
var colors = [CGColor]() { didSet { setNeedsDisplay() } }
var locations: [CGFloat]?
override func draw(in ctx: CGContext) {
ctx.saveGState()
Expand All @@ -19,14 +53,40 @@ class RadialGradientLayer: CALayer {
}
}

//class RadialGradientView: UIView {
// override class var layerClass: AnyClass {
// return RadialGradientLayer.self
// }
//}
class LayerView<T: CALayer>: UIView {
override class var layerClass: AnyClass {
return T.self
}
var _layer: T {
return layer as! T
}
}

extension CALayer {
var radius: CGFloat {
return sqrt(pow(bounds.width / 2, 2) + pow(bounds.height / 2, 2))
struct GradientSnapshotter {
typealias GradientLayerView = LayerView<ReplicatorLayer<CAGradientLayer>>
static func snapshot(frame: CGRect, colors: [UIColor], locations: [CGFloat]?, scale: CGFloat) -> UIImage {
let layerView = GradientLayerView(frame: frame)
layerView.frame.size.height = frame.height * scale
layerView._layer.instanceSize = frame.size
layerView._layer.instanceLayer.colors = colors.map { $0.cgColor }
layerView._layer.instanceLayer.locations = locations as [NSNumber]?
return UIImage(from: layerView)
}
}

//protocol GradientLayerProtocol: class {
// associatedtype T: CALayer
// var layerView: LayerView<ReplicatorLayer<T>> { get set }
// var gradientView: T { get }
// var instanceSize: CGSize { get set }
//}
//
//extension GradientLayerProtocol {
// var gradientView: T {
// return layerView._layer.instanceLayer
// }
// var instanceSize: CGSize {
// get { return layerView._layer.instanceSize }
// set { layerView._layer.instanceSize = newValue }
// }
//}
25 changes: 21 additions & 4 deletions Sources/Gyro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,32 @@ let Gyro = GyroManager.shared

class GyroManager: CMMotionManager {
static let shared = GyroManager()
private let queue = OperationQueue()
func observe(_ observer: @escaping (_ gyro: CGVector) -> Void) {
let queue = OperationQueue()
func observe(_ observer: @escaping (_ roll: Double, _ pitch: Double, _ yaw: Double) -> Void) {
guard isDeviceMotionAvailable else { return }
deviceMotionUpdateInterval = 0.1
deviceMotionUpdateInterval = 1 / 60
startDeviceMotionUpdates(to: queue) { data, error in
guard let data = data else { return }
var pitch = data.attitude.pitch
if data.gravity.z > 0 {
if pitch > 0 {
pitch = .pi - pitch
} else {
pitch = -(.pi + pitch)
}
}
DispatchQueue.main.sync {
observer(CGVector(dx: CGFloat(data.gravity.x), dy: CGFloat(data.gravity.y)))
observer(data.attitude.roll, pitch, data.attitude.yaw)
}
}
}
}

struct Axis: OptionSet {
let rawValue: Int

static let vertical = Axis(rawValue: 1 << 0)
static let horizontal = Axis(rawValue: 1 << 1)

static let all: Axis = [.vertical, .horizontal]
}
33 changes: 33 additions & 0 deletions Sources/Scene.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// Scene.swift
// Shiny
//
// Created by Lasha Efremidze on 1/17/18.
// Copyright © 2018 Lasha Efremidze. All rights reserved.
//

import Foundation
import SceneKit

open class SceneView: SCNView {
open lazy var sphere: SCNSphere = {
self.scene = SCNScene()

let sphere = SCNSphere(radius: 5) // default
sphere.firstMaterial!.isDoubleSided = true
let sphereNode = SCNNode(geometry: sphere)
sphereNode.position = SCNVector3Make(0, 0, 0)
self.scene?.rootNode.addChildNode(sphereNode)

cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Make(0, 0, 0)
self.scene?.rootNode.addChildNode(cameraNode)

return sphere
}()
open let cameraNode = SCNNode()
open var image: UIImage? {
get { return sphere.firstMaterial!.diffuse.contents as? UIImage }
set { sphere.firstMaterial!.diffuse.contents = newValue }
}
}
51 changes: 22 additions & 29 deletions Sources/Shiny.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,59 +8,52 @@

import UIKit
import CoreMotion
import SceneKit

//@IBDesignable
open class ShinyView: UIView {

lazy var gradientLayer: RadialGradientLayer = {
let gradientLayer = RadialGradientLayer()
gradientLayer.needsDisplayOnBoundsChange = true
gradientLayer.backgroundColor = UIColor.clear.cgColor
self.layer.insertSublayer(gradientLayer, at: 0)
return gradientLayer
open lazy var sceneView: SceneView = {
let sceneView = SceneView(frame: self.bounds.insetBy(dx: -500, dy: -500))
// self.addSubview(sceneView) // testing
self.insertSubview(sceneView, at: 0)
return sceneView
}()

/**
The array of UIColor objects defining the color of each gradient stop.
*/
open var colors = [UIColor]() {
didSet {
gradientLayer.colors = colors.map { $0.cgColor }
gradientLayer.frame = self.bounds.insetBy(dx: -self.bounds.width * CGFloat(colors.count) * spread, dy: -self.bounds.height * CGFloat(colors.count) * spread)
}
}
open var colors = [UIColor]()

/**
The array of CGFloat objects defining the location of each gradient stop as a value in the range [0,1]. The values must be monotonically increasing. If a nil array is given, the stops are assumed to spread uniformly across the [0,1] range. Defaults to nil.
*/
open var locations: [CGFloat]? {
get { return gradientLayer.locations }
set { gradientLayer.locations = newValue }
}

/**
The distance between colors on the gradient.
*/
open var spread: CGFloat = 0.8
open var locations: [CGFloat]?

/**
The padding on the edges of the gradient.
The scale factor of the gradient. Defaults to 2.
*/
open var padding: CGFloat = 0.1
open var scale: CGFloat = 2

/**
The sensitivity of the gyroscopic motion.
The axis of the gradient. Defaults to vertical.
*/
open var sensitivity: CGFloat = 0.2
var axis: Axis = .vertical

/**
Starts listening to motion updates.
*/
open func startUpdates() {
Gyro.observe { [weak self] vector in
sceneView.image = GradientSnapshotter.snapshot(frame: self.bounds, colors: colors, locations: locations, scale: scale)
Gyro.observe { [weak self] roll, pitch, yaw in
guard let `self` = self else { return }
self.gradientLayer.anchorPoint.x = (vector.dx * self.sensitivity).center().add(self.padding)
self.gradientLayer.anchorPoint.y = (vector.dy * self.sensitivity).center().add(self.padding)

// SCNTransaction.animationDuration = 0
if self.axis.contains(.vertical) {
self.sceneView.cameraNode.eulerAngles.x = Float(pitch - .pi / 2)
} else if self.axis.contains(.horizontal) {
self.sceneView.cameraNode.eulerAngles.y = Float(roll)
}
// self.sceneView.cameraNode.eulerAngles = SCNVector3(x: Float(pitch - .pi/2), y: Float(roll), z: Float(yaw)) // 360° Support
}
}

Expand Down