basic_effects.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221
  1. #!/usr/bin/env python3
  2. """
  3. WLED Basic Effects for Raspberry Pi
  4. Effects 0-30: Static, Blink, Rainbow, Scan, etc.
  5. Ported from WLED FX.cpp
  6. """
  7. import random
  8. import math
  9. from ..segment import Segment
  10. from ..utils.colors import *
  11. # Effect return value is delay in milliseconds
  12. FRAMETIME = 24 # ~42 FPS
  13. def mode_static(seg: Segment) -> int:
  14. """Solid color"""
  15. seg.fill(seg.get_color(0))
  16. return 350 if seg.call == 0 else FRAMETIME
  17. def mode_blink(seg: Segment) -> int:
  18. """Blink between two colors"""
  19. cycle_time = (255 - seg.speed) * 20
  20. on_time = FRAMETIME + ((cycle_time * seg.intensity) >> 8)
  21. cycle_time += FRAMETIME * 2
  22. now = seg.now()
  23. iteration = now // cycle_time
  24. rem = now % cycle_time
  25. on = (iteration != seg.step) or (rem <= on_time)
  26. seg.step = iteration
  27. seg.fill(seg.get_color(0) if on else seg.get_color(1))
  28. return FRAMETIME
  29. def mode_strobe(seg: Segment) -> int:
  30. """Strobe effect"""
  31. cycle_time = (255 - seg.speed) * 20 + FRAMETIME * 2
  32. now = seg.now()
  33. iteration = now // cycle_time
  34. on = (iteration != seg.step)
  35. seg.step = iteration
  36. seg.fill(seg.get_color(0) if on else seg.get_color(1))
  37. return FRAMETIME
  38. def mode_breath(seg: Segment) -> int:
  39. """Breathing effect"""
  40. counter = (seg.now() * ((seg.speed >> 3) + 10)) & 0xFFFF
  41. counter = (counter >> 2) + (counter >> 4)
  42. var = 0
  43. if counter < 16384:
  44. if counter > 8192:
  45. counter = 8192 - (counter - 8192)
  46. var = sin16(counter) // 103
  47. lum = 30 + var
  48. for i in range(seg.length):
  49. seg.set_pixel_color(i, color_blend(seg.get_color(1),
  50. seg.color_from_palette(i),
  51. lum & 0xFF))
  52. return FRAMETIME
  53. def mode_fade(seg: Segment) -> int:
  54. """Fade between two colors"""
  55. counter = seg.now() * ((seg.speed >> 3) + 10)
  56. lum = triwave16(counter & 0xFFFF) >> 8
  57. for i in range(seg.length):
  58. seg.set_pixel_color(i, color_blend(seg.get_color(1),
  59. seg.color_from_palette(i),
  60. lum))
  61. return FRAMETIME
  62. def mode_scan(seg: Segment) -> int:
  63. """Scanning pixel"""
  64. if seg.length <= 1:
  65. return mode_static(seg)
  66. cycle_time = 750 + (255 - seg.speed) * 150
  67. perc = seg.now() % cycle_time
  68. prog = (perc * 65535) // cycle_time
  69. size = 1 + ((seg.intensity * seg.length) >> 9)
  70. led_index = (prog * ((seg.length * 2) - size * 2)) >> 16
  71. seg.fill(seg.get_color(1))
  72. led_offset = led_index - (seg.length - size)
  73. led_offset = abs(led_offset)
  74. for j in range(led_offset, min(led_offset + size, seg.length)):
  75. seg.set_pixel_color(j, seg.color_from_palette(j))
  76. return FRAMETIME
  77. def mode_dual_scan(seg: Segment) -> int:
  78. """Dual scanning pixels"""
  79. if seg.length <= 1:
  80. return mode_static(seg)
  81. cycle_time = 750 + (255 - seg.speed) * 150
  82. perc = seg.now() % cycle_time
  83. prog = (perc * 65535) // cycle_time
  84. size = 1 + ((seg.intensity * seg.length) >> 9)
  85. led_index = (prog * ((seg.length * 2) - size * 2)) >> 16
  86. seg.fill(seg.get_color(1))
  87. led_offset = led_index - (seg.length - size)
  88. led_offset = abs(led_offset)
  89. # First scanner
  90. for j in range(led_offset, min(led_offset + size, seg.length)):
  91. seg.set_pixel_color(j, seg.color_from_palette(j))
  92. # Second scanner (opposite direction)
  93. for j in range(led_offset, min(led_offset + size, seg.length)):
  94. i2 = seg.length - 1 - j
  95. seg.set_pixel_color(i2, seg.color_from_palette(i2))
  96. return FRAMETIME
  97. def mode_rainbow(seg: Segment) -> int:
  98. """Solid rainbow (cycles through hues)"""
  99. counter = (seg.now() * ((seg.speed >> 2) + 2)) & 0xFFFF
  100. counter = counter >> 8
  101. if seg.intensity < 128:
  102. color = color_blend(color_wheel(counter), WHITE,
  103. 128 - seg.intensity)
  104. else:
  105. color = color_wheel(counter)
  106. seg.fill(color)
  107. return FRAMETIME
  108. def mode_rainbow_cycle(seg: Segment) -> int:
  109. """Rainbow distributed across strip"""
  110. counter = (seg.now() * ((seg.speed >> 2) + 2)) & 0xFFFF
  111. counter = counter >> 8
  112. for i in range(seg.length):
  113. # intensity controls density
  114. index = (i * (16 << (seg.intensity // 29)) // seg.length) + counter
  115. seg.set_pixel_color(i, color_wheel(index & 0xFF))
  116. return FRAMETIME
  117. def mode_theater_chase(seg: Segment) -> int:
  118. """Theater chase effect"""
  119. width = 3 + (seg.intensity >> 4)
  120. cycle_time = 50 + (255 - seg.speed)
  121. iteration = seg.now() // cycle_time
  122. for i in range(seg.length):
  123. if (i % width) == seg.aux0:
  124. seg.set_pixel_color(i, seg.color_from_palette(i))
  125. else:
  126. seg.set_pixel_color(i, seg.get_color(1))
  127. if iteration != seg.step:
  128. seg.aux0 = (seg.aux0 + 1) % width
  129. seg.step = iteration
  130. return FRAMETIME
  131. def mode_running_lights(seg: Segment) -> int:
  132. """Running lights with sine wave"""
  133. x_scale = seg.intensity >> 2
  134. counter = (seg.now() * seg.speed) >> 9
  135. for i in range(seg.length):
  136. a = i * x_scale - counter
  137. s = sin8(a & 0xFF)
  138. color = color_blend(seg.get_color(1),
  139. seg.color_from_palette(i), s)
  140. seg.set_pixel_color(i, color)
  141. return FRAMETIME
  142. def mode_color_wipe(seg: Segment) -> int:
  143. """Color wipe effect"""
  144. if seg.length <= 1:
  145. return mode_static(seg)
  146. cycle_time = 750 + (255 - seg.speed) * 150
  147. perc = seg.now() % cycle_time
  148. prog = (perc * 65535) // cycle_time
  149. back = prog > 32767
  150. if back:
  151. prog -= 32767
  152. if seg.step == 0:
  153. seg.step = 1
  154. else:
  155. if seg.step == 2:
  156. seg.step = 3
  157. led_index = (prog * seg.length) >> 15
  158. rem = (prog * seg.length) * 2
  159. rem //= (seg.intensity + 1)
  160. rem = min(255, rem)
  161. col0 = seg.get_color(0)
  162. col1 = seg.get_color(1)
  163. for i in range(seg.length):
  164. if i < led_index:
  165. seg.set_pixel_color(i, col1 if back else col0)
  166. else:
  167. seg.set_pixel_color(i, col0 if back else col1)
  168. if i == led_index:
  169. blended = color_blend(col1 if back else col0,
  170. col0 if back else col1,
  171. rem)
  172. seg.set_pixel_color(i, blended)
  173. return FRAMETIME
  174. def mode_random_color(seg: Segment) -> int:
  175. """Random solid colors with fade"""
  176. cycle_time = 200 + (255 - seg.speed) * 50
  177. iteration = seg.now() // cycle_time
  178. rem = seg.now() % cycle_time
  179. fade_dur = (cycle_time * seg.intensity) >> 8
  180. fade = 255
  181. if fade_dur:
  182. fade = (rem * 255) // fade_dur
  183. fade = min(255, fade)
  184. if seg.call == 0:
  185. seg.aux0 = random.randint(0, 255)
  186. seg.step = 2
  187. if iteration != seg.step:
  188. seg.aux1 = seg.aux0
  189. seg.aux0 = random.randint(0, 255)
  190. seg.step = iteration
  191. color = color_blend(color_wheel(seg.aux1),
  192. color_wheel(seg.aux0), fade)
  193. seg.fill(color)
  194. return FRAMETIME
  195. def mode_dynamic(seg: Segment) -> int:
  196. """Dynamic random colors per pixel"""
  197. if seg.call == 0:
  198. seg.data = [random.randint(0, 255) for _ in range(seg.length)]
  199. cycle_time = 50 + (255 - seg.speed) * 15
  200. iteration = seg.now() // cycle_time
  201. if iteration != seg.step and seg.speed != 0:
  202. for i in range(seg.length):
  203. if random.randint(0, 255) <= seg.intensity:
  204. seg.data[i] = random.randint(0, 255)
  205. seg.step = iteration
  206. for i in range(seg.length):
  207. seg.set_pixel_color(i, color_wheel(seg.data[i]))
  208. return FRAMETIME
  209. def mode_twinkle(seg: Segment) -> int:
  210. """Twinkle effect"""
  211. seg.fade_out(224)
  212. cycle_time = 20 + (255 - seg.speed) * 5
  213. iteration = seg.now() // cycle_time
  214. if iteration != seg.step:
  215. max_on = max(1, (seg.intensity * seg.length) // 255)
  216. if seg.aux0 >= max_on:
  217. seg.aux0 = 0
  218. seg.aux1 = random.randint(0, 0xFFFF)
  219. seg.aux0 += 1
  220. seg.step = iteration
  221. prng = seg.aux1
  222. for _ in range(seg.aux0):
  223. prng = (prng * 2053 + 13849) & 0xFFFF
  224. j = (prng * seg.length) >> 16
  225. if j < seg.length:
  226. seg.set_pixel_color(j, seg.color_from_palette(j))
  227. return FRAMETIME
  228. def mode_sparkle(seg: Segment) -> int:
  229. """Single sparkle effect"""
  230. for i in range(seg.length):
  231. seg.set_pixel_color(i, seg.color_from_palette(i))
  232. cycle_time = 10 + (255 - seg.speed) * 2
  233. iteration = seg.now() // cycle_time
  234. if iteration != seg.step:
  235. seg.aux0 = random.randint(0, seg.length - 1)
  236. seg.step = iteration
  237. seg.set_pixel_color(seg.aux0, seg.get_color(0))
  238. return FRAMETIME
  239. def mode_fire(seg: Segment) -> int:
  240. """Fire/flame effect"""
  241. if seg.call == 0:
  242. seg.data = [0] * seg.length
  243. # Cooling parameter (higher = cooler flames)
  244. cooling = ((100 - (seg.intensity >> 1)) * 10) // seg.length + 2
  245. # Heat decay for all pixels
  246. for i in range(seg.length):
  247. cool_down = random.randint(0, cooling)
  248. seg.data[i] = max(0, seg.data[i] - cool_down)
  249. # Heat drift upward
  250. for i in range(seg.length - 1, 2, -1):
  251. seg.data[i] = (seg.data[i - 1] + seg.data[i - 2] + seg.data[i - 2]) // 3
  252. # Randomly ignite new sparks near bottom
  253. if random.randint(0, 255) < seg.intensity:
  254. spark_pos = random.randint(0, min(7, seg.length - 1))
  255. seg.data[spark_pos] = min(255, seg.data[spark_pos] + random.randint(160, 255))
  256. # Convert heat to colors
  257. for i in range(seg.length):
  258. heat = seg.data[i]
  259. # Black -> Red -> Yellow -> White
  260. if heat < 85:
  261. color = (heat * 3, 0, 0)
  262. elif heat < 170:
  263. h = heat - 85
  264. color = (255, h * 3, 0)
  265. else:
  266. h = heat - 170
  267. color = (255, 255, h * 3)
  268. r, g, b = color
  269. seg.set_pixel_color(i, (r << 16) | (g << 8) | b)
  270. return FRAMETIME
  271. def mode_comet(seg: Segment) -> int:
  272. """Comet/shooting star effect"""
  273. if seg.call == 0:
  274. seg.aux0 = 0
  275. seg.aux1 = 0
  276. seg.fade_out(128)
  277. size = 1 + ((seg.intensity * seg.length) >> 9)
  278. cycle_time = 10 + (255 - seg.speed)
  279. iteration = seg.now() // cycle_time
  280. if iteration != seg.step:
  281. seg.aux0 = (seg.aux0 + 1) % seg.length
  282. seg.step = iteration
  283. # Draw comet
  284. for i in range(size):
  285. pos = (seg.aux0 - i) % seg.length
  286. brightness = 255 - (i * 255 // max(1, size))
  287. color = color_blend(0, seg.color_from_palette(pos), brightness)
  288. seg.set_pixel_color(pos, color)
  289. return FRAMETIME
  290. def mode_chase(seg: Segment) -> int:
  291. """Chase effect with colored segments"""
  292. if seg.call == 0:
  293. seg.aux0 = 0
  294. size = max(1, seg.length // 4)
  295. cycle_time = 10 + (255 - seg.speed)
  296. iteration = seg.now() // cycle_time
  297. if iteration != seg.step:
  298. seg.aux0 = (seg.aux0 + 1) % seg.length
  299. seg.step = iteration
  300. seg.fill(seg.get_color(1))
  301. for i in range(size):
  302. pos = (seg.aux0 + i) % seg.length
  303. seg.set_pixel_color(pos, seg.color_from_palette(pos))
  304. return FRAMETIME
  305. def mode_police(seg: Segment) -> int:
  306. """Police lights (red/blue alternating)"""
  307. cycle_time = 25 + (255 - seg.speed)
  308. on_time = cycle_time // 2
  309. now = seg.now()
  310. iteration = now // cycle_time
  311. rem = now % cycle_time
  312. on = rem < on_time
  313. half = seg.length // 2
  314. # Red on left, blue on right
  315. red = (255, 0, 0)
  316. blue = (0, 0, 255)
  317. off_color = (0, 0, 0)
  318. for i in range(half):
  319. if (iteration % 2 == 0 and on) or (iteration % 2 == 1 and not on):
  320. seg.set_pixel_color(i, (red[0] << 16) | (red[1] << 8) | red[2])
  321. else:
  322. seg.set_pixel_color(i, (off_color[0] << 16) | (off_color[1] << 8) | off_color[2])
  323. for i in range(half, seg.length):
  324. if (iteration % 2 == 1 and on) or (iteration % 2 == 0 and not on):
  325. seg.set_pixel_color(i, (blue[0] << 16) | (blue[1] << 8) | blue[2])
  326. else:
  327. seg.set_pixel_color(i, (off_color[0] << 16) | (off_color[1] << 8) | off_color[2])
  328. return FRAMETIME
  329. def mode_lightning(seg: Segment) -> int:
  330. """Lightning flash effect"""
  331. if seg.call == 0:
  332. seg.aux0 = 0
  333. seg.aux1 = 0
  334. cycle_time = 50 + (255 - seg.speed) * 10
  335. iteration = seg.now() // cycle_time
  336. if iteration != seg.step:
  337. # Random chance of lightning
  338. if random.randint(0, 255) < seg.intensity:
  339. seg.aux0 = random.randint(3, 8) # Number of flashes
  340. seg.aux1 = seg.now()
  341. seg.step = iteration
  342. # Flash sequence
  343. if seg.aux0 > 0:
  344. flash_duration = 50
  345. time_since = seg.now() - seg.aux1
  346. if time_since < flash_duration:
  347. # Flash on
  348. brightness = 255 - (time_since * 255 // flash_duration)
  349. for i in range(seg.length):
  350. color = color_blend(0, WHITE, brightness)
  351. seg.set_pixel_color(i, color)
  352. else:
  353. # Flash off, wait for next
  354. if time_since > flash_duration + random.randint(10, 100):
  355. seg.aux0 -= 1
  356. seg.aux1 = seg.now()
  357. else:
  358. seg.fill(seg.get_color(1))
  359. else:
  360. seg.fill(seg.get_color(1))
  361. return FRAMETIME
  362. def mode_fireworks(seg: Segment) -> int:
  363. """Fireworks effect"""
  364. seg.fade_out(64)
  365. cycle_time = 20 + (255 - seg.speed)
  366. iteration = seg.now() // cycle_time
  367. if iteration != seg.step:
  368. # Launch new firework
  369. if random.randint(0, 255) < seg.intensity:
  370. pos = random.randint(0, seg.length - 1)
  371. color = color_wheel(random.randint(0, 255))
  372. # Bright center
  373. seg.set_pixel_color(pos, color)
  374. # Dimmer neighbors
  375. if pos > 0:
  376. seg.set_pixel_color(pos - 1, color_blend(0, color, 128))
  377. if pos < seg.length - 1:
  378. seg.set_pixel_color(pos + 1, color_blend(0, color, 128))
  379. seg.step = iteration
  380. return FRAMETIME
  381. def mode_ripple(seg: Segment) -> int:
  382. """Ripple effect"""
  383. if seg.call == 0:
  384. seg.data = [0] * seg.length
  385. seg.aux0 = seg.length // 2
  386. seg.fade_out(250)
  387. cycle_time = 50 + (255 - seg.speed) * 2
  388. iteration = seg.now() // cycle_time
  389. if iteration != seg.step:
  390. # New ripple
  391. if random.randint(0, 255) < seg.intensity:
  392. seg.aux0 = random.randint(0, seg.length - 1)
  393. seg.data[seg.aux0] = 255
  394. # Propagate ripple
  395. new_data = seg.data.copy()
  396. for i in range(1, seg.length - 1):
  397. new_data[i] = (seg.data[i - 1] + seg.data[i + 1]) // 2
  398. seg.data = new_data
  399. seg.step = iteration
  400. for i in range(seg.length):
  401. if seg.data[i] > 0:
  402. color = color_blend(seg.get_color(1),
  403. seg.color_from_palette(i),
  404. seg.data[i])
  405. seg.set_pixel_color(i, color)
  406. return FRAMETIME
  407. def mode_flow(seg: Segment) -> int:
  408. """Smooth flowing color movement"""
  409. counter = seg.now() * ((seg.speed >> 3) + 1)
  410. for i in range(seg.length):
  411. pos = ((i * 256 // seg.length) + counter) & 0xFFFF
  412. color = color_wheel((pos >> 8) & 0xFF)
  413. # Apply intensity as brightness modulation
  414. brightness = 128 + ((sin8((pos >> 7) & 0xFF) - 128) * seg.intensity // 255)
  415. color = color_blend(0, color, brightness)
  416. seg.set_pixel_color(i, color)
  417. return FRAMETIME
  418. def mode_colorloop(seg: Segment) -> int:
  419. """Smooth color loop across entire strip"""
  420. counter = (seg.now() * ((seg.speed >> 3) + 1)) & 0xFFFF
  421. for i in range(seg.length):
  422. # Create gradient based on position and time
  423. hue = ((i * 256 // max(1, seg.length)) + (counter >> 7)) & 0xFF
  424. color = color_wheel(hue)
  425. # Intensity controls saturation
  426. if seg.intensity < 255:
  427. color = color_blend(color, WHITE, 255 - seg.intensity)
  428. seg.set_pixel_color(i, color)
  429. return FRAMETIME
  430. def mode_palette_flow(seg: Segment) -> int:
  431. """Flowing palette colors"""
  432. counter = seg.now() * ((seg.speed >> 3) + 1)
  433. for i in range(seg.length):
  434. # Get color from palette based on position and time
  435. palette_pos = ((i * 255 // max(1, seg.length)) + (counter >> 7)) & 0xFF
  436. color = seg.color_from_palette(palette_pos)
  437. # Intensity controls brightness modulation
  438. if seg.intensity < 255:
  439. brightness = 128 + ((sin8(palette_pos) - 128) * seg.intensity // 255)
  440. color = color_blend(0, color, brightness)
  441. seg.set_pixel_color(i, color)
  442. return FRAMETIME
  443. def mode_gradient(seg: Segment) -> int:
  444. """Smooth gradient between colors"""
  445. for i in range(seg.length):
  446. # Create gradient from color 0 to color 2
  447. blend_amount = (i * 255) // max(1, seg.length - 1)
  448. color = color_blend(seg.get_color(0), seg.get_color(2), blend_amount)
  449. # Intensity controls a pulsing brightness
  450. if seg.intensity > 0:
  451. counter = (seg.now() * ((seg.speed >> 3) + 1)) & 0xFFFF
  452. pulse = sin8((counter >> 8) & 0xFF)
  453. brightness = 128 + ((pulse - 128) * seg.intensity // 255)
  454. color = color_blend(0, color, brightness)
  455. seg.set_pixel_color(i, color)
  456. return FRAMETIME
  457. def mode_multi_strobe(seg: Segment) -> int:
  458. """Multi-color strobe effect"""
  459. cycle_time = 50 + (255 - seg.speed)
  460. flash_duration = max(5, cycle_time // 4)
  461. now = seg.now()
  462. iteration = now // cycle_time
  463. rem = now % cycle_time
  464. if rem < flash_duration:
  465. # Strobe on with color from palette
  466. color_index = (iteration * 85) & 0xFF
  467. color = color_wheel(color_index)
  468. seg.fill(color)
  469. else:
  470. # Strobe off
  471. seg.fill(seg.get_color(1))
  472. return FRAMETIME
  473. def mode_waves(seg: Segment) -> int:
  474. """Sine wave effect"""
  475. counter = seg.now() * ((seg.speed >> 3) + 1)
  476. for i in range(seg.length):
  477. # Create wave pattern
  478. wave_pos = (i * 255 // max(1, seg.length)) + (counter >> 7)
  479. brightness = sin8(wave_pos & 0xFF)
  480. # Intensity controls wave amplitude
  481. brightness = 128 + ((brightness - 128) * seg.intensity // 255)
  482. color = color_blend(seg.get_color(1),
  483. seg.color_from_palette(i),
  484. brightness)
  485. seg.set_pixel_color(i, color)
  486. return FRAMETIME
  487. def mode_bpm(seg: Segment) -> int:
  488. """BPM (beats per minute) pulse effect"""
  489. # Calculate BPM based on speed (60-180 BPM)
  490. bpm = 60 + ((seg.speed * 120) >> 8)
  491. ms_per_beat = 60000 // bpm
  492. beat_phase = (seg.now() % ms_per_beat) * 255 // ms_per_beat
  493. brightness = sin8(beat_phase)
  494. for i in range(seg.length):
  495. # Create traveling beat
  496. offset = (i * 255 // max(1, seg.length))
  497. local_brightness = sin8((beat_phase + offset) & 0xFF)
  498. # Intensity controls brightness range
  499. local_brightness = 128 + ((local_brightness - 128) * seg.intensity // 255)
  500. color = color_blend(seg.get_color(1),
  501. seg.color_from_palette(i),
  502. local_brightness)
  503. seg.set_pixel_color(i, color)
  504. return FRAMETIME
  505. def mode_juggle(seg: Segment) -> int:
  506. """Juggling colored dots"""
  507. if seg.call == 0:
  508. seg.data = [0] * 8 # Track 8 dot positions
  509. seg.fade_out(224)
  510. cycle_time = 10 + (255 - seg.speed) // 2
  511. iteration = seg.now() // cycle_time
  512. if iteration != seg.step:
  513. # Update dot positions using different sine waves
  514. for dot in range(min(8, 1 + seg.intensity // 32)):
  515. phase = (seg.now() * (dot + 1)) & 0xFFFF
  516. pos = (sin16(phase) + 32768) * seg.length // 65536
  517. pos = max(0, min(seg.length - 1, pos))
  518. hue = (dot * 32) & 0xFF
  519. color = color_wheel(hue)
  520. seg.set_pixel_color(pos, color)
  521. seg.step = iteration
  522. return FRAMETIME
  523. def mode_meteor(seg: Segment) -> int:
  524. """Meteor shower effect with trails"""
  525. if seg.call == 0:
  526. seg.aux0 = 0
  527. seg.aux1 = 0
  528. # Fade all pixels
  529. seg.fade_out(200)
  530. size = 1 + ((seg.intensity * seg.length) >> 8)
  531. cycle_time = 10 + (255 - seg.speed)
  532. iteration = seg.now() // cycle_time
  533. if iteration != seg.step:
  534. seg.aux0 = (seg.aux0 + 1) % (seg.length + size)
  535. seg.step = iteration
  536. # Draw meteor head and tail
  537. if seg.aux0 < seg.length:
  538. for i in range(size):
  539. pos = seg.aux0 - i
  540. if 0 <= pos < seg.length:
  541. brightness = 255 - (i * 200 // max(1, size))
  542. color = color_blend(0, seg.color_from_palette(pos), brightness)
  543. # Add to existing color for brighter effect
  544. existing = seg.get_pixel_color(pos)
  545. seg.set_pixel_color(pos, color_add(existing, color))
  546. return FRAMETIME
  547. def mode_pride(seg: Segment) -> int:
  548. """Pride flag colors moving effect"""
  549. counter = seg.now() * ((seg.speed >> 3) + 1)
  550. # Pride flag colors (6 stripes)
  551. pride_colors = [
  552. (0xE4, 0x00, 0x3A), # Red
  553. (0xFF, 0x8C, 0x00), # Orange
  554. (0xFF, 0xED, 0x00), # Yellow
  555. (0x00, 0x81, 0x1F), # Green
  556. (0x00, 0x4C, 0xFF), # Blue
  557. (0x76, 0x01, 0x89), # Purple
  558. ]
  559. for i in range(seg.length):
  560. # Determine which stripe this pixel belongs to
  561. stripe_size = max(1, seg.length // 6)
  562. offset = (counter >> 7) & 0xFF
  563. stripe_idx = ((i + offset) // stripe_size) % 6
  564. r, g, b = pride_colors[stripe_idx]
  565. color = (r << 16) | (g << 8) | b
  566. # Intensity controls blending with background
  567. if seg.intensity < 255:
  568. color = color_blend(seg.get_color(1), color, seg.intensity)
  569. seg.set_pixel_color(i, color)
  570. return FRAMETIME
  571. def mode_pacifica(seg: Segment) -> int:
  572. """Ocean/water simulation"""
  573. if seg.call == 0:
  574. seg.data = [0] * seg.length
  575. counter = seg.now() * ((seg.speed >> 4) + 1)
  576. for i in range(seg.length):
  577. # Create multiple layered waves
  578. wave1 = sin8(((i * 5) + (counter >> 5)) & 0xFF)
  579. wave2 = sin8(((i * 3) + (counter >> 3)) & 0xFF)
  580. wave3 = sin8(((i * 7) + (counter >> 6)) & 0xFF)
  581. # Combine waves
  582. brightness = (wave1 + wave2 + wave3) // 3
  583. # Blue-green ocean colors
  584. if brightness < 64:
  585. # Deep blue
  586. r, g, b = 0, 0, 60 + brightness
  587. elif brightness < 128:
  588. # Blue-cyan
  589. val = brightness - 64
  590. r, g, b = 0, val * 2, 100 + val
  591. else:
  592. # Cyan-white (foam)
  593. val = brightness - 128
  594. r, g, b = val, 128 + val, 180 + (val // 2)
  595. # Apply intensity
  596. brightness = 128 + ((brightness - 128) * seg.intensity // 255)
  597. color = (r << 16) | (g << 8) | b
  598. color = color_blend(0, color, brightness)
  599. seg.set_pixel_color(i, color)
  600. return FRAMETIME
  601. def mode_plasma(seg: Segment) -> int:
  602. """Plasma effect"""
  603. counter = seg.now() * ((seg.speed >> 3) + 1)
  604. for i in range(seg.length):
  605. # Create plasma using multiple sine waves
  606. phase1 = sin8(((i * 16) + (counter >> 5)) & 0xFF)
  607. phase2 = sin8(((i * 8) + (counter >> 6)) & 0xFF)
  608. phase3 = sin8((counter >> 4) & 0xFF)
  609. # Combine phases
  610. hue = (phase1 + phase2 + phase3) & 0xFF
  611. color = color_wheel(hue)
  612. # Intensity controls saturation
  613. if seg.intensity < 255:
  614. color = color_blend(color, WHITE, 255 - seg.intensity)
  615. seg.set_pixel_color(i, color)
  616. return FRAMETIME
  617. def mode_dissolve(seg: Segment) -> int:
  618. """Random pixel dissolve/fade"""
  619. if seg.call == 0:
  620. seg.data = [0] * seg.length
  621. for i in range(seg.length):
  622. seg.data[i] = random.randint(0, 255)
  623. cycle_time = 20 + (255 - seg.speed)
  624. iteration = seg.now() // cycle_time
  625. if iteration != seg.step:
  626. # Randomly update some pixels
  627. for i in range(seg.length):
  628. if random.randint(0, 255) < seg.intensity:
  629. seg.data[i] = random.randint(0, 255)
  630. seg.step = iteration
  631. for i in range(seg.length):
  632. brightness = seg.data[i]
  633. color = color_blend(seg.get_color(1),
  634. seg.color_from_palette(i),
  635. brightness)
  636. seg.set_pixel_color(i, color)
  637. return FRAMETIME
  638. def mode_glitter(seg: Segment) -> int:
  639. """Sparkle glitter overlay"""
  640. # Fill with base color
  641. for i in range(seg.length):
  642. seg.set_pixel_color(i, seg.color_from_palette(i))
  643. # Add random sparkles based on intensity
  644. sparkle_chance = seg.intensity
  645. num_sparkles = max(1, (seg.length * sparkle_chance) // 255)
  646. for _ in range(num_sparkles):
  647. if random.randint(0, 255) < seg.speed:
  648. pos = random.randint(0, seg.length - 1)
  649. seg.set_pixel_color(pos, WHITE)
  650. return FRAMETIME
  651. def mode_confetti(seg: Segment) -> int:
  652. """Random colored pixels"""
  653. seg.fade_out(224)
  654. cycle_time = 10 + (255 - seg.speed)
  655. iteration = seg.now() // cycle_time
  656. if iteration != seg.step:
  657. # Add random confetti
  658. density = 1 + (seg.intensity >> 5)
  659. for _ in range(density):
  660. pos = random.randint(0, seg.length - 1)
  661. hue = random.randint(0, 255)
  662. color = color_wheel(hue)
  663. seg.set_pixel_color(pos, color)
  664. seg.step = iteration
  665. return FRAMETIME
  666. def mode_sinelon(seg: Segment) -> int:
  667. """Sine wave colored dot"""
  668. seg.fade_out(224)
  669. counter = seg.now() * ((seg.speed >> 2) + 1)
  670. phase = (counter >> 8) & 0xFFFF
  671. # Calculate position using sine
  672. pos = (sin16(phase) + 32768) * seg.length // 65536
  673. pos = max(0, min(seg.length - 1, pos))
  674. # Color rotates
  675. hue = (counter >> 7) & 0xFF
  676. color = color_wheel(hue)
  677. # Intensity controls trail length (via fade)
  678. if seg.intensity < 255:
  679. color = color_blend(0, color, seg.intensity)
  680. seg.set_pixel_color(pos, color)
  681. return FRAMETIME
  682. def mode_candle(seg: Segment) -> int:
  683. """Flickering candle simulation"""
  684. if seg.call == 0:
  685. seg.data = [128] * seg.length
  686. # Random flicker for each pixel
  687. for i in range(seg.length):
  688. # Small random changes
  689. change = random.randint(-20, 20)
  690. seg.data[i] = max(30, min(255, seg.data[i] + change))
  691. # Speed affects flicker rate
  692. if seg.speed > 128 and random.randint(0, 255) < (seg.speed - 128):
  693. seg.data[i] = random.randint(50, 200)
  694. # Warm orange/yellow color
  695. brightness = seg.data[i]
  696. r = brightness
  697. g = (brightness * 2) // 3
  698. b = (brightness * seg.intensity) // 512 # Intensity controls blue
  699. color = (r << 16) | (g << 8) | b
  700. seg.set_pixel_color(i, color)
  701. return FRAMETIME
  702. def mode_aurora(seg: Segment) -> int:
  703. """Northern lights effect"""
  704. if seg.call == 0:
  705. seg.data = [0] * seg.length
  706. counter = seg.now() * ((seg.speed >> 4) + 1)
  707. for i in range(seg.length):
  708. # Multiple slow-moving waves
  709. wave1 = sin8(((i * 3) + (counter >> 6)) & 0xFF)
  710. wave2 = sin8(((i * 5) + (counter >> 7) + 85) & 0xFF)
  711. wave3 = sin8(((i * 2) + (counter >> 5) + 170) & 0xFF)
  712. # Aurora colors: green, blue, purple
  713. brightness = (wave1 + wave2 + wave3) // 3
  714. if brightness < 85:
  715. # Green
  716. r, g, b = 0, brightness * 2, brightness // 2
  717. elif brightness < 170:
  718. # Blue-green
  719. val = brightness - 85
  720. r, g, b = 0, 100 + val, 100 + val
  721. else:
  722. # Purple-pink
  723. val = brightness - 170
  724. r, g, b = val * 2, val, 150 + val
  725. # Apply intensity for brightness variation
  726. brightness_mod = 128 + ((brightness - 128) * seg.intensity // 255)
  727. color = (r << 16) | (g << 8) | b
  728. color = color_blend(0, color, brightness_mod)
  729. seg.set_pixel_color(i, color)
  730. return FRAMETIME
  731. def mode_rain(seg: Segment) -> int:
  732. """Rain drops falling"""
  733. if seg.call == 0:
  734. seg.data = [0] * seg.length
  735. seg.aux0 = 0
  736. seg.fade_out(235)
  737. cycle_time = 15 + (255 - seg.speed)
  738. iteration = seg.now() // cycle_time
  739. if iteration != seg.step:
  740. # New rain drops
  741. if random.randint(0, 255) < seg.intensity:
  742. pos = random.randint(0, min(5, seg.length - 1)) # Start near beginning
  743. seg.data[pos] = 255
  744. # Move drops down
  745. new_data = [0] * seg.length
  746. for i in range(seg.length - 1):
  747. if seg.data[i] > 0:
  748. # Drop moves forward
  749. if i + 1 < seg.length:
  750. new_data[i + 1] = seg.data[i] - 10
  751. seg.data = new_data
  752. seg.step = iteration
  753. # Draw rain drops (blue)
  754. for i in range(seg.length):
  755. if seg.data[i] > 0:
  756. brightness = seg.data[i]
  757. color = color_blend(0, seg.color_from_palette(i), brightness)
  758. seg.set_pixel_color(i, color)
  759. return FRAMETIME
  760. def mode_halloween(seg: Segment) -> int:
  761. """Halloween orange and purple"""
  762. counter = seg.now() * ((seg.speed >> 3) + 1)
  763. # Alternating orange and purple
  764. orange = (0xFF << 16) | (0x44 << 8) | 0x00
  765. purple = (0x88 << 16) | (0x00 << 8) | 0xFF
  766. for i in range(seg.length):
  767. # Create moving pattern
  768. phase = ((i * 255 // max(1, seg.length)) + (counter >> 7)) & 0xFF
  769. wave = sin8(phase)
  770. # Blend between orange and purple
  771. if wave < 128:
  772. color = orange
  773. else:
  774. color = purple
  775. # Intensity controls blending smoothness
  776. if seg.intensity > 0:
  777. blend_amt = (wave * seg.intensity) // 255
  778. color = color_blend(orange, purple, blend_amt)
  779. seg.set_pixel_color(i, color)
  780. return FRAMETIME
  781. def mode_noise(seg: Segment) -> int:
  782. """Perlin-like noise pattern"""
  783. if seg.call == 0:
  784. seg.data = [random.randint(0, 255) for _ in range(seg.length)]
  785. cycle_time = 20 + (255 - seg.speed) // 2
  786. iteration = seg.now() // cycle_time
  787. if iteration != seg.step:
  788. # Smooth noise by averaging neighbors
  789. new_data = seg.data.copy()
  790. for i in range(1, seg.length - 1):
  791. avg = (seg.data[i - 1] + seg.data[i] * 2 + seg.data[i + 1]) // 4
  792. variation = random.randint(-20, 20)
  793. new_data[i] = max(0, min(255, avg + variation))
  794. # Occasionally inject new random values
  795. if random.randint(0, 255) < seg.intensity:
  796. pos = random.randint(0, seg.length - 1)
  797. new_data[pos] = random.randint(0, 255)
  798. seg.data = new_data
  799. seg.step = iteration
  800. # Map noise to colors
  801. for i in range(seg.length):
  802. hue = seg.data[i]
  803. color = color_wheel(hue)
  804. seg.set_pixel_color(i, color)
  805. return FRAMETIME
  806. def mode_funky_plank(seg: Segment) -> int:
  807. """Multiple colored bars moving"""
  808. if seg.call == 0:
  809. seg.aux0 = 0
  810. cycle_time = 10 + (255 - seg.speed)
  811. iteration = seg.now() // cycle_time
  812. if iteration != seg.step:
  813. seg.aux0 = (seg.aux0 + 1) % seg.length
  814. seg.step = iteration
  815. num_planks = max(2, 1 + (seg.intensity >> 5))
  816. plank_size = max(1, seg.length // num_planks)
  817. for i in range(seg.length):
  818. plank_idx = ((i + seg.aux0) // plank_size) % num_planks
  819. hue = (plank_idx * 256 // num_planks) & 0xFF
  820. color = color_wheel(hue)
  821. seg.set_pixel_color(i, color)
  822. return FRAMETIME
  823. def mode_ball_tracking(seg: Segment) -> int:
  824. """
  825. Ball tracking effect - follows the ball bearing's position in real-time
  826. Reads position data from state.ball_tracking_manager
  827. """
  828. # Import state here to avoid circular dependency
  829. from modules.core.state import state
  830. import logging
  831. logger = logging.getLogger(__name__)
  832. # Get ball tracking manager
  833. manager = state.ball_tracking_manager
  834. if not manager or not manager._active:
  835. # No active tracking, show static color or turn off
  836. if seg.call % 100 == 0: # Log occasionally
  837. logger.debug(f"Ball tracking effect: manager active={manager._active if manager else 'N/A'}")
  838. seg.fill(0x000000)
  839. return FRAMETIME
  840. # Get tracking data from manager
  841. tracking_data = manager.get_tracking_data()
  842. if not tracking_data:
  843. # No position data yet
  844. if seg.call % 100 == 0: # Log occasionally
  845. logger.debug("Ball tracking effect: No tracking data available yet")
  846. seg.fill(0x000000)
  847. return FRAMETIME
  848. # Log tracking data occasionally
  849. if seg.call % 100 == 0:
  850. logger.info(f"Ball tracking effect: LED index={tracking_data['led_index']}, spread={tracking_data['spread']}, buffer_size={len(manager.position_buffer)}")
  851. center_led = tracking_data['led_index']
  852. spread = tracking_data['spread']
  853. brightness = tracking_data['brightness']
  854. color_rgb = tracking_data['color']
  855. # Convert RGB tuple to 32-bit color
  856. r, g, b = color_rgb
  857. color_32bit = (r << 16) | (g << 8) | b
  858. # Initialize tracking: store last lit LEDs in seg.data
  859. if seg.call == 0:
  860. seg.data = []
  861. # Calculate which LEDs should be lit this frame
  862. current_lit_leds = set()
  863. half_spread = spread // 2
  864. for i in range(-half_spread, half_spread + 1):
  865. led_index = (center_led + i) % seg.length
  866. current_lit_leds.add(led_index)
  867. # Convert last frame's lit LEDs from list to set
  868. last_lit_leds = set(seg.data) if seg.data else set()
  869. # Turn off LEDs that were on but shouldn't be anymore
  870. leds_to_turn_off = last_lit_leds - current_lit_leds
  871. for led_idx in leds_to_turn_off:
  872. seg.set_pixel_color(led_idx, 0x000000)
  873. # Update LEDs that should be on with brightness fade
  874. for i in range(-half_spread, half_spread + 1):
  875. led_index = (center_led + i) % seg.length
  876. # Calculate intensity fade from center
  877. if spread > 1:
  878. distance = abs(i)
  879. intensity = 1.0 - (distance / (spread / 2.0)) * 0.5 # 50-100%
  880. else:
  881. intensity = 1.0
  882. # Apply brightness
  883. final_brightness = int(brightness * intensity * 255)
  884. # Scale color by brightness
  885. final_r = (r * final_brightness) // 255
  886. final_g = (g * final_brightness) // 255
  887. final_b = (b * final_brightness) // 255
  888. final_color = (final_r << 16) | (final_g << 8) | final_b
  889. seg.set_pixel_color(led_index, final_color)
  890. # Remember which LEDs are lit for next frame
  891. seg.data = list(current_lit_leds)
  892. return FRAMETIME
  893. # Effect registry
  894. EFFECTS = {
  895. 0: ("Static", mode_static),
  896. 1: ("Blink", mode_blink),
  897. 2: ("Breathe", mode_breath),
  898. 3: ("Wipe", mode_color_wipe),
  899. 4: ("Fade", mode_fade),
  900. 5: ("Scan", mode_scan),
  901. 6: ("Dual Scan", mode_dual_scan),
  902. 7: ("Rainbow Cycle", mode_rainbow),
  903. 8: ("Rainbow", mode_rainbow_cycle),
  904. 9: ("Theater Chase", mode_theater_chase),
  905. 10: ("Running Lights", mode_running_lights),
  906. 11: ("Random Color", mode_random_color),
  907. 12: ("Dynamic", mode_dynamic),
  908. 13: ("Twinkle", mode_twinkle),
  909. 14: ("Sparkle", mode_sparkle),
  910. 15: ("Strobe", mode_strobe),
  911. 16: ("Fire", mode_fire),
  912. 17: ("Comet", mode_comet),
  913. 18: ("Chase", mode_chase),
  914. 19: ("Police", mode_police),
  915. 20: ("Lightning", mode_lightning),
  916. 21: ("Fireworks", mode_fireworks),
  917. 22: ("Ripple", mode_ripple),
  918. 23: ("Flow", mode_flow),
  919. 24: ("Colorloop", mode_colorloop),
  920. 25: ("Palette Flow", mode_palette_flow),
  921. 26: ("Gradient", mode_gradient),
  922. 27: ("Multi Strobe", mode_multi_strobe),
  923. 28: ("Waves", mode_waves),
  924. 29: ("BPM", mode_bpm),
  925. 30: ("Juggle", mode_juggle),
  926. 31: ("Meteor", mode_meteor),
  927. 32: ("Pride", mode_pride),
  928. 33: ("Pacifica", mode_pacifica),
  929. 34: ("Plasma", mode_plasma),
  930. 35: ("Dissolve", mode_dissolve),
  931. 36: ("Glitter", mode_glitter),
  932. 37: ("Confetti", mode_confetti),
  933. 38: ("Sinelon", mode_sinelon),
  934. 39: ("Candle", mode_candle),
  935. 40: ("Aurora", mode_aurora),
  936. 41: ("Rain", mode_rain),
  937. 42: ("Halloween", mode_halloween),
  938. 43: ("Noise", mode_noise),
  939. 44: ("Funky Plank", mode_funky_plank),
  940. 45: ("Ball Tracking", mode_ball_tracking),
  941. }
  942. def get_effect(effect_id: int):
  943. """Get effect function by ID"""
  944. if effect_id in EFFECTS:
  945. return EFFECTS[effect_id][1]
  946. return mode_static
  947. def get_effect_name(effect_id: int) -> str:
  948. """Get effect name by ID"""
  949. if effect_id in EFFECTS:
  950. return EFFECTS[effect_id][0]
  951. return "Unknown"
  952. def get_all_effects():
  953. """Get list of all effects"""
  954. return [(k, v[0]) for k, v in sorted(EFFECTS.items())]