|  | 
|  | 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-led-ring') | 
|  | 9 | +export class LEDRingElement extends LitElement { | 
|  | 10 | +  /** | 
|  | 11 | +   * Number of pixels to in the LED 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 | +} | 
0 commit comments