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` + + + + + + + + + ${pixelElements} + + `; + } +} 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; +}