Skip to content

Instantly share code, notes, and snippets.

@carlynorama
Last active July 23, 2022 20:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save carlynorama/ad0a7449367b9a08e8c8525d5dcc2f8a to your computer and use it in GitHub Desktop.
Save carlynorama/ad0a7449367b9a08e8c8525d5dcc2f8a to your computer and use it in GitHub Desktop.
A slider with a nonlinear taper, customizable. Most current version: https://github.com/carlynorama/TaperSlider
//MOST RECENT VERSION: https://github.com/carlynorama/TaperSlider
//
// CustomTaperSlider.swift
//
//
// Created by Carlyn Maw on 7/15/22.
// License MIT
//
// Thanks to:
// https://gist.github.com/prachigauriar/c508799bad359c3aa271ccc0865de231
import SwiftUI
struct TaperSliderExampleView: View {
@State var slider1Value = 5.0
@State var slider2Value = 50.0
@State var slider3Value = 5.0
@State var slider4Value = 5.0
var body: some View {
VStack {
Text("Slider 1 Value: \(slider1Value)")
TaperSlider(value: $slider1Value, in: 0...10, taperStyle: .logp1)
Text("Slider 2 Value: \(slider2Value)")
TaperSlider(value: $slider2Value, in: 0...100, taperStyle: .logp1)
Text("Slider 3Value: \(slider3Value)")
//fix below one issue / document issue for custom values.
TaperSlider(
value: $slider3Value,
in: 1...10,
taperStyle: .customlogbase(base: 3),
taperRange: 75...100.0)
Text("Slider 4 Value: \(slider4Value)")
TaperSlider(
value: $slider4Value,
in: 1...10,
taperStyle: .customlogbase(base: 10)
)
}
}
}
struct ExampleTaperSliderView_Previews: PreviewProvider {
static var previews: some View {
TaperSliderExampleView()
}
}
struct TaperSlider: View {
@Binding var value:Double
var pair:FunctionPair
var onEditingChanged: (Bool) -> Void
init?(value:Binding<Double>, in range: ClosedRange<Double>? = nil, taperStyle:TaperProfile.TaperStyle, taperRange:ClosedRange<Double>? = nil, onEditingChanged: @escaping (Bool) -> Void = { _ in }) {
let profile = TaperProfile(style: taperStyle, rangeOfInterest: taperRange, inoutRange: range)
let attemptedPair = FunctionPair(profile: profile)
if attemptedPair != nil {
self.pair = attemptedPair!
} else {
return nil
}
self._value = value
self.onEditingChanged = onEditingChanged
}
var body: some View {
Slider.withCustomTaper(value: $value, withPair: pair)
}
}
fileprivate extension Binding where Value == Double {
func customPair(_ functionPair:FunctionPair) -> Binding<Double> {
Binding(
get: {
let v = functionPair.function(self.wrappedValue)
//print("GETTER wrapped:\(self.wrappedValue), output:\(v)")
return v
},
set: { (newValue) in
let calculatedValue = functionPair.inverse(newValue)
//print("SETTER input:\(newValue), calculated:\(calculatedValue)")
self.wrappedValue = calculatedValue
}
)
}
}
fileprivate extension Slider where Label == EmptyView, ValueLabel == EmptyView {
static func withCustomTaper(
value: Binding<Double>,
withPair functionPair:FunctionPair,
onEditingChanged: @escaping (Bool) -> Void = { _ in }
) -> Slider {
return self.init(
value: value.customPair(functionPair),
in: functionPair.function(functionPair.domain.lowerBound) ... functionPair.function(functionPair.domain.upperBound),
onEditingChanged: onEditingChanged
)
}
}
//MARK: Define a Taper
struct TaperProfile {
let style:TaperStyle
var rangeOfInterest:ClosedRange<Double>?
var inoutRange:ClosedRange<Double>?
enum TaperStyle {
case logp1
case expm1
case customlogbase(base:Double)
case custominvlogbase(base:Double)
case dangereuse(pair:FunctionPair, isClamped:Bool)
}
}
extension TaperProfile.TaperStyle {
var defaultRangeOfInterest:ClosedRange<Double> {
switch self {
case .logp1:
return 0.0...9.0
case .expm1:
return 0.0...1.5
case .customlogbase(_):
return 0.7...2 //TODO: Better values
case .custominvlogbase(_):
return 0.7...2 //TODO: Better values
case .dangereuse(pair: let pair, isClamped: let isClamped):
if isClamped {
return pair.domain
} else {
return 1.0...9.0 // this is a dumb range.
}
}
}
var defaultInoutRange:ClosedRange<Double> {
switch self {
case .dangereuse(pair: let pair, isClamped: let isClamped):
if isClamped {
return pair.domain
} else {
return 1.0...9.0 // this is a dumb range. needs to be the same as in dRoI above.
}
default:
return 0.0...1.0
}
}
}
fileprivate extension Double {
func fuzzyMatch(_ compareTo:Double) -> Bool {
return (self - compareTo).magnitude < 0.00000001
}
}
struct FunctionPair {
//Bijective
let function: (Double) -> Double
let inverse: (Double) -> Double
let domain:ClosedRange<Double>
var interestingRange:ClosedRange<Double>
init?(function:@escaping (Double) -> Double, inverse: @escaping (Double) -> Double, domain:ClosedRange<Double>) {
if FunctionPair.testRelationship(function: function, inverse: inverse, domain: domain) {
self.function = function
self.inverse = inverse
self.domain = domain
self.interestingRange = domain
} else {
return nil
}
}
init?(profile: TaperProfile) {
let iorange = profile.inoutRange ?? profile.style.defaultInoutRange
let functionrange = profile.rangeOfInterest ?? profile.style.defaultRangeOfInterest
var attemptedpair:FunctionPair? = nil
switch profile.style {
case .logp1:
attemptedpair = FunctionPair.clampedPair(
for: .variableWidthLog1p(multiplier: 1)!,
rangeOfInterest: functionrange,
inoutRange: iorange)
case .expm1:
attemptedpair = FunctionPair.clampedPair(
for: .variableWidthExpm(multiplier: 1)!,
rangeOfInterest: functionrange,
inoutRange: iorange)
case .customlogbase(base: let base):
attemptedpair = FunctionPair.clampedPair(
for: .customBaseLogPair(base: base)!,
rangeOfInterest: functionrange,
inoutRange: iorange)
case .custominvlogbase(base: let base):
attemptedpair = FunctionPair.clampedPair(
for: .customBaseInvLogPair(base: base)!,
rangeOfInterest: functionrange,
inoutRange: iorange)
case .dangereuse(pair: let pair, isClamped: let isClamped):
if isClamped {
attemptedpair = pair
} else {
attemptedpair = FunctionPair.clampedPair(
for: pair,
rangeOfInterest: functionrange,
inoutRange: iorange)
}
}
if attemptedpair != nil {
self = attemptedpair!
} else {
return nil
}
}
static func testRelationship(function:(Double) -> Double, inverse: (Double) -> Double, domain:ClosedRange<Double>) -> Bool {
let result = true
let minTest = runTest(domain.lowerBound, function: function, inverse: inverse)
let maxTest = runTest(domain.upperBound, function: function, inverse: inverse)
if !minTest || !maxTest {
return false
}
for _ in 0...10 {
let testCase = Double.random(in: domain)
let testResult = runTest(testCase, function: function, inverse: inverse)
if testResult == false {
return false
}
}
return result
}
static func runTest(_ testCase:Double, function:(Double) -> Double, inverse: (Double) -> Double) -> Bool {
let result = true
let fr = function(testCase)
let ir = inverse(fr)
print("for value \(testCase), function returns \(fr), inverse returns \(ir)")
if !(inverse(function(testCase))).fuzzyMatch(testCase) {
return false
}
return result
}
func printDescription() {
let testCase = Double.random(in: domain)
let functionResult = function(testCase)
let inverseResult = inverse(functionResult)
print("for value \(testCase), function returns \(functionResult), inverse returns \(inverseResult)")
}
}
//MARK: Static Function Pair Builders
extension FunctionPair {
static func customBaseInvLogPair(base:Double, multiplier:Double = 1.0) -> FunctionPair? {
func myFunction(value:Double) -> Double { pow(base, value) * multiplier }
func myInverse(value:Double) -> Double { customBaseLog(base: base, value: (value / multiplier)) }
//Tested for positive non zero bases ranging from 0.3 to 10.2
//fractional ± values for multiplier all okay.
let myDomain:ClosedRange<Double> = -310...300
return FunctionPair(function: myFunction, inverse: myInverse, domain: myDomain )
}
//Tested for positive multipliers 0.0005 to 5000
static func variableWidthLog1p(multiplier:Double) -> FunctionPair? {
//function of form log(mx+1)
//expm == eˣ-1 and log1p == log(1+x)
func myFunction(value:Double) -> Double { log1p(multiplier*value) }
func myInverse(value:Double) -> Double { expm1(value)/multiplier }
// lowerbound: fails below 0 if multiplier > 1, so just leaving it off for now.
// upperbound: over ~1mil precision errors
let myDomain:ClosedRange<Double> = 0...100000
return FunctionPair(function: myFunction, inverse: myInverse, domain: myDomain )
}
static func variableWidthExpm(multiplier:Double) -> FunctionPair? {
//function of form log(mx+1)
//expm == eˣ-1 and log1p == log(1+x)
func myFunction(value:Double) -> Double { expm1(value)/multiplier }
func myInverse(value:Double) -> Double { log1p(multiplier*value) }
//lowerbounds: much below -19 looses too much precision to fuzzyMatch.
//upperbounds: Higher than 700 Inverse returns inf
let myDomain:ClosedRange<Double> = -19...700
return FunctionPair(function: myFunction, inverse: myInverse, domain: myDomain )
}
static func clampedLogPair(rangeOfInterest:ClosedRange<Double>, inoutRange:ClosedRange<Double>) -> FunctionPair? {
//function of form log(mx+1)
//expm == eˣ-1 and log1p == log(1+x)
// lowerbound: fails below 0 if multiplier > 1, so just leaving it off for now.
// upperbound: over ~1mil precision errors
let validDomain:ClosedRange<Double> = 0...100000
let newRange = rangeOfInterest.clamped(to: validDomain)
guard newRange == rangeOfInterest else {
fatalError("rangeOfInterest lies outside of valid domain for this function pair.")
}
let domainForPair = inoutRange
func myVariableBaseFunction(value:Double) -> Double {
let normalizedValue = inoutRange.normalizedValue(value)
if !(0.0...1.1).contains(normalizedValue) {
print("value submitted is out side the bounds of \(inoutRange.lowerBound) and \(inoutRange.upperBound)")
}
let valueToSubmit = rangeOfInterest.valueForNormal(normalizedValue)//(value - constant)/scale
print("Submitted Value: \(valueToSubmit)")
return log1p(valueToSubmit)
}
func myVariableBaseInverse(value:Double) -> Double {
let returnValue = expm1(value)
print("Returned Value: \(returnValue)")
let shiftBack = rangeOfInterest.normalizedValue(returnValue)
let expandIntoRange = inoutRange.valueForNormal(shiftBack)
// if !(0.0...1.1).contains(shiftBack) {
// print("Inverse is not returning a value between 0 and 1")
// }
return expandIntoRange
}
return FunctionPair(function: myVariableBaseFunction, inverse: myVariableBaseInverse, domain: domainForPair)
}
func clampedPair(rangeOfInterest:ClosedRange<Double>, inoutRange:ClosedRange<Double>) -> FunctionPair? {
FunctionPair.clampedPair(function: self.function, inverse: self.inverse, domain: self.domain, rangeOfInterest: rangeOfInterest, inoutRange: inoutRange)
}
static func clampedPair(for existingPair:FunctionPair, rangeOfInterest:ClosedRange<Double>, inoutRange:ClosedRange<Double>) -> FunctionPair? {
clampedPair(function: existingPair.function, inverse: existingPair.inverse, domain: existingPair.domain, rangeOfInterest: rangeOfInterest, inoutRange: inoutRange)
}
static func clampedPair(function: @escaping (Double) -> Double, inverse: @escaping (Double) -> Double, domain:ClosedRange<Double>, rangeOfInterest:ClosedRange<Double>, inoutRange:ClosedRange<Double>) -> FunctionPair? {
let newRange = rangeOfInterest.clamped(to: domain)
guard newRange == rangeOfInterest else {
fatalError("rangeOfInterest lies outside of valid domain for this function pair.")
}
func myVariableBaseFunction(value:Double) -> Double {
let normalizedValue = inoutRange.normalizedValue(value)
if !(0.0...1.1).contains(normalizedValue) {
print("value submitted is out side the bounds of \(inoutRange.lowerBound) and \(inoutRange.upperBound)")
}
let valueToSubmit = rangeOfInterest.valueForNormal(normalizedValue)//(value - constant)/scale
print("Submitted Value: \(valueToSubmit)")
return function(valueToSubmit)
}
func myVariableBaseInverse(value:Double) -> Double {
let returnValue = inverse(value)
print("Returned Value: \(returnValue)")
let shiftBack = rangeOfInterest.normalizedValue(returnValue)
let expandIntoRange = inoutRange.valueForNormal(shiftBack)
// if !(0.0...1.1).contains(shiftBack) {
// print("Inverse is not returning a value between 0 and 1")
// }
return expandIntoRange
}
return FunctionPair(function: myVariableBaseFunction, inverse: myVariableBaseInverse, domain: inoutRange)
}
}
//MARK: Defined Pairs
extension FunctionPair {
static let favoriteLogCurvePair:FunctionPair = {
//function of form log(mx+1)
return variableWidthLog1p(multiplier: 9)!
}()
static let favoriteInvLogPair:FunctionPair = {
//function of form eˣ-1 (x == mx)
return variableWidthExpm(multiplier: 9)!
}()
//Purely for test purposes. Will have no effect on a slider really.
static let linearPair:FunctionPair = {
let slope = 2.0
let intercept = 0.0
func myFunction(_ value:Double) -> Double { (value * slope) + intercept }
func myInverse(_ value:Double) -> Double { (value - intercept) / 2 }
return FunctionPair(function: myFunction, inverse: myInverse, domain: -1000.0...1000.0)!
}()
static var log2Pair:FunctionPair = {
func myFunction(_ value:Double) -> Double { log2(value) }
func myInverse(_ value:Double) -> Double { pow(2, value) }
//lower bounds 0.5 function returns -1, at 1 function returns 0.0
//upper bounds not showing muc limit.
let myDomain:ClosedRange<Double> = 1...1000000.0
return FunctionPair(function: myFunction, inverse: myInverse, domain: myDomain )!
}()
static let invlog2Pair:FunctionPair = {
func myFunction(_ value:Double) -> Double { pow(2, value) }
func myInverse(_ value:Double) -> Double { log2(value) }
//lowerbounds: negative numbers work fine. See note on upperbound.
//upperbounds: Magnitude much bigger than 1k fails.
let myDomain:ClosedRange<Double> = -10000.0...10000.0
return FunctionPair(function: myFunction, inverse: myInverse, domain: myDomain )!
}()
static var log10Pair:FunctionPair = {
func myFunction(_ value:Double) -> Double { log10(value) }
func myInverse(_ value:Double) -> Double { pow(10, value) }
//lower bounds 0.5 function returns -1, at 1 function returns 0.0
//upper bounds not showing muc limit.
let myDomain:ClosedRange<Double> = 1...1000000.0
return FunctionPair(function: myFunction, inverse: myInverse, domain: myDomain )!
}()
static let invlog10Pair:FunctionPair = {
func myFunction(_ value:Double) -> Double { pow(10, value) }
func myInverse(_ value:Double) -> Double { log10(value) }
//lowerbounds: negative numbers work fine. See note on upperbound.
//upperbounds: Magnitude much bigger than 1k fails.
let myDomain:ClosedRange<Double> = -10000.0...10000.0
return FunctionPair(function: myFunction, inverse: myInverse, domain: myDomain )!
}()
static private func customBaseLog(base:Double, value: Double) -> Double {
return Darwin.log10(value)/Darwin.log10(base)
}
static func customBaseLogPair(base:Double, multiplier:Double = 1.0) -> FunctionPair? {
func myFunction(value:Double) -> Double { (customBaseLog(base: base, value: value) * multiplier) }
func myInverse(value:Double) -> Double { pow(base, (value / multiplier)) }
//Tested for positive non zero bases ranging from 0.3 to 10.2
//fractional ± values for multiplier all okay.
let myDomain:ClosedRange<Double> = 0.1...100000
return FunctionPair(function: myFunction, inverse: myInverse, domain: myDomain )
}
}
fileprivate extension ClosedRange where Bound == Double {
//Assumes lowerbound is infact the smaller number.
var span:Bound {
upperBound - lowerBound
}
var signedspan:Bound {
upperBound.magnitude - lowerBound.magnitude
}
func normalizedValue(_ value:Bound) -> Bound {
(value - lowerBound)/span
}
func valueForNormal(_ percent:Double) -> Bound {
span*percent + lowerBound
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment