Skip to content

Commit 0caa429

Browse files
committed
feat(neopixel-ring): add neopixel ring element
customizable number of pixels, background color and pixel spacing
1 parent 6d0cb25 commit 0caa429

File tree

6 files changed

+201
-6
lines changed

6 files changed

+201
-6
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export { ServoElement } from './servo-element';
1919
export { DHT22Element as Dht22Element } from './dht22-element';
2020
export { ArduinoMegaElement } from './arduino-mega-element';
2121
export { ArduinoNanoElement } from './arduino-nano-element';
22+
export { NeopixelRingElement } from './neopixel-ring-element';

src/neopixel-matrix-element.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import { css, customElement, html, LitElement, property, svg } from 'lit-element';
2+
import { RGB } from './types/rgb';
23

34
const pixelWidth = 5.66;
45
const pixelHeight = 5;
56

6-
export interface RGB {
7-
r: number;
8-
g: number;
9-
b: number;
10-
}
11-
127
/**
138
* Renders a matrix of NeoPixels (smart RGB LEDs).
149
* Optimized for displaying large matrices (up to thousands of elements).

src/neopixel-ring-element.stories.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { html } from 'lit-html';
2+
import './neopixel-ring-element';
3+
4+
export default {
5+
title: 'NeoPixel Ring',
6+
component: 'wokwi-neopixel-ring',
7+
argTypes: {
8+
animation: { control: 'boolean' },
9+
pixels: { control: { type: 'number', min: 1, max: 64, step: 1 } },
10+
pixelSpacing: { control: { type: 'range', min: 0, max: 10, step: 0.1 } },
11+
background: { control: { type: 'color' } },
12+
pinInfo: { control: { type: null } },
13+
},
14+
args: {
15+
background: '#363',
16+
pixels: 16,
17+
pixelSpacing: 0,
18+
animation: true,
19+
},
20+
};
21+
22+
const Template = ({ animation, background, pixels, pixelSpacing }) =>
23+
html`<wokwi-neopixel-ring
24+
.animation=${animation}
25+
background=${background}
26+
pixels=${pixels}
27+
pixelSpacing=${pixelSpacing}
28+
></wokwi-neopixel-ring>`;
29+
30+
export const Ring8 = Template.bind({});
31+
Ring8.args = { pixels: 8 };
32+
33+
export const Ring12 = Template.bind({});
34+
Ring12.args = { pixels: 12 };
35+
36+
export const Ring16 = Template.bind({});
37+
Ring16.args = { pixels: 16 };
38+
39+
export const Ring24 = Template.bind({});
40+
Ring24.args = { pixels: 24 };

src/neopixel-ring-element.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { customElement, html, LitElement, property, queryAll, svg } from 'lit-element';
2+
import { ElementPin } from './pin';
3+
import { RGB } from './types/rgb';
4+
5+
const pinHeight = 3;
6+
const pcbWidth = 6;
7+
8+
@customElement('wokwi-neopixel-ring')
9+
export class NeopixelRingElement extends LitElement {
10+
/**
11+
* Number of pixels to in the NeoPixel ring
12+
*/
13+
@property() pixels = 16;
14+
15+
/**
16+
* Space between pixels (in mm)
17+
*/
18+
@property({ type: Number }) pixelSpacing = 0;
19+
20+
/**
21+
* Background (PCB) color
22+
*/
23+
@property() background = '#363';
24+
25+
/**
26+
* Animate the LEDs in the matrix. Used primarily for testing in Storybook.
27+
* The animation sequence is not guaranteed and may change in future releases of
28+
* this element.
29+
*/
30+
@property() animation = false;
31+
32+
@queryAll('.pixel') pixelElements: SVGCircleElement[] = [];
33+
34+
private animationFrame: number | null = null;
35+
36+
get radius() {
37+
return ((this.pixelSpacing + 5) * this.pixels) / 2 / Math.PI + pcbWidth;
38+
}
39+
40+
get pinInfo(): ElementPin[] {
41+
const { radius } = this;
42+
const mmToPix = 3.78;
43+
const pinSpacing = 2.54;
44+
const y = (radius * 2 + pinHeight) * mmToPix;
45+
const cx = radius * mmToPix;
46+
const p = pinSpacing * mmToPix;
47+
48+
return [
49+
{
50+
name: 'GND',
51+
x: cx - 1.5 * p,
52+
y,
53+
signals: [{ type: 'power', signal: 'GND' }],
54+
},
55+
{ name: 'VCC', x: cx - 0.5 * p, y, signals: [{ type: 'power', signal: 'VCC' }] },
56+
{ name: 'DIN', x: cx + 0.5 * p, y, signals: [] },
57+
{ name: 'DOUT', x: cx + 1.5 * p, y, signals: [] },
58+
];
59+
}
60+
61+
setPixel(pixel: number, { r, g, b }: RGB) {
62+
const { pixelElements } = this;
63+
if (pixel < 0 || pixel >= pixelElements.length) {
64+
return;
65+
}
66+
pixelElements[pixel].style.fill = `rgb(${r * 255},${g * 255},${b * 255})`;
67+
}
68+
69+
/**
70+
* Resets all the pixels to off state (r=0, g=0, b=0).
71+
*/
72+
reset() {
73+
for (const element of this.pixelElements) {
74+
element.style.fill = '';
75+
}
76+
}
77+
78+
private animateStep = () => {
79+
const time = new Date().getTime();
80+
const { pixels } = this;
81+
const pixelValue = (n: number) => (n % 2000 > 1000 ? 1 - (n % 1000) / 1000 : (n % 1000) / 1000);
82+
for (let pixel = 0; pixel < pixels; pixel++) {
83+
this.setPixel(pixel, {
84+
r: pixelValue(pixel * 100 + time),
85+
g: pixelValue(pixel * 100 + time + 200),
86+
b: pixelValue(pixel * 100 + time + 400),
87+
});
88+
}
89+
this.animationFrame = requestAnimationFrame(this.animateStep);
90+
};
91+
92+
updated() {
93+
if (this.animation && !this.animationFrame) {
94+
this.animationFrame = requestAnimationFrame(this.animateStep);
95+
} else if (!this.animation && this.animationFrame) {
96+
cancelAnimationFrame(this.animationFrame);
97+
this.animationFrame = null;
98+
}
99+
}
100+
101+
render() {
102+
const { pixels, radius, background } = this;
103+
const pixelElements = [];
104+
const width = radius * 2;
105+
const height = radius * 2 + pinHeight;
106+
for (let i = 0; i < pixels; i++) {
107+
const angle = (i / pixels) * 360;
108+
pixelElements.push(
109+
svg`<rect
110+
class="pixel"
111+
x="${radius - 2.5}"
112+
y="${pcbWidth / 2 - 2.5}"
113+
width="5"
114+
height="5"
115+
fill="white"
116+
stroke="black"
117+
stroke-width="0.25"
118+
transform="rotate(${angle} ${radius} ${radius})"/>`
119+
);
120+
}
121+
return html`
122+
<svg
123+
width="${width}mm"
124+
height="${height}mm"
125+
version="1.1"
126+
viewBox="0 0 ${width} ${height}"
127+
xmlns="http://www.w3.org/2000/svg"
128+
>
129+
<defs>
130+
<pattern id="pin-pattern" height="2" width="2.54" patternUnits="userSpaceOnUse">
131+
<rect x="1.02" y="0" height="2" width="0.5" fill="#aaa" />
132+
</pattern>
133+
</defs>
134+
<rect
135+
fill="url(#pin-pattern)"
136+
height="${pinHeight + 1}"
137+
width=${4 * 2.54}
138+
transform="translate(${radius - (4 * 2.54) / 2}, ${radius * 2 - 1})"
139+
/>
140+
<circle
141+
cx="${radius}"
142+
cy="${radius}"
143+
r="${radius - pcbWidth / 2}"
144+
fill="transparent"
145+
stroke-width="${pcbWidth}"
146+
stroke="${background}"
147+
/>
148+
${pixelElements}
149+
</svg>
150+
`;
151+
}
152+
}

src/react-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ServoElement } from './servo-element';
1818
import { DHT22Element } from './dht22-element';
1919
import { ArduinoMegaElement } from './arduino-mega-element';
2020
import { ArduinoNanoElement } from './arduino-nano-element';
21+
import { NeopixelRingElement } from './neopixel-ring-element';
2122

2223
declare global {
2324
namespace JSX {
@@ -39,6 +40,7 @@ declare global {
3940
'wokwi-dht22': Partial<DHT22Element>;
4041
'wokwi-arduino-mega': Partial<ArduinoMegaElement>;
4142
'wokwi-arduino-nano': Partial<ArduinoNanoElement>;
43+
'wokwi-neopixel-ring': Partial<NeopixelRingElement>;
4244
}
4345
}
4446
}

src/types/rgb.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface RGB {
2+
r: number;
3+
g: number;
4+
b: number;
5+
}

0 commit comments

Comments
 (0)