namespace pxsim.visuals { const MB_STYLE = ` svg.sim { margin-bottom:1em; } svg.sim.grayscale { -moz-filter: grayscale(1); -webkit-filter: grayscale(1); filter: grayscale(1); } .sim-button { pointer-events: none; } .sim-button-outer:hover { stroke:grey; stroke-width: 3px; } .sim-button-nut { fill:#704A4A; pointer-events:none; } .sim-button-nut:hover { stroke:1px solid #704A4A; } .sim-pin:hover { stroke:#D4AF37; stroke-width:2px; } .sim-pin-touch.touched:hover { stroke:darkorange; } .sim-led-back:hover { stroke:#a0a0a0; stroke-width:3px; } .sim-led:hover { stroke:#ff7f7f; stroke-width:3px; } .sim-systemled { fill:#333; stroke:#555; stroke-width: 1px; } .sim-light-level-button { stroke:#fff; stroke-width: 3px; } .sim-antenna { stroke:#555; stroke-width: 2px; } .sim-text { font-family:"Lucida Console", Monaco, monospace; font-size:10px; fill:#fff; pointer-events: none; user-select: none; } .sim-text.inverted { fill:#000; } .sim-text-pin { font-family:"Lucida Console", Monaco, monospace; font-size:20px; fill:#fff; pointer-events: none; } .sim-thermometer { stroke:#aaa; stroke-width: 3px; } #rgbledcircle:hover { r:8px; } /* animations */ .sim-theme-glow { animation-name: sim-theme-glow-animation; animation-timing-function: ease-in-out; animation-direction: alternate; animation-iteration-count: infinite; animation-duration: 1.25s; } @keyframes sim-theme-glow-animation { from { opacity: 1; } to { opacity: 0.75; } } .sim-flash { animation-name: sim-flash-animation; animation-duration: 0.1s; } @keyframes sim-flash-animation { from { fill: yellow; } to { fill: default; } } .sim-flash-stroke { animation-name: sim-flash-stroke-animation; animation-duration: 0.4s; animation-timing-function: ease-in; } @keyframes sim-flash-stroke-animation { from { stroke: yellow; } to { stroke: default; } } /* wireframe */ .sim-wireframe * { fill: none; stroke: black; } .sim-wireframe .sim-display, .sim-wireframe .sim-led, .sim-wireframe .sim-led-back, .sim-wireframe .sim-head, .sim-wireframe .sim-theme, .sim-wireframe .sim-button-group, .sim-wireframe .sim-button-label, .sim-wireframe .sim-button, .sim-wireframe .sim-text-pin { visibility: hidden; } .sim-wireframe .sim-label { stroke: none; fill: #777; } .sim-wireframe .sim-board { stroke-width: 2px; } `; const BOARD_SVG = ` ` const pins4onXs = [66.7, 79.1, 91.4, 103.7, 164.3, 176.6, 188.9, 201.3, 213.6, 275.2, 287.5, 299.8, 312.1, 324.5, 385.1, 397.4, 409.7, 422]; const pins4onMids = pins4onXs.map(x => x + 5); const littlePinDist = pins4onMids[1] - pins4onMids[0]; const bigPinWidth = pins4onMids[4] - pins4onMids[3]; const pin0mid = pins4onXs[0] - bigPinWidth / 2.0; const pin3mid = pin0mid - bigPinWidth / 2.0; const pin1mid = pins4onMids[3] + bigPinWidth / 2.0; const pin2mid = pins4onMids[8] + bigPinWidth / 2.0; const pin3Vmid = pins4onMids[13] + bigPinWidth / 2.0; const pinGNDmid = pins4onMids[pins4onMids.length - 1] + bigPinWidth / 2.0; const pinGND2mid = pinGNDmid + bigPinWidth / 2.0; const pinMids = [pin0mid, pin1mid, pin2mid, pin3mid].concat(pins4onMids).concat([pinGNDmid, pin3Vmid, pinGND2mid]); const pinNames = [ "P0", "P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P9", "P10", "P11", "P12", "P13", "P14", "P15", "P16", "P17", "P18", "P19", "P20", "GND0", "GND", "+3v3", "GND1"]; const pinTitles = [ "P0, ANALOG IN", "P1, ANALOG IN", "P2, ANALOG IN", "P3, ANALOG IN, LED Col 1", "P4, ANALOG IN, LED Col 2", "P5, BUTTON A", "P6, LED Col 9", "P7, LED Col 8", "P8", "P9, LED Col 7", "P10, ANALOG IN, LED Col 3", "P11, BUTTON B", "P12, RESERVED ACCESSIBILITY", "P13, SPI - SCK", "P14, SPI - MISO", "P15, SPI - MOSI", "P16, SPI - Chip Select", "P17, +3v3", "P18, +3v3", "P19, I2C - SCL", "P20, I2C - SDA", "GND", "GND", "+3v3", "GND" ]; const MB_WIDTH = 251.8; const MB_HEIGHT = 222.2; export interface IBoardTheme { accent?: string; display?: string; pin?: string; pinTouched?: string; pinActive?: string; ledOn?: string; ledOff?: string; buttonOuter?: string; buttonUps: string[]; buttonDown?: string; virtualButtonOuter?: string; virtualButtonUp?: string; virtualButtonDown?: string; lightLevelOn?: string; lightLevelOff?: string; } export var themes: IBoardTheme[] = ["#3ADCFE"].map(accent => { return { accent: accent, pin: "#D4AF37", pinTouched: "#FFA500", pinActive: "#FF5500", ledOn: "#ff7777", ledOff: "#666", buttonOuter: "#979797", buttonUps: ["#186A8C", "#D82E50"], buttonDown: "#FFA500", virtualButtonDown: "#FFA500", virtualButtonOuter: "#333", virtualButtonUp: "#fff", lightLevelOn: "yellow", lightLevelOff: "#555" } }); export function randomTheme(): IBoardTheme { return themes[Math.floor(Math.random() * themes.length)]; } export interface IBoardProps { runtime?: pxsim.Runtime; theme?: IBoardTheme; disableTilt?: boolean; wireframe?: boolean; } export class MicrobitBoardSvg implements BoardView { public element: SVGSVGElement; private style: SVGStyleElement; private defs: SVGDefsElement; private g: SVGGElement; private buttons: SVGElement[]; private buttonsOuter: SVGElement[]; private pins: SVGElement[]; private pinGradients: SVGLinearGradientElement[]; private pinTexts: SVGTextElement[]; private ledsOuter: SVGElement[]; private leds: SVGElement[]; 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: SVGElement; public board: pxsim.DalBoard; private pinNmToCoord: Map = {}; private rgbLed: SVGElement; constructor(public props: IBoardProps) { this.recordPinCoords(); this.buildDom(); if (props && props.wireframe) svg.addClass(this.element, "sim-wireframe"); if (props && props.theme) this.updateTheme(); if (props && props.runtime) { this.board = this.props.runtime.board as pxsim.DalBoard; this.board.updateSubscribers.push(() => this.updateState()); this.updateState(); this.attachEvents(); } } public getView(): SVGAndSize { return { el: this.element, y: 0, x: 0, w: MB_WIDTH, h: MB_HEIGHT }; } public getCoord(pinNm: string): Coord { return this.pinNmToCoord[pinNm]; } public highlightPin(pinNm: string): void { //TODO: for instructions } public getPinDist(): number { return littlePinDist * 1.7; } public recordPinCoords() { const pinsY = 356.7 + 40; pinNames.forEach((nm, i) => { let x = pinMids[i]; this.pinNmToCoord[nm] = [x, pinsY]; }); } private updateTheme() { let theme = this.props.theme; svg.fills(this.leds, theme.ledOn); svg.fills(this.ledsOuter, theme.ledOff); svg.fills(this.buttonsOuter.slice(0, 2), theme.buttonOuter); svg.fill(this.buttons[0], theme.buttonUps[0]); svg.fill(this.buttons[1], theme.buttonUps[1]); svg.fill(this.buttonsOuter[2], theme.virtualButtonOuter); svg.fill(this.buttons[2], theme.virtualButtonUp); this.pinGradients.forEach(lg => svg.setGradientColors(lg, theme.pin, theme.pinActive)); svg.setGradientColors(this.lightLevelGradient, theme.lightLevelOn, theme.lightLevelOff); svg.setGradientColors(this.thermometerGradient, theme.ledOff, theme.ledOn); } public updateState() { let state = this.board; if (!state) return; let theme = this.props.theme; let bpState = state.buttonPairState; let buttons = [bpState.aBtn, bpState.bBtn, bpState.abBtn]; buttons.forEach((btn, index) => { svg.fill(this.buttons[index], btn.pressed ? (btn.virtual ? theme.virtualButtonDown : theme.buttonDown) : (btn.virtual ? theme.virtualButtonUp : theme.buttonUps[index])); }); let bw = state.ledMatrixState.displayMode == pxsim.DisplayMode.bw let img = state.ledMatrixState.image; this.leds.forEach((led, i) => { let sel = (led) sel.style.opacity = ((bw ? img.data[i] > 0 ? 255 : 0 : img.data[i]) / 255.0) + ""; }) this.updatePins(); this.updateTilt(); this.updateHeading(); this.updateLightLevel(); this.updateTemperature(); this.updateButtonAB(); this.updateGestures(); this.updateRgbLed(); if (!runtime || runtime.dead) svg.addClass(this.element, "grayscale"); else svg.removeClass(this.element, "grayscale"); } private updateRgbLed() { let state = this.board; if (state.rgbLedState) { if (!this.rgbLed) this.rgbLed = this.element.getElementById("rgbledcircle") as SVGCircleElement; const c = state.rgbLedState; const b = c & 0xFF; const g = (c >> 8) & 0xFF; const r = (c >> 16) & 0xFF; const w = (c >> 24) & 0xFF; const ch = `rgba(${r}, ${g}, ${b}, 1)`; svg.fill(this.rgbLed, ch); } else if (this.rgbLed) { svg.fill(this.rgbLed, 'white'); } } private updateGestures() { let state = this.board; if (state.accelerometerState.useShake && !this.shakeButton) { let shake = this.mkBtn(26, MB_HEIGHT - 67); this.shakeButton = shake.inner; svg.fill(this.shakeButton, this.props.theme.virtualButtonUp) svg.buttonEvents(shake.outer, ev => { }, (ev) => { svg.fill(this.shakeButton, this.props.theme.virtualButtonDown) }, (ev) => { svg.fill(this.shakeButton, this.props.theme.virtualButtonUp); this.board.bus.queue(DAL.MICROBIT_ID_GESTURE, 11); // GESTURE_SHAKE } ) let shakeText = svg.child(shake.outer, "text", { x: 20, y: MB_HEIGHT - 40, class: "sim-text inverted" }) as SVGTextElement; shakeText.textContent = "SHAKE" } } private updateButtonAB() { let state = this.board; if (state.buttonPairState.usesButtonAB && (this.buttons[2]).style.visibility != "visible") { (this.buttonsOuter[2]).style.visibility = "visible"; (this.buttons[2]).style.visibility = "visible"; this.updateTheme(); } } private updatePin(pin: Pin, index: number) { if (!pin) return; let text = this.pinTexts[index]; let v = ""; if (pin.mode & PinFlags.Analog) { v = Math.floor(100 - (pin.value || 0) / 1023 * 100) + "%"; if (text) text.textContent = (pin.period ? "~" : "") + (pin.value || 0) + ""; } else if (pin.mode & PinFlags.Digital) { v = pin.value > 0 ? "0%" : "100%"; if (text) text.textContent = pin.value > 0 ? "1" : "0"; } else if (pin.mode & PinFlags.Touch) { v = pin.touched ? "0%" : "100%"; if (text) text.textContent = ""; } else { v = "100%"; if (text) text.textContent = ""; } if (v) svg.setGradientValue(this.pinGradients[index], v); } private updateTemperature() { let state = this.board; if (!state || !state.thermometerState.usesTemperature) return; let tmin = -5; let tmax = 50; if (!this.thermometer) { let gid = "gradient-thermometer"; this.thermometerGradient = svg.linearGradient(this.defs, gid); this.thermometer = svg.child(this.g, "rect", { class: "sim-thermometer", x: 120, y: 110, width: 20, height: 160, rx: 5, ry: 5, fill: `url(#${gid})` }); this.thermometerText = svg.child(this.g, "text", { class: 'sim-text', x: 58, y: 130 }) as SVGTextElement; this.updateTheme(); let pt = this.element.createSVGPoint(); svg.buttonEvents(this.thermometer, (ev) => { let cur = svg.cursorPoint(pt, this.element, ev); let t = Math.max(0, Math.min(1, (260 - cur.y) / 140)) state.thermometerState.temperature = Math.floor(tmin + t * (tmax - tmin)); this.updateTemperature(); }, ev => { }, ev => { }) } let t = Math.max(tmin, Math.min(tmax, state.thermometerState.temperature)) let per = Math.floor((state.thermometerState.temperature - tmin) / (tmax - tmin) * 100) svg.setGradientValue(this.thermometerGradient, 100 - per + "%"); this.thermometerText.textContent = t + "°C"; } private updateHeading() { let xc = 258; let yc = 75; let state = this.board; if (!state || !state.compassState.usesHeading) return; /* if (!this.headInitialized) { let p = this.head.firstChild.nextSibling as SVGPathElement; p.setAttribute("d", "m269.9,50.134647l0,0l-39.5,0l0,0c-14.1,0.1 -24.6,10.7 -24.6,24.8c0,13.9 10.4,24.4 24.3,24.7l0,0l39.6,0c14.2,0 40.36034,-22.97069 40.36034,-24.85394c0,-1.88326 -26.06034,-24.54606 -40.16034,-24.64606m-0.2,39l0,0l-39.3,0c-7.7,-0.1 -14,-6.4 -14,-14.2c0,-7.8 6.4,-14.2 14.2,-14.2l39.1,0c7.8,0 14.2,6.4 14.2,14.2c0,7.9 -6.4,14.2 -14.2,14.2l0,0l0,0z"); this.updateTheme(); let pt = this.element.createSVGPoint(); svg.buttonEvents( this.head, (ev: MouseEvent) => { let cur = svg.cursorPoint(pt, this.element, ev); state.compassState.heading = Math.floor(Math.atan2(cur.y - yc, cur.x - xc) * 180 / Math.PI + 90); if (state.compassState.heading < 0) state.compassState.heading += 360; this.updateHeading(); }); this.headInitialized = true; } let txt = state.compassState.heading.toString() + "°"; if (txt != this.headText.textContent) { svg.rotateElement(this.head, xc, yc, state.compassState.heading + 180); this.headText.textContent = txt; } */ } private lastFlashTime: number = 0; public flashSystemLed() { if (!this.systemLed) this.systemLed = svg.child(this.g, "circle", { class: "sim-systemled", cx: 75, cy: MB_HEIGHT - 171, r: 2 }) let now = Date.now(); if (now - this.lastFlashTime > 150) { this.lastFlashTime = now; svg.animate(this.systemLed, "sim-flash") } } private lastAntennaFlash: number = 0; public flashAntenna() { if (!this.antenna) { let ax = 380; let dax = 18; let ayt = 10; let ayb = 40; this.antenna = svg.child(this.g, "polyline", { class: "sim-antenna", points: `${ax},${ayb} ${ax},${ayt} ${ax += dax},${ayt} ${ax},${ayb} ${ax += dax},${ayb} ${ax},${ayt} ${ax += dax},${ayt} ${ax},${ayb} ${ax += dax},${ayb} ${ax},${ayt} ${ax += dax},${ayt}` }) } let now = Date.now(); if (now - this.lastAntennaFlash > 200) { this.lastAntennaFlash = now; svg.animate(this.antenna, 'sim-flash-stroke') } } private updatePins() { let state = this.board; if (!state) return; state.edgeConnectorState.pins.forEach((pin, i) => this.updatePin(pin, i)); } private updateLightLevel() { let state = this.board; if (!state || !state.lightSensorState.usesLightLevel) return; if (!this.lightLevelButton) { let gid = "gradient-light-level"; this.lightLevelGradient = svg.linearGradient(this.defs, gid) let cy = 50; let r = 35; this.lightLevelButton = svg.child(this.g, "circle", { cx: `50px`, cy: `${cy}px`, r: `${r}px`, class: 'sim-light-level-button', fill: `url(#${gid})` }) as SVGCircleElement; let pt = this.element.createSVGPoint(); svg.buttonEvents(this.lightLevelButton, (ev) => { let pos = svg.cursorPoint(pt, this.element, ev); let rs = r / 2; let level = Math.max(0, Math.min(255, Math.floor((pos.y - (cy - rs)) / (2 * rs) * 255))); if (level != this.board.lightSensorState.lightLevel) { this.board.lightSensorState.lightLevel = level; this.applyLightLevel(); } }, ev => { }, ev => { }) this.lightLevelText = svg.child(this.g, "text", { x: 85, y: cy + r - 5, text: '', class: 'sim-text' }) as SVGTextElement; this.updateTheme(); } svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(state.lightSensorState.lightLevel * 100 / 255))) + '%') this.lightLevelText.textContent = state.lightSensorState.lightLevel.toString(); } private applyLightLevel() { let lv = this.board.lightSensorState.lightLevel; svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(lv * 100 / 255))) + '%') this.lightLevelText.textContent = lv.toString(); } private updateTilt() { if (this.props.disableTilt) return; let state = this.board; if (!state || !state.accelerometerState.accelerometer.isActive) return; let x = state.accelerometerState.accelerometer.getX(); let y = state.accelerometerState.accelerometer.getY(); let af = 8 / 1023; this.element.style.transform = "perspective(30em) rotateX(" + y * af + "deg) rotateY(" + x * af + "deg)" this.element.style.perspectiveOrigin = "50% 50% 50%"; this.element.style.perspective = "30em"; } private buildDom() { this.element = svg.elt("svg") this.element.innerHTML = BOARD_SVG; svg.hydrate(this.element, { "version": "1.0", "viewBox": `0 0 ${MB_WIDTH} ${MB_HEIGHT}`, "class": "sim", "x": "0px", "y": "0px", "width": MB_WIDTH + "px", "height": MB_HEIGHT + "px", }); this.style = svg.child(this.element, "style", {}); this.style.textContent = MB_STYLE; this.defs = svg.child(this.element, "defs", {}); this.g = svg.elt("g"); this.element.appendChild(this.g); // filters let glow = svg.child(this.defs, "filter", { id: "filterglow", x: "-5%", y: "-5%", width: "120%", height: "120%" }); svg.child(glow, "feGaussianBlur", { stdDeviation: "5", result: "glow" }); let merge = svg.child(glow, "feMerge", {}); for (let i = 0; i < 3; ++i) svg.child(merge, "feMergeNode", { in: "glow" }) // leds this.leds = []; this.ledsOuter = []; const left = 103.04, top = MB_HEIGHT - 158.59; const ledoffw = 10.91, ledoffh = 10.68; const ledw = 2.7; const ledh = 5.47; for (let i = 0; i < 5; ++i) { let ledtop = i * ledoffh + top; for (let j = 0; j < 5; ++j) { let ledleft = j * ledoffw + left; let k = i * 5 + j; this.ledsOuter.push(svg.child(this.g, "rect", { class: "sim-led-back", x: ledleft, y: ledtop, width: ledw, height: ledh, rx: 1, ry: 1 })); this.leds.push(svg.child(this.g, "rect", { class: "sim-led", x: ledleft - 1, y: ledtop - 1, width: ledw + 2, height: ledh + 2, rx: 2, ry: 2, title: `(${j},${i})` })); } } // https://www.microbit.co.uk/device/pins // P0, P1, P2 this.pins = [ "M16.5,341.2c0,0.4-0.1,0.9-0.1,1.3v60.7c4.1,1.7,8.6,2.7,12.9,2.7h34.4v-64.7h0.3c0,0,0-0.1,0-0.1c0-13-10.6-23.6-23.7-23.6C27.2,317.6,16.5,328.1,16.5,341.2z M21.2,341.6c0-10.7,8.7-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3c0,10.7-8.6,19.3-19.3,19.3C29.9,360.9,21.2,352.2,21.2,341.6z", "M139.1,317.3c-12.8,0-22.1,10.3-23.1,23.1V406h46.2v-65.6C162.2,327.7,151.9,317.3,139.1,317.3zM139.3,360.1c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C158.6,351.5,150,360.1,139.3,360.1z", "M249,317.3c-12.8,0-22.1,10.3-23.1,23.1V406h46.2v-65.6C272.1,327.7,261.8,317.3,249,317.3z M249.4,360.1c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C268.7,351.5,260.1,360.1,249.4,360.1z" ].map((p, pi) => svg.path(this.g, "sim-pin sim-pin-touch", p)); // P3 this.pins.push(svg.path(this.g, "sim-pin", "M0,357.7v19.2c0,10.8,6.2,20.2,14.4,25.2v-44.4H0z")); pins4onXs.forEach(x => { this.pins.push(svg.child(this.g, "rect", { x: x, y: 356.7, width: 10, height: 50, class: "sim-pin" })); }) this.pins.push(svg.path(this.g, "sim-pin", "M483.6,402c8.2-5,14.4-14.4,14.4-25.1v-19.2h-14.4V402z")); this.pins.push(svg.path(this.g, "sim-pin", "M359.9,317.3c-12.8,0-22.1,10.3-23.1,23.1V406H383v-65.6C383,327.7,372.7,317.3,359.9,317.3z M360,360.1c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C379.3,351.5,370.7,360.1,360,360.1z")); this.pins.push(svg.path(this.g, "sim-pin", "M458,317.6c-13,0-23.6,10.6-23.6,23.6c0,0,0,0.1,0,0.1h0V406H469c4.3,0,8.4-1,12.6-2.7v-60.7c0-0.4,0-0.9,0-1.3C481.6,328.1,471,317.6,458,317.6z M457.8,360.9c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C477.1,352.2,468.4,360.9,457.8,360.9z")); this.pins.forEach((p, i) => svg.hydrate(p, { title: pinTitles[i] })); this.pinGradients = this.pins.map((pin, i) => { let gid = "gradient-pin-" + i let lg = svg.linearGradient(this.defs, gid) pin.setAttribute("fill", `url(#${gid})`); return lg; }) this.pinTexts = [67, 165, 275].map(x => svg.child(this.g, "text", { class: "sim-text-pin", x: x, y: 345 })); // BTN A, B const btnids = ["BTN_A", "BTN_B"]; this.buttonsOuter = btnids.map(n => this.element.getElementById(n + "_BOX") as SVGElement); this.buttonsOuter.forEach(b => svg.addClass(b, "sim-button-outer")); this.buttons = btnids.map(n => this.element.getElementById(n) as SVGElement); this.buttons.forEach(b => svg.addClass(b, "sim-button")); // BTN A+B const outerBtn = (left: number, top: number) => { const button = this.mkBtn(left, top); this.buttonsOuter.push(button.outer); this.buttons.push(button.inner); return button; } let ab = outerBtn(210, MB_HEIGHT - 168); let abtext = svg.child(ab.outer, "text", { x: 208, y: MB_HEIGHT - 173, class: "sim-text inverted" }) as SVGTextElement; abtext.textContent = "A+B"; (this.buttonsOuter[2]).style.visibility = "hidden"; (this.buttons[2]).style.visibility = "hidden"; } private mkBtn(left: number, top: number): { outer: SVGElement, inner: SVGElement } { const btnr = 2; const btnw = 16; const btnn = 1.6; const btnnm = 2; const btnb = 5; let btng = svg.child(this.g, "g", { class: "sim-button-group" }); svg.child(btng, "rect", { class: "sim-button-outer", x: left, y: top, rx: btnr, ry: btnr, width: btnw, height: btnw }); svg.child(btng, "circle", { class: "sim-button-nut", cx: left + btnnm, cy: top + btnnm, r: btnn }); svg.child(btng, "circle", { class: "sim-button-nut", cx: left + btnnm, cy: top + btnw - btnnm, r: btnn }); svg.child(btng, "circle", { class: "sim-button-nut", cx: left + btnw - btnnm, cy: top + btnw - btnnm, r: btnn }); svg.child(btng, "circle", { class: "sim-button-nut", cx: left + btnw - btnnm, cy: top + btnnm, r: btnn }); const outer = btng; const inner = svg.child(btng, "circle", { class: "sim-button", cx: left + btnw / 2, cy: top + btnw / 2, r: btnb }); return { outer, inner }; } private attachEvents() { Runtime.messagePosted = (msg) => { switch (msg.type || "") { case "serial": this.flashSystemLed(); break; case "radiopacket": this.flashAntenna(); break; } } let tiltDecayer = 0; this.element.addEventListener(pointerEvents.move, (ev: MouseEvent) => { let state = this.board; if (!state.accelerometerState.accelerometer.isActive) return; if (tiltDecayer) { clearInterval(tiltDecayer); tiltDecayer = 0; } let ax = (ev.clientX - this.element.clientWidth / 2) / (this.element.clientWidth / 3); let ay = (ev.clientY - this.element.clientHeight / 2) / (this.element.clientHeight / 3); let x = - Math.max(- 1023, Math.min(1023, Math.floor(ax * 1023))); let y = Math.max(- 1023, Math.min(1023, Math.floor(ay * 1023))); let z2 = 1023 * 1023 - x * x - y * y; let z = Math.floor((z2 > 0 ? -1 : 1) * Math.sqrt(Math.abs(z2))); state.accelerometerState.accelerometer.update(x, y, z); this.updateTilt(); }, false); this.element.addEventListener(pointerEvents.leave, (ev: MouseEvent) => { let state = this.board; if (!state.accelerometerState.accelerometer.isActive) return; if (!tiltDecayer) { tiltDecayer = setInterval(() => { let accx = state.accelerometerState.accelerometer.getX(MicroBitCoordinateSystem.RAW); accx = Math.floor(Math.abs(accx) * 0.85) * (accx > 0 ? 1 : -1); let accy = state.accelerometerState.accelerometer.getY(MicroBitCoordinateSystem.RAW); accy = Math.floor(Math.abs(accy) * 0.85) * (accy > 0 ? 1 : -1); let accz = -Math.sqrt(Math.max(0, 1023 * 1023 - accx * accx - accy * accy)); if (Math.abs(accx) <= 24 && Math.abs(accy) <= 24) { clearInterval(tiltDecayer); tiltDecayer = 0; accx = 0; accy = 0; accz = -1023; } state.accelerometerState.accelerometer.update(accx, accy, accz); this.updateTilt(); }, 50) } }, false); this.pins.forEach((pin, index) => { if (!this.board.edgeConnectorState.pins[index]) return; let pt = this.element.createSVGPoint(); svg.buttonEvents(pin, // move ev => { let state = this.board; let pin = state.edgeConnectorState.pins[index]; let svgpin = this.pins[index]; if (pin.mode & PinFlags.Input) { let cursor = svg.cursorPoint(pt, this.element, ev); let v = (400 - cursor.y) / 40 * 1023 pin.value = Math.max(0, Math.min(1023, Math.floor(v))); } this.updatePin(pin, index); }, // start ev => { let state = this.board; let pin = state.edgeConnectorState.pins[index]; let svgpin = this.pins[index]; svg.addClass(svgpin, "touched"); if (pin.mode & PinFlags.Input) { let cursor = svg.cursorPoint(pt, this.element, ev); let v = (400 - cursor.y) / 40 * 1023 pin.value = Math.max(0, Math.min(1023, Math.floor(v))); } this.updatePin(pin, index); }, // stop (ev: MouseEvent) => { let state = this.board; let pin = state.edgeConnectorState.pins[index]; let svgpin = this.pins[index]; svg.removeClass(svgpin, "touched"); this.updatePin(pin, index); return false; }); }) this.pins.slice(0, 3).forEach((btn, index) => { btn.addEventListener(pointerEvents.down, ev => { let state = this.board; state.edgeConnectorState.pins[index].touched = true; this.updatePin(state.edgeConnectorState.pins[index], index); }) btn.addEventListener(pointerEvents.leave, ev => { let state = this.board; state.edgeConnectorState.pins[index].touched = false; this.updatePin(state.edgeConnectorState.pins[index], index); }) btn.addEventListener(pointerEvents.up, ev => { let state = this.board; state.edgeConnectorState.pins[index].touched = false; this.updatePin(state.edgeConnectorState.pins[index], index); this.board.bus.queue(state.edgeConnectorState.pins[index].id, DAL.MICROBIT_BUTTON_EVT_UP); this.board.bus.queue(state.edgeConnectorState.pins[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK); }) }) let bpState = this.board.buttonPairState; let stateButtons = [bpState.aBtn, bpState.bBtn, bpState.abBtn]; this.buttonsOuter.slice(0, 2).forEach((btn, index) => { btn.addEventListener(pointerEvents.down, ev => { let state = this.board; stateButtons[index].pressed = true; svg.fill(this.buttons[index], this.props.theme.buttonDown); }) btn.addEventListener(pointerEvents.leave, ev => { let state = this.board; stateButtons[index].pressed = false; svg.fill(this.buttons[index], this.props.theme.buttonUps[index]); }) btn.addEventListener(pointerEvents.up, ev => { let state = this.board; stateButtons[index].pressed = false; svg.fill(this.buttons[index], this.props.theme.buttonUps[index]); this.board.bus.queue(stateButtons[index].id, DAL.MICROBIT_BUTTON_EVT_UP); this.board.bus.queue(stateButtons[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK); }) }) this.buttonsOuter[2].addEventListener(pointerEvents.down, ev => { let state = this.board; stateButtons[0].pressed = true; stateButtons[1].pressed = true; stateButtons[2].pressed = true; svg.fill(this.buttons[0], this.props.theme.buttonDown); svg.fill(this.buttons[1], this.props.theme.buttonDown); svg.fill(this.buttons[2], this.props.theme.buttonDown); }) this.buttonsOuter[2].addEventListener(pointerEvents.leave, ev => { let state = this.board; stateButtons[0].pressed = false; stateButtons[1].pressed = false; stateButtons[2].pressed = false; svg.fill(this.buttons[0], this.props.theme.buttonUps[0]); svg.fill(this.buttons[1], this.props.theme.buttonUps[1]); svg.fill(this.buttons[2], this.props.theme.virtualButtonUp); }) this.buttonsOuter[2].addEventListener(pointerEvents.up, ev => { let state = this.board; stateButtons[0].pressed = false; stateButtons[1].pressed = false; stateButtons[2].pressed = false; svg.fill(this.buttons[0], this.props.theme.buttonUps[0]); svg.fill(this.buttons[1], this.props.theme.buttonUps[1]); svg.fill(this.buttons[2], this.props.theme.virtualButtonUp); this.board.bus.queue(stateButtons[2].id, DAL.MICROBIT_BUTTON_EVT_UP); this.board.bus.queue(stateButtons[2].id, DAL.MICROBIT_BUTTON_EVT_CLICK); }) } } }