Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rotating a gradient based off device pitch, roll, and yaw - creating a hologram / security sticker effect

Tags:

ios

swift

swiftui

I'm trying to implement a little fun addition into an app I'm working on, and can't seem to figure out how to get the animation to work completely / correctly.

At the moment I have something that looks like this: https://i.imgur.com/95O1Wul.mp4

What I'm trying to achieve

I'm trying to create a hologram like you would see on some shiny game cards or in some bank notes or on those VOID stickers.

I'm using the x position of the device to determine whether to shift the start and end point of the gradient. I'm trying to figure out in my head how to use these values to make the gradient's start and end points shift based on the values. And if I incorporated the other axes how it would move around if the device is rotated.

As for code, at the moment I have:

View Model

final class ContentViewModel: ObservableObject {
  @Published var gyroRotation = CMRotationRate()
  private let manager = CMMotionManager()
  private var timer: Timer?
 
  func startGyroscope() {
    if(manager.isGyroAvailable) {
      manager.gyroUpdateInterval = 1.0
      manager.startGyroUpdates()
      self.timer = Timer(fire: Date(), interval: (1.0/60.0), repeats: true, block: { timer in
        if let data = self.manager.gyroData {
          self.gyroRotation = data.rotationRate
        }
      })
      RunLoop.current.add(self.timer!, forMode: .default)
    }
  }

  func stopGyroscope() {
    if(self.timer != nil) {
      self.timer?.invalidate()
      self.timer = nil
      self.manager.stopGyroUpdates()
    }
  }
}

View

struct ContentView: View {
  @StateObject var cm = ContentViewModel()
  var rotation: CMRotationRate { cm.gyroRotation }
  let gridItems = [
    GridItem(.flexible(minimum: 40, maximum: 90)),
    GridItem(.flexible(minimum: 40, maximum: 90)),
    GridItem(.flexible(minimum: 40, maximum: 90)),
    GridItem(.flexible(minimum: 40, maximum: 90)),
    GridItem(.flexible(minimum: 40, maximum: 90))
  ]

  var body: some View {
    LazyVGrid(columns: gridItems, spacing: 30) {
      ForEach(0..<60) { _ in
        LinearGradient(
          colors: [.red, .orange, .yellow, .blue, .purple, .green],
          startPoint: rotation.x > 0 ? .leading : .trailing,
          endPoint: rotation.x > 0 ? .trailing : .leading
        )
        .animation(.linear, value: rotation.x)
        .frame(width: 50, height: 50)
        .mask(
         Image("logo")
           .resizable()
           .scaledToFit()
           .opacity(0.3)
         )
       }
     }
     .padding(20)
     .onAppear { cm.startGyroscope() }
  }
}
like image 840
markb Avatar asked Nov 15 '22 18:11

markb


1 Answers

Continuing on this to make it work, and I realised I didn't need the LinearGradient to animate but the Color itself:

View

struct CoreMotion: View {
    
    // -- where the data lives
    @ObservedObject var coreMotionVM = CoreMotionViewModel()
    
    let gridItems = [
        GridItem(), GridItem(), GridItem(), GridItem()
    ]
    
    // -- the view
    var body: some View {
        
        LazyVGrid(columns: gridItems, spacing: 5) {
            ForEach( 0 ..< 60 ) { _ in

                coreMotionVM.tintColour
                    .animation(
                        .easeInOut,
                        value: coreMotionVM.roll
                    )
                    .frame(width: 60, height: 60)
                    .padding(.vertical)
                    .mask(
                        Image("logo")
                            .resizable()
                            .scaledToFit()
                            .opacity(0.2)
                    )
            }
        }
        .padding(.horizontal)
        .padding(.top)
    }
}

ViewModel

final class CoreMotionViewModel: ObservableObject {
    
    // -- what we share
    @Published var roll: Double = 0.0
    
    // -- core motion manager
    private var manager: CMMotionManager

    // -- initialisation
    init() {
        
        // -- create the instance
        self.manager = CMMotionManager()
        
        // -- how often to get the data
        self.manager.deviceMotionUpdateInterval = 1/60
        
        // -- start it on the main thread
        self.manager.startDeviceMotionUpdates(to: .main) { motionData, error in
            
            // -- error lets get out of here
            guard error == nil else {
                print(error!)
                return
            }
            
            // -- update the data
            if let motionData = motionData {
                self.roll = motionData.attitude.roll
            }
        }
    }
    
    // cycle to the next point
    var tintColour: Color {
        switch roll {
            case ..<(-1.5):
                return .red
                
            case -1.5 ..< -1.0 :
                return .orange
                
            case -1.0 ..< -0.5 :
                return .yellow
                
            case -0.5 ..< 0.5 :
                return .green
                
            case 0.5 ..< 1.0 :
                return .blue
                
            case 1.0... :
                return .purple
                
            default:
                return .pink
        }
    }
}
like image 52
markb Avatar answered Mar 29 '23 23:03

markb