Skip to content

Instantly share code, notes, and snippets.

@johnnewman
Last active March 10, 2022 22:32
Show Gist options
  • Save johnnewman/b2b76aae3cde66b1e3a0d77555647b93 to your computer and use it in GitHub Desktop.
Save johnnewman/b2b76aae3cde66b1e3a0d77555647b93 to your computer and use it in GitHub Desktop.
CircleLayer rapid property update pulse
//
// CircleManualUpdatePulse.swift
//
// Created by John Newman on 3/10/22.
// Copyright © 2022 Roadtrippers LLC. All rights reserved.
//
import Foundation
import MapboxMaps
class StyleLayerPulseRenderer {
private let pulseLayerIdentifier = "my-pulse-layer"
private let pulseSourceIdentifier = "my-pulse-source"
let pulseRepeatInterval: TimeInterval = 3
let pulseDuration: TimeInterval = 1.5
var pauseBeforeStarting: DispatchTime {
.now() + pulseRepeatInterval - pulseDuration
}
let desiredFramerate: Double = 60
var frameRenderInterval: DispatchTime {
.now() + TimeInterval(1.0) / desiredFramerate
}
let strokeWidth = (3.0, 5.0)
let strokeOpacity = (1.0, 0.0)
let radius = (0.0, 62.0)
private var map: MapView
private var layerPosition: LayerPosition
init(map: MapView, layerPosition: LayerPosition) {
self.map = map
self.layerPosition = layerPosition
}
func startPulseAnimation(forAnnotation annotation: PointAnnotation) {
guard !map.mapboxMap.style.sourceExists(withId: pulseSourceIdentifier),
!map.mapboxMap.style.layerExists(withId: pulseLayerIdentifier) else {
print("Pulse layer/source already exist in style. Aborting.")
return
}
var pulseSource = GeoJSONSource()
pulseSource.data = .feature(.init(geometry: annotation.geometry))
var pulseLayer = CircleLayer(id: pulseLayerIdentifier)
pulseLayer.source = pulseSourceIdentifier
// Set up starting values
pulseLayer.circleStrokeColor = .constant(StyleColor(UIColor.blue))
pulseLayer.circleOpacity = .constant(0)
pulseLayer.circleRadius = .constant(0)
pulseLayer.circleStrokeWidth = .constant(0)
pulseLayer.circleStrokeOpacity = .constant(0)
// Avoid any Mapbox animations.
pulseLayer.circleBlurTransition = .init(duration: 0, delay: 0)
pulseLayer.circleColorTransition = .init(duration: 0, delay: 0)
pulseLayer.circleRadiusTransition = .init(duration: 0, delay: 0)
pulseLayer.circleOpacityTransition = .init(duration: 0, delay: 0)
pulseLayer.circleTranslateTransition = .init(duration: 0, delay: 0)
pulseLayer.circleStrokeColorTransition = .init(duration: 0, delay: 0)
pulseLayer.circleStrokeWidthTransition = .init(duration: 0, delay: 0)
pulseLayer.circleStrokeOpacityTransition = .init(duration: 0, delay: 0)
try! map.mapboxMap.style.addSource(pulseSource, id: pulseSourceIdentifier)
try! map.mapboxMap.style.addLayer(pulseLayer, layerPosition: layerPosition)
DispatchQueue.main.asyncAfter(deadline: pauseBeforeStarting, execute: newPulseIteration)
}
private func newPulseIteration() {
let animationStartTime = CACurrentMediaTime()
renderFrame()
func renderFrame() {
let completedAnimationDuration = CACurrentMediaTime() - animationStartTime
do {
let t = min(1, completedAnimationDuration / pulseDuration)
try map.mapboxMap.style.updateLayer(withId: pulseLayerIdentifier, type: CircleLayer.self) { layer in
layer.circleRadius?.lerp(radius, t: t)
layer.circleStrokeWidth?.lerp(strokeWidth, t: t)
layer.circleStrokeOpacity?.lerp(strokeOpacity, t: t)
}
if completedAnimationDuration >= pulseDuration {
DispatchQueue.main.asyncAfter(deadline: pauseBeforeStarting, execute: newPulseIteration)
} else {
DispatchQueue.main.asyncAfter(deadline: frameRenderInterval, execute: renderFrame)
}
} catch {
print("Error: \(error)")
}
}
}
}
extension Value where T == Double {
mutating func lerp(_ range: (T, T), t: Double) {
self = .constant((1 - t) * range.0 + t * range.1)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment