# maff module: extend math module with convenience functions, by Christopher Night, CC0 # https://github.com/cosmologicon/maff from __future__ import division import math __version__ = '0.1' __versioninfo__ = (0, 1) # Circle constant. Already available in 3.6+ tau = math.tau if hasattr(math, "tau") else 2 * math.pi # Golden ratio phi = (1 + math.sqrt(5)) / 2 Phi = phi - 1 # So-called golden divergence angle of phyllotaxis, around 137.5 deg. phyllo = tau * (2 - phi) # GLSL functions def sign(x): return 1. if x > 0 else -1. if x < 0 else 0. def clamp(x, a, b): return a if x < a else b if x > b else x def mix(x, y, a): a = clamp(a, 0, 1) try: return tuple(b * (1 - a) + c * a for b, c in zip(x, y)) except TypeError: return x * (1 - a) + y * a def imix(x, y, a): a = clamp(a, 0, 1) try: return tuple(int(round(b * (1 - a) + c * a)) for b, c in zip(x, y)) except TypeError: return int(round(x * (1 - a) + y * a)) def step(edge, x): return float(x >= edge) def smoothstep(edge0, edge1, x): if x >= edge1: return 1 if x <= edge0: return 0 a = (x - edge0) / (edge1 - edge0) return a * a * (3 - 2 * a) def length(v): return math.sqrt(sum(a ** 2 for a in v)) def distance(v0, v1): return math.sqrt(sum((a0 - a1) ** 2 for a0, a1 in zip(v0, v1))) def dot(v0, v1): return sum(a * b for a, b in zip(v0, v1)) # Normalize with optional length def normalize(v, r = 1): l = length(v) if l == 0: return [r] + [0] * (len(v) - 1) f = r / l return [a * f for a in v] norm = normalize # Equal to smoothstep(0, 1, x) def ease(x): return 0 if x <= 0 else 1 if x >= 1 else x * x * (3 - 2 * x) # Fade function def fade(x, x0, dx): x -= x0 if dx < 0: return 1 - fade(-x, 0, -dx) if x <= 0: return 0 if x >= dx: return 1 return x / dx # Equal to smoothstep(x0, x0 + dx, x) def smoothfade(x, x0, dx): return ease(fade(x, x0, dx)) # Double fade function def dfade(x, x0, x1, dx): return min(fade(x, x0, dx), 1 - fade(x, x1 - dx, dx)) def dsmoothfade(x, x0, x1, dx): return ease(dfade(x, x0, x1, dx)) # Fade between function def fadebetween(x, x0, y0, x1, y1): if x1 < x0: x1, y1, x0, y0 = x0, y0, x1, y1 a = fade(x, x0, x1 - x0) return mix(y0, y1, a) interp = fadebetween def smoothfadebetween(x, x0, y0, x1, y1): if x1 < x0: x1, y1, x0, y0 = x0, y0, x1, y1 a = smoothfade(x, x0, x1 - x0) return mix(y0, y1, a) smoothinterp = smoothfadebetween # Cycle between 0 and 1 def cycle(a): return 0.5 - 0.5 * math.cos(tau * a) # Approach functions def approach(x, target, dx): try: d = distance(x, target) except TypeError: d = abs(x - target) if d <= dx: return target return mix(x, target, dx / d) def softapproach(x, target, dlogx, dxmax = float("inf"), dymin = 0.1): try: d = distance(x, target) except TypeError: d = abs(x - target) a = -math.expm1(-dlogx) if a * d > dxmax: a = dxmax / d if (1 - a) * d < dymin: return target return mix(x, target, a) # Angular analogues - angles are treated modulo tau. def dA(A): return (A + tau / 2) % tau - tau / 2 def mixA(A0, A1, a): return A1 if a >= 1 else A0 + dA(A1 - A0) * clamp(a, 0, 1) def fadebetweenA(x, x0, A0, x1, A1): a = fadebetween(x, x0, 0, x1, 1) return mixA(A0, A1, a) interpA = fadebetweenA def smoothfadebetweenA(x, x0, A0, x1, A1): a = smoothfadebetween(x, x0, 0, x1, 1) return mixA(A0, A1, a) smoothinterpA = smoothfadebetweenA def approachA(A, targetA, deltaA): d = dA(targetA - A) if abs(d) <= deltaA: return targetA return A + deltaA * sign(d) def softapproachA(A, targetA, dlogA, dAmax = float("inf"), dAmin = 0.01): d = abs(dA(targetA - A)) a = -math.expm1(-dlogA) if a * d > dAmax: a = dAmax / d if (1 - a) * d < dAmin: return targetA return mixA(A, targetA, a) # Log-space analogues def mixL(xL, yL, a): return math.exp(mix(math.log(xL), math.log(yL), a)) def fadebetweenL(x, x0, yL0, x1, yL1): return math.exp(fadebetween(x, x0, math.log(yL0), x1, math.log(yL1))) interpL = fadebetweenL def smoothfadebetweenL(x, x0, yL0, x1, yL1): return math.exp(smoothfadebetween(x, x0, math.log(yL0), x1, math.log(yL1))) smoothinterpL = smoothfadebetweenL def approachL(xL, targetL, dx): return math.exp(approach(math.log(xL), math.log(targetL), dx)) def softapproachL(xL, targetL, dlogx, dxmax = float("inf"), dymin = 0.1): return math.exp(softapproach(math.log(xL), math.log(targetL), dlogx, dxmax, dymin)) # Polar coordinates def CS(theta, r = 1, center = (0, 0)): return center[0] + r * math.cos(theta), center[1] + r * math.sin(theta) def CSround(ntheta, r = 1, jtheta0 = 0, center = (0, 0)): return [CS((jtheta + jtheta0) / ntheta * tau, r, center) for jtheta in range(ntheta)] # Rotation transform def R(theta, v = None): C, S = CS(theta) if v is not None: x, y = v return C * x - S * y, S * x + C * y else: def R(v): x, y = v return C * x - S * y, S * x + C * y return R # Deterministic pseudorandom def fuzz(*args): a = 0.12 for x in args: a += x + 3.45 a *= 6.78 return 91011.12 * math.sin(a) % 1 def fuzzrange(a, b, *args): return mix(a, b, fuzz(*args)) # Add to math module _globals = dict(globals()) for k, v in _globals.items(): if not k.startswith("_"): setattr(math, k, v)