diff --git a/src/index.ts b/src/index.ts
index 0b1d607..3fe2e80 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -20,3 +20,4 @@ export { DHT22Element as Dht22Element } from './dht22-element';
export { ArduinoMegaElement } from './arduino-mega-element';
export { ArduinoNanoElement } from './arduino-nano-element';
export { Ds1307Element } from './ds1307-element';
+export { LEDRingElement } from './led-ring-element';
diff --git a/src/led-ring-element.stories.ts b/src/led-ring-element.stories.ts
new file mode 100644
index 0000000..c021351
--- /dev/null
+++ b/src/led-ring-element.stories.ts
@@ -0,0 +1,40 @@
+import { html } from 'lit-html';
+import './led-ring-element';
+
+export default {
+ title: 'LED Ring',
+ component: 'wokwi-led-ring',
+ argTypes: {
+ animation: { control: 'boolean' },
+ pixels: { control: { type: 'number', min: 1, max: 64, step: 1 } },
+ pixelSpacing: { control: { type: 'range', min: 0, max: 10, step: 0.1 } },
+ background: { control: { type: 'color' } },
+ pinInfo: { control: { type: null } },
+ },
+ args: {
+ background: '#363',
+ pixels: 16,
+ pixelSpacing: 0,
+ animation: true,
+ },
+};
+
+const Template = ({ animation, background, pixels, pixelSpacing }) =>
+ html``;
+
+export const Ring8 = Template.bind({});
+Ring8.args = { pixels: 8 };
+
+export const Ring12 = Template.bind({});
+Ring12.args = { pixels: 12 };
+
+export const Ring16 = Template.bind({});
+Ring16.args = { pixels: 16 };
+
+export const Ring24 = Template.bind({});
+Ring24.args = { pixels: 24 };
diff --git a/src/led-ring-element.ts b/src/led-ring-element.ts
new file mode 100644
index 0000000..9742973
--- /dev/null
+++ b/src/led-ring-element.ts
@@ -0,0 +1,152 @@
+import { customElement, html, LitElement, property, queryAll, svg } from 'lit-element';
+import { ElementPin } from './pin';
+import { RGB } from './types/rgb';
+
+const pinHeight = 3;
+const pcbWidth = 6;
+
+@customElement('wokwi-led-ring')
+export class LEDRingElement extends LitElement {
+ /**
+ * Number of pixels to in the LED ring
+ */
+ @property() pixels = 16;
+
+ /**
+ * Space between pixels (in mm)
+ */
+ @property({ type: Number }) pixelSpacing = 0;
+
+ /**
+ * Background (PCB) color
+ */
+ @property() background = '#363';
+
+ /**
+ * Animate the LEDs in the matrix. Used primarily for testing in Storybook.
+ * The animation sequence is not guaranteed and may change in future releases of
+ * this element.
+ */
+ @property() animation = false;
+
+ @queryAll('.pixel') pixelElements: SVGCircleElement[] = [];
+
+ private animationFrame: number | null = null;
+
+ get radius() {
+ return ((this.pixelSpacing + 5) * this.pixels) / 2 / Math.PI + pcbWidth;
+ }
+
+ get pinInfo(): ElementPin[] {
+ const { radius } = this;
+ const mmToPix = 3.78;
+ const pinSpacing = 2.54;
+ const y = (radius * 2 + pinHeight) * mmToPix;
+ const cx = radius * mmToPix;
+ const p = pinSpacing * mmToPix;
+
+ return [
+ {
+ name: 'GND',
+ x: cx - 1.5 * p,
+ y,
+ signals: [{ type: 'power', signal: 'GND' }],
+ },
+ { name: 'VCC', x: cx - 0.5 * p, y, signals: [{ type: 'power', signal: 'VCC' }] },
+ { name: 'DIN', x: cx + 0.5 * p, y, signals: [] },
+ { name: 'DOUT', x: cx + 1.5 * p, y, signals: [] },
+ ];
+ }
+
+ setPixel(pixel: number, { r, g, b }: RGB) {
+ const { pixelElements } = this;
+ if (pixel < 0 || pixel >= pixelElements.length) {
+ return;
+ }
+ pixelElements[pixel].style.fill = `rgb(${r * 255},${g * 255},${b * 255})`;
+ }
+
+ /**
+ * Resets all the pixels to off state (r=0, g=0, b=0).
+ */
+ reset() {
+ for (const element of this.pixelElements) {
+ element.style.fill = '';
+ }
+ }
+
+ private animateStep = () => {
+ const time = new Date().getTime();
+ const { pixels } = this;
+ const pixelValue = (n: number) => (n % 2000 > 1000 ? 1 - (n % 1000) / 1000 : (n % 1000) / 1000);
+ for (let pixel = 0; pixel < pixels; pixel++) {
+ this.setPixel(pixel, {
+ r: pixelValue(pixel * 100 + time),
+ g: pixelValue(pixel * 100 + time + 200),
+ b: pixelValue(pixel * 100 + time + 400),
+ });
+ }
+ this.animationFrame = requestAnimationFrame(this.animateStep);
+ };
+
+ updated() {
+ if (this.animation && !this.animationFrame) {
+ this.animationFrame = requestAnimationFrame(this.animateStep);
+ } else if (!this.animation && this.animationFrame) {
+ cancelAnimationFrame(this.animationFrame);
+ this.animationFrame = null;
+ }
+ }
+
+ render() {
+ const { pixels, radius, background } = this;
+ const pixelElements = [];
+ const width = radius * 2;
+ const height = radius * 2 + pinHeight;
+ for (let i = 0; i < pixels; i++) {
+ const angle = (i / pixels) * 360;
+ pixelElements.push(
+ svg``
+ );
+ }
+ return html`
+
+ `;
+ }
+}
diff --git a/src/neopixel-matrix-element.ts b/src/neopixel-matrix-element.ts
index 6bff7d3..460ab2b 100644
--- a/src/neopixel-matrix-element.ts
+++ b/src/neopixel-matrix-element.ts
@@ -1,15 +1,10 @@
import { css, customElement, html, LitElement, property, svg } from 'lit-element';
import { ElementPin, GND, VCC } from './pin';
+import { RGB } from './types/rgb';
const pixelWidth = 5.66;
const pixelHeight = 5;
-export interface RGB {
- r: number;
- g: number;
- b: number;
-}
-
/**
* Renders a matrix of NeoPixels (smart RGB LEDs).
* Optimized for displaying large matrices (up to thousands of elements).
diff --git a/src/react-types.ts b/src/react-types.ts
index 18b4668..1757990 100644
--- a/src/react-types.ts
+++ b/src/react-types.ts
@@ -19,6 +19,7 @@ import { DHT22Element } from './dht22-element';
import { ArduinoMegaElement } from './arduino-mega-element';
import { ArduinoNanoElement } from './arduino-nano-element';
import { Ds1307Element } from './ds1307-element';
+import { LEDRingElement } from './led-ring-element';
declare global {
namespace JSX {
@@ -41,6 +42,7 @@ declare global {
'wokwi-arduino-mega': Partial;
'wokwi-arduino-nano': Partial;
'wokwi-ds1307': Partial;
+ 'wokwi-neopixel-ring': Partial;
}
}
}
diff --git a/src/types/rgb.ts b/src/types/rgb.ts
new file mode 100644
index 0000000..3a01d8f
--- /dev/null
+++ b/src/types/rgb.ts
@@ -0,0 +1,5 @@
+export interface RGB {
+ r: number;
+ g: number;
+ b: number;
+}