Skip to content

Instantly share code, notes, and snippets.

@ubuntor
Created November 11, 2022 03:50
Show Gist options
  • Save ubuntor/8abad211e95ff511ef6ef21663d0d401 to your computer and use it in GitHub Desktop.
Save ubuntor/8abad211e95ff511ef6ef21663d0d401 to your computer and use it in GitHub Desktop.
import math
import cmath
import numpy as np
NUM_SAMPLES = 4096
import matplotlib.pyplot as plt
def resample(signal, samples):
signal.append((1, signal[-1][1]))
resampled = []
for i in range(samples):
t = i/samples
for j in range(len(signal)):
if signal[j][0] > t:
t0, x0 = signal[j-1]
t1, x1 = signal[j]
x = x0 + ((t-t0)/(t1-t0)) * (x1-x0)
resampled.append(x)
break
return resampled
def approximate(target, period, num_components, show_circles=False, normalize=False):
target = [(t,x+y*1j) for t,x,y in target]
target = resample(target, NUM_SAMPLES)
components = np.fft.fft(target, norm='forward')
harmonics = [(i + NUM_SAMPLES//2) % NUM_SAMPLES - NUM_SAMPLES//2 for i in range(NUM_SAMPLES)] # 0...n/2-1, -n/2...-1
components = list(zip(components, harmonics))
if normalize:
components = components[1:] # ignore dc component
components.sort(key=lambda x: abs(x[0]), reverse=True)
for coeff, frequency in components[:num_components]:
radius = abs(coeff)
phase = cmath.phase(coeff) % (2*math.pi)
if frequency > 0:
phase = 2*math.pi - phase
component_period = period / abs(frequency) if frequency != 0 else 1
delay = component_period * (phase/(2*math.pi) - 1)
direction = ' reverse' if frequency >= 0 else '' # spin goes clockwise (decreasing angle)
state = '' if frequency != 0 else ' paused'
inverse_direction = '' if frequency >= 0 else ' reverse'
if show_circles:
print(f'<div style="animation: {component_period:.6f}s linear {delay:.6f}s infinite{direction}{state} spin; border-radius: 50%; border: 2px dashed hsl(210deg 100% 75%); height: {2*radius:.6f}px; width:{2*radius:.6f}px; display: flex; justify-content: center; align-items: center">')
print(f'<div style="width:{radius:.6f}px; height: 2px; border-top: 2px dashed hsl(210deg 100% 75%); position:absolute; transform:translate({radius/2:.6f}px, 1px)"></div>')
else:
print(f'<div style="animation: {component_period:.6f}s linear {delay:.6f}s infinite{direction}{state} spin; display: flex; flex-shrink: 0; justify-content: center; align-items: center">')
print(f'<div style="transform:translate({radius:.6f}px)">')
# reset rotation
print(f'<div style="animation: {component_period:.6f}s linear {delay:.6f}s infinite{inverse_direction}{state} spin; display: flex; flex-shrink: 0; justify-content: center; align-items: center">')
print('<div style="font-size: 40px; width:52px; border-radius: 10px; border: 2px solid hsl(0deg 100% 75%)">:eggbug:</div>')
print('</div>'*(num_components*3))
# target: pairs of (t, x, y) where 0 <= t <= 1, y is in pixels (positive y is up)
# target will be linearly interpolated
target = [(0,-100,0), (0.5,-100,0), (0.5,100,0)] # 2 points
target = [(0,100,100), (0.25,100,100), (0.25,100,-100), (0.5,100,-100), (0.5,-100,-100), (0.75,-100,-100), (0.75,-100,100)] # 4 points
target = [(i/512,100*math.sin(2*math.pi*i/512),0) for i in range(513)] # sine wave
target = [(0,-100,0), (0.5,100,0), (1,-100,0)] # triangle wave
target = [(0,-100,0), (1,100,0)] # sawtooth wave
target = [(i/3, 200*math.cos(math.radians(i*120)), 200*math.sin(math.radians(i*120))) for i in range(4)] # triangle
target = [(0,100,100), (0.25,100,-100), (0.5,-100,-100), (0.75,-100,100), (1,100,100)] # square
target = [(0,100,0), (0.9,-100,0), (0.933,-100,-100), (0.966,100,-100), (1,100,0)] # marquee
target = [(i/512, 250*(abs((i/256*5)%2-1)-0.5), 200*(abs((i/256*4)%2-1)-0.5)) for i in range(513)] # dvd screensaver
# bounce
target = []
for i in range(2048//2 + 1):
t = 2 * i/2048
x = 0.5 * (0*((1-t)**3) + 0.8*3*t*((1-t)**2) + 1*3*(t**2)*(1-t) + 1*(t**3))
y = 200 * (0.25 * (1 - (0*((1-t)**3) + 0*3*t*((1-t)**2) + 1*3*(t**2)*(1-t) + 1*(t**3))) - 0.15)
target.append((x,0,y))
target += [(1-x,0,y) for x,_,y in target[:-1][::-1]]
'''
# eggbug
import svgpathtools
paths, attrs = svgpathtools.svg2paths('shadow_eggbug.svg')
target = []
for i in range(4097):
t = i/4096
p = paths[0].point(t)
target.append((t, 20*p.real, -20*p.imag))
'''
approximate(target, 10, 20, show_circles=True)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment