|
| 1 | +# Bouncing Pixels |
| 2 | + |
| 3 | +author: [Jorin](https://github.com/jorins) |
| 4 | + |
| 5 | +date: 2025-04-12 |
| 6 | + |
| 7 | +labels: gates, random, simulation, triggers |
| 8 | + |
| 9 | +Pixels bounce around the display and trigger gates when hitting the edges. |
| 10 | +Inspired by the classic bouncing DVD logo! |
| 11 | + |
| 12 | +## Usage |
| 13 | + |
| 14 | +Just boot up the script and you're already going! You can use the knobs to |
| 15 | +adjust several parameters. The analogue input can be bound to any of them. |
| 16 | +By default, it affects the speed. The analogue input is summed with the knob |
| 17 | +value of its affected parameter, meaning that the knob sets a minimum value |
| 18 | +for the analogue input to add to. Note that since the EuroPi takes analogue |
| 19 | +inputs between 0V and 10V, AC signals must be biased properly or the bottom |
| 20 | +half of the input will be clipped. |
| 21 | + |
| 22 | +### Parameters |
| 23 | + |
| 24 | +* *Speed* affects how fast the simulation runs. It translates to how fast the |
| 25 | + pixels move. |
| 26 | +* *Width* controls how wide the playing field is. |
| 27 | +* *Impulse speed* controls how much speed is added to each pixel when an impulse |
| 28 | + occurs. |
| 29 | +* *Ball count* controls how many pixels are in play. |
| 30 | + |
| 31 | +### Impulses |
| 32 | + |
| 33 | +Impulses can be caused globally by pressing B2 or sending a signal to the |
| 34 | +digital input (assuming it has not been reconfigured). When an impulse occurs, |
| 35 | +velocity in a random direction. The speed added is the product of the impulse |
| 36 | +speed parameter and a random number (between 0.5 and 2.0 by default). |
| 37 | + |
| 38 | +### Deactivation |
| 39 | + |
| 40 | +One possible over and under speed behaviour is that pixels are deactivated. |
| 41 | +When deactivated, a pixel will not be processed or rendered until reset. Resets |
| 42 | +can be manually triggered by pressing B1 or using the digital input (must be |
| 43 | +configured). A reset is automatically triggered when all pixels in play are |
| 44 | +deactivated. |
| 45 | + |
| 46 | +## Outputs |
| 47 | + |
| 48 | +* CV1: collision with top edge (25ms) |
| 49 | +* CV2: collision with left edge (25ms) |
| 50 | +* CV3: collision with right edge (25ms) |
| 51 | +* CV4: collision with bottom edge (25ms) |
| 52 | +* CV5: collision with any edge (10ms) |
| 53 | +* CV6: collision with corner (100ms) |
| 54 | + |
| 55 | +Denoted gate hold lengths are defaults. They can be changed in the |
| 56 | +[configuration file](#Configuration). |
| 57 | + |
| 58 | +## Controls |
| 59 | + |
| 60 | +* K1: simulation speed |
| 61 | +* K2: width |
| 62 | +* Either button + K1: ball count |
| 63 | +* Either button + K2: impulse speed |
| 64 | +* B1 short press: reset |
| 65 | +* B2 short press: send impulse |
| 66 | +* Digital input: send impulse (customisable) |
| 67 | +* Analogue input: speed (customisable) |
| 68 | + |
| 69 | +Second layer knobs are "locking", which is to say you need to physically set the |
| 70 | +knob to a position close to the registered value in order for it to latch and |
| 71 | +start changing. This is to prevent abrupt changes in values when you press the |
| 72 | +buttons. If you lose track of a knob position, try sweeping the full range to |
| 73 | +get it to latch. |
| 74 | + |
| 75 | +The short press threshold is set to 500ms by default. You can adjust this |
| 76 | +length in the [configuration file](#Configuration). |
| 77 | + |
| 78 | +The digital input can be set to trigger a reset instead of an impulse. Likewise, |
| 79 | +the analogue input can be set to control any of the parameters controlled by |
| 80 | +knobs. These behaviours can be defined [configuration file](#Configuration). |
| 81 | + |
| 82 | +## Configuration |
| 83 | + |
| 84 | +The script can be thoroughly customised using a configuration file. The |
| 85 | +configuration file must be located at `/config/BouncingPixels.json` on the micro |
| 86 | +controller. |
| 87 | + |
| 88 | +### Sample configuration file |
| 89 | + |
| 90 | +```json |
| 91 | +{ |
| 92 | + "LONG_PRESS_LENGTH": 500.0, |
| 93 | + "TIMESCALE_MIN": 0.0, |
| 94 | + "TIMESCALE_MAX": 100.0, |
| 95 | + "KNOB_CHANGE_THRESHOLD": 0.01, |
| 96 | + "DIN_FUNCTION": "impulse", |
| 97 | + "AIN_FUNCTION": "speed", |
| 98 | + "GATE_HOLD_LENGTH_TOP": 25.0, |
| 99 | + "GATE_HOLD_LENGTH_LEFT": 25.0, |
| 100 | + "GATE_HOLD_LENGTH_RIGHT": 25.0, |
| 101 | + "GATE_HOLD_LENGTH_BOTTOM": 25.0, |
| 102 | + "GATE_HOLD_LENGTH_ANY": 10.0, |
| 103 | + "GATE_HOLD_LENGTH_CORNER": 100.0, |
| 104 | + "ARENA_WIDTH_MIN": 30.0, |
| 105 | + "ARENA_WIDTH_MAX": 1920.0, |
| 106 | + "BALL_COUNT_MAX": 100, |
| 107 | + "BALL_COUNT_MIN": 1, |
| 108 | + "CORNER_COLLISION_MARGIN": 15.0, |
| 109 | + "START_SPEED_MIN": 10.0, |
| 110 | + "START_SPEED_MAX": 100.0, |
| 111 | + "ACCEL_MIN": -5.0, |
| 112 | + "ACCEL_MAX": 5.0, |
| 113 | + "BOUNCINESS_MIN": 0.8, |
| 114 | + "BOUNCINESS_MAX": 1.2, |
| 115 | + "BOUNCE_ANGLE_DEVIATION_MAX": 15.0, |
| 116 | + "UNDER_SPEED_BEHAVIOUR": "reset", |
| 117 | + "OVER_SPEED_BEHAVIOUR": "reset", |
| 118 | + "UNDER_SPEED_THRESHOLD": 5.0, |
| 119 | + "OVER_SPEED_THRESHOLD": 1000000, |
| 120 | + "IMPULSE_SPEED_MIN": 0, |
| 121 | + "IMPULSE_SPEED_MAX": 100000, |
| 122 | + "IMPULSE_SPEED_VARIATION_MIN": 0.5, |
| 123 | + "IMPULSE_SPEED_VARIATION_MAX": 2.0, |
| 124 | +} |
| 125 | +``` |
| 126 | + |
| 127 | +### Configuration values |
| 128 | + |
| 129 | +| Key | Type | Possible values | Default value | Description | |
| 130 | +|-------------------------------|-----------------------|--------------------------------------------------------|---------------|-------------| |
| 131 | +| `POLL_FREQUENCY` | Floating point number | >= 5 | 30 | How frequently the application polls for new inputs, expressed as times per second.<br><br>⚠️ **Changing this value is not recommended.** | |
| 132 | +| `SAVE_PERIOD` | Floating point number | >= 0 | 5000 | How frequently the application state is saved at most, expressed as seconds between saves.<br><br>⚠️ **Changing this value is not recommended.** | |
| 133 | +| `RENDER_FREQUENCY` | Floating point number | >= 1 | 30 | How frequently the display display is updated at most, expressed as times per second.<br><br>⚠️ **Changing this value is not recommended.** | |
| 134 | +| `LONG_PRESS_LENGTH` | Floating point number | >= 0 | 500 | How many milliseconds a button must be pressed for it to be considered a long press. | |
| 135 | +| `TIMESCALE_MIN` | Floating point number | any | 0 | The speed at which the simulation runs when the speed parameter is set to minimum. | |
| 136 | +| `TIMESCALE_MAX` | Floating point number | any | 100 | The speed at which the simulation runs when the speed parameter is set to maximum. | |
| 137 | +| `KNOB_CHANGE_THRESHOLD` | Floating point number | 0.0 - 0.1 | 0.01 | How much a knob must change for its value to update. A higher value reduces jitter, but decreases sensitivity. | |
| 138 | +| `DIN_FUNCTION` | Choice | One of `impulse`, `reset` | `impulse` | The function to trigger when the digital input is raised. | |
| 139 | +| `AIN_FUNCTION` | Choice | One of `speed`, `impulse_speed`, `ball_count`, `width` | `speed` | The parameter that the analogue input modulates. | |
| 140 | +| `GATE_HOLD_LENGTH_TOP` | Floating point number | 1 - 10,000 | 25 | How long the gate for top edge collisions is held open, expressed in milliseconds. | |
| 141 | +| `GATE_HOLD_LENGTH_LEFT` | Floating point number | 1 - 10,000 | 25 | How long the gate for left edge collisions is held open, expressed in milliseconds. | |
| 142 | +| `GATE_HOLD_LENGTH_RIGHT` | Floating point number | 1 - 10,000 | 25 | How long the gate for right edge collisions is held open, expressed in milliseconds. | |
| 143 | +| `GATE_HOLD_LENGTH_BOTTOM` | Floating point number | 1 - 10,000 | 25 | How long the gate for bottom edge collisions is held open, expressed in milliseconds. | |
| 144 | +| `GATE_HOLD_LENGTH_ANY` | Floating point number | 1 - 10,000 | 10 | How long the gate for any edge collisions is held open, expressed in milliseconds. | |
| 145 | +| `GATE_HOLD_LENGTH_CORNER` | Floating point number | 1 - 10,000 | 100 | How long the gate for corner collisions is held open, expressed in milliseconds. | |
| 146 | +| `ARENA_WIDTH_MIN` | Floating point number | 30 - 1,920 | 30 | The smallest width the playing field can have. Fifteen units equal one pixel. | |
| 147 | +| `ARENA_WIDTH_MAX` | Floating point number | 30 - 1,920 | 1920 | The largest width the playing field can have. Fifteen units equal one pixel. | |
| 148 | +| `BALL_COUNT_MIN` | Integer number | 1 - 100 | 1 | The number of balls in play when the `ball_count` parameter is set to minimum. | |
| 149 | +| `BALL_COUNT_MAX` | Integer number | 1 - 100 | 100 | The number of balls in play when the `ball_count` parameter is set to maximum. | |
| 150 | +| `CORNER_COLLISION_MARGIN` | Floating point number | 0 - 240 | 15 | How large a portion of the side edges are considered corners. Fifteen units equal one pixel on the display. | |
| 151 | +| `START_SPEED_MIN` | Floating point number | >= 0 | 10 | The minimum speed a pixel may get upon reset, expressed as units per second. | |
| 152 | +| `START_SPEED_MAX` | Floating point number | >= 0 | 100 | The maximum speed a pixel may get upon reset, expressed as units per second. | |
| 153 | +| `ACCEL_MIN` | Floating point number | any | -5.0 | The minimum acceleration a pixel may get upon reset, expressed as units per second squared. | |
| 154 | +| `ACCEL_MAX` | Floating point number | any | 5.0 | The maximum acceleration a pixel may get upon reset, expressed as units per second squared. | |
| 155 | +| `BOUNCINESS_MIN` | Floating point number | >= 0.0001 | 0.8 | The minimum bounce speed multiplier a pixel may get upon reset. | |
| 156 | +| `BOUNCINESS_MAX` | Floating point number | >= 0.0001 | 1.2 | The maximum bounce speed multiplier a pixel may get upon reset. | |
| 157 | +| `BOUNCE_ANGLE_DEVIATION_MAX` | Floating point number | 0 - 180 | 15 | The maximum angle a pixel might deviate from its calculated direction upon bouncing, expressed in degrees. | |
| 158 | +| `UNDER_SPEED_BEHAVIOUR` | Choice | One of `impulse`, `reset`, `deactivate`, `noop` | `reset` | What a pixel should do when its speed goes below the under speed threshold. | |
| 159 | +| `OVER_SPEED_BEHAVIOUR` | Choice | One of `reset`, `deactivate` | `reset` | What a pixel should do when its speed goes above the over speed threshold. | |
| 160 | +| `UNDER_SPEED_THRESHOLD` | Floating point number | >= 0 | 5.0 | The speed threshold under which a pixel activates its under speed behaviour. | |
| 161 | +| `OVER_SPEED_THRESHOLD` | Floating point number | >= 0 | 1,000,000 | The speed threshold over which a pixel activates its over speed threshold. | |
| 162 | +| `IMPULSE_SPEED_MIN` | Floating point number | >= 0 | 0 | The speed added to pixels by an impulse when the `impulse` parameter is at minimum. | |
| 163 | +| `IMPULSE_SPEED_MAX` | Floating point number | >= 0 | 100,000 | The speed added to pixels by an impulse when the `impulse` parameter is at maximum. | |
| 164 | +| `IMPULSE_SPEED_VARIATION_MIN` | Floating point number | >= 0 | 0.5 | The smallest possible random multiplier for impulse speed. | |
| 165 | +| `IMPULSE_SPEED_VARIATION_MAX` | Floating point number | >= 0 | 2.0 | The largest possible random multiplier for impulse speed. | |
| 166 | + |
| 167 | +## Known issues & limitations |
| 168 | + |
| 169 | +* Gate lengths are not entirely precise. They're guaranteed to be at least the |
| 170 | + set length, but they may exceed it slightly on account of running on |
| 171 | + tick-based timers. |
| 172 | +* Corners aren't actually corners, they're just the top and bottom most |
| 173 | + parts of the side edges. This means that if a pixel is traveling parallel and |
| 174 | + close to the top or bottom edge, it's possible that it bounces away from the |
| 175 | + edge upon hitting the corner collision area and thus triggers a corner gate |
| 176 | + without hitting both a horizontal and a vertical edge. Another consequence is |
| 177 | + that a corner gate will always trigger at the same time as a side gate, |
| 178 | + regardless of whether the pixel collides with a horizontal or vertical edge |
| 179 | + first. |
| 180 | +* Collision handling cannot handle a pixel going fast enough to bounce with two |
| 181 | + opposing walls in one tick. If a pixel goes fast enough to do this, it |
| 182 | + triggers the over speed behaviour (i.e. by default, they will reset). |
| 183 | +* This script consumes a relatively large amount of memory. Max ball counts over |
| 184 | + 100 risk crashing the script. Ball counts over 20 consume enough memory to |
| 185 | + cause connection issues with Thonny. If you need to debug the script, you will |
| 186 | + probably want to lower the max ball count. |
| 187 | + |
| 188 | +## Further development |
| 189 | + |
| 190 | +If you fancy developing this script further, here are a few suggestions for what |
| 191 | +you can do: |
| 192 | + |
| 193 | +* Address any of the issues mentioned above. |
| 194 | +* Implement gravity! You can add another set of knob banks (let the buttons |
| 195 | + trigger different banks) that control gravity magnitude and direction. |
| 196 | + The tricky part of implementing gravity comes from the fact that a pixel will |
| 197 | + have zero speed at the top of its arc when bouncing in place, and triggers the |
| 198 | + under speed behaviour. |
| 199 | +* Allow more in-app configuration instead of relying on the configuration file. |
| 200 | +* Implement visual feedback on the locked knob inputs. This would make the use |
| 201 | + of the second layer knobs easier. |
0 commit comments