diff --git a/libs/core/rgb.cpp b/libs/core/rgb.cpp index af9ba8b..e774ce3 100644 --- a/libs/core/rgb.cpp +++ b/libs/core/rgb.cpp @@ -34,4 +34,19 @@ namespace rgb { pixels[0].b = b; ledShow(LED_BUILTIN_RGB, pixels, 1); } + + /** + * Sets an RGB sticker led to a specific red, green, blue color. + * @param name the pin name + * @param index the lef index + * @param red the red color + * @param green the green color + * @param blue the blue color + */ + //% parts="rgbled" + void setRGBStickerLed(int name, int index, int r, int g, int b) { + if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) { + return; + } + } } \ No newline at end of file diff --git a/libs/core/rgb.ts b/libs/core/rgb.ts index c629791..86b3dc9 100644 --- a/libs/core/rgb.ts +++ b/libs/core/rgb.ts @@ -33,7 +33,7 @@ namespace rgb { /** * Make the on-board RGB LED show an RGB color (range 0-255 for r, g, b). - * @param rgb RGB color of the LED, eg: Colors.Red + * @param rgb RGB color of the LED, eg: 0xff0000 */ //% blockId="rgb_set_color" block="set rgb to %rgb=colorNumberPicker" //% weight=90 help="rgb/set-color" @@ -116,6 +116,26 @@ namespace rgb { return rgb(255 - wheelPos * 3, wheelPos * 3, 255); } + + /** + * Make an RGB sticker LED show an RGB color (range 0-255 for r, g, b). + * @param rgb RGB color of the LED, eg: 0xff0000 + */ + //% blockId="rgb_sticker_set_color" block="set rgb sticker on %pin| with index %index| to %rgb=colorNumberPicker" + //% weight=90 help="rgb/set-color" + export function setStickerColor(pin: DigitalPin, index: number, rgb: number) { + if (_brightness == undefined) { + _brightness = 20; + } + + rgb = fade(rgb, _brightness); + let red = unpackR(rgb); + let green = unpackG(rgb); + let blue = unpackB(rgb); + + setRGBStickerLed(pin, index, red, green, blue); + } + /** * Get the RGB value of a known color */ diff --git a/libs/core/shims.d.ts b/libs/core/shims.d.ts index b906f15..5ee964d 100644 --- a/libs/core/shims.d.ts +++ b/libs/core/shims.d.ts @@ -107,6 +107,17 @@ declare namespace rgb { */ //% parts="rgbled" shim=rgb::setRGBLed function setRGBLed(r: int32, g: int32, b: int32): void; + + /** + * Sets an RGB sticker led to a specific red, green, blue color. + * @param name the pin name + * @param index the lef index + * @param red the red color + * @param green the green color + * @param blue the blue color + */ + //% parts="rgbled" shim=rgb::setRGBStickerLed + function setRGBStickerLed(name: int32, index: int32, r: int32, g: int32, b: int32): void; } // Auto-generated. Do not edit. Really. diff --git a/sim/ltcboard.ts b/sim/ltcboard.ts index 9806698..3b1ca52 100644 --- a/sim/ltcboard.ts +++ b/sim/ltcboard.ts @@ -60,6 +60,7 @@ namespace pxsim { serialState: LtcSerialState; // TODO: not singletons neopixelState: NeoPixelState; + rgbStickerState: RGBStickerState; constructor() { super() @@ -94,6 +95,8 @@ namespace pxsim { this.builtinPartVisuals["buttonpair"] = (xy: visuals.Coord) => visuals.mkBtnSvg(xy); this.builtinPartVisuals["neopixel"] = (xy: visuals.Coord) => visuals.mkNeoPixelPart(xy); + + this.rgbStickerState = new RGBStickerState(); } receiveMessage(msg: SimulatorMessage) { diff --git a/sim/state/rgb.ts b/sim/state/rgb.ts index 5b7ae88..18d595a 100644 --- a/sim/state/rgb.ts +++ b/sim/state/rgb.ts @@ -10,4 +10,74 @@ namespace pxsim.rgb { led.updateBuffer(newColor, 0); runtime.queueDisplayUpdate(); } + + export function setRGBStickerLed(name: number, index: number, r: number, g: number, b: number): void { + const state = board().rgbStickerState; + state.setLED(name, index, r, g, b); + runtime.queueDisplayUpdate(); + } +} + +namespace pxsim { + export class RGBStickerState { + protected buffers: Uint8Array[] = []; + protected lengths: number[] = []; + protected colors: Map = {}; + protected dirty: Map = {}; + + public setLED(pin: number, index: number, r: number, g: number, b: number) { + let buf = this.buffers[pin]; + + if (!buf) buf = new Uint8Array(60); + + const start = index * 3; + + buf[start] = g; + buf[start + 1] = r; + buf[start + 2] = b; + + this.lengths[pin] = Math.max(this.lengths[pin] || 0, index + 1); + + this.updateBuffer(buf, pin); + } + + public getColors(pin: number): RGBW[] { + let outColors = this.colors[pin] || (this.colors[pin] = []); + if (this.dirty[pin]) { + let buf = this.buffers[pin] || (this.buffers[pin] = new Uint8Array([])); + this.readNeoPixelBuffer(buf, outColors, NeoPixelMode.RGB, this.lengths[pin]); + this.dirty[pin] = false; + } + return outColors; + } + + usedPins(): number[] { + const names: number[] = []; + this.buffers.filter((buf, index) => { + if (buf) names.push(index); + }) + return names; + } + + protected updateBuffer(buffer: Uint8Array, pin: number) { + this.buffers[pin] = buffer; + this.dirty[pin] = true; + } + + private readNeoPixelBuffer(inBuffer: Uint8Array, outColors: RGBW[], mode: NeoPixelMode, pixelCount = 0) { + let buf = inBuffer; + let stride = mode === NeoPixelMode.RGBW ? 4 : 3; + pixelCount = pixelCount || Math.floor(buf.length / stride); + for (let i = 0; i < pixelCount; i++) { + // NOTE: for whatever reason, NeoPixels pack GRB not RGB + let r = buf[i * stride + 1] + let g = buf[i * stride + 0] + let b = buf[i * stride + 2] + let w = 0; + if (stride === 4) + w = buf[i * stride + 3] + outColors[i] = [r, g, b, w] + } + } + } } \ No newline at end of file diff --git a/sim/visuals/ltc.ts b/sim/visuals/ltc.ts index f9211e8..c1ad160 100644 --- a/sim/visuals/ltc.ts +++ b/sim/visuals/ltc.ts @@ -243,31 +243,15 @@ namespace pxsim.visuals { private style: SVGStyleElement; private defs: SVGDefsElement; private g: SVGGElement; - - private logos: SVGElement[]; - private head: SVGGElement; private headInitialized = false; - private headText: SVGTextElement; - private display: SVGElement; - private buttons: SVGElement[]; - private buttonsOuter: SVGElement[]; - private buttonABText: SVGTextElement; private pins: SVGElement[]; - private pinLabels: SVGElement[]; private pinGradients: SVGLinearGradientElement[]; private pinTexts: SVGTextElement[]; - private ledsOuter: SVGElement[]; private leds: SVGRectElement[]; private rgbLed: SVGCircleElement; private systemLed: SVGCircleElement; - private antenna: SVGPolylineElement; - private lightLevelButton: SVGCircleElement; - private lightLevelGradient: SVGLinearGradientElement; - private lightLevelText: SVGTextElement; private thermometerGradient: SVGLinearGradientElement; private thermometer: SVGRectElement; private thermometerText: SVGTextElement; - private shakeButton: SVGCircleElement; - private shakeText: SVGTextElement; public board: pxsim.LtcBoard; private pinNmToCoord: Map = {}; @@ -277,6 +261,8 @@ namespace pxsim.visuals { private scopeTextNode2: SVGTextElement; private scopeTextNode3: SVGTextElement; + private ledStickers: RGBStickerStrip[]; + constructor(public props: IBoardProps) { this.recordPinCoords(); this.buildDom(); @@ -345,6 +331,7 @@ namespace pxsim.visuals { this.updateTemperature(); this.updateRgbLed(); this.updateSerial(); + this.updateRGBStickers(); if (!runtime || runtime.dead) svg.addClass(this.element, "grayscale"); else svg.removeClass(this.element, "grayscale"); @@ -494,6 +481,36 @@ namespace pxsim.visuals { } } + private updateRGBStickers() { + const state = this.board.rgbStickerState; + state.usedPins().forEach(pin => { + let rgbStrip = this.ledStickers[pin]; + if (!rgbStrip) { + rgbStrip = (this.ledStickers[pin] = new RGBStickerStrip()); + this.g.appendChild(rgbStrip.getSVG()); + svg.hydrate(this.element, { + "version": "1.0", + "viewBox": `0 0 230 250`, + "class": "sim", + "x": "112.5px", + "y": "0px", + "width": "100%", + "height": "100%" + }); + + rgbStrip.moveTo(45, 140); + rgbStrip.setDataPinLocation([20 + 24 * (pin + 1), 110]) + rgbStrip.setGroundPinLocation([20, 110]) + rgbStrip.setPowerPinLocation([208, 110]) + } + const colors = state.getColors(pin); + for (let i = 0; i < colors.length; i++) { + const color = colors[i]; + rgbStrip.setLED(i, color) + } + }); + } + private updateSerial() { let state = this.board; if (!state || !state.serialState) return; @@ -663,6 +680,10 @@ namespace pxsim.visuals { svg.child(neopixelmerge, "feMergeNode", { in: "coloredBlur" }) svg.child(neopixelmerge, "feMergeNode", { in: "SourceGraphic" }) + let smallerNeopixelGlow = svg.child(this.defs, "filter", { id: "smallneopixelglow", x: "-200%", y: "-200%", width: "400%", height: "400%" }); + svg.child(smallerNeopixelGlow, "feGaussianBlur", { stdDeviation: "2", result: "coloredBlur" }); + smallerNeopixelGlow.appendChild(neopixelmerge.cloneNode(true)); + let ledglow = svg.child(this.defs, "filter", { id: "ledglow", x: "-200%", y: "-200%", width: "400%", height: "400%" }); svg.child(ledglow, "feGaussianBlur", { stdDeviation: "3", result: "coloredBlur" }); let ledglowmerge = svg.child(ledglow, "feMerge", {}); @@ -707,6 +728,8 @@ namespace pxsim.visuals { 133, 157 ].map(x => svg.child(this.g, "text", { class: "sim-text-pin no-drag", x: x + 7, y: 125, textAnchor: "middle" })); + + this.ledStickers = []; } private attachEvents() { diff --git a/sim/visuals/rgb_single.svg b/sim/visuals/rgb_single.svg new file mode 100644 index 0000000..cfb097c --- /dev/null +++ b/sim/visuals/rgb_single.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sim/visuals/rgbsticker.ts b/sim/visuals/rgbsticker.ts new file mode 100644 index 0000000..394cffd --- /dev/null +++ b/sim/visuals/rgbsticker.ts @@ -0,0 +1,205 @@ +namespace pxsim.visuals { + export class RGBSticker { + protected root: SVGSVGElement; + protected led: SVGCircleElement; + + protected left: number; + protected top: number; + + constructor() { + this.root = pxsim.svg.parseString(RGB_STICKER_SVG); + this.led = this.root.getElementsByTagName("circle").item(0); + } + + getSVG() { + return this.root; + } + + setLED(color: [number, number, number]) { + + let hsl = visuals.rgbToHsl(color); + let [h, s, l] = hsl; + let lx = Math.max(l * 1.3, 85); + // at least 10% luminosity + l = l * 90 / 100 + 10; + this.led.style.stroke = `hsl(${h}, ${s}%, ${Math.min(l * 3, 75)}%)` + this.led.style.strokeWidth = "1.5"; + svg.fill(this.led, `hsl(${h}, ${s}%, ${lx}%)`) + svg.filter(this.led, `url(#smallneopixelglow)`); + } + + moveTo(left: number, top: number) { + this.root.setAttribute("x", left + ""); + this.root.setAttribute("y", top + ""); + this.left = left; + this.top = top; + } + } + + export class RGBStickerStrip { + protected stickers: RGBSticker[]; + protected root: SVGGElement; + + protected wireRoot: SVGGElement; + protected stickerRoot: SVGGElement; + + protected groundWire: SVGPathElement; + protected dataWire: SVGPathElement; + protected powerWire: SVGPathElement; + + protected left: number; + protected top: number; + + protected bottomSticker: SVGElement; + protected dataSourceLocation: [number, number]; + protected groundSourceLocation: [number, number]; + protected powerSourceLocation: [number, number]; + + get right() { + return (this.left || 0) + this.stickers.length * 25; + } + + constructor() { + this.stickers = []; + + this.root = svg.elt("g") as SVGGElement; + this.stickerRoot = svg.child(this.root, "g") as SVGGElement; + this.wireRoot = svg.child(this.root, "g") as SVGGElement; + } + + moveTo(left: number, top: number) { + this.left = left; + this.top = top; + + this.updateTransform(); + } + + getSVG() { + return this.root; + } + + setLED(index: number, color: [number, number, number]) { + let didGrow = false; + while (this.stickers.length < index + 1) { + const newSticker = new RGBSticker(); + this.stickerRoot.insertBefore(newSticker.getSVG(), this.bottomSticker); + newSticker.moveTo(25 * this.stickers.length, 0); + this.stickers.push(newSticker); + this.bottomSticker = newSticker.getSVG(); + didGrow = true; + } + + if (didGrow) { + this.updateAllPaths(); + } + + this.stickers[index].setLED(color); + } + + groundLocation(): [number, number] { + // The end of the ground copper tape path + return [this.right - 10, (this.top | 0) + 55]; + } + + powerLocation(): [number, number] { + // The end of the power copper tape path + return [this.left + 5, (this.top | 0) + 35]; + } + + dataLocation(): [number, number] { + // The end of the data copper tape path + return [this.right, (this.top | 0) + 20]; + } + + setDataPinLocation(point: [number, number]) { + this.dataSourceLocation = point; + this.updateDataPath(); + } + + setGroundPinLocation(point: [number, number]) { + this.groundSourceLocation = point; + this.updateGroundPath(); + } + + setPowerPinLocation(point: [number, number]) { + this.powerSourceLocation = point; + this.updatePowerPath(); + } + + protected updateAllPaths() { + this.updateDataPath(); + this.updateGroundPath(); + this.updatePowerPath(); + } + + protected updateGroundPath() { + if (!this.groundSourceLocation) return; + if (this.groundWire) this.wireRoot.removeChild(this.groundWire); + + const end = this.groundLocation(); + const path: [number, number][] = [ + this.groundSourceLocation, + [this.groundSourceLocation[0], end[1]], + end + ]; + + this.groundWire = createCopperTapePath(path); + this.wireRoot.appendChild(this.groundWire); + } + + protected updateDataPath() { + if (!this.dataSourceLocation) return; + if (this.dataWire) this.wireRoot.removeChild(this.dataWire); + + const end = this.dataLocation(); + const bendY = this.dataSourceLocation[1] + ((this.top - this.dataSourceLocation[1]) / 2); + const path: [number, number][] = [ + this.dataSourceLocation, + [this.dataSourceLocation[0], bendY], + [this.left - 10, bendY], + [this.left - 10, end[1]], + end + ]; + + this.dataWire = createCopperTapePath(path); + this.wireRoot.appendChild(this.dataWire); + } + + protected updatePowerPath() { + if (!this.powerSourceLocation) return; + if (this.powerWire) this.wireRoot.removeChild(this.powerWire); + + const end = this.powerLocation(); + const bendY = this.powerSourceLocation[1] + ((this.top - this.powerSourceLocation[1]) / 2); + const path: [number, number][] = [ + this.powerSourceLocation, + [this.powerSourceLocation[0], bendY], + [this.right + 20, bendY], + [this.right + 20, end[1]], + end + ]; + + this.powerWire = createCopperTapePath(path); + this.wireRoot.appendChild(this.powerWire); + } + + protected updateTransform() { + this.stickerRoot.setAttribute("transform", `translate(${this.left} ${this.top})`) + } + } + + function createCopperTapePath(points: [number, number][]) { + const p: SVGPathElement = svg.elt("path") as SVGPathElement; + let d = `M ${points[0][0]} ${points[0][1]}` + for (let i = 1; i < points.length; i++) { + d += ` L ${points[i][0]} ${points[i][1]}` + } + + p.setAttribute("d", d); + p.setAttribute("stroke-width", "5px"); + p.setAttribute("stroke", "#ffd42a"); + p.setAttribute("fill", "none"); + + return p; + } +} \ No newline at end of file diff --git a/sim/visuals/rgbsvg.ts b/sim/visuals/rgbsvg.ts new file mode 100644 index 0000000..7c36b12 --- /dev/null +++ b/sim/visuals/rgbsvg.ts @@ -0,0 +1,3 @@ +namespace pxsim.visuals { + export const RGB_STICKER_SVG = `` +} \ No newline at end of file