Color Pulse Effect
Animate a grid cell’s color layer to pulse, useful for highlighting interactable tiles, objectives, warnings, or magical effects.
ColorLayer stores plain Color values per cell — layer.at(x, y) returns a
snapshot Color, not a live, animatable object, and ColorLayer itself has no
.animate() method (only Drawable/Entity-family objects do). So a “pulse”
is driven by a repeating mcrfpy.Timer that recomputes the alpha each tick and
writes it back with layer.set((x, y), color).
Quick Example
import mcrfpy
import math
def get_or_create_layer(grid, name="pulse", z_index=1):
"""Find grid's named ColorLayer, or create and attach one."""
existing = grid.layer(name)
if existing is not None:
return existing
layer = mcrfpy.ColorLayer(z_index=z_index, name=name)
grid.add_layer(layer)
return layer
def pulse_cell(grid, x, y, color, duration=1.0):
"""
Pulse a cell's color overlay from transparent to visible and back once.
Args:
grid: Grid to overlay
x, y: Cell coordinates
color: RGB tuple (r, g, b)
duration: Full cycle duration in seconds
"""
layer = get_or_create_layer(grid)
r, g, b = color
half_duration = duration / 2.0
state = {"t0": None}
def tick(timer, runtime):
if state["t0"] is None:
state["t0"] = runtime
t = (runtime - state["t0"]) / 1000.0
if t >= duration:
layer.set((x, y), mcrfpy.Color(r, g, b, 0))
timer.stop()
return
# Triangle wave: 0 -> 1 over the first half, 1 -> 0 over the second
phase = t / half_duration
alpha_frac = phase if phase <= 1.0 else 2.0 - phase
layer.set((x, y), mcrfpy.Color(r, g, b, int(200 * alpha_frac)))
mcrfpy.Timer(f"pulse_{x}_{y}", tick, 33)
# Usage
scene = mcrfpy.Scene("demo")
mcrfpy.current_scene = scene
grid = mcrfpy.Grid(grid_size=(16, 12), pos=(0, 0), size=(512, 384))
scene.children.append(grid)
pulse_cell(grid, 5, 5, (255, 200, 0), duration=1.5) # Golden pulse
Continuous Pulsing
Keep a cell pulsing until stopped, using a single repeating Timer driven by
math.sin instead of chained one-shot animations:
import mcrfpy
import math
class PulsingCell:
"""A cell that continuously pulses until stopped."""
def __init__(self, grid, x, y, color, period=1.0, max_alpha=180):
"""
Args:
grid: Grid with color layer
x, y: Cell position
color: RGB tuple
period: Time for one complete pulse cycle
max_alpha: Maximum alpha value (0-255)
"""
self.grid = grid
self.x = x
self.y = y
self.color = color
self.period = period
self.max_alpha = max_alpha
self.is_pulsing = False
self.layer = self._get_or_create_layer()
self.timer = None
self.t0 = None
def _get_or_create_layer(self):
"""Ensure a named color layer exists on the grid."""
existing = self.grid.layer("pulse")
if existing is not None:
return existing
layer = mcrfpy.ColorLayer(z_index=1, name="pulse")
self.grid.add_layer(layer)
return layer
def _tick(self, timer, runtime):
if not self.is_pulsing:
return
if self.t0 is None:
self.t0 = runtime
r, g, b = self.color
# Sine wave oscillating between 0 and max_alpha
t = (runtime - self.t0) / 1000.0
wave = (math.sin(2 * math.pi * t / self.period) + 1.0) / 2.0
alpha = int(self.max_alpha * wave)
self.layer.set((self.x, self.y), mcrfpy.Color(r, g, b, alpha))
def start(self):
"""Start continuous pulsing."""
if self.is_pulsing:
return
self.is_pulsing = True
self.t0 = None
self.timer = mcrfpy.Timer(f"pulse_{id(self)}", self._tick, 33)
def stop(self):
"""Stop pulsing and clear the cell."""
self.is_pulsing = False
if self.timer is not None:
self.timer.stop()
self.timer = None
r, g, b = self.color
self.layer.set((self.x, self.y), mcrfpy.Color(r, g, b, 0))
def set_color(self, color):
"""Change pulse color for subsequent ticks."""
self.color = color
# Usage
objective_pulse = PulsingCell(grid, 10, 10, (0, 255, 100), period=1.5)
objective_pulse.start()
# Later, when objective is reached:
objective_pulse.stop()
Multiple Pulsing Cells Manager
Manage multiple pulsing effects:
import mcrfpy
class PulseManager:
"""Manages multiple pulsing cell effects."""
# Preset colors
OBJECTIVE = (0, 255, 100) # Green - goals, exits
WARNING = (255, 100, 0) # Orange - danger zones
TREASURE = (255, 215, 0) # Gold - loot
MAGIC = (150, 50, 255) # Purple - magical
HEAL = (100, 200, 255) # Light blue - healing
DAMAGE = (255, 50, 50) # Red - damage zones
def __init__(self, grid):
self.grid = grid
self.pulses = {} # (x, y) -> PulsingCell
def add(self, x, y, color, period=1.0, max_alpha=180):
"""Add a pulsing cell."""
key = (x, y)
if key in self.pulses:
self.pulses[key].stop()
pulse = PulsingCell(self.grid, x, y, color, period, max_alpha)
pulse.start()
self.pulses[key] = pulse
return pulse
def remove(self, x, y):
"""Stop and remove a pulsing cell."""
key = (x, y)
if key in self.pulses:
self.pulses[key].stop()
del self.pulses[key]
def clear(self):
"""Stop all pulsing cells."""
for pulse in self.pulses.values():
pulse.stop()
self.pulses.clear()
def has_pulse(self, x, y):
"""Check if a cell is pulsing."""
return (x, y) in self.pulses
# Convenience methods with presets
def mark_objective(self, x, y):
"""Mark a cell as an objective."""
return self.add(x, y, self.OBJECTIVE, period=1.5)
def mark_warning(self, x, y):
"""Mark a cell as dangerous."""
return self.add(x, y, self.WARNING, period=0.8, max_alpha=150)
def mark_treasure(self, x, y):
"""Mark a cell with treasure."""
return self.add(x, y, self.TREASURE, period=1.2)
def mark_magic(self, x, y):
"""Mark a magical cell."""
return self.add(x, y, self.MAGIC, period=2.0)
# Usage
pulses = PulseManager(grid)
# Mark various cells
pulses.mark_objective(15, 10) # Exit door
pulses.mark_treasure(8, 5) # Chest location
pulses.mark_warning(12, 12) # Trap
# When player reaches objective
pulses.remove(15, 10)
# Clear all when changing levels
pulses.clear()
Area Pulse Effect
Pulse a group of cells together with one shared timer:
import mcrfpy
import math
class AreaPulse:
"""Pulse a rectangular or circular area."""
def __init__(self, grid):
self.grid = grid
self.cells = [] # list of (x, y)
self.is_pulsing = False
self.timer = None
self.color_layer = self._get_or_create_layer()
def _get_or_create_layer(self):
existing = self.grid.layer("area_pulse")
if existing is not None:
return existing
layer = mcrfpy.ColorLayer(z_index=1, name="area_pulse")
self.grid.add_layer(layer)
return layer
def set_rect(self, x, y, width, height, color):
"""Set a rectangular area to pulse."""
self.color = color
self.cells = [
(x + dx, y + dy)
for dy in range(height)
for dx in range(width)
]
def set_circle(self, center_x, center_y, radius, color):
"""Set a circular area to pulse."""
self.color = color
self.cells = [
(center_x + dx, center_y + dy)
for dy in range(-radius, radius + 1)
for dx in range(-radius, radius + 1)
if dx * dx + dy * dy <= radius * radius
]
def _write(self, alpha):
r, g, b = self.color
for (x, y) in self.cells:
self.color_layer.set((x, y), mcrfpy.Color(r, g, b, alpha))
def pulse_once(self, duration=0.5, max_alpha=180):
"""Single pulse of the area: fade in, then fade out."""
half = duration / 2.0
state = {"t0": None}
def tick(timer, runtime):
if state["t0"] is None:
state["t0"] = runtime
t = (runtime - state["t0"]) / 1000.0
if t >= duration:
self._write(0)
timer.stop()
return
phase = t / half
frac = phase if phase <= 1.0 else 2.0 - phase
self._write(int(max_alpha * frac))
mcrfpy.Timer(f"area_once_{id(self)}", tick, 33)
def start_continuous(self, period=1.0, max_alpha=150):
"""Start continuous pulsing of the area."""
self.is_pulsing = True
state = {"t0": None}
def tick(timer, runtime):
if not self.is_pulsing:
return
if state["t0"] is None:
state["t0"] = runtime
t = (runtime - state["t0"]) / 1000.0
wave = (math.sin(2 * math.pi * t / period) + 1.0) / 2.0
self._write(int(max_alpha * wave))
self.timer = mcrfpy.Timer(f"area_continuous_{id(self)}", tick, 33)
def stop(self):
"""Stop pulsing and clear."""
self.is_pulsing = False
if self.timer is not None:
self.timer.stop()
self.timer = None
self._write(0)
# Usage
area = AreaPulse(grid)
# Highlight a room
area.set_rect(5, 5, 4, 4, (100, 200, 255))
area.start_continuous(period=2.0)
# Or highlight explosion radius
area.set_circle(10, 10, 3, (255, 100, 0))
area.pulse_once(duration=0.8)
Ripple Effect
Animate an expanding ring of color, each ring fading independently:
import mcrfpy
def ripple_effect(grid, center_x, center_y, color, max_radius=5, duration=1.0):
"""
Create an expanding ripple effect.
Args:
grid: Grid with color layer
center_x, center_y: Ripple origin
color: RGB tuple
max_radius: Maximum ripple size
duration: Total animation time
"""
layer = grid.layer("ripple")
if layer is None:
layer = mcrfpy.ColorLayer(z_index=1, name="ripple")
grid.add_layer(layer)
step_duration = duration / max_radius
r, g, b = color
for radius in range(max_radius + 1):
# Cells approximately on the ring edge at this radius
ring_cells = []
for dy in range(-radius, radius + 1):
for dx in range(-radius, radius + 1):
dist_sq = dx * dx + dy * dy
if radius * radius - radius <= dist_sq <= radius * radius + radius:
ring_cells.append((center_x + dx, center_y + dy))
fade_duration = step_duration * 2
def make_ring_animator(cells):
state = {"t0": None}
def tick(timer, runtime):
if state["t0"] is None:
state["t0"] = runtime
t = (runtime - state["t0"]) / 1000.0
frac = max(0.0, 1.0 - t / fade_duration)
alpha = int(200 * frac)
for cx, cy in cells:
layer.set((cx, cy), mcrfpy.Color(r, g, b, alpha))
if t >= fade_duration:
for cx, cy in cells:
layer.set((cx, cy), mcrfpy.Color(r, g, b, 0))
timer.stop()
return tick
def start_ring(timer, runtime, cells=ring_cells):
mcrfpy.Timer(f"ripple_fade_{id(cells)}", make_ring_animator(cells), 33)
timer.stop()
delay = int(radius * step_duration * 1000)
mcrfpy.Timer(f"ripple_{radius}", start_ring, max(delay, 1), once=True)
# Usage
ripple_effect(grid, 10, 10, (100, 200, 255), max_radius=6, duration=0.8)
Key Concepts
- ColorLayer: Grid overlay for cell-based color effects; created standalone
with
mcrfpy.ColorLayer(z_index=..., name=...)and attached withgrid.add_layer(layer)(nevergrid.add_layer("color")— the argument is always a layer object). Look layers up withgrid.layer(name). - No live cell handle:
layer.at(x, y)returns a snapshotColor, andColorLayerhas no.animate(). Pulsing means recomputing alpha yourself (triangle wave for a single pulse,math.sinfor continuous) and writing it withlayer.set((x, y), color)on everyTimertick. - Timer coordination: A repeating
mcrfpy.Timer(name, callback, interval_ms)drives each pulse; the callback receives(timer, runtime_ms), whereruntime_msis the total engine runtime in milliseconds (NOT relative to when the timer started); one-shot effects should record the runtime of their first tick and measure elapsed time from it. Calltimer.stop()to end it. - Alpha animation: All effects here only animate the
achannel of aColor, leavingr,g,bfixed.
Tips
- Use lower
max_alpha(120-180) for subtle, non-intrusive highlights - Longer periods (1.5-2.0s) feel calmer; shorter (0.5-0.8s) feel urgent
- Combine with other effects (floating text, sound) for important events
- Consider color blindness: don’t rely solely on color for meaning
- Clean up pulses when cells become irrelevant (player moves, item collected) —
always call
timer.stop(), since a runningTimerkeeps firing even if its target cell no longer matters