Skip to content

Commit 2764258

Browse files
committed
testing gif to anim generator
1 parent 3b2d4ba commit 2764258

File tree

7 files changed

+1506
-0
lines changed

7 files changed

+1506
-0
lines changed
141 KB
Loading

src/main/java/org/steelhawks/RobotContainer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ public RobotContainer() {
261261

262262
// s_LEDMatrix.rainbowWaveCommand(3).schedule();
263263
s_LEDMatrix.fireCommand(50, 120).schedule();
264+
// s_LEDMatrix.pacmanCommand().schedule();
264265
// s_LEDMatrix.bouncingBallCommand(LEDMatrix.Color.GREEN, 3).schedule();
265266
// s_LEDMatrix.scrollingTextCommand("STEEL HAWKS", LEDMatrix.Color.RED, 2).schedule();
266267
// s_LEDMatrix.plasmaCommand(3).schedule();

src/main/java/org/steelhawks/subsystems/led/LEDMatrix.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import edu.wpi.first.wpilibj.AddressableLEDBuffer;
55
import edu.wpi.first.wpilibj2.command.*;
66
import org.steelhawks.Constants.LEDConstants;
7+
import org.steelhawks.subsystems.led.anim.FrameAnimation;
8+
import org.steelhawks.subsystems.led.anim.PacMan;
79

810
/**
911
* LED Matrix subsystem for controlling 2D LED arrays with animations
@@ -770,6 +772,16 @@ public Command bouncingBallCommand(Color color, int size) {
770772
.ignoringDisable(true);
771773
}
772774

775+
public Command pacmanCommand() {
776+
return Commands.runOnce(() -> playAnimation(new PacMan(width, height)), this)
777+
.ignoringDisable(true);
778+
}
779+
780+
public Command frameAnimationCommand(FrameAnimation animation) {
781+
return Commands.runOnce(() -> playAnimation(animation), this)
782+
.ignoringDisable(true);
783+
}
784+
773785
public Command clearCommand() {
774786
return Commands.runOnce(() -> {
775787
clear();

src/main/java/org/steelhawks/subsystems/led/anim/AnimationLibrary.java

Lines changed: 879 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package org.steelhawks.subsystems.led.anim;
2+
3+
import org.steelhawks.subsystems.led.LEDMatrix;
4+
5+
/**
6+
* LIGHTWEIGHT Frame-Based Animation for LED Matrix
7+
* Pre-bake your animations as frame arrays - no GIF decoding at runtime!
8+
*/
9+
public class FrameAnimation extends LEDMatrix.Animation {
10+
11+
private final int[][][] frames; // [frame][x][y] = RGB packed int
12+
private final int[] frameDelays; // delay in periodic cycles
13+
private int currentFrameIndex = 0;
14+
private int frameDelayCounter = 0;
15+
private final boolean loop;
16+
private boolean finished = false;
17+
18+
/**
19+
* Create animation from pre-computed frame data
20+
* @param frames Array of frames, each frame is [x][y] RGB packed as int (0xRRGGBB)
21+
* @param frameDelays Delay for each frame in 20ms cycles (1 = 20ms, 2 = 40ms, etc.)
22+
* @param loop Whether to loop
23+
*/
24+
public FrameAnimation(int[][][] frames, int[] frameDelays, boolean loop) {
25+
this.frames = frames;
26+
this.frameDelays = frameDelays;
27+
this.loop = loop;
28+
}
29+
30+
/**
31+
* Create animation with uniform frame delay
32+
*/
33+
public FrameAnimation(int[][][] frames, int frameDelay, boolean loop) {
34+
this.frames = frames;
35+
this.frameDelays = new int[frames.length];
36+
for (int i = 0; i < frames.length; i++) {
37+
this.frameDelays[i] = frameDelay;
38+
}
39+
this.loop = loop;
40+
}
41+
42+
@Override
43+
public void render(LEDMatrix matrix) {
44+
if (finished || frames.length == 0) return;
45+
46+
int[][] currentFrame = frames[currentFrameIndex];
47+
48+
// Draw current frame - optimized
49+
for (int x = 0; x < Math.min(currentFrame.length, matrix.getWidth()); x++) {
50+
for (int y = 0; y < Math.min(currentFrame[x].length, matrix.getHeight()); y++) {
51+
int rgb = currentFrame[x][y];
52+
int r = (rgb >> 16) & 0xFF;
53+
int g = (rgb >> 8) & 0xFF;
54+
int b = rgb & 0xFF;
55+
matrix.setPixel(x, y, new LEDMatrix.Color(r, g, b));
56+
}
57+
}
58+
59+
// Handle frame timing
60+
frameDelayCounter++;
61+
if (frameDelayCounter >= frameDelays[currentFrameIndex]) {
62+
frameDelayCounter = 0;
63+
currentFrameIndex++;
64+
65+
if (currentFrameIndex >= frames.length) {
66+
if (loop) {
67+
currentFrameIndex = 0;
68+
} else {
69+
finished = true;
70+
currentFrameIndex = frames.length - 1;
71+
}
72+
}
73+
}
74+
}
75+
76+
@Override
77+
public void reset() {
78+
super.reset();
79+
currentFrameIndex = 0;
80+
frameDelayCounter = 0;
81+
finished = false;
82+
}
83+
}
84+
85+
// ==================== USAGE GUIDE ====================
86+
87+
/*
88+
HOW TO CREATE CUSTOM ANIMATIONS:
89+
90+
Method 1: Hand-code simple animations (shown above)
91+
-------------------------------------------------
92+
public static FrameAnimation myAnimation() {
93+
int[][][] frames = new int[numFrames][width][height];
94+
95+
// Fill each frame with RGB values
96+
frames[0][x][y] = 0xFF0000; // Red pixel at (x,y) in frame 0
97+
frames[1][x][y] = 0x00FF00; // Green pixel
98+
99+
return new FrameAnimation(frames, frameDelay, loop);
100+
}
101+
102+
103+
Method 2: Use Python script to convert GIF → Java array
104+
--------------------------------------------------------
105+
Create a Python script to pre-process GIFs into frame arrays:
106+
107+
```python
108+
from PIL import Image
109+
import os
110+
111+
def gif_to_java_frames(gif_path, output_name):
112+
img = Image.open(gif_path)
113+
frames = []
114+
115+
try:
116+
while True:
117+
# Resize to target size
118+
resized = img.resize((32, 8), Image.NEAREST)
119+
rgb_img = resized.convert('RGB')
120+
121+
# Extract pixels
122+
frame_data = []
123+
for x in range(32):
124+
col = []
125+
for y in range(8):
126+
r, g, b = rgb_img.getpixel((x, y))
127+
rgb_int = (r << 16) | (g << 8) | b
128+
col.append(f"0x{rgb_int:06X}")
129+
frame_data.append(col)
130+
frames.append(frame_data)
131+
132+
img.seek(img.tell() + 1)
133+
except EOFError:
134+
pass
135+
136+
# Generate Java code
137+
java_code = f"public static FrameAnimation {output_name}() {{\n"
138+
java_code += f" int[][][] frames = new int[{len(frames)}][32][8];\n\n"
139+
140+
for i, frame in enumerate(frames):
141+
java_code += f" // Frame {i}\n"
142+
for x, col in enumerate(frame):
143+
java_code += f" frames[{i}][{x}] = new int[]{{ {', '.join(col)} }};\n"
144+
java_code += "\n"
145+
146+
java_code += " return new FrameAnimation(frames, 2, true);\n"
147+
java_code += "}\n"
148+
149+
return java_code
150+
151+
# Usage:
152+
print(gif_to_java_frames("fire.gif", "fireAnimation"))
153+
```
154+
155+
Then copy-paste the output into AnimationLibrary.java
156+
157+
158+
Method 3: Load from CSV at robot init (one-time load)
159+
------------------------------------------------------
160+
Create CSV files with frame data, load once at startup:
161+
162+
frames.csv format:
163+
frame,x,y,r,g,b
164+
0,0,0,255,0,0
165+
0,0,1,255,0,0
166+
...
167+
168+
Then load in robot init (NOT during match):
169+
```java
170+
public static FrameAnimation loadFromCSV(String path) {
171+
// Parse CSV and build frame array
172+
// This only runs ONCE at robot startup
173+
}
174+
```
175+
176+
177+
USAGE IN CODE:
178+
--------------
179+
private void configureBindings() {
180+
// Pre-made animations (zero overhead)
181+
driver.a().onTrue(leds.frameAnimationCommand(
182+
AnimationLibrary.simpleBlinkRed8x8()
183+
));
184+
185+
driver.b().onTrue(leds.frameAnimationCommand(
186+
AnimationLibrary.scanningLine32x8()
187+
));
188+
}
189+
190+
191+
PERFORMANCE:
192+
-----------
193+
✅ Pre-computed frames: Fast
194+
✅ Simple array lookups: Minimal CPU
195+
✅ No file I/O during match: Safe
196+
✅ Memory usage: Reasonable (a 32x8x30-frame animation ≈ 7.6KB)
197+
198+
MEMORY CALCULATION:
199+
------------------
200+
Memory per animation = width × height × frames × 4 bytes
201+
32×8×30 = 7,680 pixels × 4 bytes = ~31KB
202+
203+
Keep total animations under 500KB to be safe on roboRIO.
204+
*/
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package org.steelhawks.subsystems.led.anim;
2+
3+
import org.steelhawks.subsystems.led.LEDMatrix;
4+
5+
/**
6+
* Pac-Man Animation for LED Matrix
7+
*/
8+
public class PacMan extends LEDMatrix.Animation {
9+
10+
private final int width;
11+
private final int height;
12+
private int pacmanX = 0;
13+
private int ghostX = 0;
14+
private boolean mouthOpen = true;
15+
private int mouthCounter = 0;
16+
17+
public PacMan(int width, int height) {
18+
this.width = width;
19+
this.height = height;
20+
this.pacmanX = -3;
21+
this.ghostX = -8;
22+
}
23+
24+
@Override
25+
public void render(LEDMatrix matrix) {
26+
matrix.clear();
27+
28+
int centerY = height / 2;
29+
30+
// Toggle mouth every 3 frames
31+
if (frameCount % 3 == 0) {
32+
mouthOpen = !mouthOpen;
33+
}
34+
35+
// Draw dots for Pac-Man to eat
36+
for (int x = 0; x < width; x += 4) {
37+
if (x > pacmanX + 3 && x >= 0 && x < width) { // Only show dots ahead of Pac-Man
38+
matrix.setPixel(x, centerY, LEDMatrix.Color.WHITE);
39+
}
40+
}
41+
42+
// Draw Ghost first (so Pac-Man appears on top)
43+
drawGhost(matrix, ghostX, centerY);
44+
45+
// Draw Pac-Man
46+
drawPacMan(matrix, pacmanX, centerY, mouthOpen);
47+
48+
// Move Pac-Man forward every 2 frames
49+
if (frameCount % 2 == 0) {
50+
pacmanX++;
51+
if (pacmanX > width + 3) {
52+
pacmanX = -3;
53+
}
54+
}
55+
56+
// Move Ghost slower (every 3 frames), always behind Pac-Man
57+
if (frameCount % 3 == 0) {
58+
ghostX++;
59+
if (ghostX > width + 3) {
60+
ghostX = -8; // Start further back
61+
}
62+
}
63+
}
64+
65+
private void drawPacMan(LEDMatrix matrix, int x, int y, boolean mouthOpen) {
66+
LEDMatrix.Color yellow = new LEDMatrix.Color(255, 255, 0);
67+
68+
// Bounds check helper
69+
if (x < 0 || x >= width) return;
70+
71+
if (height >= 16) {
72+
// Large Pac-Man for 16x16 (3x3 sprite)
73+
for (int dy = -1; dy <= 1; dy++) {
74+
for (int dx = -1; dx <= 1; dx++) {
75+
if (mouthOpen && dy == 0 && dx == 1) {
76+
continue; // Mouth open (skip right-center pixel)
77+
}
78+
int px = x + dx;
79+
int py = y + dy;
80+
if (px >= 0 && px < width && py >= 0 && py < height) {
81+
matrix.setPixel(px, py, yellow);
82+
}
83+
}
84+
}
85+
} else {
86+
// Small Pac-Man for 32x8 (2x2 sprite)
87+
int[][] pixels = mouthOpen
88+
? new int[][]{{0, 0}, {0, -1}, {-1, 0}}
89+
: new int[][]{{0, 0}, {0, -1}, {-1, 0}, {1, 0}};
90+
91+
for (int[] pixel : pixels) {
92+
int px = x + pixel[0];
93+
int py = y + pixel[1];
94+
if (px >= 0 && px < width && py >= 0 && py < height) {
95+
matrix.setPixel(px, py, yellow);
96+
}
97+
}
98+
}
99+
}
100+
101+
private void drawGhost(LEDMatrix matrix, int x, int y) {
102+
LEDMatrix.Color cyan = new LEDMatrix.Color(0, 255, 255);
103+
LEDMatrix.Color white = new LEDMatrix.Color(255, 255, 255);
104+
105+
// Bounds check
106+
if (x < -2 || x >= width + 2) return;
107+
108+
if (height >= 16) {
109+
// Large ghost for 16x16 (3x3 sprite)
110+
for (int dy = -1; dy <= 1; dy++) {
111+
for (int dx = -1; dx <= 1; dx++) {
112+
int gx = x + dx;
113+
int gy = y + dy;
114+
if (gx >= 0 && gx < width && gy >= 0 && gy < height) {
115+
matrix.setPixel(gx, gy, cyan);
116+
}
117+
}
118+
}
119+
// Eyes
120+
if (x - 1 >= 0 && x - 1 < width && y - 1 >= 0 && y - 1 < height) {
121+
matrix.setPixel(x - 1, y - 1, white);
122+
}
123+
if (x + 1 >= 0 && x + 1 < width && y - 1 >= 0 && y - 1 < height) {
124+
matrix.setPixel(x + 1, y - 1, white);
125+
}
126+
} else {
127+
// Small ghost for 32x8 (2x2 sprite)
128+
int[][] pixels = {{0, 0}, {0, -1}, {-1, 0}, {1, 0}};
129+
130+
for (int[] pixel : pixels) {
131+
int gx = x + pixel[0];
132+
int gy = y + pixel[1];
133+
if (gx >= 0 && gx < width && gy >= 0 && gy < height) {
134+
matrix.setPixel(gx, gy, cyan);
135+
}
136+
}
137+
// Eye
138+
if (x >= 0 && x < width && y - 1 >= 0 && y - 1 < height) {
139+
matrix.setPixel(x, y - 1, white);
140+
}
141+
}
142+
}
143+
144+
@Override
145+
public void reset() {
146+
super.reset();
147+
pacmanX = -3;
148+
ghostX = -8;
149+
mouthOpen = true;
150+
}
151+
}

0 commit comments

Comments
 (0)