Skip to content

Instantly share code, notes, and snippets.

@sampottinger
Created August 5, 2026 17:58
Show Gist options
  • Select an option

  • Save sampottinger/7bee89492f51579a36114ae6712192df to your computer and use it in GitHub Desktop.

Select an option

Save sampottinger/7bee89492f51579a36114ae6712192df to your computer and use it in GitHub Desktop.
Changing attractors simulation under BSD-3-Clause license runnable in editor.sketchingpy.org.
import math
import random
import sketchingpy
BACKGROUND_COLOR = '#333333'
CENTER_MASS = 10
CENTER_X = 350
CENTER_Y = 350
START_RADIUS = 200
FIELD_COLORS = {
'a': '#FF5555',
'b': '#55FF55',
'c': '#5555FF',
'none': '#FFFFFF'
}
WIDTH = 700
HEIGHT = 700
MAX_SPEED = 4
FORCE_STRENGTH = 800
class Particle:
def __init__(self, field, x, y, mass, direction, magnitude):
self._field = field
self._x = x
self._y = y
self._mass = mass
self._direction = direction
self._magnitude = magnitude
def update(self, active_field, center_x, center_y):
# Decompose current velocity into x/y components
vx = self._magnitude * math.cos(self._direction)
vy = self._magnitude * math.sin(self._direction)
# Vector from particle toward center
dx = center_x - self._x
dy = center_y - self._y
distance = math.sqrt(dx * dx + dy * dy)
if distance > 0:
# Normalise direction
nx = dx / distance
ny = dy / distance
# Force falls off with distance (gravity-like), scaled by mass
force = (FORCE_STRENGTH * self._mass) / (distance * distance + 100)
# Matching field → attracted; others → repelled
if self._field == active_field:
vx += nx * force
vy += ny * force
else:
vx -= nx * force
vy -= ny * force
# Cap speed so particles don't fly off instantly
speed = math.sqrt(vx * vx + vy * vy)
if speed > MAX_SPEED:
vx = (vx / speed) * MAX_SPEED
vy = (vy / speed) * MAX_SPEED
speed = MAX_SPEED
# Store back as direction + magnitude
self._direction = math.atan2(vy, vx)
self._magnitude = speed
# Move particle
self._x += vx
self._y += vy
# Wrap around screen edges so particles don't escape permanently
self._x = self._x % WIDTH
self._y = self._y % HEIGHT
def get_field(self):
return self._field
def get_x(self):
return self._x
def get_y(self):
return self._y
def get_mass(self):
return self._mass
class Simulation:
def __init__(self, center_x=CENTER_X, center_y=CENTER_Y,
center_mass=CENTER_MASS):
self._center_x = center_x
self._center_y = center_y
self._center_mass = center_mass
self._particles = [self._make_random_particle() for x in range(0, 1000)]
self._active_field = 'none'
def step(self, sketch):
for particle in self._particles:
particle.update(self._active_field, self._center_x, self._center_y)
if random.randint(0, 200) == 0:
self._select_new_field()
sketch.clear(BACKGROUND_COLOR)
self._draw_center(sketch)
sketch.clear_stroke()
sketch.set_ellipse_mode('radius')
for particle in self._particles:
field = particle.get_field()
color = FIELD_COLORS[field] + '50'
sketch.set_fill(color)
sketch.draw_ellipse(
particle.get_x(),
particle.get_y(),
particle.get_mass(),
particle.get_mass()
)
def _draw_center(self, sketch):
sketch.clear_fill()
sketch.set_stroke_weight(5)
sketch.set_stroke(FIELD_COLORS[self._active_field] + '80')
sketch.set_ellipse_mode('radius')
sketch.draw_ellipse(
self._center_x,
self._center_y,
self._center_mass,
self._center_mass
)
def _make_random_particle(self):
field = self._randomly_choose_field()
angle = random.uniform(0, 2 * math.pi)
x = self._center_x + START_RADIUS * math.cos(angle)
y = self._center_y + START_RADIUS * math.sin(angle)
mass = random.randint(1, 5)
return Particle(field, x, y, mass, 0, 0)
def _select_new_field(self):
# Only pick from real fields, never back to 'none'
real_fields = [k for k in FIELD_COLORS if k != 'none']
self._active_field = random.choice(real_fields)
def _randomly_choose_field(self):
# Particles are only ever assigned real fields, never 'none'
real_fields = [k for k in FIELD_COLORS if k != 'none']
return random.choice(real_fields)
simulation = Simulation()
sketch = sketchingpy.Sketch2DWeb(700, 700)
sketch.on_step(lambda x: simulation.step(x))
sketch.show()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment