I only have file-reading and skill tools in this session, so I couldn't write the file or run it. Below is the program, plus the steps to run and validate it. It's written against the skill's real GIFBuilder and validators APIs, which I read first.
Two things in the skill's code shaped the design:
save(..., optimize_for_emoji=True)throws away frames when there are more than 12. That would wreck a pulse-then-sparkle sequence. The program passesnum_colors=48directly instead and keeps all 24 frames.validate_gifonly checks dimensions, size and frame count. It doesn't check colors or the 3-second limit, so the program has its own checks for those.
make_notification_bell.py
Place it in the project root, next to .claude/skills/.
#!/usr/bin/env python3
"""128x128 Slack emoji: blue bell pulses twice, then emits three gold sparkles."""
import math
import sys
from pathlib import Path
from PIL import Image, ImageDraw
sys.path.insert(0, str(Path(__file__).parent / ".claude" / "skills" / "slack-gif-creator"))
from core.gif_builder import GIFBuilder # noqa: E402
from core.validators import validate_gif # noqa: E402
SIZE = 128
SS = 4 # supersampling factor for smooth edges
FPS = 10 # 24 frames @ 10 fps = 2.4s
NUM_FRAMES = 24
NUM_COLORS = 48
OUTPUT = "notification-bell.gif"
BG = (22, 30, 58)
BELL = (52, 132, 232)
BELL_LIGHT = (130, 190, 255)
BELL_DARK = (24, 76, 160)
RIM = (86, 156, 245)
GOLD = (255, 200, 40)
GOLD_CORE = (255, 238, 150)
BELL_CENTER = (64, 74)
PULSE_FRAMES = 7 # frames per pulse
PULSE_AMOUNT = 0.18
# (x, y, start_frame) for each sparkle
SPARKLES = [(20, 30, 12), (64, 14, 14), (108, 30, 16)]
SPARKLE_LIFE = 8
SPARKLE_RADIUS = 12
def bell(draw, scale):
cx, cy = BELL_CENTER
def p(x, y):
return ((cx + x * scale) * SS, (cy + y * scale) * SS)
def box(x1, y1, x2, y2):
return [*p(x1, y1), *p(x2, y2)]
w = max(2, round(2 * SS))
draw.ellipse(box(-4, -38, 4, -30), fill=BELL_DARK) # knob
draw.ellipse(box(-20, -32, 20, 8), fill=BELL, outline=BELL_DARK, width=w) # dome
draw.polygon([p(-20, -12), p(20, -12), p(28, 18), p(-28, 18)], fill=BELL) # skirt
draw.line([p(-20, -12), p(-28, 18)], fill=BELL_DARK, width=w)
draw.line([p(20, -12), p(28, 18)], fill=BELL_DARK, width=w)
draw.rounded_rectangle(box(-31, 16, 31, 25), radius=4 * SS, fill=RIM,
outline=BELL_DARK, width=w)
draw.line([p(-12, -22), p(-16, 6)], fill=BELL_LIGHT, width=3 * SS) # highlight
draw.ellipse(box(-7, 24, 7, 38), fill=BELL_DARK) # clapper
def sparkle(draw, x, y, r):
def star(radius, color):
pts = []
for k in range(8):
ang = math.radians(-90 + k * 45)
rad = radius if k % 2 == 0 else radius * 0.3
pts.append(((x + rad * math.cos(ang)) * SS, (y + rad * math.sin(ang)) * SS))
draw.polygon(pts, fill=color)
star(r, GOLD)
star(r * 0.5, GOLD_CORE)
def render(i):
img = Image.new("RGB", (SIZE * SS, SIZE * SS), BG)
draw = ImageDraw.Draw(img)
scale = 1.0
if i < 2 * PULSE_FRAMES:
phase = (i % PULSE_FRAMES) / PULSE_FRAMES
scale += PULSE_AMOUNT * math.sin(math.pi * phase)
bell(draw, scale)
for sx, sy, start in SPARKLES:
t = (i - start) / SPARKLE_LIFE
if 0 <= t < 1:
drift = -6 * t # float upward as it fades
sparkle(draw, sx, sy + drift, SPARKLE_RADIUS * math.sin(math.pi * t))
return img.resize((SIZE, SIZE), Image.Resampling.LANCZOS)
def main():
builder = GIFBuilder(width=SIZE, height=SIZE, fps=FPS)
for i in range(NUM_FRAMES):
builder.add_frame(render(i))
# No optimize_for_emoji: it would drop frames above 12. Colors are capped explicitly.
builder.save(OUTPUT, num_colors=NUM_COLORS, remove_duplicates=False)
validate_gif(OUTPUT, is_emoji=True, verbose=True)
if __name__ == "__main__":
main()
Run it
pip install pillow imageio numpy
python make_notification_bell.py
Validate the output
-
Skill validator. The script calls
validate_gifitself. It confirms 128x128 and reports file size, frame count and duration. Expect about 24 frames at 10 fps, so 2.4s. -
Colors and duration. The skill validator doesn't check these two limits, so run this:
from PIL import Image, ImageSequence im = Image.open("notification-bell.gif") frames = [f.convert("RGB") for f in ImageSequence.Iterator(im)] colors = set() for f in frames: colors |= {c for _, c in f.getcolors(maxcolors=128 * 128)} total = sum(f.info.get("duration", 100) for f in ImageSequence.Iterator(im)) / 1000 print(im.size, len(frames), "frames,", len(colors), "colors,", f"{total:.1f}s") assert im.size == (128, 128) and len(colors) <= 48 and total < 3 -
Visual check. Open the GIF in a browser or Quick Look. Confirm that:
- the bell pulses twice, during roughly the first 1.4s;
- three gold sparkles then pop up in sequence and fade;
- the loop restarts cleanly.
-
Slack check. Upload it through Customize Workspace → Emoji. Slack's emoji limit is 128 KB, and this should come in well under that. Check the file size to be sure.
Since I haven't run it, the sparkle positions may overlap the bell's top corners a little at peak pulse. If so, adjust the SPARKLES coordinates or PULSE_AMOUNT. If you'd like me to run it and fix anything, I'd need file-write and shell access.
Real run recorded with claude-code / claude-sonnet-5-5. Output is shown verbatim, unmodified.
Zweck
Bietet praktische Vorgaben, Animationshinweise und Python-Hilfen für GIFs, die für Slack geeignet sind.
Arbeitsweise
- Erstellt Bildframes mit den Zeichenprimitiven von Pillow.
- Nutzt Hilfen für Easing, Frame-Komposition, Farbverringerung und Entfernen doppelter Frames.
- Speichert ein GIF und prüft Maße, Dateigröße, Frame-Anzahl und Laufzeitinformationen.
Geeignet für
Eigene Slack-Emoji, kleine Schleifenanimationen für Nachrichten und die Validierung erzeugter GIFs.
Enthält Python-Module, die von Ihnen ausgewählte Bild- oder GIF-Pfade lesen und ein GIF in den angegebenen Ausgabepfad schreiben. Es hat keinen Netzwerk- oder Zugangsdatenzugriff. Erfordert Pillow, imageio, imageio-ffmpeg und NumPy; prüfen Sie generierte Medien und Ausgabepfade vor der Verwendung.