├── LICENSE ├── circle.go ├── fastmath ├── mod.go ├── mod_test.go ├── pow.go └── pow_test.go ├── gmath.go ├── gmath_test.go ├── go.mod ├── helpers.go ├── int_math.go ├── ivec.go ├── pos.go ├── rad.go ├── rad_test.go ├── rand.go ├── rand_picker.go ├── rand_picker_test.go ├── rand_select.go ├── rand_source.go ├── rand_source_test.go ├── range.go ├── rect.go ├── running_average.go ├── scale.go ├── slider.go ├── stdwrap.go ├── utils.go ├── vec.go └── vec_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Iskander (Alex) Sharipov / quasilyte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /circle.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | type Circle struct { 4 | Center Vec 5 | 6 | Radius float64 7 | } 8 | -------------------------------------------------------------------------------- /fastmath/mod.go: -------------------------------------------------------------------------------- 1 | package fastmath 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | func Mod(x, y float64) float64 { 8 | return x - y*math.Floor(x/y) 9 | } 10 | -------------------------------------------------------------------------------- /fastmath/mod_test.go: -------------------------------------------------------------------------------- 1 | package fastmath 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | const eps = 1e-9 11 | 12 | func equalApprox(a, b float64) bool { 13 | return math.Abs(a-b) <= eps 14 | } 15 | 16 | func deviation(x, y float64) float64 { 17 | if x == y { 18 | return 1 19 | } 20 | 21 | if x == 0 { 22 | x += eps 23 | } 24 | if y == 0 { 25 | y += eps 26 | } 27 | 28 | if x < y { 29 | x, y = y, x 30 | } 31 | return x / y 32 | } 33 | 34 | func TestMod(t *testing.T) { 35 | r := rand.New(rand.NewSource(time.Now().Unix())) 36 | multipliers := []float64{1, -1} 37 | for _, m := range multipliers { 38 | for i := 0; i < 5000; i++ { 39 | x := r.Float64() * m 40 | y := r.Float64() * m 41 | have := Mod(x, y) 42 | want := math.Mod(x, y) 43 | // if have != want { 44 | if !equalApprox(have, want) { 45 | t.Fatalf("[%d] mod(%v, %v):\nhave=%v\nwant=%v", i, x, y, have, want) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /fastmath/pow.go: -------------------------------------------------------------------------------- 1 | package fastmath 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | func IntPow(x float64, y int) float64 { 8 | if y < 0 { 9 | return 1 / IntPow(x, -y) 10 | } 11 | 12 | result := 1.0 13 | exp := y 14 | base := x 15 | for exp != 0 { 16 | if (exp & 1) != 0 { 17 | result *= base 18 | } 19 | base *= base 20 | exp >>= 1 21 | } 22 | return result 23 | } 24 | 25 | func Pow1_5(x float64) float64 { 26 | return x * math.Sqrt(x) 27 | } 28 | -------------------------------------------------------------------------------- /fastmath/pow_test.go: -------------------------------------------------------------------------------- 1 | package fastmath 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | var powTestValues = []float64{ 9 | 0, 10 | 11 | 1, 5, 9, 140, 19495, 8988538, 912435823852384, 12 | 13 | 0.1, 0.114, 1.3, 1.7, 1.7949, 2.5436, 8345.234895, 999.9999991, 14 | } 15 | 16 | func TestIntPow(t *testing.T) { 17 | exponents := []int{ 18 | 0, 19 | 1, 2, 3, 6, 19, 95, 20 | -1, -5, -94, 21 | } 22 | for _, v := range powTestValues { 23 | for _, exp := range exponents { 24 | have := IntPow(v, exp) 25 | want := math.Pow(v, float64(exp)) 26 | errorPercent := deviation(have, want) - 1 27 | if errorPercent >= 0.0001 { 28 | t.Errorf("pow(%f, %f):\nhave: %f\nwant: %f\nerror: %f", float64(exp), v, have, want, errorPercent) 29 | } 30 | } 31 | } 32 | } 33 | 34 | func TestPow1_5(t *testing.T) { 35 | for _, v := range powTestValues { 36 | have := Pow1_5(v) 37 | want := math.Pow(v, 1.5) 38 | errorPercent := deviation(have, want) - 1 39 | if errorPercent >= 0.0001 { 40 | t.Errorf("pow(%f, 1.5):\nhave: %f\nwant: %f\nerror: %f", v, have, want, errorPercent) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /gmath.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | const Epsilon = 1e-9 8 | 9 | func EqualApprox[T float](a, b T) bool { 10 | return math.Abs(float64(a-b)) <= Epsilon 11 | } 12 | 13 | // CeilN applies Ceil(x) and then rounds up to the closest n multiplier. 14 | // 15 | // This function is useful when trying to turn an arbitrary value 16 | // into a pretty value. E.g. it can turn a price of 139 into 140 17 | // if x=139 and n=5. 18 | // 19 | // The return value is float, but it's guaranteed to be integer-like 20 | // (unless you're working with really high x values). 21 | // 22 | // * CeilN(0, 0) => 0 23 | // * CeilN(0, 2) => 0 24 | // * CeilN(1, 2) => 2 25 | // * CeilN(2, 2) => 2 26 | // * CeilN(3, 2) => 4 27 | // * CeilN(144, 5) => 145 28 | // * CeilN(145, 5) => 145 29 | // * CeilN(146, 5) => 150 30 | func CeilN(x float64, n int) float64 { 31 | iv := int(math.Ceil(x)) 32 | 33 | extra := 0 34 | if n != 0 { 35 | extra = n - (iv % n) 36 | if extra == n { 37 | extra = 0 38 | } 39 | } 40 | 41 | return float64(iv + extra) 42 | } 43 | 44 | // TriangularCurve finds y on a triangular curve for the specified [x]. 45 | // The y is 1.0 when [x]=[peak], 46 | // it rises linearly from [a] to [peak], 47 | // and then it falls linearly from [peak] to [b]. 48 | // 49 | // This function makes the most sense when [a] < [peak] < [b]. 50 | // [x] can have any value, but it is usually better 51 | // to keep it inside the [a, b] range. 52 | func TriangularCurve(x, a, peak, b float64) float64 { 53 | switch { 54 | case x <= a || x >= b: 55 | return 0 56 | case x < peak: 57 | return (x - a) / (peak - a) 58 | case x > peak: 59 | return (b - x) / (b - peak) 60 | default: 61 | return 1 62 | } 63 | } 64 | 65 | // ScaledSum calculates an arithmetic progression that is 66 | // often used in level-up experience requirement scaling. 67 | // For example, if baseValue (exp for level 2) is 100, 68 | // and the step (increase) is 25, then we have these values per level: 69 | // * level=0 => 0 (the default value) 70 | // * level=1 => 100 (+100) 71 | // * level=2 => 225 (+125) 72 | // * level=3 => 375 (+150) 73 | // * level=4 => 550 (+175) 74 | // ... 75 | // It can also handle fractional level values: 76 | // * level=1.5 => ~160 for the example above 77 | // 78 | // As a special case, it always returns 0 for levels<=0. 79 | func ScaledSum(baseValue, step, level float64) float64 { 80 | if level <= 0 { 81 | return 0 82 | } 83 | k := level 84 | return (k * (2*baseValue + (k-1)*step)) * 0.5 85 | } 86 | 87 | // Lerp linearly interpolates between [from] and [to] using the weight [t]. 88 | // The [t] value is usually in the range from 0 to 1. 89 | func Lerp[T float](from, to, t T) T { 90 | return from + ((to - from) * t) 91 | } 92 | 93 | // InvLerp returns an interpolation or extrapolation factor considering the given range and weight [t]. 94 | // The [t] value is usually in the range from 0 to 1. 95 | func InvLerp[T float](from, to, value T) T { 96 | return (value - from) / (to - from) 97 | } 98 | 99 | // LerpClamped linearly interpolates between [from] and [to] using the weight [t]. 100 | // The [t] value is clamped to the range from 0 to 1. 101 | func LerpClamped[T float](from, to, t T) T { 102 | t = Clamp(t, 0, 1) 103 | return from + ((to - from) * t) 104 | } 105 | 106 | // Remap maps a value from one range to another. 107 | // 108 | // The first range is defined by a pair of [fromMin] and [fromMax]. 109 | // The second range is defined by a pair of [toMin] and [toMax]. 110 | // The [t] value is usually in the range from 0 to 1. 111 | func Remap[T float](fromMin, fromMax, toMin, toMax, value T) T { 112 | t := InvLerp(fromMin, fromMax, value) 113 | return Lerp(toMin, toMax, t) 114 | } 115 | 116 | func ArcContains(angle, measure Rad, pos, point Vec) bool { 117 | startAngle := (angle - measure/2) 118 | endAngle := (angle + measure/2) 119 | if endAngle < startAngle { 120 | endAngle += 2 * math.Pi 121 | } 122 | half := (endAngle - startAngle) / 2 123 | mid := (endAngle + startAngle) / 2 124 | coshalf := math.Cos(float64(half)) 125 | angleToPoint := pos.AngleToPoint(point).Normalized() 126 | return math.Cos(float64(angleToPoint-mid)) >= coshalf 127 | } 128 | 129 | func ArcSectionContains(angle, measure Rad, r float64, pos, point Vec) bool { 130 | if pos.DistanceSquaredTo(point) > r*r { 131 | return false 132 | } 133 | return ArcContains(angle, measure, pos, point) 134 | } 135 | 136 | func Clamp[T numeric](v, min, max T) T { 137 | if v < min { 138 | return min 139 | } 140 | if v > max { 141 | return max 142 | } 143 | return v 144 | } 145 | 146 | func ClampMin[T numeric](v, min T) T { 147 | if v < min { 148 | return min 149 | } 150 | return v 151 | } 152 | 153 | func ClampMax[T numeric](v, max T) T { 154 | if v > max { 155 | return max 156 | } 157 | return v 158 | } 159 | 160 | func Percentage[T numeric](value, max T) T { 161 | if max == 0 && value == 0 { 162 | return 0 163 | } 164 | return T(100 * (float64(value) / float64(max))) 165 | } 166 | 167 | // Scale multiplies value by m. 168 | // 169 | // For a floating-point value this operation would 170 | // not make any sense as it can be expressed value*m, 171 | // but integer value scaling with a floating point m 172 | // involves conversions and rounding. 173 | // 174 | // This function rounds the scaled integer to the 175 | // nearest integer result. 176 | // For example: 177 | // - Scale(1, 1.8) => 2 (rounds up) 178 | // - Scale(1, 1.4) => 1 (rounds down) 179 | // - Scale(1, 1) => 1 180 | // - Scale(1, 0) => 0 181 | func Scale[T integer](value T, m float64) T { 182 | return T(math.Round(float64(value) * m)) 183 | } 184 | 185 | // Iround is a helper to perform int(math.Round(x)) operation. 186 | // 187 | // This function reduces the number of parenthesis the final 188 | // expression will have. 189 | func Iround(x float64) int { 190 | return int(math.Round(x)) 191 | } 192 | 193 | // Deviation reports the weight of the difference between x and y. 194 | // 195 | // In other words, it returns the multiplier to make the lower value 196 | // between the two identical to other. 197 | // 198 | // Some examples: 199 | // * (1, 2) => 2 (1*2 = 2) 200 | // * (10, 2) => 5 (2*5 = 10) 201 | // * (3, 3) => 1 202 | // 203 | // The order of parameters do not affect the result. 204 | func Deviation[T float](x, y T) T { 205 | if x == y { 206 | return 1 207 | } 208 | 209 | if x == 0 { 210 | x += Epsilon 211 | } 212 | if y == 0 { 213 | y += Epsilon 214 | } 215 | 216 | if x < y { 217 | x, y = y, x 218 | } 219 | return x / y 220 | } 221 | 222 | // InBounds reports whether v is in the specified inclusive range. 223 | func InBounds[T numeric](v, min, max T) bool { 224 | return v >= min && v <= max 225 | } 226 | 227 | type integer interface { 228 | signedInteger | 229 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 230 | } 231 | 232 | type signedInteger interface { 233 | ~int | ~int8 | ~int16 | ~int32 | ~int64 234 | } 235 | 236 | type numeric interface { 237 | integer | float 238 | } 239 | 240 | type float interface { 241 | ~float32 | ~float64 242 | } 243 | -------------------------------------------------------------------------------- /gmath_test.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import "testing" 4 | 5 | func TestCeilN(t *testing.T) { 6 | tests := []struct { 7 | x float64 8 | n int 9 | want float64 10 | }{ 11 | {0, 0, 0}, 12 | {0, 2, 0}, 13 | {1, 2, 2}, 14 | {2, 2, 2}, 15 | {3, 2, 4}, 16 | {144, 5, 145}, 17 | {145, 5, 145}, 18 | {146, 5, 150}, 19 | } 20 | 21 | for _, test := range tests { 22 | have := CeilN(test.x, test.n) 23 | if have != test.want { 24 | t.Fatalf("ceiln(x=%f, n=%d):\nhave: %v\nwant: %v", test.x, test.n, have, test.want) 25 | } 26 | } 27 | } 28 | 29 | func TestDeviation(t *testing.T) { 30 | tests := []struct { 31 | x float64 32 | y float64 33 | result float64 34 | }{ 35 | {1, 1, 1}, 36 | {0, 0, 1}, 37 | 38 | {1, 2, 2}, 39 | {2, 1, 2}, 40 | {100, 200, 2}, 41 | {200, 100, 2}, 42 | {50, 200, 4}, 43 | {200, 50, 4}, 44 | } 45 | 46 | for i := range tests { 47 | test := tests[i] 48 | have := Deviation(test.x, test.y) 49 | want := test.result 50 | if !EqualApprox(have, want) { 51 | t.Fatalf("results mismatched for (%v, %v):\nhave: %v\nwant: %v", 52 | test.x, test.y, have, want) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/quasilyte/gmath 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import "github.com/quasilyte/gmath/fastmath" 4 | 5 | func cubicInterpolate(from, to, pre, post, t float64) float64 { 6 | return 0.5 * 7 | ((from * 2.0) + 8 | (-pre+to)*t + 9 | (2.0*pre-5.0*from+4.0*to-post)*(t*t) + 10 | (-pre+3.0*from-3.0*to+post)*(t*t*t)) 11 | } 12 | 13 | func fposmod(x, y float64) float64 { 14 | value := fastmath.Mod(x, y) 15 | if ((value < 0) && (y > 0)) || ((value > 0) && (y < 0)) { 16 | value += y 17 | } 18 | return value 19 | } 20 | -------------------------------------------------------------------------------- /int_math.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | func Iabs[T integer](x T) T { 4 | if x < 0 { 5 | return -x 6 | } 7 | return x 8 | } 9 | -------------------------------------------------------------------------------- /ivec.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | type Ivec8 = Ivec[int8] 4 | 5 | type Ivec[T integer] struct { 6 | X T 7 | Y T 8 | } 9 | 10 | // ManhattanDistanceTo finds a Manhattan distance between 11 | // the two vectors interpreted as coordinates (2D points). 12 | func (v Ivec[T]) ManhattanDistanceTo(other Ivec[T]) T { 13 | return Iabs(v.X-other.X) + Iabs(v.Y-other.Y) 14 | } 15 | 16 | func (v Ivec[T]) Add(other Ivec[T]) Ivec[T] { 17 | return Ivec[T]{ 18 | X: v.X + other.X, 19 | Y: v.Y + other.Y, 20 | } 21 | } 22 | 23 | func (v Ivec[T]) Sub(other Ivec[T]) Ivec[T] { 24 | return Ivec[T]{ 25 | X: v.X - other.X, 26 | Y: v.Y - other.Y, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pos.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | // Pos represents a position with optional offset relative to its base. 4 | type Pos struct { 5 | Base *Vec 6 | Offset Vec 7 | } 8 | 9 | func MakePos(base Vec) Pos { 10 | return Pos{Base: &base} 11 | } 12 | 13 | func (p Pos) Resolve() Vec { 14 | if p.Base == nil { 15 | return p.Offset 16 | } 17 | return p.Base.Add(p.Offset) 18 | } 19 | 20 | func (p *Pos) SetBase(base Vec) { 21 | p.Base = &base 22 | } 23 | 24 | func (p *Pos) Set(base *Vec, offsetX, offsetY float64) { 25 | p.Base = base 26 | p.Offset.X = offsetX 27 | p.Offset.Y = offsetY 28 | } 29 | 30 | func (p Pos) WithOffset(offsetX, offsetY float64) Pos { 31 | return Pos{ 32 | Base: p.Base, 33 | Offset: Vec{X: p.Offset.X + offsetX, Y: p.Offset.Y + offsetY}, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rad.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/quasilyte/gmath/fastmath" 7 | ) 8 | 9 | // Rad represents a radian value. 10 | // It's not capped in [0, 2*Pi] range. 11 | // 12 | // In terms of the orientations, Pi rotation points the object down (South). 13 | // Zero radians point towards the right side (East). 14 | type Rad float64 15 | 16 | func DegToRad(deg float64) Rad { 17 | return Rad(deg * (math.Pi / 180)) 18 | } 19 | 20 | func RadToDeg(r Rad) float64 { 21 | return float64(r * (180 / math.Pi)) 22 | } 23 | 24 | // Positive returns the equivalent radian value expressed as a positive value. 25 | func (r Rad) Positive() Rad { 26 | if r >= 0 { 27 | return r 28 | } 29 | return r + 2*math.Pi 30 | } 31 | 32 | // Normalized returns the equivalent radians value in [0, 2*Pi] range. 33 | // For example, 3*Pi becomes just Pi. 34 | func (r Rad) Normalized() Rad { 35 | angle := float64(r) 36 | angle -= math.Floor(angle/(2*math.Pi)) * 2 * math.Pi 37 | return Rad(angle) 38 | } 39 | 40 | // EqualApprox compares two radian values using EqualApprox function. 41 | // Note that you may want to normalize the operands in some way before doing this. 42 | func (r Rad) EqualApprox(other Rad) bool { 43 | return EqualApprox(float64(r), float64(other)) 44 | } 45 | 46 | // AngleDelta returns an angle delta between two radian values. 47 | // The sign is preserved. 48 | // 49 | // When using this function to calculate a rotation direction (CW vs CCW), 50 | // r is a current rotation and r2 is a target rotation. 51 | // 52 | // It doesn't need the angles to be normalized, 53 | // r=0 and r=2*Pi are considered to have no delta. 54 | // The return value is always normalized. 55 | func (r Rad) AngleDelta(r2 Rad) Rad { 56 | return Rad(fposmod(float64(r2-r+math.Pi), 2*math.Pi) - math.Pi) 57 | } 58 | 59 | func (r Rad) LerpAngle(toAngle Rad, weight float64) Rad { 60 | difference := fastmath.Mod(float64(toAngle)-float64(r), 2*math.Pi) 61 | dist := fastmath.Mod(2.0*difference, 2*math.Pi) - difference 62 | rotationAmount := Rad(dist * weight) 63 | // TODO: can this be optimized? 64 | // AngleDelta should be replaced by something more efficient. 65 | // Or maybe we can avoid abs on the both sides? 66 | if r.AngleDelta(toAngle).Abs() < rotationAmount.Abs() { 67 | return toAngle 68 | } 69 | return r + rotationAmount 70 | } 71 | 72 | func (r Rad) RotatedTowards(toAngle, amount Rad) Rad { 73 | difference := fastmath.Mod(float64(toAngle)-float64(r), 2*math.Pi) 74 | dist := fastmath.Mod(2.0*difference, 2*math.Pi) - difference 75 | if EqualApprox(dist, 0) { 76 | return toAngle 77 | } 78 | lerpa1 := Rad(float64(r) + dist) 79 | if min := r - amount; lerpa1 < min { 80 | return min 81 | } 82 | if max := r + amount; lerpa1 > max { 83 | return max 84 | } 85 | return lerpa1 86 | } 87 | 88 | func (r Rad) Abs() float64 { 89 | return math.Abs(float64(r)) 90 | } 91 | 92 | func (r Rad) Cos() float64 { 93 | return math.Cos(float64(r)) 94 | } 95 | 96 | func (r Rad) Sin() float64 { 97 | return math.Sin(float64(r)) 98 | } 99 | -------------------------------------------------------------------------------- /rad_test.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestRadPositive(t *testing.T) { 9 | tests := []struct { 10 | a Rad 11 | want Rad 12 | }{ 13 | {0, 0}, 14 | {-1, 5.2831853072}, 15 | {-math.Pi, math.Pi}, 16 | {1, 1}, 17 | {4 * math.Pi, 4 * math.Pi}, 18 | {math.Pi, math.Pi}, 19 | } 20 | 21 | for _, test := range tests { 22 | have := test.a.Positive() 23 | if !have.EqualApprox(test.want) { 24 | t.Fatalf("Positive(%f):\nhave: %.10f\nwant: %.10f", test.a, have, test.want) 25 | } 26 | } 27 | } 28 | 29 | func TestRadNormalized(t *testing.T) { 30 | tests := []struct { 31 | a Rad 32 | want Rad 33 | }{ 34 | {0, 0}, 35 | {1, 1}, 36 | {3 * math.Pi, math.Pi}, 37 | {5 * math.Pi, math.Pi}, 38 | {7 * math.Pi, math.Pi}, 39 | {19 * math.Pi, math.Pi}, 40 | {20 * math.Pi, 0}, 41 | {math.Pi, math.Pi}, 42 | {-math.Pi, math.Pi}, 43 | {-0.2, 2*math.Pi - 0.2}, 44 | {-2 * math.Pi, 0}, 45 | {-4 * math.Pi, 0}, 46 | {-5 * math.Pi, math.Pi}, 47 | } 48 | 49 | for _, test := range tests { 50 | have := test.a.Normalized() 51 | if !have.EqualApprox(test.want) { 52 | t.Fatalf("Normalized(%f):\nhave: %f\nwant: %f", test.a, have, test.want) 53 | } 54 | } 55 | } 56 | 57 | func TestRadAngleDelta(t *testing.T) { 58 | tests := []struct { 59 | a Rad 60 | b Rad 61 | want Rad 62 | }{ 63 | {0, 0, 0}, 64 | {1, 1, 0}, 65 | {-0.2, 0.2, 0.4}, 66 | {0.4, 0.2, -0.2}, 67 | {-0.5, -0.2, 0.3}, 68 | {0.4, 0, -0.4}, 69 | {math.Pi, 0, -math.Pi}, 70 | {-math.Pi, 0, -math.Pi}, 71 | {2 * math.Pi, 0, 0}, 72 | {4 * math.Pi, 0, 0}, 73 | {6 * math.Pi, 0, 0}, 74 | {3 * math.Pi, 0, -math.Pi}, 75 | {0, 3 * math.Pi, -math.Pi}, 76 | {0, 2.9 * math.Pi, Rad(2.9 * math.Pi).Normalized()}, 77 | {2*math.Pi - 0.1, 0.1, 0.2}, 78 | {0.1, 2*math.Pi - 0.1, -0.2}, 79 | {4*math.Pi - 0.1, 0.1, 0.2}, 80 | {0.1, 4*math.Pi - 0.1, -0.2}, 81 | {8*math.Pi - 0.1, 2*math.Pi + 0.1, 0.2}, 82 | {2*math.Pi + 0.1, 8*math.Pi - 0.1, -0.2}, 83 | {math.Pi / 2, -0.2, -(math.Pi/2 + 0.2)}, 84 | {-0.2, math.Pi / 2, math.Pi/2 + 0.2}, 85 | } 86 | 87 | for _, test := range tests { 88 | have := test.a.AngleDelta(test.b) 89 | if !have.EqualApprox(test.want) { 90 | t.Fatalf("AngleDelta(from=%f, to=%f):\nhave: %f\nwant: %f", 91 | test.a, test.b, have, test.want) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /rand.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | ) 7 | 8 | type Rand struct { 9 | rng *rand.Rand 10 | } 11 | 12 | func (r *Rand) SetSeed(seed int64) { 13 | src := rand.NewSource(seed) 14 | r.rng = rand.New(src) 15 | } 16 | 17 | func (r *Rand) SetSource(source rand.Source) { 18 | r.rng = rand.New(source) 19 | } 20 | 21 | func (r *Rand) Offset(min, max float64) Vec { 22 | return Vec{X: r.FloatRange(min, max), Y: r.FloatRange(min, max)} 23 | } 24 | 25 | func (r *Rand) Chance(probability float64) bool { 26 | return r.rng.Float64() <= probability 27 | } 28 | 29 | func (r *Rand) Bool() bool { 30 | return r.rng.Float64() < 0.5 31 | } 32 | 33 | func (r *Rand) IntRange(min, max int) int { 34 | return min + r.rng.Intn(max-min+1) 35 | } 36 | 37 | func (r *Rand) PositiveInt64() int64 { 38 | return r.rng.Int63() 39 | } 40 | 41 | func (r *Rand) PositiveInt() int { 42 | return r.rng.Int() 43 | } 44 | 45 | func (r *Rand) Uint64() uint64 { 46 | return r.rng.Uint64() 47 | } 48 | 49 | func (r *Rand) Uint32() uint32 { 50 | return r.rng.Uint32() 51 | } 52 | 53 | func (r *Rand) Float() float64 { 54 | return r.rng.Float64() 55 | } 56 | 57 | func (r *Rand) FloatRange(min, max float64) float64 { 58 | return min + r.rng.Float64()*(max-min) 59 | } 60 | 61 | func (r *Rand) Rad() Rad { 62 | return Rad(r.FloatRange(0, 2*math.Pi)) 63 | } 64 | 65 | func RandIndex[T any](r *Rand, slice []T) int { 66 | if len(slice) == 0 { 67 | return -1 68 | } 69 | return r.IntRange(0, len(slice)-1) 70 | } 71 | 72 | func RandElem[T any](r *Rand, slice []T) (elem T) { 73 | if len(slice) == 0 { 74 | return elem // Zero value 75 | } 76 | if len(slice) == 1 { 77 | return slice[0] 78 | } 79 | return slice[r.rng.Intn(len(slice))] 80 | } 81 | 82 | func RandIterate[T any](r *Rand, slice []T, f func(x T) bool) T { 83 | var result T 84 | 85 | if len(slice) == 0 { 86 | return result 87 | } 88 | if len(slice) == 1 { 89 | // Don't use rand() if there is only 1 element. 90 | x := slice[0] 91 | if f(x) { 92 | result = x 93 | } 94 | return result 95 | } 96 | 97 | var slider Slider 98 | slider.SetBounds(0, len(slice)-1) 99 | slider.TrySetValue(r.IntRange(0, len(slice)-1)) 100 | inc := r.Bool() 101 | for i := 0; i < len(slice); i++ { 102 | x := slice[slider.Value()] 103 | if inc { 104 | slider.Inc() 105 | } else { 106 | slider.Dec() 107 | } 108 | if f(x) { 109 | result = x 110 | break 111 | } 112 | } 113 | return result 114 | } 115 | 116 | func RandIterateRef[T any](r *Rand, slice []T, f func(x *T) bool) *T { 117 | if len(slice) == 0 { 118 | return nil 119 | } 120 | if len(slice) == 1 { 121 | // Don't use rand() if there is only 1 element. 122 | x := &slice[0] 123 | if f(x) { 124 | return x 125 | } 126 | return nil 127 | } 128 | 129 | var result *T 130 | var slider Slider 131 | slider.SetBounds(0, len(slice)-1) 132 | slider.TrySetValue(r.IntRange(0, len(slice)-1)) 133 | inc := r.Bool() 134 | for i := 0; i < len(slice); i++ { 135 | x := &slice[slider.Value()] 136 | if inc { 137 | slider.Inc() 138 | } else { 139 | slider.Dec() 140 | } 141 | if f(x) { 142 | result = x 143 | break 144 | } 145 | } 146 | return result 147 | } 148 | 149 | func Shuffle[T any](r *Rand, slice []T) { 150 | r.rng.Shuffle(len(slice), func(i, j int) { 151 | slice[i], slice[j] = slice[j], slice[i] 152 | }) 153 | } 154 | -------------------------------------------------------------------------------- /rand_picker.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | // RandPicker performs a uniformly distributed random probing among the given objects with weights. 8 | // Higher the weight, higher the chance of that object of being picked. 9 | type RandPicker[T any] struct { 10 | r *Rand 11 | 12 | keys randPickerKeySlice 13 | values []T 14 | 15 | threshold float64 16 | sorted bool 17 | } 18 | 19 | type randPickerKey struct { 20 | index int 21 | threshold float64 22 | } 23 | 24 | type randPickerKeySlice []randPickerKey 25 | 26 | func (s *randPickerKeySlice) Len() int { return len(*s) } 27 | func (s *randPickerKeySlice) Less(i, j int) bool { return (*s)[i].threshold < (*s)[j].threshold } 28 | func (s *randPickerKeySlice) Swap(i, j int) { (*s)[i], (*s)[j] = (*s)[j], (*s)[i] } 29 | 30 | func NewRandPicker[T any](r *Rand) *RandPicker[T] { 31 | return &RandPicker[T]{r: r} 32 | } 33 | 34 | func (p *RandPicker[T]) Reset() { 35 | p.keys = p.keys[:0] 36 | p.values = p.values[:0] 37 | p.threshold = 0 38 | p.sorted = false 39 | } 40 | 41 | func (p *RandPicker[T]) AddOption(value T, weight float64) { 42 | if weight == 0 { 43 | return // Zero probability in any case 44 | } 45 | p.threshold += weight 46 | p.keys = append(p.keys, randPickerKey{ 47 | threshold: p.threshold, 48 | index: len(p.values), 49 | }) 50 | p.values = append(p.values, value) 51 | p.sorted = false 52 | } 53 | 54 | func (p *RandPicker[T]) IsEmpty() bool { 55 | return len(p.values) == 0 56 | } 57 | 58 | func (p *RandPicker[T]) Pick() T { 59 | var result T 60 | if len(p.values) == 0 { 61 | return result // Zero value 62 | } 63 | if len(p.values) == 1 { 64 | return p.values[0] 65 | } 66 | 67 | p.ensureSorted() 68 | 69 | roll := p.r.FloatRange(0, p.threshold) 70 | i := sort.Search(len(p.keys), func(i int) bool { 71 | return roll <= p.keys[i].threshold 72 | }) 73 | if i < len(p.keys) && roll <= p.keys[i].threshold { 74 | result = p.values[p.keys[i].index] 75 | } else { 76 | result = p.values[len(p.values)-1] 77 | } 78 | return result 79 | } 80 | 81 | func (p *RandPicker[T]) ensureSorted() { 82 | // In a normal use case the random picker is initialized and then used 83 | // without adding extra options, so this sorting will happen only once in that case. 84 | if !p.sorted { 85 | sort.Sort(&p.keys) 86 | p.sorted = true 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /rand_picker_test.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func BenchmarkRandPicker(b *testing.B) { 8 | var rng Rand 9 | rng.SetSeed(2493) 10 | picker := NewRandPicker[float64](&rng) 11 | 12 | b.ResetTimer() 13 | 14 | for i := 0; i < b.N; i++ { 15 | for j := 0; j < 100; j++ { 16 | picker.Reset() 17 | picker.AddOption(132, 0.4) 18 | picker.AddOption(1320, 0.2) 19 | picker.AddOption(13200, 0.1) 20 | picker.AddOption(132000, 0.5) 21 | picker.AddOption(1320000, 0.001) 22 | picker.Pick() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rand_select.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | type RandSelectConfig[T any] struct { 4 | Rand *Rand 5 | 6 | // Whether higher weights should end up in 7 | // the beginning of the slice. 8 | HigherFirst bool 9 | 10 | Options []RandSelectOption[T] 11 | } 12 | 13 | type RandSelectOption[T any] struct { 14 | Weight float64 15 | Data T 16 | } 17 | 18 | func ToWeightThresholds[T any](options []RandSelectOption[T]) float64 { 19 | totalWeight := 0.0 20 | for i := range options { 21 | o := &options[i] 22 | totalWeight += o.Weight 23 | o.Weight = totalWeight 24 | } 25 | return totalWeight 26 | } 27 | 28 | func RandSelect[T any](config RandSelectConfig[T]) { 29 | options := config.Options 30 | 31 | totalWeight := 0.0 32 | for _, o := range options { 33 | totalWeight += o.Weight 34 | } 35 | 36 | if config.HigherFirst { 37 | randSelectHigherFirst(config.Rand, totalWeight, options) 38 | } else { 39 | randSelectLowerFirst(config.Rand, totalWeight, options) 40 | } 41 | } 42 | 43 | func randSelectHigherFirst[T any](rand *Rand, totalWeight float64, options []RandSelectOption[T]) { 44 | for i := 0; i < len(options)-1; i++ { 45 | r := rand.Float() * totalWeight 46 | j := 0 47 | 48 | sum := 0.0 49 | for j = i; j < len(options); j++ { 50 | sum += options[j].Weight 51 | if sum >= r { 52 | break 53 | } 54 | } 55 | 56 | w := options[j].Weight 57 | options[i], options[j] = options[j], options[i] 58 | totalWeight -= w 59 | } 60 | } 61 | 62 | func randSelectLowerFirst[T any](rand *Rand, totalWeight float64, options []RandSelectOption[T]) { 63 | for i := len(options) - 1; i > 0; i-- { 64 | r := rand.Float() * totalWeight 65 | j := 0 66 | 67 | sum := options[0].Weight 68 | for sum < r { 69 | j++ 70 | sum += options[j].Weight 71 | } 72 | 73 | w := options[j].Weight 74 | options[i], options[j] = options[j], options[i] 75 | totalWeight -= w 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /rand_source.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | const ( 4 | pcgMultiplier = 6364136223846793005 5 | ) 6 | 7 | // RandSource is a PCG-64 implementation of a rand source. 8 | // 9 | // Its benefits are: 10 | // * efficient storage (2 uints instead of tables) 11 | // * can save/load the source state (just 1 uint64 for a state) 12 | // * fast seeding/reseeding 13 | // 14 | // A zero value of this rand source is not enough. 15 | // Call [Seed] before using it or load the existing state. 16 | type RandSource struct { 17 | state uint64 18 | inc uint64 19 | } 20 | 21 | func (s *RandSource) Seed(v int64) { 22 | s.setSeed(uint64(v)) 23 | } 24 | 25 | func (s *RandSource) Int63() int64 { 26 | // Rand v1 requires Int63 to return a non-negative 27 | // int64-typed value, hence the mask hacks. 28 | 29 | const ( 30 | rngMax = 1 << 63 31 | rngMask = rngMax - 1 32 | ) 33 | 34 | v := s.Uint64() 35 | return int64(v & rngMask) 36 | } 37 | 38 | func (s *RandSource) Uint64() uint64 { 39 | state := s.state 40 | s.step() 41 | return s.rand64(state) 42 | } 43 | 44 | func (s *RandSource) setSeed(seed uint64) { 45 | s.state = 0 46 | s.inc = 1442695040888963407 + (seed * 151) 47 | 48 | s.step() 49 | s.state += seed 50 | s.step() 51 | } 52 | 53 | func (s *RandSource) GetState() uint64 { 54 | return s.state 55 | } 56 | 57 | func (s *RandSource) SetState(state uint64) { 58 | s.state = state 59 | } 60 | 61 | func (s *RandSource) step() { 62 | s.state = s.state*pcgMultiplier + s.inc 63 | } 64 | 65 | func (s *RandSource) rand64(state uint64) uint64 { 66 | word := ((state >> ((state >> 59) + 5)) ^ state) * 12605985483714917081 67 | return (word >> 43) ^ word 68 | } 69 | -------------------------------------------------------------------------------- /rand_source_test.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import ( 4 | "math/rand" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | var ( 10 | _ rand.Source = (*RandSource)(nil) 11 | _ rand.Source64 = (*RandSource)(nil) 12 | ) 13 | 14 | func TestRandSourceReload(t *testing.T) { 15 | const ( 16 | seed = 435 17 | numSkips = 5 18 | ) 19 | 20 | var rng RandSource 21 | rng.Seed(seed) 22 | 23 | for i := 0; i < numSkips; i++ { 24 | rng.Uint64() 25 | } 26 | state := rng.GetState() 27 | 28 | values := make([]uint64, 10) 29 | for i := range values { 30 | values[i] = rng.Uint64() 31 | } 32 | 33 | var rng2 RandSource 34 | rng2.Seed(seed) 35 | rng2.SetState(state) 36 | values2 := make([]uint64, 10) 37 | for i := range values2 { 38 | values2[i] = rng2.Uint64() 39 | } 40 | 41 | for i := range values { 42 | v1 := values[i] 43 | v2 := values2[i] 44 | if v1 != v2 { 45 | t.Fatalf("[i=%d] value mismatch:\nhave: %d\nwant: %d", i, v2, v1) 46 | } 47 | } 48 | 49 | rng.Seed(seed) 50 | for i := 0; i < numSkips; i++ { 51 | rng.Uint64() 52 | } 53 | values3 := make([]uint64, 10) 54 | for i := range values2 { 55 | values3[i] = rng.Uint64() 56 | } 57 | 58 | if !reflect.DeepEqual(values, values3) { 59 | t.Fatalf("second round failed") 60 | } 61 | } 62 | 63 | func TestRandSourceQuality(t *testing.T) { 64 | 65 | seeds := []int64{ 66 | 0, 67 | 1, 68 | 548238, 69 | 19, 70 | 902395988182, 71 | } 72 | 73 | for _, seed := range seeds { 74 | var rng RandSource 75 | rng.Seed(seed) 76 | valueSet := make(map[int64]struct{}, 10000) 77 | for i := 0; i < 10000; i++ { 78 | v := rng.Int63() 79 | if _, ok := valueSet[v]; ok { 80 | t.Fatalf("seed=%d i=%d has repeated value", seed, i) 81 | } 82 | valueSet[v] = struct{}{} 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /range.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | type Range[T numeric] struct { 4 | Min T 5 | Max T 6 | } 7 | 8 | func MakeRange[T numeric](min, max T) Range[T] { 9 | return Range[T]{Min: min, Max: max} 10 | } 11 | 12 | func (rng Range[T]) IsZero() bool { 13 | return rng == Range[T]{} 14 | } 15 | 16 | func (rng Range[T]) IsValid() bool { 17 | return rng.Max >= rng.Min 18 | } 19 | 20 | func (rng Range[T]) Contains(v T) bool { 21 | return v >= rng.Min && v <= rng.Max 22 | } 23 | 24 | func (rng Range[T]) Clamp(v T) T { 25 | return Clamp(v, rng.Min, rng.Max) 26 | } 27 | 28 | func (rng Range[T]) InBounds(minValue, maxValue T) bool { 29 | return rng.Min >= minValue && rng.Max <= maxValue && 30 | rng.IsValid() 31 | } 32 | 33 | func (rng Range[T]) Len() int { 34 | return int(rng.Max - rng.Min) 35 | } 36 | -------------------------------------------------------------------------------- /rect.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | type Rect struct { 8 | Min Vec 9 | Max Vec 10 | } 11 | 12 | // RectFromStd converts an [image.Rectangle] into a [Rect]. 13 | // There is [Rect.ToStd] method to reverse it. 14 | func RectFromStd(src image.Rectangle) Rect { 15 | return Rect{ 16 | Min: VecFromStd(src.Min), 17 | Max: VecFromStd(src.Max), 18 | } 19 | } 20 | 21 | // ToStd converts an [Rect] into a [image.Rectangle]. 22 | // There is [RectFromStd] function to reverse it. 23 | func (r Rect) ToStd() image.Rectangle { 24 | return image.Rectangle{ 25 | Min: r.Min.ToStd(), 26 | Max: r.Max.ToStd(), 27 | } 28 | } 29 | 30 | func (r Rect) IsZero() bool { 31 | return r == Rect{} 32 | } 33 | 34 | func (r Rect) Width() float64 { return r.Max.X - r.Min.X } 35 | 36 | func (r Rect) Height() float64 { return r.Max.Y - r.Min.Y } 37 | 38 | // Size returns r dimensions in the form of a [Vec]. 39 | // In other words, it's identical to Vec{X: r.Width(), Y: r.Height()}. 40 | func (r Rect) Size() Vec { 41 | return Vec{ 42 | X: r.Width(), 43 | Y: r.Height(), 44 | } 45 | } 46 | 47 | // Center returns the center point of this rectangle. 48 | // 49 | // This center point may need some rounding, 50 | // since a rect of a 3x3 size would return {1.5, 1.5}. 51 | func (r Rect) Center() Vec { 52 | return Vec{ 53 | X: r.Max.X - r.Width()*0.5, 54 | Y: r.Max.Y - r.Height()*0.5, 55 | } 56 | } 57 | 58 | func (r Rect) X1() float64 { return r.Min.X } 59 | 60 | func (r Rect) Y1() float64 { return r.Min.Y } 61 | 62 | func (r Rect) X2() float64 { return r.Max.X } 63 | 64 | func (r Rect) Y2() float64 { return r.Max.Y } 65 | 66 | func (r Rect) IsEmpty() bool { 67 | return r.Min.X >= r.Max.X || r.Min.Y >= r.Max.Y 68 | } 69 | 70 | func (r Rect) Contains(p Vec) bool { 71 | return r.Min.X <= p.X && p.X < r.Max.X && 72 | r.Min.Y <= p.Y && p.Y < r.Max.Y 73 | } 74 | 75 | func (r Rect) ContainsRect(other Rect) bool { 76 | if other.IsEmpty() { 77 | return true 78 | } 79 | return r.Min.X <= other.Min.X && other.Max.X <= r.Max.X && 80 | r.Min.Y <= other.Min.Y && other.Max.Y <= r.Max.Y 81 | } 82 | 83 | // Intersects reports whether r and other have a common intersection. 84 | func (r Rect) Intersects(other Rect) bool { 85 | return !r.IsEmpty() && !other.IsEmpty() && 86 | r.Min.X < other.Max.X && other.Min.X < r.Max.X && 87 | r.Min.Y < other.Max.Y && other.Min.Y < r.Max.Y 88 | } 89 | 90 | func (r Rect) Add(p Vec) Rect { 91 | return Rect{ 92 | Min: r.Min.Add(p), 93 | Max: r.Max.Add(p), 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /running_average.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import "math" 4 | 5 | type RunningWeightedAverage[T numeric] struct { 6 | TotalWeight T 7 | TotalValue T 8 | } 9 | 10 | func (avg *RunningWeightedAverage[T]) Add(value, weight T) { 11 | avg.TotalValue += value * weight 12 | avg.TotalWeight += weight 13 | } 14 | 15 | func (avg *RunningWeightedAverage[T]) Scale(v float64) { 16 | avg.TotalValue = T(math.Round(float64(avg.TotalValue) * v)) 17 | avg.TotalWeight = T(math.Round(float64(avg.TotalWeight) * v)) 18 | } 19 | 20 | func (avg *RunningWeightedAverage[T]) Value() T { 21 | if avg.TotalWeight == 0 { 22 | return 0 23 | } 24 | return avg.TotalValue / avg.TotalWeight 25 | } 26 | -------------------------------------------------------------------------------- /scale.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import "math" 4 | 5 | // LogScale is a faster alternative to math.Pow(x, scale). 6 | // You need to adjust the scale parameter for it to give 7 | // the compatible results. 8 | // An example usage is math.Pow(x, 1.05) => LogScale(x, 0.06) 9 | // 10 | // LogScale is usually 5-10 times faster than math.Pow. 11 | func LogScale(x float64, scale float64) float64 { 12 | return x * (1 + scale*math.Log(x)) 13 | } 14 | 15 | // SqrScale is a faster alternative to math.Pow(x, scale). 16 | // You need to adjust the scale parameter for it to give 17 | // the compatible results. 18 | // 19 | // Note that due to the x^2 nature it will grow very fast 20 | // even with a smaller scale value. 21 | // You may want to pre-multiply before applying this function 22 | // or use it only for relatively small x (e.g. values <=100k). 23 | func SqrScale(x float64, scale float64) float64 { 24 | return x + scale*x*x 25 | } 26 | -------------------------------------------------------------------------------- /slider.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | // Slider is a value that can be increased and decreased with 4 | // a custom overflow/underflow behavior. 5 | // 6 | // Min/Max fields control the range of the accepted values. 7 | // 8 | // It's a useful foundation for more high-level concepts 9 | // like progress bars and gauges, paginators, option button selector, etc. 10 | type Slider struct { 11 | min int 12 | max int 13 | value int 14 | 15 | // Clamp makes the slider use clamping overflow/underflow strategy 16 | // instead of the default wrapping around strategy. 17 | Clamp bool 18 | } 19 | 20 | // SetBounds sets the slider values range. 21 | // It also sets the current value to min. 22 | // Use TrySetValue if you need to override that. 23 | // 24 | // If max= s.min && v <= s.max { 38 | s.value = v 39 | return true 40 | } 41 | return false 42 | } 43 | 44 | // Value returns the current slider value. 45 | // The returned value is guarandeed to be in [min, max] range. 46 | func (s *Slider) Value() int { return s.value } 47 | 48 | // Len returns the range of the values. 49 | // Basically, it returns the max-min result. 50 | func (s *Slider) Len() int { return s.max - s.min } 51 | 52 | // Sub subtracts v from the current slider value. 53 | // The overflow/underflow behavior depends on the slider settings. 54 | func (s *Slider) Sub(v int) { 55 | s.Add(0 - v) 56 | } 57 | 58 | // Add adds v to the current slider value. 59 | // The overflow/underflow behavior depends on the slider settings. 60 | func (s *Slider) Add(v int) { 61 | switch v { 62 | case 1: 63 | s.Inc() 64 | case -1: 65 | s.Dec() 66 | default: 67 | if s.Clamp { 68 | s.value = Clamp(s.value+v, s.min, s.max) 69 | } else { 70 | value := s.value + v 71 | l := s.max - s.min + 1 72 | if value < s.min { 73 | value += l * ((s.min-value)/l + 1) 74 | } 75 | s.value = s.min + (value-s.min)%l 76 | } 77 | } 78 | } 79 | 80 | // Dec subtracts 1 from the slider value. 81 | // Semantically identical to Sub(1), but more efficient. 82 | func (s *Slider) Dec() { 83 | s.addBit(-1) 84 | } 85 | 86 | // Inc adds 1 to the slider value. 87 | // Semantically identical to Add(1), but more efficient. 88 | func (s *Slider) Inc() { 89 | s.addBit(1) 90 | } 91 | 92 | func (s *Slider) addBit(v int) { 93 | value := s.value + v 94 | if value < s.min { 95 | if s.Clamp { 96 | value = s.min 97 | } else { 98 | value = s.max 99 | } 100 | } else if value > s.max { 101 | if s.Clamp { 102 | value = s.max 103 | } else { 104 | value = s.min 105 | } 106 | } 107 | s.value = value 108 | } 109 | -------------------------------------------------------------------------------- /stdwrap.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | func Abs[T float](x T) T { 8 | return T(math.Abs(float64(x))) 9 | } 10 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | ) 7 | 8 | func parseFloat(s []byte) (float64, error) { 9 | s = bytes.TrimSpace(s) 10 | 11 | sign := false 12 | if s[0] == '-' { 13 | sign = true 14 | s = s[1:] 15 | } 16 | f, err := strconv.ParseFloat(string(s), 64) 17 | if err != nil { 18 | return 0, err 19 | } 20 | if sign { 21 | return -f, nil 22 | } 23 | return f, nil 24 | } 25 | -------------------------------------------------------------------------------- /vec.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "image" 8 | "math" 9 | "strconv" 10 | "unsafe" 11 | ) 12 | 13 | // Vec is a 2-element structure that is used to represent positions, 14 | // velocities, and other kinds numerical pairs. 15 | // 16 | // Its implementation as well as its API is inspired by Vector2 type 17 | // of the Godot game engine. Where feasible, its adjusted to fit Go 18 | // coding conventions better. Also, earlier versions of Godot used 19 | // 32-bit values for X and Y; our vector uses 64-bit values. 20 | // 21 | // Since Go has no operator overloading, we implement scalar forms of 22 | // operations with "f" suffix. So, Add() is used to add two vectors 23 | // while Addf() is used to add scalar to the vector. 24 | // 25 | // If you need float32 components, use [Vec32] type, 26 | // but keep in mind that [Vec] should be preferred most of the time. 27 | type Vec = vec[float64] 28 | 29 | // Vec32 is like [Vec], but with float32-typed fields. 30 | // You should generally prefer [Vec], but for some specific low-level 31 | // stuff you might want to use a float32 variant. 32 | // 33 | // Most functions of this package operate on [Vec], so you will 34 | // lose some of the convenience while using [Vec32]. 35 | // 36 | // If anything, [Vec32] will be slower on most operations due to the 37 | // intermediate float32->float64 conversions that will happen here and there. 38 | // It should be only used as a space optimization, when you need 39 | // to store lots of vectors and memory locality dominates a small processing overhead. 40 | // If in doubts, use [Vec]. 41 | type Vec32 = vec[float32] 42 | 43 | type vec[T float] struct { 44 | X T 45 | Y T 46 | } 47 | 48 | // RadToVec converts a given angle into a normalized vector that encodes that direction. 49 | func RadToVec(angle Rad) Vec { 50 | return Vec{X: angle.Cos(), Y: angle.Sin()} 51 | } 52 | 53 | // VecFromStd converts an [image.Point] into a [Vec]. 54 | // There is [Vec.ToStd] method to reverse it. 55 | func VecFromStd(src image.Point) Vec { 56 | return Vec{ 57 | X: float64(src.X), 58 | Y: float64(src.Y), 59 | } 60 | } 61 | 62 | // ToStd converts [Vec] into [image.Point]. 63 | // There is [VecFromStd] function to reverse it. 64 | func (v vec[T]) ToStd() image.Point { 65 | return image.Point{ 66 | X: int(v.X), 67 | Y: int(v.Y), 68 | } 69 | } 70 | 71 | // String returns a pretty-printed representation of a 2D vector object. 72 | func (v vec[T]) String() string { 73 | return fmt.Sprintf("[%f, %f]", v.X, v.Y) 74 | } 75 | 76 | // IsZero reports whether v is a zero value vector. 77 | // A zero value vector has X=0 and Y=0, created with Vec{}. 78 | // 79 | // The zero value vector has a property that its length is 0, 80 | // but not all zero length vectors are zero value vectors. 81 | func (v vec[T]) IsZero() bool { 82 | return v.X == 0 && v.Y == 0 83 | } 84 | 85 | // IsNormalizer reports whether the vector is normalized. 86 | // A vector is considered to be normalized if its length is 1. 87 | func (v vec[T]) IsNormalized() bool { 88 | return EqualApprox(v.LenSquared(), 1) 89 | } 90 | 91 | // DistanceTo calculates the distance between the two vectors. 92 | func (v vec[T]) DistanceTo(v2 vec[T]) T { 93 | return T(math.Sqrt(float64((v.X-v2.X)*(v.X-v2.X) + (v.Y-v2.Y)*(v.Y-v2.Y)))) 94 | } 95 | 96 | func (v vec[T]) DistanceSquaredTo(v2 vec[T]) T { 97 | return ((v.X - v2.X) * (v.X - v2.X)) + ((v.Y - v2.Y) * (v.Y - v2.Y)) 98 | } 99 | 100 | // Dot returns a dot-product of the two vectors. 101 | func (v vec[T]) Dot(v2 vec[T]) T { 102 | return (v.X * v2.X) + (v.Y * v2.Y) 103 | } 104 | 105 | // Len reports the length of this vector (also known as magnitude). 106 | func (v vec[T]) Len() T { 107 | return T(math.Sqrt(float64(v.LenSquared()))) 108 | } 109 | 110 | // LenSquared returns the squared length of this vector. 111 | // 112 | // This function runs faster than Len(), 113 | // so prefer it if you need to compare vectors 114 | // or need the squared distance for some formula. 115 | func (v vec[T]) LenSquared() T { 116 | return v.Dot(v) 117 | } 118 | 119 | func (v vec[T]) Rotated(angle Rad) vec[T] { 120 | sine, cosi := math.Sincos(float64(angle)) 121 | // For 64-bit it should be a no-op recognizable by the compiler. 122 | tsin := T(sine) 123 | tcos := T(cosi) 124 | return vec[T]{ 125 | X: v.X*tcos - v.Y*tsin, 126 | Y: v.X*tsin + v.Y*tcos, 127 | } 128 | } 129 | 130 | func (v vec[T]) Angle() Rad { 131 | return Rad(math.Atan2(float64(v.Y), float64(v.X))) 132 | } 133 | 134 | // AngleToPoint returns the angle from v towards the given point. 135 | func (v vec[T]) AngleToPoint(pos vec[T]) Rad { 136 | return pos.Sub(v).Angle() 137 | } 138 | 139 | func (v vec[T]) DirectionTo(v2 vec[T]) vec[T] { 140 | return v.Sub(v2).Normalized() 141 | } 142 | 143 | func (v vec[T]) VecTowards(pos vec[T], length T) vec[T] { 144 | angle := v.AngleToPoint(pos) 145 | result := vec[T]{X: T(angle.Cos()), Y: T(angle.Sin())} 146 | return result.Mulf(length) 147 | } 148 | 149 | func (v vec[T]) MoveTowards(pos vec[T], length T) vec[T] { 150 | direction := pos.Sub(v) // Not normalized 151 | dist := direction.Len() 152 | if dist <= length || dist < Epsilon { 153 | return pos 154 | } 155 | return v.Add(direction.Divf(dist).Mulf(length)) 156 | } 157 | 158 | func (v vec[T]) EqualApprox(other vec[T]) bool { 159 | return EqualApprox(v.X, other.X) && EqualApprox(v.Y, other.Y) 160 | } 161 | 162 | func (v vec[T]) MoveInDirection(dist T, dir Rad) vec[T] { 163 | return vec[T]{ 164 | X: v.X + T(float64(dist)*dir.Cos()), 165 | Y: v.Y + T(float64(dist)*dir.Sin()), 166 | } 167 | } 168 | 169 | func (v vec[T]) Mulf(scalar T) vec[T] { 170 | return vec[T]{ 171 | X: v.X * scalar, 172 | Y: v.Y * scalar, 173 | } 174 | } 175 | 176 | func (v vec[T]) Mul(other vec[T]) vec[T] { 177 | return vec[T]{ 178 | X: v.X * other.X, 179 | Y: v.Y * other.Y, 180 | } 181 | } 182 | 183 | func (v vec[T]) Divf(scalar T) vec[T] { 184 | return vec[T]{ 185 | X: v.X / scalar, 186 | Y: v.Y / scalar, 187 | } 188 | } 189 | 190 | func (v vec[T]) Div(other vec[T]) vec[T] { 191 | return vec[T]{ 192 | X: v.X / other.X, 193 | Y: v.Y / other.Y, 194 | } 195 | } 196 | 197 | func (v vec[T]) Add(other vec[T]) vec[T] { 198 | return vec[T]{ 199 | X: v.X + other.X, 200 | Y: v.Y + other.Y, 201 | } 202 | } 203 | 204 | func (v vec[T]) Sub(other vec[T]) vec[T] { 205 | return vec[T]{ 206 | X: v.X - other.X, 207 | Y: v.Y - other.Y, 208 | } 209 | } 210 | 211 | func (v vec[T]) Rounded() vec[T] { 212 | return vec[T]{ 213 | X: T(math.Round(float64(v.X))), 214 | Y: T(math.Round(float64(v.Y))), 215 | } 216 | } 217 | 218 | func (v vec[T]) Floored() vec[T] { 219 | return vec[T]{ 220 | X: T(math.Floor(float64(v.X))), 221 | Y: T(math.Floor(float64(v.Y))), 222 | } 223 | } 224 | 225 | func (v vec[T]) Ceiled() vec[T] { 226 | return vec[T]{ 227 | X: T(math.Ceil(float64(v.X))), 228 | Y: T(math.Ceil(float64(v.Y))), 229 | } 230 | } 231 | 232 | // Normalized returns the vector scaled to unit length. 233 | // Functionally equivalent to `v.Divf(v.Len())`. 234 | // 235 | // Special case: for zero value vectors it returns that unchanged. 236 | func (v vec[T]) Normalized() vec[T] { 237 | l := v.LenSquared() 238 | if l != 0 { 239 | return v.Mulf(T(1 / math.Sqrt(float64(l)))) 240 | } 241 | return v 242 | } 243 | 244 | func (v vec[T]) ClampLen(limit T) vec[T] { 245 | l := v.Len() 246 | if l > 0 && l > limit { 247 | v = v.Divf(l) 248 | v = v.Mulf(limit) 249 | } 250 | return v 251 | } 252 | 253 | // Neg applies unary minus (-) to the vector. 254 | func (v vec[T]) Neg() vec[T] { 255 | return vec[T]{ 256 | X: -v.X, 257 | Y: -v.Y, 258 | } 259 | } 260 | 261 | // CubicInterpolate interpolates between a (this vector) and b using 262 | // preA and postB as handles. 263 | // The t arguments specifies the interpolation progression (a value from 0 to 1). 264 | // With t=0 it returns a, with t=1 it returns b. 265 | func (v vec[T]) CubicInterpolate(preA, b, postB Vec, t T) vec[T] { 266 | res := v 267 | res.X = T(cubicInterpolate(float64(res.X), float64(b.X), preA.X, postB.X, float64(t))) 268 | res.Y = T(cubicInterpolate(float64(res.Y), float64(b.Y), preA.Y, postB.Y, float64(t))) 269 | return res 270 | } 271 | 272 | // LinearInterpolate interpolates between two points by a normalized value. 273 | // This function is commonly named "lerp". 274 | func (v vec[T]) LinearInterpolate(to vec[T], t T) vec[T] { 275 | return vec[T]{ 276 | X: T(Lerp(float64(v.X), float64(to.X), float64(t))), 277 | Y: T(Lerp(float64(v.Y), float64(to.Y), float64(t))), 278 | } 279 | } 280 | 281 | // Midpoint returns the middle point vector of two point vectors. 282 | // 283 | // If we imagine [v] and [to] form a line, the midpoint would 284 | // be a central point of this line. 285 | func (v vec[T]) Midpoint(to vec[T]) vec[T] { 286 | return v.Add(to).Mulf(0.5) 287 | } 288 | 289 | // BoundsRect creates a rectangle with a center or v, width of w and height of h. 290 | // This is useful when a vector interpreted as a point needs to be extended to an area. 291 | // 292 | // Note that the result is not rounded. 293 | func (v vec[T]) BoundsRect(w, h T) Rect { 294 | offset := vec[T]{ 295 | X: w * 0.5, 296 | Y: h * 0.5, 297 | } 298 | return Rect{ 299 | Min: v.Sub(offset).AsVec64(), 300 | Max: v.Add(offset).AsVec64(), 301 | } 302 | } 303 | 304 | func (v vec[T]) MarshalJSON() ([]byte, error) { 305 | if v.IsZero() { 306 | // Zero vectors are quite common. 307 | // Encode them with a shorter notation. 308 | return []byte("[]"), nil 309 | } 310 | buf := make([]byte, 0, 16) 311 | buf = append(buf, '[') 312 | buf = strconv.AppendFloat(buf, float64(v.X), 'f', -1, 64) 313 | buf = append(buf, ',') 314 | buf = strconv.AppendFloat(buf, float64(v.Y), 'f', -1, 64) 315 | buf = append(buf, ']') 316 | return buf, nil 317 | } 318 | 319 | func (v *vec[T]) UnmarshalJSON(data []byte) error { 320 | if string(data) == "[]" { 321 | // Recognize a MarshalJSON-produced empty vector notation. 322 | *v = vec[T]{} 323 | return nil 324 | } 325 | 326 | if data[0] != '[' { 327 | return errors.New("missing opening '['") 328 | } 329 | if data[len(data)-1] != ']' { 330 | return errors.New("missing closing ']'") 331 | } 332 | 333 | data = data[1:] // '[' 334 | data = data[:len(data)-1] // ']' 335 | 336 | commaIndex := bytes.IndexByte(data, ',') 337 | if commaIndex == -1 { 338 | return errors.New("missing ',' between X and Y values") 339 | } 340 | x, err := parseFloat(data[:commaIndex]) 341 | if err != nil { 342 | return err 343 | } 344 | y, err := parseFloat(data[commaIndex+1:]) 345 | if err != nil { 346 | return err 347 | } 348 | 349 | v.X = T(x) 350 | v.Y = T(y) 351 | return err 352 | } 353 | 354 | func (v vec[T]) AsVec64() Vec { 355 | // For vec[float64] this should be no-op. 356 | // For vec[float32] it should do a float32->float64 conversion. 357 | return Vec{ 358 | X: float64(v.X), 359 | Y: float64(v.Y), 360 | } 361 | } 362 | 363 | func (v vec[T]) AsVec32() Vec32 { 364 | // For vec[float32] this should be no-op. 365 | // For vec[float64] it should do a float64->float32 conversion. 366 | return Vec32{ 367 | X: float32(v.X), 368 | Y: float32(v.Y), 369 | } 370 | } 371 | 372 | // AsSlice returns vector as a slice view. 373 | // 374 | // This view can be used to read and write to Vec, 375 | // but it should not be used as append operand. 376 | // 377 | // This operation doesn't allocate. 378 | // 379 | // For 64-bit vectors, it returns []float64, 380 | // For 32-bit vectors, it returns []float32. 381 | // 382 | // The most common use case for this function is 383 | // uniform variables binding in Ebitengine, as 384 | // it wants vec's as []float32 slices. 385 | // Allocating real slices for it is a waste, 386 | // therefore we can use a convenient Vec API while 387 | // still being compatible with Ebitengine needs without 388 | // any redundant allocations. 389 | func (v *vec[T]) AsSlice() []T { 390 | return unsafe.Slice(&v.X, 2) 391 | } 392 | -------------------------------------------------------------------------------- /vec_test.go: -------------------------------------------------------------------------------- 1 | package gmath 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math" 7 | "testing" 8 | ) 9 | 10 | func BenchmarkVecJSONEncode(b *testing.B) { 11 | v := &Vec{X: 54.7, Y: 3.0} 12 | 13 | b.ResetTimer() 14 | for i := 0; i < b.N; i++ { 15 | json.Marshal(v) 16 | } 17 | } 18 | 19 | func BenchmarkVecJSONDecode(b *testing.B) { 20 | v := &Vec{X: 54.7, Y: 3.0} 21 | data, err := json.Marshal(v) 22 | if err != nil { 23 | b.Fatal(err) 24 | } 25 | 26 | b.ResetTimer() 27 | for i := 0; i < b.N; i++ { 28 | dst := &Vec{} 29 | if err := json.Unmarshal(data, dst); err != nil { 30 | b.Fatal(err) 31 | } 32 | if *dst != *v { 33 | b.Fatal(err) 34 | } 35 | } 36 | } 37 | 38 | func BenchmarkVecJSONEncodeZero(b *testing.B) { 39 | v := &Vec{} 40 | 41 | b.ResetTimer() 42 | for i := 0; i < b.N; i++ { 43 | json.Marshal(v) 44 | } 45 | } 46 | 47 | func BenchmarkVecJSONDecodeZero(b *testing.B) { 48 | v := &Vec{} 49 | data, err := json.Marshal(v) 50 | if err != nil { 51 | b.Fatal(err) 52 | } 53 | 54 | b.ResetTimer() 55 | for i := 0; i < b.N; i++ { 56 | dst := &Vec{} 57 | if err := json.Unmarshal(data, dst); err != nil { 58 | b.Fatal(err) 59 | } 60 | if *dst != *v { 61 | b.Fatal(err) 62 | } 63 | } 64 | } 65 | 66 | func TestVecJSON(t *testing.T) { 67 | vectors := []Vec{ 68 | {0, 0}, 69 | {0.1, 0.1}, 70 | {1.5354, 395.6452934}, 71 | {0.0000342, 0.0103}, 72 | {0.999999, 1.11111}, 73 | {0.0000000002399999, 0.0000000000002199999}, 74 | {0.00000000000001399999, 0.00000000000000002199999}, 75 | {93243285823.9359234932, 12982.0}, 76 | {1, 1}, 77 | {60000000, 5300340}, 78 | {-0, 1}, 79 | } 80 | formats := []string{ 81 | "[%#v,%#v]", 82 | "[%#v ,%#v]", 83 | "[%#v, %#v]", 84 | "[ %#v, %#v]", 85 | "[%#v , %#v]", 86 | "[ %#v , %#v ]", 87 | } 88 | signs := []Vec{ 89 | {1, 1}, 90 | {1, -1}, 91 | {-1, 1}, 92 | {-1, -1}, 93 | } 94 | for _, testVec := range vectors { 95 | for _, signVec := range signs { 96 | v := testVec.Mul(signVec) 97 | for _, f := range formats { 98 | s := fmt.Sprintf(f, v.X, v.Y) 99 | var v1 Vec 100 | if err := json.Unmarshal([]byte(s), &v1); err != nil { 101 | t.Fatalf("unmarshal %q (signs=%v): %v", s, signVec, err) 102 | } 103 | if v1 != v { 104 | t.Fatalf("unmarshal %q (signs=%v):\n%#v != %#v", s, signVec, v1, v) 105 | } 106 | s2, err := json.Marshal(v1) 107 | if err != nil { 108 | t.Fatalf("marshal %#v after unmarshalling %q (signs=%v): %v", v1, s, signs, err) 109 | } 110 | var v2 Vec 111 | if err := json.Unmarshal(s2, &v2); err != nil { 112 | t.Fatalf("unmarhal-marshal-unmarshal failed, s=%q, s2=%q (signs=%v)", s, string(s2), signs) 113 | } 114 | if v2 != v { 115 | t.Fatalf("unmarhal-marshal-unmarshal comparison failed, s=%q, s2=%q (signs=%v)", s, string(s2), signs) 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | func TestVecAPI(t *testing.T) { 123 | assertTrue := func(v bool) { 124 | t.Helper() 125 | if !v { 126 | t.Fatal("assertion failed") 127 | } 128 | } 129 | 130 | // Make sure that zero values can be used as literals. 131 | // Also make sure that we can use *Result methods on rvalue. 132 | 133 | assertTrue(Vec{}.EqualApprox(Vec{})) 134 | assertTrue(Vec{}.IsZero()) 135 | assertTrue(Vec{}.Len() == 0) 136 | assertTrue(Vec{X: 1}.Neg() == Vec{X: -1}) 137 | 138 | // A special case. 139 | assertTrue(Vec{}.Normalized() == Vec{}) 140 | } 141 | 142 | //go:noinline 143 | func benchmarkNormalized(vectors []Vec) float64 { 144 | v := float64(0) 145 | for i := 0; i < len(vectors)-1; i++ { 146 | v += vectors[i].Normalized().X + vectors[i+1].Normalized().Y 147 | } 148 | return v 149 | } 150 | 151 | func BenchmarkVecNormalized(b *testing.B) { 152 | vectors := []Vec{ 153 | {-1, 0}, 154 | {0.5, 5}, 155 | {10, 13}, 156 | {-5.3, -294}, 157 | {1, 1}, 158 | {0, 3}, 159 | {-3, 1}, 160 | {0, 0}, 161 | } 162 | b.ResetTimer() 163 | for i := 0; i < b.N; i++ { 164 | benchmarkNormalized(vectors) 165 | } 166 | } 167 | 168 | func TestVecNormalized(t *testing.T) { 169 | tests := []struct { 170 | v Vec 171 | want Vec 172 | }{ 173 | {Vec{1, 0}, Vec{1, 0}}, 174 | {Vec{-1, 0}, Vec{-1, 0}}, 175 | {Vec{0, 1}, Vec{0, 1}}, 176 | {Vec{0, -1}, Vec{0, -1}}, 177 | {Vec{3, 0}, Vec{1, 0}}, 178 | {Vec{0, 3}, Vec{0, 1}}, 179 | {Vec{1, 1}, Vec{0.70710678118654, 0.70710678118654}}, 180 | {Vec{10, 13}, Vec{0.6097107608, 0.7926239891}}, 181 | } 182 | 183 | for _, test := range tests { 184 | have := test.v.Normalized() 185 | if !have.EqualApprox(test.want) { 186 | t.Fatalf("Normalized(%s):\nhave: %v\nwant: %v", test.v, have, test.want) 187 | } 188 | have2 := test.v.Divf(test.v.Len()) 189 | if !have.EqualApprox(have2) { 190 | t.Fatalf("div+len of %s:\nhave: %v\nwant: %v", test.v, have, test.want) 191 | } 192 | if !have.IsNormalized() { 193 | t.Fatalf("IsNormalized(Normalized(%s)) returned false", test.v) 194 | } 195 | } 196 | } 197 | 198 | func TestVecAngleTo(t *testing.T) { 199 | tests := []struct { 200 | a Vec 201 | b Vec 202 | want Rad 203 | }{ 204 | {Vec{0, 0}, Vec{0, 0}, 0}, 205 | {Vec{1, 1}, Vec{0, 0}, -3 * math.Pi / 4}, 206 | {Vec{0, 0}, Vec{1, 1}, math.Pi / 4}, 207 | {Vec{-1, 1}, Vec{1, -1}, -0.7853981633974483}, 208 | {Vec{10, 10}, Vec{6, 6}, -2.356194490192345}, 209 | {Vec{10, 10}, Vec{5, 5}, -2.356194490192345}, 210 | {Vec{10, 10}, Vec{3, 3}, -2.356194490192345}, 211 | {Vec{31, 4.5}, Vec{6.2, 57.4}, 2.0091813174935758}, 212 | {Vec{-140.20, -44.14}, Vec{-4.6, -4.1}, 0.28712113078006946}, 213 | } 214 | for _, test := range tests { 215 | have := test.a.AngleToPoint(test.b) 216 | if !EqualApprox(float64(have), float64(test.want)) { 217 | t.Fatalf("AngleToPoint(%s, %s):\nhave: %v\nwant: %v", test.a, test.b, have, test.want) 218 | } 219 | } 220 | } 221 | 222 | func TestVecLen(t *testing.T) { 223 | tests := []struct { 224 | v Vec 225 | want float64 226 | }{ 227 | {Vec{}, 0}, 228 | {Vec{1, 0}, 1}, 229 | {Vec{0, 1}, 1}, 230 | {Vec{1, 1}, 1.414213562373}, 231 | {Vec{2, 1}, 2.236067977499}, 232 | {Vec{-1, 0}, 1}, 233 | {Vec{0, -1}, 1}, 234 | } 235 | 236 | for _, test := range tests { 237 | have := test.v.Len() 238 | if !EqualApprox(have, test.want) { 239 | t.Fatalf("Len(%s):\nhave: %v\nwant: %v", test.v, have, test.want) 240 | } 241 | } 242 | } 243 | 244 | func TestVecEqualApprox(t *testing.T) { 245 | tests := []struct { 246 | a Vec 247 | b Vec 248 | want bool 249 | }{ 250 | {Vec{}, Vec{}, true}, 251 | {Vec{}, Vec{1, 1}, false}, 252 | {Vec{1, 1}, Vec{1, 1}, true}, 253 | {Vec{0.5, 0.1}, Vec{-1, -0.3}, false}, 254 | {Vec{0.01, 0.01}, Vec{}, false}, 255 | {Vec{1, 1}, Vec{1 + Epsilon/2, 1 - Epsilon/2}, true}, 256 | {Vec{0, 0 + Epsilon}, Vec{}, true}, 257 | {Vec{0, 0 - Epsilon}, Vec{}, true}, 258 | {Vec{0.000000001, 0}, Vec{}, true}, 259 | {Vec{0.0000000001, 0}, Vec{}, true}, 260 | } 261 | 262 | for _, test := range tests { 263 | have := test.a.EqualApprox(test.b) 264 | if have != test.want { 265 | t.Fatalf("EqualApprox(%s, %s):\nhave: %v\nwant: %v", test.a, test.b, have, test.want) 266 | } 267 | have2 := test.b.EqualApprox(test.a) 268 | if have2 != test.want { 269 | t.Fatalf("EqualApprox(%s, %s):\nhave: %v\nwant: %v", test.b, test.a, have2, test.want) 270 | } 271 | } 272 | } 273 | 274 | func TestVecAdd(t *testing.T) { 275 | tests := []struct { 276 | a Vec 277 | b Vec 278 | want Vec 279 | }{ 280 | {Vec{}, Vec{}, Vec{}}, 281 | {Vec{1, 1}, Vec{}, Vec{1, 1}}, 282 | {Vec{}, Vec{1, 1}, Vec{1, 1}}, 283 | {Vec{1, 1}, Vec{1, 1}, Vec{2, 2}}, 284 | {Vec{0.5, 0.1}, Vec{-1, -0.3}, Vec{-0.5, -0.2}}, 285 | } 286 | 287 | for _, test := range tests { 288 | have := test.a.Add(test.b) 289 | if !have.EqualApprox(test.want) { 290 | t.Fatalf("Add(%s, %s):\nhave: %s\nwant: %s", test.a, test.b, have, test.want) 291 | } 292 | have2 := test.b.Add(test.a) 293 | if !have2.EqualApprox(test.want) { 294 | t.Fatalf("Add(%s, %s):\nhave: %s\nwant: %s", test.b, test.a, have2, test.want) 295 | } 296 | } 297 | } 298 | 299 | func TestVecNeg(t *testing.T) { 300 | tests := []struct { 301 | arg Vec 302 | want Vec 303 | }{ 304 | {Vec{0, 0}, Vec{0, 0}}, 305 | {Vec{1, 1}, Vec{-1, -1}}, 306 | {Vec{-1, 2}, Vec{1, -2}}, 307 | {Vec{1.5, 0.5}, Vec{-1.5, -0.5}}, 308 | {Vec{-1.5, -0.5}, Vec{1.5, 0.5}}, 309 | } 310 | 311 | for _, test := range tests { 312 | have := test.arg.Neg() 313 | if !have.EqualApprox(test.want) { 314 | t.Fatalf("Neg(%s):\nhave: %s\nwant: %s", test.arg, have, test.want) 315 | } 316 | } 317 | } 318 | --------------------------------------------------------------------------------