├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .gitpod.yml ├── .husky ├── .gitignore └── pre-commit ├── .hygen.js ├── .prettierrc ├── .storybook ├── main.js ├── manager.js └── preview.js ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── _templates └── element │ ├── help │ └── index.ejs.t │ └── new │ ├── element.stories.ts.t │ ├── element.ts.t │ ├── index.ts.t │ ├── react-types-import.ts.t │ └── react-types.ts.t ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── 7segment-element.stories.ts ├── 7segment-element.ts ├── analog-joystick-element.stories.ts ├── analog-joystick-element.ts ├── arduino-mega-element.stories.ts ├── arduino-mega-element.ts ├── arduino-nano-element.stories.ts ├── arduino-nano-element.ts ├── arduino-uno-element.stories.ts ├── arduino-uno-element.ts ├── biaxial-stepper-element.stories.ts ├── biaxial-stepper-element.ts ├── big-sound-sensor-element.stories.ts ├── big-sound-sensor-element.ts ├── buzzer-element.stories.ts ├── buzzer-element.ts ├── dht22-element.stories.ts ├── dht22-element.ts ├── dip-switch-8-element.stories.ts ├── dip-switch-8-element.ts ├── ds1307-element.stories.ts ├── ds1307-element.ts ├── esp32-devkit-v1-element.stories.ts ├── esp32-devkit-v1-element.ts ├── flame-sensor-element.stories.ts ├── flame-sensor-element.ts ├── franzininho-element.stories.ts ├── franzininho-element.ts ├── gas-sensor-element.stories.ts ├── gas-sensor-element.ts ├── hc-sr04-element.stories.ts ├── hc-sr04-element.ts ├── heart-beat-sensor-element.stories.ts ├── heart-beat-sensor-element.ts ├── hx711-element.stories.ts ├── hx711-element.ts ├── ili9341-element.stories.ts ├── ili9341-element.ts ├── index.ts ├── ir-receiver-element.stories.ts ├── ir-receiver-element.ts ├── ir-remote-element.stories.ts ├── ir-remote-element.ts ├── ks2e-m-dc5-element.stories.ts ├── ks2e-m-dc5-element.ts ├── ky-040-element.stories.ts ├── ky-040-element.ts ├── lcd1602-element.stories.ts ├── lcd1602-element.ts ├── lcd1602-font-a00.ts ├── lcd1602-font-a02.ts ├── lcd2004-element.stories.ts ├── lcd2004-element.ts ├── led-bar-graph-element.stories.ts ├── led-bar-graph-element.ts ├── led-element.stories.ts ├── led-element.ts ├── led-ring-element.stories.ts ├── led-ring-element.ts ├── membrane-keypad-element.stories.ts ├── membrane-keypad-element.ts ├── microsd-card-element.stories.ts ├── microsd-card-element.ts ├── mpu6050-element.stories.ts ├── mpu6050-element.ts ├── nano-rp2040-connect-element.stories.ts ├── nano-rp2040-connect-element.ts ├── neopixel-element.stories.ts ├── neopixel-element.ts ├── neopixel-matrix-element.stories.ts ├── neopixel-matrix-element.ts ├── ntc-temperature-sensor-element.stories.ts ├── ntc-temperature-sensor-element.ts ├── patterns │ └── pins-female.ts ├── photoresistor-sensor-element.stories.ts ├── photoresistor-sensor-element.ts ├── pin.ts ├── pir-motion-sensor-element.stories.ts ├── pir-motion-sensor-element.ts ├── potentiometer-element.stories.ts ├── potentiometer-element.ts ├── pushbutton-6mm-element.stories.ts ├── pushbutton-6mm-element.ts ├── pushbutton-element.stories.ts ├── pushbutton-element.ts ├── react-types.ts ├── resistor-element.stories.ts ├── resistor-element.ts ├── rgb-led-element.stories.ts ├── rgb-led-element.ts ├── rotary-dailer-element.stories.ts ├── rotary-dialer-element.ts ├── servo-element.stories.ts ├── servo-element.ts ├── slide-potentiometer-element.stories.ts ├── slide-potentiometer-element.ts ├── slide-switch-element.stories.ts ├── slide-switch-element.ts ├── small-sound-sensor-element.stories.ts ├── small-sound-sensor-element.ts ├── ssd1306-element.stories.ts ├── ssd1306-element.ts ├── stepper-motor-element.stories.ts ├── stepper-motor-element.ts ├── storybook-events-logger.d.ts ├── tilt-switch-element.stories.ts ├── tilt-switch-element.ts ├── types │ └── rgb.ts └── utils │ ├── clamp.ts │ ├── ctm-workaround.ts │ ├── geometry.ts │ ├── keys.ts │ ├── logo.ts │ ├── show-pins-element.ts │ └── units.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2018, 5 | "sourceType": "module" 6 | }, 7 | "plugins": ["@typescript-eslint", "prettier", "json"], 8 | "env": { 9 | "browser": true, 10 | "es6": true, 11 | "node": true 12 | }, 13 | "extends": ["eslint:recommended", "plugin:storybook/recommended", "plugin:@typescript-eslint/recommended", "prettier"], 14 | "rules": { 15 | "prettier/prettier": "error", 16 | "@typescript-eslint/explicit-module-boundary-types": 0 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .history 3 | .idea 4 | node_modules 5 | dist 6 | build 7 | *.log 8 | storybook-static 9 | custom-elements.json 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: npm install 3 | command: npm run storybook 4 | 5 | vscode: 6 | extensions: 7 | - esbenp.prettier-vscode@5.1.3:t532ajsImUSrA9N8Bd7jQw== 8 | - dbaeumer.vscode-eslint@2.1.3:1NRvj3UKNTNwmYjptmUmIw== 9 | - runem.lit-plugin@1.1.10:/yk3Arh4MIQ3RwIcHwbX3Q== 10 | 11 | ports: 12 | - port: 6006 13 | onOpen: open-preview 14 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.hygen.js: -------------------------------------------------------------------------------- 1 | const { classify } = require('inflection'); 2 | 3 | module.exports = { 4 | helpers: { 5 | className: s => classify(s.replace(/[- ]/g, '_')) 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "endOfLine": "auto" 7 | } 8 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | core: { builder: 'webpack5' }, 3 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 4 | addons: [ 5 | '@storybook/addon-essentials', 6 | '@storybook/addon-knobs', 7 | '@storybook/addon-google-analytics', 8 | ], 9 | 10 | // See https://github.com/storybookjs/storybook/issues/12578#issuecomment-702664081 11 | babel: async (options) => { 12 | Object.assign( 13 | options.plugins.find((plugin) => plugin[0].includes('plugin-proposal-decorators'))[1], 14 | { 15 | decoratorsBeforeExport: true, 16 | legacy: false, 17 | } 18 | ); 19 | return options; 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | window.STORYBOOK_GA_ID = 'UA-150413053-2'; 2 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: '^on[A-Z].*' }, 3 | }; 4 | 5 | import { setCustomElements } from '@storybook/web-components'; 6 | import customElements from '../custom-elements.json'; 7 | 8 | import '../src/utils/show-pins-element'; 9 | 10 | // Configure Storybook Docs Addon for Web Components 11 | setCustomElements(customElements); 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | 4 | "recommendations": [ 5 | "editorconfig.editorconfig", 6 | "esbenp.prettier-vscode", 7 | "bierner.lit-html", 8 | "dbaeumer.vscode-eslint" 9 | ], 10 | 11 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 12 | "unwantedRecommendations": [] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 3 | "tslint.enable": false 4 | } 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Wokwi Elements 2 | 3 | First of all, thank you for considering contributing to Wokwi Elements! 4 | Please go over these guidelines to ensure that your contribution lands 5 | successfully. 6 | 7 | ## Creating a New Element 8 | 9 | Before starting to work on a new element, please 10 | [file an issue](https://github.com/wokwi/wokwi-elements/issues/new/choose) 11 | to discuss the implementation. 12 | 13 | Also, please make sure that any 3d-party graphics that you use 14 | for the element is released under a permissive license (such as 15 | MIT, BSD, Apache, or CC-BY), and that you give credits to the 16 | original authors as required. 17 | 18 | To create the element, use our hygen generator, by running: 19 | 20 | ``` 21 | npm run new-element --name demo 22 | ``` 23 | 24 | Replacing `demo` with your actual element name (e.g. `led`). 25 | 26 | Running this command will also generate a Storybook file for 27 | the new element. You can then start Storybook by runnning: 28 | 29 | ``` 30 | npm run storybook 31 | ``` 32 | 33 | This will open a local dev server at http://localhost:6006, where you 34 | can interact with your new element and see any changes live, similar to 35 | https://elements.wokwi.com. 36 | 37 | ## Element Guidelines 38 | 39 | Please make sure to follow these guidelines when working on your new element: 40 | 41 | 1. If the element is an input element (knob, button, slider, etc.), it has 42 | to be keyboard accessible (i.e. adding the `tabindex` attribute, listening 43 | for the `keydown` and `keyup` events, etc). 44 | 2. Make sure that your element renders well and functions on mobile devices. 45 | 3. Add docstring to the element class and any public properties/methods. These 46 | doc strings will appear in the Docs tag in storybook. Check out the [Resistor 47 | Element](src/resistor-element.ts) for an example. 48 | 4. Include a `pinInfo` property in your elements. This property defines the pins 49 | of the element, specifying at least the name and x/y position of each pin. 50 | 51 | Example (from the [LED element](src/led-element.ts)): 52 | 53 | ```typescript 54 | readonly pinInfo: ElementPin[] = [ 55 | { name: 'A', x: 24, y: 42, signals: [], description: 'Anode' }, 56 | { name: 'C', x: 16, y: 42, signals: [], description: 'Cathode' }, 57 | ]; 58 | ``` 59 | 60 | 5. Create a storybook story for each element configuration. For example, check the [LCD1602 61 | stories](src/lcd1602-element.stories.ts) 62 | 6. Your commit messages should follow the [conventional commits 63 | standard](https://www.conventionalcommits.org/), e.g.: 64 | `feat: add neopixel element` 65 | 66 | ## Debugging Pin Info 67 | 68 | When working on the `pinInfo` property, it is often useful to visually see the 69 | pins that you define. To see the pin locations, add the `` 70 | utility element to your story: 71 | 72 | 1. Wrap your element with the element, e.g. 73 | ``` 74 | export const HCSR04 = () => html` 75 | 76 | 77 | 78 | `; 79 | ``` 80 | 2. When you are happy with the pin definition, please remove the `` 81 | from your story. It is only a debugging tool, and shouldn't be present in the final story. 82 | 83 | ## Video Tutorial and Blog Post 84 | 85 | The [Membrane keypad element live-coding video tutorial](https://www.youtube.com/watch?v=gh27icNatwA) walks 86 | through the complete process behind creating an element: research, drawing, and writing the code / 87 | stories. 88 | 89 | If you prefer reading over watching a video, please check out [Recreating The Arduino Pushbutton Using SVG And <lit-element>](https://www.smashingmagazine.com/2020/01/recreating-arduino-pushbutton-svg/). 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Uri Shaked 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wokwi Elements 2 | 3 | Web Components for Arduino and various electronic parts. 4 | 5 | [![NPM Version](https://img.shields.io/npm/v/@wokwi/elements)](https://www.npmjs.com/package/@wokwi/elements) 6 | [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/wokwi/wokwi-elements) 7 | 8 | Check out [the component catalog](https://elements.wokwi.com). 9 | 10 | Note: these elements only provide the presentation and display of the represented hardware. They do not provide the functional simulation code of the hardware. That is dependant on the application (simulator) that you wish to use these with, and thus up to you to create. 11 | 12 | ## Using Wokwi Elements 13 | 14 | You can install the package with `npm` and then import it into your code: 15 | 16 | ```js 17 | import '@wokwi/elements'; 18 | ``` 19 | 20 | Alternatively, you can load the Wokwi Elements bundle from unpkg's CDN: 21 | 22 | ```html 23 | 24 | ``` 25 | 26 | Replace 0.48.3 with the latest version number. You can find a list of all the versions in the [releases](https://github.com/wokwi/wokwi-elements/) page. 27 | 28 | ## Local development 29 | 30 | To prepare for local development, clone this repo, and then install 31 | the dependencies: 32 | 33 | ``` 34 | npm install 35 | ``` 36 | 37 | Then start storybook: 38 | 39 | ``` 40 | npm run storybook 41 | ``` 42 | 43 | This will open a local dev server at http://localhost:6006, where you 44 | can interact with the elements and see your changes live, similar to 45 | https://elements.wokwi.com. 46 | 47 | ## Creating a new element 48 | 49 | The easiest way to create a new element is to run the generator: 50 | 51 | ``` 52 | npm run new-element --name demo 53 | ``` 54 | 55 | This will generate a new element called `demo`. It will also 56 | create a storybook file, so you will be able to see the new element 57 | in storybook (see the "Local development" section above). 58 | 59 | Note: updates the docstrings in the code will not be reflected 60 | in Storybook's Docs tab unless you restart Storybook, or run the 61 | following command manually and refresh the page: 62 | 63 | ``` 64 | npm run analyze-components 65 | ``` 66 | 67 | Check out the [Contributing Guide](CONTRIBUTING.md) for more details. 68 | 69 | ## Learn how to create elements 70 | 71 | ### Video tutorial 72 | 73 | The [Membrane keypad element live-coding tutorial](https://www.youtube.com/watch?v=gh27icNatwA) walks 74 | through the complete process behind creating an element: research, drawing, and writing the code / 75 | stories. 76 | 77 | ### Blog posts 78 | 79 | * [Recreating The Arduino Pushbutton Using SVG And <lit-element>](https://www.smashingmagazine.com/2020/01/recreating-arduino-pushbutton-svg/) 80 | * [Turning Arduino OLED Display into a Web Component](https://blog.wokwi.com/making-an-arduino-ssd1306-lit-element/) 81 | 82 | ## License 83 | 84 | Wokwi Elements are released under the [MIT license](LICENSE). 85 | -------------------------------------------------------------------------------- /_templates/element/help/index.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | message: | 3 | hygen {bold element new} --name [NAME] 4 | --- -------------------------------------------------------------------------------- /_templates/element/new/element.stories.ts.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: src/<%= name %>-element.stories.ts 3 | --- 4 | import { html } from 'lit'; 5 | import './<%= name %>-element'; 6 | 7 | export default { 8 | title: '<%= h.changeCase.title(h.className(name)) %>', 9 | component: 'wokwi-<%= name %>', 10 | argTypes: { 11 | value: { control: { type: 'number', min: 1, max: 10, step: 1 } }, 12 | }, 13 | args: { 14 | value: 5, 15 | }, 16 | }; 17 | 18 | const Template = ({ value }) => html` value=${value}>>`; 19 | 20 | export const Default = Template.bind({}); 21 | Default.args = { value: 5 }; 22 | 23 | export const Large = Template.bind({}); 24 | Large.args = { value: 10 }; 25 | -------------------------------------------------------------------------------- /_templates/element/new/element.ts.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: src/<%= name %>-element.ts 3 | --- 4 | import { html, LitElement, svg } from 'lit'; 5 | import { customElement, property} from 'lit/decorators.js'; 6 | 7 | @customElement('wokwi-<%= name %>') 8 | export class <%= h.className(name) %>Element extends LitElement { 9 | @property() value = 0; 10 | 11 | render() { 12 | const { value } = this; 13 | return html` 14 | 21 | 22 | 23 | `; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /_templates/element/new/index.ts.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: src/index.ts 3 | inject: true 4 | skip_if: <%= h.className(name) %>Element 5 | append: true 6 | --- 7 | export { <%= h.className(name) %>Element } from './<%= name %>-element'; -------------------------------------------------------------------------------- /_templates/element/new/react-types-import.ts.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: src/react-types.ts 3 | inject: true 4 | before: \ntype WokwiElement 5 | skip_if: \{ <%= h.className(name) %>Element \} 6 | --- 7 | import { <%= h.className(name) %>Element } from './<%= name %>-element'; -------------------------------------------------------------------------------- /_templates/element/new/react-types.ts.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: src/react-types.ts 3 | inject: true 4 | before: \}\s+\}\s+\} 5 | skip_if: 'wokwi-<%= name %>' 6 | --- 7 | 'wokwi-<%= name %>': WokwiElement<<%= h.className(name) %>Element>; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wokwi/elements", 3 | "version": "1.7.0", 4 | "main": "dist/cjs/index.js", 5 | "module": "./dist/esm/index.js", 6 | "types": "./dist/esm/index.d.ts", 7 | "repository": "https://github.com/wokwi/wokwi-elements", 8 | "author": "Uri Shaked ", 9 | "license": "MIT", 10 | "files": [ 11 | "dist" 12 | ], 13 | "engines": { 14 | "node": ">=16" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.15.0", 18 | "@babel/runtime": "^7.15.3", 19 | "@storybook/addon-actions": "^6.5.13", 20 | "@storybook/addon-essentials": "^6.5.13", 21 | "@storybook/addon-google-analytics": "^6.3.0-next.1", 22 | "@storybook/addon-knobs": "^6.4.0", 23 | "@storybook/builder-webpack5": "^6.5.13", 24 | "@storybook/manager-webpack5": "^6.5.13", 25 | "@storybook/web-components": "^6.5.13", 26 | "@typescript-eslint/eslint-plugin": "^5.42.1", 27 | "@typescript-eslint/parser": "^5.42.1", 28 | "babel-loader": "^9.1.0", 29 | "eslint": "^8.27.0", 30 | "eslint-config-prettier": "^8.5.0", 31 | "eslint-plugin-json": "^3.1.0", 32 | "eslint-plugin-prettier": "^4.2.1", 33 | "eslint-plugin-storybook": "^0.6.7", 34 | "husky": "^8.0.0", 35 | "hygen": "^6.1.0", 36 | "lint-staged": "^10.2.11", 37 | "prettier": "^2.3.2", 38 | "react-is": "^16.13.1", 39 | "rimraf": "^3.0.2", 40 | "rollup": "^2.79.2", 41 | "rollup-plugin-commonjs": "^10.1.0", 42 | "rollup-plugin-node-resolve": "^5.2.0", 43 | "terser": "^4.8.0", 44 | "typescript": "^4.3.5", 45 | "web-component-analyzer": "^1.1.6" 46 | }, 47 | "dependencies": { 48 | "@types/react": ">=16", 49 | "lit": "^2.0.0" 50 | }, 51 | "scripts": { 52 | "build": "rimraf dist && tsc --sourceMap false && tsc -m esnext --outDir dist/esm --sourceMap false && rollup -c rollup.config.js && terser -c -m -o dist/wokwi-elements.bundle.min.js dist/wokwi-elements.bundle.js", 53 | "test": "npm run lint", 54 | "lint": "eslint src/**/*.ts", 55 | "prepare": "husky install && npm run build", 56 | "analyze-components": "web-component-analyzer analyze src/*-element.ts --outFile custom-elements.json", 57 | "storybook": "npm run analyze-components && start-storybook -p 6006", 58 | "build-storybook": "npm run analyze-components && build-storybook", 59 | "new-element": "hygen element new", 60 | "clear-cache": "rimraf node_modules/.cache" 61 | }, 62 | "lint-staged": { 63 | "src/**/*.{ts,tsx}": [ 64 | "eslint --fix", 65 | "prettier --write" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import commonJS from 'rollup-plugin-commonjs'; 4 | 5 | export default { 6 | input: 'dist/esm/index.js', 7 | output: { 8 | file: 'dist/wokwi-elements.bundle.js', 9 | name: 'elements', 10 | format: 'iife', 11 | }, 12 | context: 'window', 13 | plugins: [ 14 | resolve(), 15 | commonJS({ 16 | include: 'node_modules/**', 17 | }), 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /src/7segment-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { withKnobs, select } from '@storybook/addon-knobs'; 2 | import { storiesOf, forceReRender } from '@storybook/web-components'; 3 | import { html } from 'lit'; 4 | import './7segment-element'; 5 | 6 | class SpinnerAnimation { 7 | private index = 0; 8 | values: number[] = [0, 0, 0, 0, 0, 0, 0, 0]; 9 | 10 | step() { 11 | this.values = [0, 0, 0, 0, 0, 0, 0, 0]; 12 | this.values[this.index++ % 6] = 1; 13 | } 14 | } 15 | 16 | class BlinkAnimation { 17 | value = false; 18 | 19 | step() { 20 | this.value = !this.value; 21 | } 22 | } 23 | 24 | const spinnerAnimation = new SpinnerAnimation(); 25 | const blinkAnimation = new BlinkAnimation(); 26 | setInterval(() => { 27 | spinnerAnimation.step(); 28 | blinkAnimation.step(); 29 | forceReRender(); 30 | }, 100); 31 | 32 | storiesOf('7 Segment', module) 33 | .addParameters({ component: 'wokwi-7segment' }) 34 | .addDecorator(withKnobs) 35 | .add( 36 | 'Red 4.', 37 | () => 38 | html`` 43 | ) 44 | .add( 45 | 'Green 5', 46 | () => html`` 47 | ) 48 | .add( 49 | '2 yellow Digits', 50 | () => 51 | html`` 56 | ) 57 | .add( 58 | '3 white digits', 59 | () => 60 | html`` 65 | ) 66 | .add( 67 | 'Clock mode', 68 | () => 69 | html`` 77 | ) 78 | .add( 79 | 'Blue spinner', 80 | () => 81 | html`` 86 | ); 87 | -------------------------------------------------------------------------------- /src/analog-joystick-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './analog-joystick-element'; 3 | 4 | export default { 5 | title: 'Analog Joystick', 6 | component: 'wokwi-analog-joystick', 7 | }; 8 | 9 | export const Joystick = () => html``; 10 | -------------------------------------------------------------------------------- /src/arduino-mega-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { boolean, withKnobs } from '@storybook/addon-knobs'; 2 | import { storiesOf } from '@storybook/web-components'; 3 | import { html } from 'lit'; 4 | import './arduino-mega-element'; 5 | import { action } from '@storybook/addon-actions'; 6 | 7 | storiesOf('Arduino Mega', module) 8 | .addParameters({ component: 'wokwi-arduino-mega' }) 9 | .addDecorator(withKnobs) 10 | .add( 11 | 'Mega', 12 | () => html` 13 | 21 | ` 22 | ); 23 | -------------------------------------------------------------------------------- /src/arduino-nano-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { boolean, withKnobs } from '@storybook/addon-knobs'; 2 | import { storiesOf } from '@storybook/web-components'; 3 | import { html } from 'lit'; 4 | import { action } from '@storybook/addon-actions'; 5 | import './arduino-nano-element'; 6 | 7 | storiesOf('Arduino Nano', module) 8 | .addParameters({ component: 'wokwi-arduino-nano' }) 9 | .addDecorator(withKnobs) 10 | .add( 11 | 'Nano', 12 | () => html` 13 | 21 | ` 22 | ); 23 | -------------------------------------------------------------------------------- /src/arduino-uno-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@storybook/addon-actions'; 2 | import { withKnobs, boolean } from '@storybook/addon-knobs'; 3 | import { storiesOf } from '@storybook/web-components'; 4 | import { html } from 'lit'; 5 | import './arduino-uno-element'; 6 | 7 | storiesOf('Arduino Uno', module) 8 | .addParameters({ component: 'wokwi-arduino-uno' }) 9 | .addDecorator(withKnobs) 10 | .add( 11 | 'Uno R3', 12 | () => html` 13 | 21 | ` 22 | ) 23 | .add( 24 | 'Uno R3 (Large)', 25 | () => html` 26 | 35 | ` 36 | ); 37 | -------------------------------------------------------------------------------- /src/biaxial-stepper-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './biaxial-stepper-element'; 3 | 4 | export default { 5 | title: 'Biaxial Stepper', 6 | component: 'wokwi-biaxial-stepper', 7 | argTypes: { 8 | innerHandLength: { control: { type: 'range', min: 20, max: 70 } }, 9 | innerHandAngle: { control: { type: 'range', min: 0, max: 360 } }, 10 | innerHandColor: { control: { type: 'color' } }, 11 | innerHandShape: { options: ['arrow', 'plain', 'ornate'], control: { type: 'select' } }, 12 | outerHandLength: { control: { type: 'range', min: 20, max: 70 } }, 13 | outerHandAngle: { control: { type: 'range', min: 0, max: 360 } }, 14 | outerHandColor: { control: { type: 'color' } }, 15 | outerHandShape: { options: ['arrow', 'plain', 'ornate'], control: { type: 'select' } }, 16 | }, 17 | args: { 18 | outerHandLength: 20, 19 | outerHandAngle: 0, 20 | outerHandColor: 'grey', 21 | outerHandShape: 'plain', 22 | innerHandLength: 20, 23 | innerHandAngle: 0, 24 | innerHandColor: 'darkgrey', 25 | innerHandShape: 'plain', 26 | }, 27 | }; 28 | 29 | const Template = ({ 30 | innerHandLength, 31 | innerHandColor, 32 | innerHandShape, 33 | innerHandAngle, 34 | outerHandLength, 35 | outerHandColor, 36 | outerHandShape, 37 | outerHandAngle, 38 | }) => html``; 48 | 49 | export const Default = Template.bind({}); 50 | Default.args = { 51 | innerHandLength: 70, 52 | innerHandColor: 'silver', 53 | innerHandShape: 'plain', 54 | innerHandAngle: 90, 55 | outerHandLength: 70, 56 | outerHandColor: 'gold', 57 | outerHandShape: 'plain', 58 | outerHandAngle: 270, 59 | }; 60 | 61 | export const NineOclock = Template.bind({}); 62 | NineOclock.args = { 63 | innerHandLength: 70, 64 | innerHandColor: 'silver', 65 | innerHandShape: 'plain', 66 | innerHandAngle: 0, 67 | outerHandLength: 40, 68 | outerHandColor: 'gold', 69 | outerHandShape: 'plain', 70 | outerHandAngle: 270, 71 | }; 72 | 73 | export const SixOclock = Template.bind({}); 74 | SixOclock.args = { 75 | innerHandLength: 70, 76 | innerHandColor: 'silver', 77 | innerHandShape: 'plain', 78 | innerHandAngle: 0, 79 | outerHandLength: 40, 80 | outerHandColor: 'gold', 81 | outerHandShape: 'plain', 82 | outerHandAngle: 180, 83 | }; 84 | 85 | export const ThreeOclock = Template.bind({}); 86 | ThreeOclock.args = { 87 | innerHandLength: 70, 88 | innerHandColor: 'silver', 89 | innerHandShape: 'plain', 90 | innerHandAngle: 0, 91 | outerHandLength: 50, 92 | outerHandColor: 'gold', 93 | outerHandShape: 'plain', 94 | outerHandAngle: 90, 95 | }; 96 | 97 | export const TenPastTen = Template.bind({}); 98 | TenPastTen.args = { 99 | innerHandLength: 70, 100 | innerHandColor: 'silver', 101 | innerHandShape: 'plain', 102 | innerHandAngle: 60, 103 | outerHandLength: 60, 104 | outerHandColor: 'gold', 105 | outerHandShape: 'plain', 106 | outerHandAngle: 300, 107 | }; 108 | 109 | export const SameLength = Template.bind({}); 110 | SameLength.args = { 111 | innerHandLength: 30, 112 | innerHandColor: 'blue', 113 | innerHandShape: 'plain', 114 | innerHandAngle: 0, 115 | outerHandLength: 30, 116 | outerHandColor: 'green', 117 | outerHandShape: 'plain', 118 | outerHandAngle: 180, 119 | }; 120 | 121 | export const LongArrows = Template.bind({}); 122 | LongArrows.args = { 123 | innerHandLength: 70, 124 | innerHandColor: 'blue', 125 | innerHandShape: 'arrow', 126 | innerHandAngle: 90, 127 | outerHandLength: 70, 128 | outerHandColor: 'green', 129 | outerHandShape: 'arrow', 130 | outerHandAngle: 270, 131 | }; 132 | 133 | export const OrnateClock = Template.bind({}); 134 | OrnateClock.args = { 135 | innerHandLength: 70, 136 | innerHandColor: 'silver', 137 | innerHandShape: 'ornate', 138 | innerHandAngle: 60, 139 | outerHandLength: 60, 140 | outerHandColor: 'gold', 141 | outerHandShape: 'ornate', 142 | outerHandAngle: 300, 143 | }; 144 | -------------------------------------------------------------------------------- /src/big-sound-sensor-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './big-sound-sensor-element'; 3 | 4 | export default { 5 | title: 'Big Sound Sensor', 6 | component: 'wokwi-big-sound-sensor', 7 | argTypes: { 8 | led1: { control: { type: 'boolean' } }, 9 | led2: { control: { type: 'boolean' } }, 10 | }, 11 | args: { 12 | led1: false, 13 | led2: false, 14 | }, 15 | }; 16 | 17 | const Template = ({ led1, led2 }) => html``; 21 | 22 | export const Default = Template.bind({}); 23 | -------------------------------------------------------------------------------- /src/buzzer-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/web-components'; 2 | import { withKnobs, boolean } from '@storybook/addon-knobs'; 3 | import { html } from 'lit'; 4 | import './buzzer-element'; 5 | 6 | storiesOf('Buzzer', module) 7 | .addParameters({ component: 'wokwi-buzzer' }) 8 | .addDecorator(withKnobs) 9 | .add( 10 | 'Buzzer', 11 | () => 12 | html`
13 | ` 14 | ); 15 | -------------------------------------------------------------------------------- /src/buzzer-element.ts: -------------------------------------------------------------------------------- 1 | import { css, html, LitElement } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin } from './pin'; 4 | 5 | /** 6 | * Renders a piezo electric buzzer. 7 | */ 8 | @customElement('wokwi-buzzer') 9 | export class BuzzerElement extends LitElement { 10 | /** 11 | * Boolean representing if an electric signal is coming in the buzzer 12 | * If true a music note will be displayed on top of the buzzer 13 | */ 14 | @property() hasSignal = false; 15 | 16 | readonly pinInfo: ElementPin[] = [ 17 | { name: '1', x: 27, y: 84, signals: [] }, 18 | { name: '2', x: 37, y: 84, signals: [] }, 19 | ]; 20 | 21 | static get styles() { 22 | return css` 23 | :host { 24 | display: inline-block; 25 | } 26 | 27 | .buzzer-container { 28 | display: flex; 29 | flex-direction: column; 30 | width: 75px; 31 | } 32 | 33 | .music-note { 34 | position: relative; 35 | left: 40px; 36 | animation-duration: 1.5s; 37 | animation-name: animate-note; 38 | animation-iteration-count: infinite; 39 | animation-timing-function: linear; 40 | transform: scale(1.5); 41 | fill: blue; 42 | offset-path: path( 43 | 'm0 0c-0.9-0.92-1.8-1.8-2.4-2.8-0.56-0.92-0.78-1.8-0.58-2.8 0.2-0.92 0.82-1.8 1.6-2.8 0.81-0.92 1.8-1.8 2.6-2.8 0.81-0.92 1.4-1.8 1.6-2.8 0.2-0.92-0.02-1.8-0.58-2.8-0.56-0.92-1.5-1.8-2.4-2.8' 44 | ); 45 | offset-rotate: 0deg; 46 | } 47 | 48 | @keyframes animate-note { 49 | 0% { 50 | offset-distance: 0%; 51 | opacity: 0; 52 | } 53 | 10% { 54 | offset-distance: 10%; 55 | opacity: 1; 56 | } 57 | 75% { 58 | offset-distance: 75%; 59 | opacity: 1; 60 | } 61 | 100% { 62 | offset-distance: 100%; 63 | opacity: 0; 64 | } 65 | } 66 | `; 67 | } 68 | 69 | render() { 70 | const buzzerOn = this.hasSignal; 71 | return html` 72 |
73 | 81 | 84 | 85 | 86 | 93 | 94 | 95 | 96 | 97 | 98 | 106 | 114 | 115 | 116 | 117 | 118 |
119 | `; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/dht22-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/web-components'; 2 | import { html } from 'lit'; 3 | import './dht22-element'; 4 | 5 | storiesOf('DHT22', module) 6 | .addParameters({ component: 'wokwi-dht22' }) 7 | .add('Default', () => html``); 8 | -------------------------------------------------------------------------------- /src/dht22-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from 'lit'; 2 | import { customElement } from 'lit/decorators.js'; 3 | import { ElementPin } from './pin'; 4 | 5 | @customElement('wokwi-dht22') 6 | export class DHT22Element extends LitElement { 7 | readonly pinInfo: ElementPin[] = [ 8 | { name: 'VCC', x: 15, y: 114.9, signals: [{ type: 'power', signal: 'VCC' }], number: 1 }, 9 | { name: 'SDA', x: 24.5, y: 114.9, signals: [], number: 2 }, 10 | { name: 'NC', x: 34.1, y: 114.9, signals: [], number: 3 }, 11 | { name: 'GND', x: 43.8, y: 114.9, signals: [{ type: 'power', signal: 'GND' }], number: 4 }, 12 | ]; 13 | 14 | render() { 15 | return html` 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 36 | 45 | DHT22 46 | 47 | 48 | `; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/dip-switch-8-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './dip-switch-8-element'; 3 | 4 | export default { 5 | title: 'DIP Switch 8', 6 | component: 'wokwi-dip-switch-8', 7 | }; 8 | 9 | const Template = () => html``; 10 | 11 | export const Default = Template.bind({}); 12 | -------------------------------------------------------------------------------- /src/dip-switch-8-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement, svg } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin } from '.'; 4 | 5 | @customElement('wokwi-dip-switch-8') 6 | export class DipSwitch8Element extends LitElement { 7 | @property({ type: Array }) values = [0, 0, 0, 0, 0, 0, 0, 0]; 8 | 9 | readonly pinInfo: ElementPin[] = [ 10 | { name: '1a', number: 1, y: 51.3, x: 8.1, signals: [] }, 11 | { name: '2a', number: 2, y: 51.3, x: 17.7, signals: [] }, 12 | { name: '3a', number: 3, y: 51.3, x: 27.3, signals: [] }, 13 | { name: '4a', number: 4, y: 51.3, x: 36.9, signals: [] }, 14 | { name: '5a', number: 5, y: 51.3, x: 46.5, signals: [] }, 15 | { name: '6a', number: 6, y: 51.3, x: 56.1, signals: [] }, 16 | { name: '7a', number: 7, y: 51.3, x: 65.7, signals: [] }, 17 | { name: '8a', number: 8, y: 51.3, x: 75.3, signals: [] }, 18 | 19 | { name: '8b', number: 9, y: 3, x: 75.3, signals: [] }, 20 | { name: '7b', number: 10, y: 3, x: 65.7, signals: [] }, 21 | { name: '6b', number: 11, y: 3, x: 56.1, signals: [] }, 22 | { name: '5b', number: 12, y: 3, x: 46.5, signals: [] }, 23 | { name: '4b', number: 13, y: 3, x: 36.9, signals: [] }, 24 | { name: '3b', number: 14, y: 3, x: 27.3, signals: [] }, 25 | { name: '2b', number: 15, y: 3, x: 17.7, signals: [] }, 26 | { name: '1b', number: 16, y: 3, x: 8.1, signals: [] }, 27 | ]; 28 | 29 | /** 30 | * Change switch state 31 | * @param index Which switch to change 32 | */ 33 | private toggleSwitch(index: number) { 34 | this.values[index] = this.values[index] ? 0 : 1; 35 | this.dispatchEvent(new InputEvent('switch-change', { detail: index })); 36 | this.requestUpdate(); // force lit to render again 37 | } 38 | 39 | /** Change switch state by keyboard 1-8 press */ 40 | private onKeyDown(e: KeyboardEvent) { 41 | e.stopPropagation(); // stop storybook reacting to the key press 42 | const keys = ['1', '2', '3', '4', '5', '6', '7', '8']; 43 | const keyIndex = keys.indexOf(e.key); 44 | if (keyIndex !== -1) { 45 | this.toggleSwitch(keyIndex); 46 | } 47 | } 48 | 49 | private drawSwitch(index: number, x: number) { 50 | return svg` 51 | this.toggleSwitch(index)} 53 | x="${x + 4.693}" 54 | y="21.2" 55 | width="5.8168" 56 | height="13" 57 | /> 58 | this.toggleSwitch(index)} 60 | xlink:href="#switch" 61 | x="${x}" 62 | y=${this.values[index] ? -7.2 : 0} 63 | />`; 64 | } 65 | 66 | private preventTextSelection(e: MouseEvent) { 67 | if (e.detail > 1) { 68 | // On double click 69 | e.preventDefault(); 70 | } 71 | } 72 | 73 | render() { 74 | return html` 75 | 85 | 86 | 95 | 96 | 97 | 98 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | ON 131 | 1 132 | 2 133 | 3 134 | 4 135 | 5 136 | 6 137 | 7 138 | 8 139 | 140 | 141 | 142 | 143 | ${this.drawSwitch(0, 0)} 144 | ${this.drawSwitch(1, 9.6)} 145 | ${this.drawSwitch(2, 19.4)} 146 | ${this.drawSwitch(3, 29.1)} 147 | ${this.drawSwitch(4, 38.5)} 148 | ${this.drawSwitch(5, 48.1)} 149 | ${this.drawSwitch(6, 57.7)} 150 | ${this.drawSwitch(7, 67.3)} 151 | 152 | 153 | `; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/ds1307-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './ds1307-element'; 3 | 4 | export default { 5 | title: 'DS1307', 6 | component: 'wokwi-ds1307', 7 | }; 8 | 9 | export const DS1307 = () => html``; 10 | -------------------------------------------------------------------------------- /src/esp32-devkit-v1-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './esp32-devkit-v1-element'; 3 | 4 | export default { 5 | title: 'ESP32 Devkit V1', 6 | component: 'wokwi-esp32-devkit-v1', 7 | argTypes: { 8 | led1: { control: { type: 'boolean' } }, 9 | ledPower: { control: { type: 'boolean' } }, 10 | }, 11 | args: { 12 | led1: false, 13 | ledPower: false, 14 | }, 15 | }; 16 | 17 | const Template = ({ led1, ledPower }) => html``; 21 | 22 | export const Default = Template.bind({}); 23 | 24 | export const LEDsOn = Template.bind({}); 25 | LEDsOn.storyName = 'LEDs On'; 26 | LEDsOn.args = { led1: true, ledPower: true }; 27 | -------------------------------------------------------------------------------- /src/flame-sensor-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './flame-sensor-element'; 3 | 4 | export default { 5 | title: 'Flame Sensor', 6 | component: 'wokwi-flame-sensor', 7 | argTypes: { 8 | ledPower: { control: { type: 'boolean' } }, 9 | ledSignal: { control: { type: 'boolean' } }, 10 | }, 11 | args: { 12 | ledPower: true, 13 | ledSignal: false, 14 | }, 15 | }; 16 | 17 | const Template = ({ ledPower, ledSignal }) => 18 | html` `; 19 | 20 | export const Default = Template.bind({}); 21 | -------------------------------------------------------------------------------- /src/franzininho-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import { action } from '@storybook/addon-actions'; 3 | import './franzininho-element'; 4 | 5 | export default { 6 | title: 'Franzininho', 7 | component: 'wokwi-franzininho', 8 | argTypes: { 9 | ledPower: { control: { type: 'boolean' } }, 10 | led1: { control: { type: 'boolean' } }, 11 | }, 12 | args: { 13 | ledPower: true, 14 | led1: false, 15 | }, 16 | }; 17 | 18 | const Template = ({ ledPower, led1 }) => 19 | html` 25 | `; 26 | 27 | export const Default = Template.bind({}); 28 | -------------------------------------------------------------------------------- /src/gas-sensor-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './gas-sensor-element'; 3 | 4 | export default { 5 | title: 'Gas Sensor', 6 | component: 'wokwi-gas-sensor', 7 | argTypes: { 8 | ledPower: { control: { type: 'boolean' } }, 9 | ledD0: { control: { type: 'boolean' } }, 10 | }, 11 | args: { 12 | ledPower: true, 13 | ledD0: false, 14 | }, 15 | }; 16 | 17 | const Template = ({ ledPower, ledD0 }) => 18 | html` `; 19 | 20 | export const Default = Template.bind({}); 21 | -------------------------------------------------------------------------------- /src/gas-sensor-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement, svg } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin } from '.'; 4 | import { GND, VCC } from './pin'; 5 | 6 | @customElement('wokwi-gas-sensor') 7 | export class GasSensorElement extends LitElement { 8 | @property() ledPower = false; 9 | @property() ledD0 = false; 10 | 11 | readonly pinInfo: ElementPin[] = [ 12 | { name: 'AOUT', y: 16.5, x: 137, number: 1, signals: [] }, 13 | { name: 'DOUT', y: 26.4, x: 137, number: 2, signals: [] }, 14 | { name: 'GND', y: 36.5, x: 137, number: 3, signals: [GND()] }, 15 | { name: 'VCC', y: 46.2, x: 137, number: 4, signals: [VCC()] }, 16 | ]; 17 | 18 | render() { 19 | const { ledPower, ledD0 } = this; 20 | return html` 21 | 29 | 30 | 31 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | AOUT 66 | DOUT 67 | GND 68 | VCC 69 | 70 | 71 | 72 | 73 | 81 | 89 | ${ledPower && 90 | svg``} 91 | 99 | 107 | ${ledD0 && svg``} 108 | 109 | PWR LED 110 | D0 LED 111 | 112 | 113 | `; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/hc-sr04-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './hc-sr04-element'; 3 | 4 | export default { 5 | title: 'HC-SR04', 6 | component: 'wokwi-hc-sr04', 7 | }; 8 | 9 | export const HCSR04 = () => html``; 10 | -------------------------------------------------------------------------------- /src/hc-sr04-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from 'lit'; 2 | import { customElement } from 'lit/decorators.js'; 3 | import { ElementPin } from './pin'; 4 | 5 | @customElement('wokwi-hc-sr04') 6 | export class HCSR04Element extends LitElement { 7 | readonly pinInfo: ElementPin[] = [ 8 | { name: 'VCC', x: 71.3, y: 94.5, signals: [{ type: 'power', signal: 'VCC', voltage: 5 }] }, 9 | { name: 'TRIG', x: 81.3, y: 94.5, signals: [] }, 10 | { name: 'ECHO', x: 91.3, y: 94.5, signals: [] }, 11 | { name: 'GND', x: 101.3, y: 94.5, signals: [{ type: 'power', signal: 'GND' }] }, 12 | ]; 13 | 14 | render() { 15 | return html` 16 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | HC-SR04 83 | 84 | 91 | VCC 92 | TRIG 93 | ECHO 94 | GND 95 | 96 | 97 | `; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/heart-beat-sensor-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './heart-beat-sensor-element'; 3 | 4 | export default { 5 | title: 'Heart Beat Sensor', 6 | component: 'wokwi-heart-beat-sensor', 7 | }; 8 | 9 | const Template = () => html``; 10 | 11 | export const Default = Template.bind({}); 12 | -------------------------------------------------------------------------------- /src/hx711-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './hx711-element'; 3 | 4 | export default { 5 | title: 'HX711', 6 | component: 'wokwi-hx711', 7 | }; 8 | 9 | const Template = ({ width, height, type = '50kg' }) => 10 | html``; 11 | 12 | export const LoadCell50kg = Template.bind({}); 13 | LoadCell50kg.args = { type: '50kg', width: 580, height: 430 }; 14 | 15 | export const LoadCell5kg = Template.bind({}); 16 | LoadCell5kg.args = { type: '5kg', width: 507, height: 269 }; 17 | 18 | export const GaugePressure = Template.bind({}); 19 | GaugePressure.args = { type: 'gauge', width: 509, height: 200 }; 20 | -------------------------------------------------------------------------------- /src/ili9341-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import { drawWokwiW } from './utils/logo'; 3 | import './ili9341-element'; 4 | 5 | export default { 6 | title: 'ILI9341', 7 | component: 'wokwi-ili9341', 8 | argTypes: { 9 | flipHorizontal: { control: { type: 'boolean' } }, 10 | flipVertical: { control: { type: 'boolean' } }, 11 | }, 12 | args: { 13 | flipHorizontal: false, 14 | flipVertical: false, 15 | }, 16 | }; 17 | 18 | function drawLogo(canvas: HTMLCanvasElement) { 19 | const ctx = canvas.getContext('2d'); 20 | if (!ctx) { 21 | return; 22 | } 23 | setInterval(() => { 24 | ctx.fillStyle = '#ddd'; 25 | ctx.fillRect(0, 0, canvas.width, canvas.height); 26 | drawWokwiW(ctx, 6, 120 * Math.random(), 220 * Math.random()); 27 | }, 1000); 28 | } 29 | 30 | export const Default = () => html` `; 31 | 32 | export const Logo = ({ flipHorizontal, flipVertical }) => 33 | html` 34 | drawLogo(e.target.canvas)} 36 | .flipHorizontal=${flipHorizontal} 37 | .flipVertical=${flipVertical} 38 | > 39 | `; 40 | -------------------------------------------------------------------------------- /src/ili9341-element.ts: -------------------------------------------------------------------------------- 1 | import { css, html, LitElement } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin, spi } from './pin'; 4 | 5 | @customElement('wokwi-ili9341') 6 | export class ILI9341Element extends LitElement { 7 | readonly screenWidth = 240; 8 | readonly screenHeight = 320; 9 | 10 | @property() flipHorizontal = false; 11 | @property() flipVertical = false; 12 | 13 | readonly pinInfo: ElementPin[] = [ 14 | { name: 'VCC', x: 48.3, y: 287.2, signals: [{ type: 'power', signal: 'VCC' }] }, 15 | { name: 'GND', x: 57.9012, y: 287.2, signals: [{ type: 'power', signal: 'GND' }] }, 16 | { name: 'CS', x: 67.5024, y: 287.2, signals: [spi('SS')] }, 17 | { name: 'RST', x: 77.1036, y: 287.2, signals: [] }, 18 | { name: 'D/C', x: 86.7048, y: 287.2, signals: [] }, 19 | { name: 'MOSI', x: 96.306, y: 287.2, signals: [spi('MOSI')] }, 20 | { name: 'SCK', x: 105.9072, y: 287.2, signals: [spi('SCK')] }, 21 | { name: 'LED', x: 115.5084, y: 287.2, signals: [] }, 22 | { name: 'MISO', x: 125.1096, y: 287.2, signals: [spi('MISO')] }, 23 | ]; 24 | 25 | static get styles() { 26 | return css` 27 | .container { 28 | position: relative; 29 | } 30 | 31 | .container > canvas { 32 | position: absolute; 33 | left: 8px; 34 | top: 28px; 35 | width: 159px; 36 | height: 212px; 37 | } 38 | 39 | .pixelated { 40 | image-rendering: crisp-edges; /* firefox */ 41 | image-rendering: pixelated; /* chrome/webkit */ 42 | } 43 | `; 44 | } 45 | 46 | get canvas() { 47 | return this.shadowRoot?.querySelector('canvas'); 48 | } 49 | 50 | firstUpdated() { 51 | this.dispatchEvent(new CustomEvent('canvas-ready')); 52 | } 53 | 54 | render() { 55 | const { screenWidth, screenHeight, flipHorizontal, flipVertical } = this; 56 | const flip = flipHorizontal || flipVertical; 57 | const scaleX = flipHorizontal ? -1 : 1; 58 | const scaleY = flipVertical ? -1 : 1; 59 | const canvasStyle = flip ? `transform: scaleX(${scaleX}) scaleY(${scaleY});` : ''; 60 | return html` 61 |
62 | 69 | 70 | 75 | 76 | 77 | 78 | 88 | 89 | 98 | 99 | 100 | 101 | 104 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 1 118 | 9 119 | ILI9341 120 | 121 | 122 | 128 |
129 | `; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './react-types'; 2 | export { ElementPin, PinSignalInfo } from './pin'; 3 | export { SevenSegmentElement } from './7segment-element'; 4 | export { ArduinoUnoElement } from './arduino-uno-element'; 5 | export { LCD1602Element } from './lcd1602-element'; 6 | export { fontA00 } from './lcd1602-font-a00'; 7 | export { fontA02 } from './lcd1602-font-a02'; 8 | export { LEDElement } from './led-element'; 9 | export { NeoPixelElement } from './neopixel-element'; 10 | export { PushbuttonElement } from './pushbutton-element'; 11 | export { Pushbutton6mmElement } from './pushbutton-6mm-element'; 12 | export { ResistorElement } from './resistor-element'; 13 | export { MembraneKeypadElement } from './membrane-keypad-element'; 14 | export { PotentiometerElement } from './potentiometer-element'; 15 | export { NeopixelMatrixElement } from './neopixel-matrix-element'; 16 | export { SSD1306Element } from './ssd1306-element'; 17 | export { BuzzerElement } from './buzzer-element'; 18 | export { RotaryDialerElement } from './rotary-dialer-element'; 19 | export { ServoElement } from './servo-element'; 20 | export { DHT22Element as Dht22Element } from './dht22-element'; 21 | export { ArduinoMegaElement } from './arduino-mega-element'; 22 | export { ArduinoNanoElement } from './arduino-nano-element'; 23 | export { Ds1307Element } from './ds1307-element'; 24 | export { LEDRingElement } from './led-ring-element'; 25 | export { SlideSwitchElement } from './slide-switch-element'; 26 | export { HCSR04Element } from './hc-sr04-element'; 27 | export { LCD2004Element } from './lcd2004-element'; 28 | export { AnalogJoystickElement } from './analog-joystick-element'; 29 | export { SlidePotentiometerElement } from './slide-potentiometer-element'; 30 | export { IRReceiverElement } from './ir-receiver-element'; 31 | export { IRRemoteElement } from './ir-remote-element'; 32 | export { PIRMotionSensorElement } from './pir-motion-sensor-element'; 33 | export { NTCTemperatureSensorElement } from './ntc-temperature-sensor-element'; 34 | export { HeartBeatSensorElement } from './heart-beat-sensor-element'; 35 | export { TiltSwitchElement } from './tilt-switch-element'; 36 | export { FlameSensorElement } from './flame-sensor-element'; 37 | export { GasSensorElement } from './gas-sensor-element'; 38 | export { FranzininhoElement } from './franzininho-element'; 39 | export { NanoRP2040ConnectElement } from './nano-rp2040-connect-element'; 40 | export { SmallSoundSensorElement } from './small-sound-sensor-element'; 41 | export { BigSoundSensorElement } from './big-sound-sensor-element'; 42 | export { MPU6050Element } from './mpu6050-element'; 43 | export { ESP32DevkitV1Element } from './esp32-devkit-v1-element'; 44 | export { KY040Element } from './ky-040-element'; 45 | export { PhotoresistorSensorElement } from './photoresistor-sensor-element'; 46 | export { RGBLedElement } from './rgb-led-element'; 47 | export { ILI9341Element } from './ili9341-element'; 48 | export { LedBarGraphElement } from './led-bar-graph-element'; 49 | export { MicrosdCardElement } from './microsd-card-element'; 50 | export { DipSwitch8Element } from './dip-switch-8-element'; 51 | export { StepperMotorElement } from './stepper-motor-element'; 52 | export { HX711Element } from './hx711-element'; 53 | export { KS2EMDC5Element } from './ks2e-m-dc5-element'; 54 | export { BiaxialStepperElement } from './biaxial-stepper-element'; 55 | -------------------------------------------------------------------------------- /src/ir-receiver-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './ir-receiver-element'; 3 | 4 | export default { 5 | title: 'IR Receiver', 6 | component: 'wokwi-ir-receiver', 7 | }; 8 | 9 | export const Default = () => html``; 10 | -------------------------------------------------------------------------------- /src/ir-receiver-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from 'lit'; 2 | import { customElement } from 'lit/decorators.js'; 3 | import { ElementPin, GND, VCC } from './pin'; 4 | 5 | @customElement('wokwi-ir-receiver') 6 | export class IRReceiverElement extends LitElement { 7 | readonly pinInfo: ElementPin[] = [ 8 | { name: 'GND', y: 87.75, x: 20.977, number: 1, signals: [GND()] }, 9 | { name: 'VCC', y: 87.75, x: 30.578, number: 2, signals: [VCC()] }, 10 | { name: 'DAT', y: 87.75, x: 40.18, number: 3, signals: [] }, 11 | ]; 12 | 13 | render() { 14 | return html` 15 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 49 | 52 | 53 | 54 | DAT 55 | VCC 56 | GND 57 | 58 | 62 | IR Reciever 63 | 64 | 65 | 66 | 67 | 71 | 75 | 79 | 80 | 85 | 90 | 91 | 94 | 95 | 96 | 100 | 101 | 102 | 103 | 104 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 103 122 | 102 123 | 124 | 125 | 126 | 127 | `; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/ir-remote-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@storybook/addon-actions'; 2 | import { html } from 'lit'; 3 | import './ir-remote-element'; 4 | 5 | export default { 6 | title: 'IR Remote', 7 | component: 'wokwi-ir-remote', 8 | }; 9 | 10 | export const Default = () => html``; 14 | -------------------------------------------------------------------------------- /src/ks2e-m-dc5-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './ks2e-m-dc5-element'; 3 | 4 | export default { 5 | title: 'KS2E-M-DC5', 6 | component: 'wokwi-ks2e-m-dc5', 7 | }; 8 | 9 | const Template = () => html``; 10 | 11 | export const Default = Template.bind({}); 12 | -------------------------------------------------------------------------------- /src/ks2e-m-dc5-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from 'lit'; 2 | import { customElement } from 'lit/decorators.js'; 3 | import { ElementPin } from './pin'; 4 | 5 | const y1Pos = 5.1; 6 | const y2Pos = 32.7; 7 | const x1Pos = 5.5; 8 | const x2Pos = 25; 9 | const x3Pos = 45; 10 | const x4Pos = 74; 11 | 12 | @customElement('wokwi-ks2e-m-dc5') 13 | export class KS2EMDC5Element extends LitElement { 14 | readonly pinInfo: ElementPin[] = [ 15 | { name: 'NO2', x: x1Pos, y: y1Pos, signals: [], number: 8 }, 16 | { name: 'NC2', x: x2Pos, y: y1Pos, signals: [], number: 6 }, 17 | { name: 'P2', x: x3Pos, y: y1Pos, signals: [], number: 4 }, 18 | { name: 'COIL2', x: x4Pos, y: y1Pos, signals: [{ type: 'power', signal: 'GND' }], number: 1 }, 19 | { name: 'NO1', x: x1Pos, y: y2Pos, signals: [], number: 9 }, 20 | { name: 'NC1', x: x2Pos, y: y2Pos, signals: [], number: 11 }, 21 | { name: 'P1', x: x3Pos, y: y2Pos, signals: [], number: 13 }, 22 | { name: 'COIL1', x: x4Pos, y: y2Pos, signals: [], number: 16 }, 23 | ]; 24 | 25 | render() { 26 | return html` 27 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | KS2E-M-DC5 50 | 51 | 52 | `; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ky-040-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@storybook/addon-actions'; 2 | import { html } from 'lit'; 3 | import './ky-040-element'; 4 | 5 | export default { 6 | title: 'KY040', 7 | component: 'wokwi-ky-040', 8 | argTypes: { 9 | angle: { control: { type: 'range', min: 0, max: 360 } }, 10 | }, 11 | args: { 12 | angle: 0, 13 | }, 14 | }; 15 | 16 | const Template = ({ angle }) => 17 | html` 24 | `; 25 | 26 | export const Default = Template.bind({}); 27 | -------------------------------------------------------------------------------- /src/lcd1602-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { withKnobs, text, number, boolean } from '@storybook/addon-knobs'; 2 | import { storiesOf } from '@storybook/web-components'; 3 | import { html } from 'lit'; 4 | import { fontA02 } from './lcd1602-font-a02'; 5 | import './lcd1602-element'; 6 | 7 | const helloWorld = 'Hello, World!'; 8 | const symbols = '\x10 I \x9d Symbols! \x11\xab \x14\x18\x17\x1e \x91\x98\x96 \x93\x97\xa9 \xbb'; 9 | 10 | storiesOf('LCD1602', module) 11 | .addParameters({ component: 'wokwi-lcd1602' }) 12 | .addDecorator(withKnobs) 13 | .add( 14 | 'Hello, World!', 15 | () => html` 16 | 24 | ` 25 | ) 26 | .add( 27 | 'White on blue', 28 | () => html` 29 | 35 | ` 36 | ) 37 | .add( 38 | 'Blinking cursor', 39 | () => html` 40 | 41 | ` 42 | ) 43 | .add( 44 | 'Cursor', 45 | () => html` 46 | 47 | ` 48 | ) 49 | .add( 50 | 'Display off (green)', // 51 | () => html`` 52 | ) 53 | .add( 54 | 'Display off (blue)', 55 | () => html` 56 | 57 | ` 58 | ) 59 | .add( 60 | 'Font A02', 61 | () => 62 | html` 63 | 71 | ` 72 | ) 73 | .add( 74 | 'I²C pins', 75 | () => html` ` 76 | ) 77 | .add( 78 | 'No pins', 79 | () => 80 | html` ` 81 | ); 82 | -------------------------------------------------------------------------------- /src/lcd2004-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import { fontA02 } from './lcd1602-font-a02'; 3 | import './lcd2004-element'; 4 | 5 | export default { 6 | title: 'LCD2004', 7 | component: 'wokwi-lcd2004', 8 | }; 9 | 10 | export const Lcd2004 = () => html``; 11 | Lcd2004.storyName = 'Hello World'; 12 | 13 | export const BlueBackground = () => 14 | html``; 19 | 20 | export const I2cPins = () => html``; 25 | I2cPins.storyName = 'I2C Pins'; 26 | -------------------------------------------------------------------------------- /src/lcd2004-element.ts: -------------------------------------------------------------------------------- 1 | import { customElement } from 'lit/decorators.js'; 2 | import { LCD1602Element } from './lcd1602-element'; 3 | 4 | @customElement('wokwi-lcd2004') 5 | export class LCD2004Element extends LCD1602Element { 6 | protected numCols = 20; 7 | protected numRows = 4; 8 | } 9 | -------------------------------------------------------------------------------- /src/led-bar-graph-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './led-bar-graph-element'; 3 | 4 | export default { 5 | title: 'Led Bar Graph', 6 | component: 'wokwi-led-bar-graph', 7 | argTypes: { 8 | color: { control: { type: 'color' } }, 9 | values: 'string', 10 | }, 11 | args: { 12 | values: '[1, 0, 1, 0, 1, 0, 1, 0, 1, 0]', 13 | color: 'red', 14 | }, 15 | }; 16 | 17 | const Template = ({ color, values }) => 18 | html``; 19 | 20 | export const Red = Template.bind({}); 21 | Red.args = { color: 'red' }; 22 | 23 | export const Green = Template.bind({}); 24 | Green.args = { color: 'lime' }; 25 | 26 | export const Off = Template.bind({}); 27 | Off.args = { color: 'lime', values: '[]' }; 28 | 29 | export const SpecialGYR = Template.bind({}); 30 | SpecialGYR.args = { color: 'GYR', values: '[1,1,1,1,1,1,1,1,1,1]' }; 31 | 32 | export const SpecialBCYR = Template.bind({}); 33 | SpecialBCYR.args = { color: 'BCYR', values: '[1,1,1,1,1,1,1,1,1,1]' }; 34 | -------------------------------------------------------------------------------- /src/led-bar-graph-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement, svg } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin } from './pin'; 4 | import { mmToPix } from './utils/units'; 5 | 6 | const segments = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 7 | const mm = mmToPix; 8 | const anodeX = 1.27 * mm; 9 | const cathodeX = 8.83 * mm; 10 | 11 | const green = '#9eff3c'; 12 | const blue = '#2c95fa'; 13 | const cyan = '#6cf9dc'; 14 | const yellow = '#f1d73c'; 15 | const red = '#dc012d'; 16 | 17 | const colorPalettes: Record = { 18 | GYR: [green, green, green, green, green, yellow, yellow, yellow, red, red], 19 | BCYR: [blue, cyan, cyan, cyan, cyan, yellow, yellow, yellow, red, red], 20 | }; 21 | 22 | @customElement('wokwi-led-bar-graph') 23 | export class LedBarGraphElement extends LitElement { 24 | /** The color of a lit segment. Either HTML color or the special values GYR / BCYR */ 25 | @property() color = 'red'; 26 | 27 | /** The color of an unlit segment */ 28 | @property() offColor = '#444'; 29 | 30 | readonly pinInfo: ElementPin[] = [ 31 | { name: 'A1', x: anodeX, y: 1.27 * mm, number: 1, description: 'Anode 1', signals: [] }, 32 | { name: 'A2', x: anodeX, y: 3.81 * mm, number: 2, description: 'Anode 2', signals: [] }, 33 | { name: 'A3', x: anodeX, y: 6.35 * mm, number: 3, description: 'Anode 3', signals: [] }, 34 | { name: 'A4', x: anodeX, y: 8.89 * mm, number: 4, description: 'Anode 4', signals: [] }, 35 | { name: 'A5', x: anodeX, y: 11.43 * mm, number: 5, description: 'Anode 5', signals: [] }, 36 | { name: 'A6', x: anodeX, y: 13.97 * mm, number: 6, description: 'Anode 6', signals: [] }, 37 | { name: 'A7', x: anodeX, y: 16.51 * mm, number: 7, description: 'Anode 7', signals: [] }, 38 | { name: 'A8', x: anodeX, y: 19.05 * mm, number: 8, description: 'Anode 8', signals: [] }, 39 | { name: 'A9', x: anodeX, y: 21.59 * mm, number: 9, description: 'Anode 9', signals: [] }, 40 | { name: 'A10', x: anodeX, y: 24.13 * mm, number: 10, description: 'Anode 10', signals: [] }, 41 | { name: 'C1', x: cathodeX, y: 1.27 * mm, number: 20, description: 'Cathode 1', signals: [] }, 42 | { name: 'C2', x: cathodeX, y: 3.81 * mm, number: 19, description: 'Cathode 2', signals: [] }, 43 | { name: 'C3', x: cathodeX, y: 6.35 * mm, number: 18, description: 'Cathode 3', signals: [] }, 44 | { name: 'C4', x: cathodeX, y: 8.89 * mm, number: 17, description: 'Cathode 4', signals: [] }, 45 | { name: 'C5', x: cathodeX, y: 11.43 * mm, number: 16, description: 'Cathode 5', signals: [] }, 46 | { name: 'C6', x: cathodeX, y: 13.97 * mm, number: 15, description: 'Cathode 6', signals: [] }, 47 | { name: 'C7', x: cathodeX, y: 16.51 * mm, number: 14, description: 'Cathode 7', signals: [] }, 48 | { name: 'C8', x: cathodeX, y: 19.05 * mm, number: 13, description: 'Cathode 8', signals: [] }, 49 | { name: 'C9', x: cathodeX, y: 21.59 * mm, number: 12, description: 'Cathode 9', signals: [] }, 50 | { name: 'C10', x: cathodeX, y: 24.13 * mm, number: 11, description: 'Cathode 10', signals: [] }, 51 | ]; 52 | 53 | /** 54 | * The values for the individual segments: 1 for a lit segment, and 0 for 55 | * an unlit segment. 56 | */ 57 | @property({ type: Array }) values: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 58 | 59 | render() { 60 | const { values, color, offColor } = this; 61 | const palette = colorPalettes[color]; 62 | return html` 63 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ${segments.map( 77 | (index) => 78 | svg`` 81 | )} 82 | 83 | `; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/led-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './led-element'; 3 | 4 | export default { 5 | title: 'LED', 6 | component: 'wokwi-led', 7 | argTypes: { 8 | value: { control: 'boolean' }, 9 | brightness: { control: { type: 'range', min: 0, max: 1.0, step: 0.05 } }, 10 | color: { control: { type: 'color' } }, 11 | lightColor: { control: { type: 'color' } }, 12 | label: 'string', 13 | flip: { control: 'boolean' }, 14 | }, 15 | args: { 16 | brightness: 1.0, 17 | flip: false, 18 | value: false, 19 | }, 20 | }; 21 | 22 | const Template = ({ color, flip, label, lightColor, value }) => 23 | html``; 30 | 31 | export const Red = Template.bind({}); 32 | Red.args = { color: 'red' }; 33 | 34 | export const RedWithLabel = Template.bind({}); 35 | RedWithLabel.args = { color: 'red', label: '12' }; 36 | 37 | export const Flipped = Template.bind({}); 38 | Flipped.args = { color: 'red', flip: true }; 39 | 40 | export const Green = Template.bind({}); 41 | Green.args = { color: 'green' }; 42 | 43 | export const Yellow = Template.bind({}); 44 | Yellow.args = { color: 'yellow' }; 45 | 46 | export const Blue = Template.bind({}); 47 | Blue.args = { color: 'blue' }; 48 | 49 | export const Orange = Template.bind({}); 50 | Orange.args = { color: 'orange' }; 51 | 52 | export const White = Template.bind({}); 53 | White.args = { color: 'white' }; 54 | 55 | export const BrightnessLevels = () => 56 | html` 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | `; 65 | -------------------------------------------------------------------------------- /src/led-element.ts: -------------------------------------------------------------------------------- 1 | import { css, html, LitElement } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin } from './pin'; 4 | 5 | const lightColors: { [key: string]: string } = { 6 | red: '#ff8080', 7 | green: '#80ff80', 8 | blue: '#8080ff', 9 | yellow: '#ffff80', 10 | orange: '#ffcf80', 11 | white: '#ffffff', 12 | purple: '#ff80ff', 13 | }; 14 | 15 | @customElement('wokwi-led') 16 | export class LEDElement extends LitElement { 17 | @property() value = false; 18 | @property() brightness = 1.0; 19 | @property() color = 'red'; 20 | @property() lightColor: string | null = null; 21 | @property() label = ''; 22 | @property({ type: Boolean }) flip = false; 23 | 24 | get pinInfo(): ElementPin[] { 25 | const anodeX = this.flip ? 15 : 25; 26 | const cathodeX = this.flip ? 25 : 15; 27 | 28 | return [ 29 | { name: 'A', x: anodeX, y: 42, signals: [], description: 'Anode' }, 30 | { name: 'C', x: cathodeX, y: 42, signals: [], description: 'Cathode' }, 31 | ]; 32 | } 33 | 34 | static get styles() { 35 | return css` 36 | :host { 37 | display: inline-block; 38 | } 39 | 40 | .led-container { 41 | display: flex; 42 | flex-direction: column; 43 | width: 40px; 44 | } 45 | 46 | .led-label { 47 | font-size: 10px; 48 | text-align: center; 49 | color: gray; 50 | position: relative; 51 | line-height: 1; 52 | top: -8px; 53 | } 54 | `; 55 | } 56 | 57 | update(changedProperties: Map) { 58 | if (changedProperties.has('flip')) { 59 | this.dispatchEvent(new CustomEvent('pininfo-change')); 60 | } 61 | super.update(changedProperties); 62 | } 63 | 64 | render() { 65 | const { color, lightColor, flip } = this; 66 | const lightColorActual = lightColor || lightColors[color?.toLowerCase()] || color; 67 | const opacity = this.brightness ? 0.3 + this.brightness * 0.7 : 0; 68 | const lightOn = this.value && this.brightness > Number.EPSILON; 69 | const xScale = flip ? -1 : 1; 70 | 71 | return html` 72 |
73 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 92 | 93 | 97 | 102 | 107 | 108 | 112 | 116 | 117 | 118 | 122 | 126 | 131 | 132 | 136 | 140 | 144 | 145 | 146 | 155 | 156 | 165 | 166 | 167 | ${this.label} 168 |
169 | `; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/led-ring-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './led-ring-element'; 3 | 4 | export default { 5 | title: 'LED Ring', 6 | component: 'wokwi-led-ring', 7 | argTypes: { 8 | animation: { control: 'boolean' }, 9 | pixels: { control: { type: 'number', min: 1, max: 64, step: 1 } }, 10 | pixelSpacing: { control: { type: 'range', min: 0, max: 10, step: 0.1 } }, 11 | background: { control: { type: 'color' } }, 12 | pinInfo: { control: { type: null } }, 13 | }, 14 | args: { 15 | background: '#363', 16 | pixels: 16, 17 | pixelSpacing: 0, 18 | animation: true, 19 | }, 20 | }; 21 | 22 | const Template = ({ animation, background, pixels, pixelSpacing }) => 23 | html``; 29 | 30 | export const Ring8 = Template.bind({}); 31 | Ring8.args = { pixels: 8 }; 32 | 33 | export const Ring12 = Template.bind({}); 34 | Ring12.args = { pixels: 12 }; 35 | 36 | export const Ring16 = Template.bind({}); 37 | Ring16.args = { pixels: 16 }; 38 | 39 | export const Ring24 = Template.bind({}); 40 | Ring24.args = { pixels: 24 }; 41 | -------------------------------------------------------------------------------- /src/led-ring-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement, svg } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin } from './pin'; 4 | import { RGB } from './types/rgb'; 5 | import { mmToPix } from './utils/units'; 6 | 7 | const pinHeight = 3; 8 | const pcbWidth = 6; 9 | 10 | @customElement('wokwi-led-ring') 11 | export class LEDRingElement extends LitElement { 12 | /** 13 | * Number of pixels to in the LED ring 14 | */ 15 | @property() pixels = 16; 16 | 17 | /** 18 | * Space between pixels (in mm) 19 | */ 20 | @property({ type: Number }) pixelSpacing = 0; 21 | 22 | /** 23 | * Background (PCB) color 24 | */ 25 | @property() background = '#363'; 26 | 27 | /** 28 | * Animate the LEDs in the matrix. Used primarily for testing in Storybook. 29 | * The animation sequence is not guaranteed and may change in future releases of 30 | * this element. 31 | */ 32 | @property() animation = false; 33 | 34 | private pixelElements: SVGCircleElement[] | null = null; 35 | 36 | private animationFrame: number | null = null; 37 | 38 | get radius() { 39 | return ((this.pixelSpacing + 5) * this.pixels) / 2 / Math.PI + pcbWidth; 40 | } 41 | 42 | get pinInfo(): ElementPin[] { 43 | const { radius } = this; 44 | const pinSpacing = 2.54; 45 | const y = (radius * 2 + pinHeight) * mmToPix; 46 | const cx = radius * mmToPix; 47 | const p = pinSpacing * mmToPix; 48 | 49 | return [ 50 | { 51 | name: 'GND', 52 | x: cx - 1.5 * p, 53 | y, 54 | signals: [{ type: 'power', signal: 'GND' }], 55 | }, 56 | { name: 'VCC', x: cx - 0.5 * p, y, signals: [{ type: 'power', signal: 'VCC' }] }, 57 | { name: 'DIN', x: cx + 0.5 * p, y, signals: [] }, 58 | { name: 'DOUT', x: cx + 1.5 * p, y, signals: [] }, 59 | ]; 60 | } 61 | 62 | private getPixelElements() { 63 | if (!this.shadowRoot) { 64 | return null; 65 | } 66 | if (!this.pixelElements) { 67 | this.pixelElements = Array.from(this.shadowRoot.querySelectorAll('rect.pixel')); 68 | } 69 | return this.pixelElements; 70 | } 71 | 72 | setPixel(pixel: number, { r, g, b }: RGB) { 73 | const pixelElements = this.getPixelElements(); 74 | if (!pixelElements) { 75 | return; 76 | } 77 | 78 | if (pixel < 0 || pixel >= pixelElements.length) { 79 | return; 80 | } 81 | pixelElements[pixel].style.fill = `rgb(${r * 255},${g * 255},${b * 255})`; 82 | } 83 | 84 | /** 85 | * Resets all the pixels to off state (r=0, g=0, b=0). 86 | */ 87 | reset() { 88 | const pixelElements = this.getPixelElements(); 89 | for (const element of pixelElements ?? []) { 90 | element.style.fill = ''; 91 | } 92 | } 93 | 94 | private animateStep = () => { 95 | const time = new Date().getTime(); 96 | const { pixels } = this; 97 | const pixelValue = (n: number) => (n % 2000 > 1000 ? 1 - (n % 1000) / 1000 : (n % 1000) / 1000); 98 | for (let pixel = 0; pixel < pixels; pixel++) { 99 | this.setPixel(pixel, { 100 | r: pixelValue(pixel * 100 + time), 101 | g: pixelValue(pixel * 100 + time + 200), 102 | b: pixelValue(pixel * 100 + time + 400), 103 | }); 104 | } 105 | this.animationFrame = requestAnimationFrame(this.animateStep); 106 | }; 107 | 108 | update(changedProperties: Map) { 109 | if (changedProperties.has('pixels') || changedProperties.has('pixelSpacing')) { 110 | this.dispatchEvent(new CustomEvent('pininfo-change')); 111 | } 112 | super.update(changedProperties); 113 | } 114 | 115 | updated() { 116 | if (this.animation && !this.animationFrame) { 117 | this.animationFrame = requestAnimationFrame(this.animateStep); 118 | } else if (!this.animation && this.animationFrame) { 119 | cancelAnimationFrame(this.animationFrame); 120 | this.animationFrame = null; 121 | } 122 | } 123 | 124 | render() { 125 | const { pixels, radius, background } = this; 126 | const pixelElements = []; 127 | const width = radius * 2; 128 | const height = radius * 2 + pinHeight; 129 | for (let i = 0; i < pixels; i++) { 130 | const angle = (i / pixels) * 360; 131 | pixelElements.push( 132 | svg`` 142 | ); 143 | } 144 | this.pixelElements = null; // Invalidate element cache 145 | 146 | return html` 147 | 154 | 155 | 156 | 157 | 158 | 159 | 165 | 173 | ${pixelElements} 174 | 175 | `; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/membrane-keypad-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@storybook/addon-actions'; 2 | import { storiesOf } from '@storybook/web-components'; 3 | import { html } from 'lit'; 4 | import './membrane-keypad-element'; 5 | 6 | storiesOf('Membrane Keypad', module) 7 | .addParameters({ component: 'wokwi-membrane-keypad' }) 8 | .add( 9 | 'Default', 10 | () => html` 11 | 15 | ` 16 | ) 17 | .add( 18 | 'With connector', 19 | () => html` 20 | 25 | ` 26 | ) 27 | .add( 28 | 'Custom keys', 29 | () => html` 30 | 35 | ` 36 | ) 37 | .add( 38 | 'Three columns', 39 | () => html` 40 | 45 | ` 46 | ) 47 | .add( 48 | 'Three columns + connector', 49 | () => html` 50 | 56 | ` 57 | ); 58 | -------------------------------------------------------------------------------- /src/microsd-card-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './microsd-card-element'; 3 | 4 | export default { 5 | title: 'microSD Card', 6 | component: 'wokwi-microsd-card', 7 | }; 8 | 9 | const Template = () => html``; 10 | 11 | export const Default = Template.bind({}); 12 | -------------------------------------------------------------------------------- /src/microsd-card-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from 'lit'; 2 | import { customElement } from 'lit/decorators.js'; 3 | import { ElementPin } from '.'; 4 | import { spi } from './pin'; 5 | 6 | @customElement('wokwi-microsd-card') 7 | export class MicrosdCardElement extends LitElement { 8 | readonly pinInfo: ElementPin[] = [ 9 | { name: 'CD', x: 76.734, y: 9.3744, signals: [] }, 10 | { name: 'DO', x: 76.734, y: 18.8622, signals: [spi('MISO')] }, 11 | { name: 'GND', x: 76.734, y: 28.4634, signals: [{ type: 'power', signal: 'GND' }] }, 12 | { name: 'SCK', x: 76.734, y: 38.178, signals: [spi('SCK')] }, 13 | { name: 'VCC', x: 76.734, y: 47.628, signals: [{ type: 'power', signal: 'VCC' }] }, 14 | { name: 'DI', x: 76.734, y: 57.456, signals: [spi('MOSI')] }, 15 | { name: 'CS', x: 76.734, y: 66.906, signals: [spi('SS')] }, 16 | ]; 17 | 18 | render() { 19 | return html` 20 | 27 | 28 | 29 | 30 | 31 | 40 | 41 | 45 | 56 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | CD 82 | DO 83 | GND 84 | SCK 85 | VCC 86 | DI 87 | CS 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | `; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/mpu6050-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './mpu6050-element'; 3 | 4 | export default { 5 | title: 'MPU6050', 6 | component: 'wokwi-mpu6050', 7 | argTypes: { 8 | led1: { control: { type: 'boolean' } }, 9 | }, 10 | args: { 11 | led1: false, 12 | }, 13 | }; 14 | 15 | const Template = ({ led1 }) => html` `; 16 | 17 | export const Default = Template.bind({}); 18 | -------------------------------------------------------------------------------- /src/mpu6050-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement, svg } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin } from '.'; 4 | import { GND, i2c, VCC } from './pin'; 5 | 6 | @customElement('wokwi-mpu6050') 7 | export class MPU6050Element extends LitElement { 8 | @property() led1 = false; 9 | 10 | readonly pinInfo: ElementPin[] = [ 11 | { name: 'INT', x: 7.28, y: 5.78, signals: [] }, 12 | { name: 'AD0', x: 16.9, y: 5.78, signals: [] }, 13 | { name: 'XCL', x: 26.4, y: 5.78, signals: [] }, 14 | { name: 'XDA', x: 36.0, y: 5.78, signals: [] }, 15 | { name: 'SDA', x: 45.6, y: 5.78, signals: [i2c('SDA')] }, 16 | { name: 'SCL', x: 55.2, y: 5.78, signals: [i2c('SCL')] }, 17 | { name: 'GND', x: 64.8, y: 5.78, signals: [GND()] }, 18 | { name: 'VCC', x: 74.4, y: 5.78, signals: [VCC()] }, 19 | ]; 20 | 21 | render() { 22 | const { led1 } = this; 23 | return html` 24 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 106 | 112 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | ${led1 && 126 | svg``} 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 148 | INT 149 | AD0 150 | XCL 151 | XDA 152 | SDA 153 | SCL 154 | GND 155 | VCC 156 | 157 | 158 | `; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/nano-rp2040-connect-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './nano-rp2040-connect-element'; 3 | 4 | export default { 5 | title: 'Nano RP2040 Connect', 6 | component: 'wokwi-nano-rp2040-connect', 7 | argTypes: { 8 | ledPower: { control: { type: 'boolean' } }, 9 | ledBuiltIn: { control: { type: 'boolean' } }, 10 | ledRed: { control: { type: 'number' } }, 11 | ledGreen: { control: { type: 'number' } }, 12 | ledBlue: { control: { type: 'number' } }, 13 | }, 14 | args: { 15 | ledPower: false, 16 | ledBuiltIn: false, 17 | ledRed: 0, 18 | ledGreen: 0, 19 | ledBlue: 0, 20 | }, 21 | }; 22 | 23 | const Template = ({ ledPower, ledBuiltIn, ledRed, ledGreen, ledBlue }) => 24 | html` `; 31 | 32 | export const Default = Template.bind({}); 33 | -------------------------------------------------------------------------------- /src/neopixel-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/web-components'; 2 | import { withKnobs, number } from '@storybook/addon-knobs'; 3 | import { html } from 'lit'; 4 | import './neopixel-element'; 5 | 6 | storiesOf('Neopixel', module) 7 | .addParameters({ component: 'wokwi-neopixel' }) 8 | .addDecorator(withKnobs) 9 | .add( 10 | 'Neopixel: Red', 11 | () => 12 | html` 13 | 18 | ` 19 | ) 20 | .add( 21 | 'Neopixel: White', 22 | () => 23 | html` 24 | 29 | ` 30 | ); 31 | -------------------------------------------------------------------------------- /src/neopixel-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin, GND, VCC } from './pin'; 4 | 5 | @customElement('wokwi-neopixel') 6 | export class NeoPixelElement extends LitElement { 7 | @property() r = 0; 8 | @property() g = 0; 9 | @property() b = 0; 10 | 11 | readonly pinInfo: ElementPin[] = [ 12 | { name: 'VDD', y: 3.5, x: 1, number: 1, signals: [VCC()] }, 13 | { name: 'DOUT', y: 14, x: 1, number: 2, signals: [] }, 14 | { name: 'VSS', y: 14, x: 21, number: 3, signals: [{ type: 'power', signal: 'GND' }] }, 15 | { name: 'DIN', y: 3.5, x: 21, number: 4, signals: [GND()] }, 16 | ]; 17 | 18 | render() { 19 | const { r, g, b } = this; 20 | const spotOpacity = (value: number) => (value > 0.001 ? 0.7 + value * 0.3 : 0); 21 | const maxOpacity = Math.max(r, g, b); 22 | const minOpacity = Math.min(r, g, b); 23 | const opacityDelta = maxOpacity - minOpacity; 24 | const multiplier = Math.max(1, 2 - opacityDelta * 20); 25 | const glowBase = 0.1 + Math.max(maxOpacity * 2 - opacityDelta * 5, 0); 26 | const glowColor = (value: number) => (value > 0.005 ? 0.1 + value * 0.9 : 0); 27 | const glowOpacity = (value: number) => (value > 0.005 ? glowBase + value * (1 - glowBase) : 0); 28 | const cssVal = (value: number) => 29 | maxOpacity ? Math.floor(Math.min(glowColor(value / maxOpacity) * multiplier, 1) * 255) : 255; 30 | const cssColor = `rgb(${cssVal(r)}, ${cssVal(g)}, ${cssVal(b)})`; 31 | const bkgWhite = 32 | 242 - 33 | (maxOpacity > 0.1 && opacityDelta < 0.2 34 | ? Math.floor(maxOpacity * 50 * (1 - opacityDelta / 0.2)) 35 | : 0); 36 | const background = `rgb(${bkgWhite}, ${bkgWhite}, ${bkgWhite})`; 37 | return html` 38 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 62 | 65 | 68 | 71 | 72 | 76 | 85 | 94 | 103 | 112 | 113 | `; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/neopixel-matrix-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { withKnobs, number, boolean } from '@storybook/addon-knobs'; 2 | import { storiesOf } from '@storybook/web-components'; 3 | import { html } from 'lit'; 4 | import './neopixel-matrix-element'; 5 | 6 | storiesOf('NeoPixel Matrix', module) 7 | .addParameters({ component: 'wokwi-neopixel-matrix' }) 8 | .addDecorator(withKnobs) 9 | .add( 10 | '8x8, green background', 11 | () => html` 12 |
13 | 19 |
20 | ` 21 | ) 22 | .add( 23 | '16x16, dark background', 24 | () => html` 25 |
26 | 31 |
32 | ` 33 | ); 34 | -------------------------------------------------------------------------------- /src/ntc-temperature-sensor-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './ntc-temperature-sensor-element'; 3 | 4 | export default { 5 | title: 'NTC Temperature Sensor', 6 | component: 'wokwi-ntc-temperature-sensor', 7 | }; 8 | 9 | const Template = () => html` `; 10 | 11 | export const Default = Template.bind({}); 12 | -------------------------------------------------------------------------------- /src/patterns/pins-female.ts: -------------------------------------------------------------------------------- 1 | import { svg } from 'lit'; 2 | 3 | export const pinsFemalePattern = svg` 4 | 5 | 6 | 7 | 12 | 17 | 22 | 27 | 28 | `; 29 | -------------------------------------------------------------------------------- /src/photoresistor-sensor-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './photoresistor-sensor-element'; 3 | 4 | export default { 5 | title: 'Photoresistor Sensor', 6 | component: 'wokwi-photoresistor-sensor', 7 | argTypes: { 8 | ledPower: { control: { type: 'boolean' } }, 9 | ledDO: { control: { type: 'boolean' } }, 10 | }, 11 | args: { 12 | ledPower: false, 13 | ledDO: false, 14 | }, 15 | }; 16 | 17 | const Template = ({ ledPower, ledDO }) => 18 | html` 19 | `; 20 | 21 | export const Default = Template.bind({}); 22 | -------------------------------------------------------------------------------- /src/pin.ts: -------------------------------------------------------------------------------- 1 | export type PinSignalInfo = 2 | | { 3 | type: 'i2c'; 4 | signal: 'SDA' | 'SCL'; 5 | bus: number; 6 | } 7 | | { 8 | type: 'spi'; 9 | signal: 'SCK' | 'MOSI' | 'MISO' | 'SS'; 10 | bus: number; 11 | } 12 | | { 13 | type: 'usart'; 14 | signal: 'RX' | 'TX'; 15 | bus: number; 16 | } 17 | | { 18 | type: 'power'; 19 | signal: 'GND' | 'VCC'; 20 | voltage?: number; 21 | } 22 | | { 23 | type: 'pwm'; 24 | } 25 | | { 26 | type: 'analog'; 27 | channel?: number; 28 | }; 29 | 30 | export interface ElementPin { 31 | /** 32 | * A name that uniquely identifies the pin, e.g. `GND` or `VCC`. 33 | * Pins that are connected internally must have the same name prefix, followed by a single dot, 34 | * and a unique suffix (e.g. `GND.1` and `GND.2`). 35 | */ 36 | name: string; 37 | 38 | /** The x-coordinate of the pin, relative to the element's origin */ 39 | x: number; 40 | 41 | /** The y-coordinate of the pin, relative to the element's origin */ 42 | y: number; 43 | 44 | /** The signals for this pin. Leave empty for generic pins without a designated signals. **/ 45 | signals: PinSignalInfo[]; 46 | 47 | /** 48 | * Pin number. Only relevant for components with numbered pins 49 | * (e.g. chips in DIP package), and always starts with 1. 50 | */ 51 | number?: number; 52 | 53 | /** 54 | * Optional pin description 55 | */ 56 | description?: string; 57 | 58 | /** 59 | * Pin who cannot be connected to breadboard 60 | */ 61 | noBreadboard?: boolean; 62 | } 63 | 64 | /** Helper function for creating PinSignalInfo objects */ 65 | export const analog = (channel: number): PinSignalInfo => ({ type: 'analog', channel }); 66 | 67 | export const i2c = (signal: 'SCL' | 'SDA', bus = 0): PinSignalInfo => ({ 68 | type: 'i2c', 69 | signal, 70 | bus, 71 | }); 72 | 73 | export const spi = (signal: 'SCK' | 'MOSI' | 'MISO' | 'SS', bus = 0): PinSignalInfo => ({ 74 | type: 'spi', 75 | signal, 76 | bus, 77 | }); 78 | 79 | export const usart = (signal: 'RX' | 'TX', bus = 0): PinSignalInfo => ({ 80 | type: 'usart', 81 | signal, 82 | bus, 83 | }); 84 | 85 | export const GND = (): PinSignalInfo => ({ type: 'power', signal: 'GND' }); 86 | export const VCC = (voltage?: number): PinSignalInfo => ({ type: 'power', signal: 'VCC', voltage }); 87 | -------------------------------------------------------------------------------- /src/pir-motion-sensor-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './pir-motion-sensor-element'; 3 | 4 | export default { 5 | title: 'PIR Motion Sensor', 6 | component: 'wokwi-pir-motion-sensor', 7 | }; 8 | 9 | const Template = () => html` `; 10 | 11 | export const Default = Template.bind({}); 12 | -------------------------------------------------------------------------------- /src/pir-motion-sensor-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from 'lit'; 2 | import { customElement } from 'lit/decorators.js'; 3 | import { ElementPin, GND, VCC } from './pin'; 4 | 5 | @customElement('wokwi-pir-motion-sensor') 6 | export class PIRMotionSensorElement extends LitElement { 7 | readonly pinInfo: ElementPin[] = [ 8 | { name: 'VCC', y: 92, x: 36.178, number: 1, signals: [VCC()] }, 9 | { name: 'OUT', y: 92, x: 45.9175, number: 2, signals: [] }, 10 | { name: 'GND', y: 92, x: 55.6415, number: 3, signals: [GND()] }, 11 | ]; 12 | 13 | render() { 14 | return html` 15 | 22 | 23 | 26 | 29 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | + 107 | D 108 | 109 | 110 | 111 | `; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/potentiometer-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import { action } from '@storybook/addon-actions'; 3 | import './potentiometer-element'; 4 | 5 | export default { 6 | title: 'Potentiometer', 7 | component: 'wokwi-potentiometer', 8 | }; 9 | 10 | const Template = ({ transform = '' }) => html` 11 | 12 | 13 | 14 | `; 15 | 16 | export const Default = Template.bind({}); 17 | Default.args = {}; 18 | 19 | export const Rotated = Template.bind({}); 20 | Rotated.args = { ...Default.args, transform: 'rotate(90deg)' }; 21 | 22 | export const Zoomed = Template.bind({}); 23 | Zoomed.args = { ...Default.args, transform: 'translate(25px, 25px) scale(1.5)' }; 24 | -------------------------------------------------------------------------------- /src/pushbutton-6mm-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@storybook/addon-actions'; 2 | import { html } from 'lit'; 3 | import './pushbutton-6mm-element'; 4 | 5 | export default { 6 | title: 'Pushbutton 6mm', 7 | component: 'wokwi-pushbutton-6mm', 8 | }; 9 | 10 | export const Red = () => 11 | html` 12 | 17 | `; 18 | 19 | export const Green = () => 20 | html` 21 | 26 | `; 27 | 28 | export const RedWithLabel = () => 29 | html` 30 | 36 | `; 37 | 38 | export const RedWithLongLabel = () => 39 | html` 40 | 46 | `; 47 | 48 | export const FourButtons = () => 49 | html` 50 | 55 | 60 | 65 | 70 | `; 71 | 72 | export const RedWithXray = () => 73 | html` `; 74 | -------------------------------------------------------------------------------- /src/pushbutton-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@storybook/addon-actions'; 2 | import { html } from 'lit'; 3 | import './pushbutton-element'; 4 | 5 | export default { 6 | title: 'Pushbutton', 7 | component: 'wokwi-pushbutton', 8 | }; 9 | 10 | export const Red = () => 11 | html` 12 | 17 | `; 18 | 19 | export const Green = () => 20 | html` 21 | 26 | `; 27 | 28 | export const RedWithXray = () => 29 | html` `; 30 | 31 | export const RedWithLabel = () => 32 | html` 33 | 39 | `; 40 | 41 | export const RedWithLongLabel = () => 42 | html` 43 | 49 | `; 50 | 51 | export const FourButtons = () => 52 | html` 53 | 58 | 63 | 68 | 73 | `; 74 | -------------------------------------------------------------------------------- /src/pushbutton-element.ts: -------------------------------------------------------------------------------- 1 | import { css, html, LitElement, svg } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin } from './pin'; 4 | import { ctrlCmdPressed, SPACE_KEYS } from './utils/keys'; 5 | 6 | @customElement('wokwi-pushbutton') 7 | export class PushbuttonElement extends LitElement { 8 | @property() color = 'red'; 9 | @property() pressed = false; 10 | @property() label = ''; 11 | @property({ type: Boolean, attribute: 'xray' }) xray = false; 12 | 13 | private static pushbuttonCounter = 0; 14 | private uniqueId; 15 | private sticky = false; 16 | 17 | readonly pinInfo: ElementPin[] = [ 18 | { name: '1.l', x: 0, y: 13, signals: [] }, 19 | { name: '2.l', x: 0, y: 32, signals: [] }, 20 | { name: '1.r', x: 67, y: 13, signals: [] }, 21 | { name: '2.r', x: 67, y: 32, signals: [] }, 22 | ]; 23 | 24 | constructor() { 25 | super(); 26 | this.uniqueId = 'pushbutton' + PushbuttonElement.pushbuttonCounter++; 27 | } 28 | 29 | static get styles() { 30 | return css` 31 | :host { 32 | display: inline-flex; 33 | flex-direction: column; 34 | } 35 | 36 | button { 37 | border: none; 38 | background: none; 39 | padding: 0; 40 | margin: 0; 41 | text-decoration: none; 42 | -webkit-appearance: none; 43 | -moz-appearance: none; 44 | } 45 | 46 | .button-active-circle { 47 | opacity: 0; 48 | } 49 | 50 | button:active .button-active-circle { 51 | opacity: 1; 52 | } 53 | 54 | .clickable-element { 55 | cursor: pointer; 56 | } 57 | 58 | .label { 59 | width: 0; 60 | min-width: 100%; 61 | font-size: 12px; 62 | text-align: center; 63 | color: gray; 64 | position: relative; 65 | line-height: 1; 66 | top: -2px; 67 | } 68 | `; 69 | } 70 | 71 | render() { 72 | const { color, label, uniqueId, xray } = this; 73 | const buttonFill = this.pressed ? `url(#grad-down-${uniqueId})` : `url(#grad-up-${uniqueId})`; 74 | 75 | return html` 76 | 171 | ${this.label} 172 | `; 173 | } 174 | 175 | private down() { 176 | if (!this.pressed) { 177 | this.pressed = true; 178 | this.dispatchEvent(new Event('button-press')); 179 | } 180 | } 181 | 182 | private up(e: KeyboardEvent | MouseEvent) { 183 | if (!this.pressed) { 184 | return; 185 | } 186 | if (ctrlCmdPressed(e)) { 187 | this.sticky = true; 188 | } else { 189 | this.sticky = false; 190 | this.pressed = false; 191 | this.dispatchEvent(new Event('button-release')); 192 | } 193 | } 194 | 195 | private leave(e: MouseEvent) { 196 | if (!this.sticky) { 197 | this.up(e); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/react-types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-namespace */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | 4 | import { SevenSegmentElement } from './7segment-element'; 5 | import { ArduinoUnoElement } from './arduino-uno-element'; 6 | import { BuzzerElement } from './buzzer-element'; 7 | import { LCD1602Element } from './lcd1602-element'; 8 | import { LEDElement } from './led-element'; 9 | import { MembraneKeypadElement } from './membrane-keypad-element'; 10 | import { NeoPixelElement } from './neopixel-element'; 11 | import { NeopixelMatrixElement } from './neopixel-matrix-element'; 12 | import { PotentiometerElement } from './potentiometer-element'; 13 | import { PushbuttonElement } from './pushbutton-element'; 14 | import { Pushbutton6mmElement } from './pushbutton-6mm-element'; 15 | import { ResistorElement } from './resistor-element'; 16 | import { RotaryDialerElement } from './rotary-dialer-element'; 17 | import { SSD1306Element } from './ssd1306-element'; 18 | import { ServoElement } from './servo-element'; 19 | import { DHT22Element } from './dht22-element'; 20 | import { ArduinoMegaElement } from './arduino-mega-element'; 21 | import { ArduinoNanoElement } from './arduino-nano-element'; 22 | import { Ds1307Element } from './ds1307-element'; 23 | import { LEDRingElement } from './led-ring-element'; 24 | import { SlideSwitchElement } from './slide-switch-element'; 25 | import { HCSR04Element } from './hc-sr04-element'; 26 | import { LCD2004Element } from './lcd2004-element'; 27 | import { AnalogJoystickElement } from './analog-joystick-element'; 28 | import { SlidePotentiometerElement } from './slide-potentiometer-element'; 29 | import { IRReceiverElement } from './ir-receiver-element'; 30 | import { IRRemoteElement } from './ir-remote-element'; 31 | import { PIRMotionSensorElement } from './pir-motion-sensor-element'; 32 | import { NTCTemperatureSensorElement } from './ntc-temperature-sensor-element'; 33 | import { HeartBeatSensorElement } from './heart-beat-sensor-element'; 34 | import { TiltSwitchElement } from './tilt-switch-element'; 35 | import { FlameSensorElement } from './flame-sensor-element'; 36 | import { GasSensorElement } from './gas-sensor-element'; 37 | import { FranzininhoElement } from './franzininho-element'; 38 | import { NanoRP2040ConnectElement } from './nano-rp2040-connect-element'; 39 | import { SmallSoundSensorElement } from './small-sound-sensor-element'; 40 | import { BigSoundSensorElement } from './big-sound-sensor-element'; 41 | import { MPU6050Element } from './mpu6050-element'; 42 | import { ESP32DevkitV1Element } from './esp32-devkit-v1-element'; 43 | import { KY040Element } from './ky-040-element'; 44 | import { PhotoresistorSensorElement } from './photoresistor-sensor-element'; 45 | import { RGBLedElement } from './rgb-led-element'; 46 | import { ILI9341Element } from './ili9341-element'; 47 | import { LedBarGraphElement } from './led-bar-graph-element'; 48 | import { MicrosdCardElement } from './microsd-card-element'; 49 | import { DipSwitch8Element } from './dip-switch-8-element'; 50 | import { StepperMotorElement } from './stepper-motor-element'; 51 | import { HX711Element } from './hx711-element'; 52 | import { KS2EMDC5Element } from './ks2e-m-dc5-element'; 53 | import { BiaxialStepperElement } from './biaxial-stepper-element'; 54 | import type React from 'react'; 55 | 56 | type WokwiElement = Partial & React.ClassAttributes; 57 | 58 | declare global { 59 | namespace JSX { 60 | interface IntrinsicElements { 61 | 'wokwi-7segment': WokwiElement; 62 | 'wokwi-arduino-uno': WokwiElement; 63 | 'wokwi-lcd1602': WokwiElement; 64 | 'wokwi-led': WokwiElement; 65 | 'wokwi-neopixel': WokwiElement; 66 | 'wokwi-pushbutton': WokwiElement; 67 | 'wokwi-pushbutton-6mm': WokwiElement; 68 | 'wokwi-resistor': WokwiElement; 69 | 'wokwi-membrane-keypad': WokwiElement; 70 | 'wokwi-potentiometer': WokwiElement; 71 | 'wokwi-neopixel-matrix': WokwiElement; 72 | 'wokwi-ssd1306': WokwiElement; 73 | 'wokwi-buzzer': WokwiElement; 74 | 'wokwi-rotary-dialer': WokwiElement; 75 | 'wokwi-servo': WokwiElement; 76 | 'wokwi-dht22': WokwiElement; 77 | 'wokwi-arduino-mega': WokwiElement; 78 | 'wokwi-arduino-nano': WokwiElement; 79 | 'wokwi-ds1307': WokwiElement; 80 | 'wokwi-neopixel-ring': WokwiElement; 81 | 'wokwi-slide-switch': WokwiElement; 82 | 'wokwi-hc-sr04': WokwiElement; 83 | 'wokwi-lcd2004': WokwiElement; 84 | 'wokwi-analog-joystick': WokwiElement; 85 | 'wokwi-slide-potentiometer': WokwiElement; 86 | 'wokwi-ir-receiver': WokwiElement; 87 | 'wokwi-ir-remote': WokwiElement; 88 | 'wokwi-pir-motion-sensor': WokwiElement; 89 | 'wokwi-ntc-temperature-sensor': WokwiElement; 90 | 'wokwi-heart-beat-sensor': WokwiElement; 91 | 'wokwi-tilt-switch': WokwiElement; 92 | 'wokwi-flame-sensor': WokwiElement; 93 | 'wokwi-gas-sensor': WokwiElement; 94 | 'wokwi-franzininho': WokwiElement; 95 | 'wokwi-nano-rp2040-connect': WokwiElement; 96 | 'wokwi-small-sound-sensor': WokwiElement; 97 | 'wokwi-big-sound-sensor': WokwiElement; 98 | 'wokwi-mpu6050': WokwiElement; 99 | 'wokwi-esp32-devkit-v1': WokwiElement; 100 | 'wokwi-ky-040': WokwiElement; 101 | 'wokwi-photoresistor-sensor': WokwiElement; 102 | 'wokwi-rgb-led': WokwiElement; 103 | 'wokwi-ili9341': WokwiElement; 104 | 'wokwi-led-bar-graph': WokwiElement; 105 | 'wokwi-microsd-card': WokwiElement; 106 | 'wokwi-dip-switch-8': WokwiElement; 107 | 'wokwi-stepper-motor': WokwiElement; 108 | 'wokwi-hx711': WokwiElement; 109 | 'wokwi-ks2e-m-dc5': WokwiElement; 110 | 'wokwi-biaxial-stepper': WokwiElement; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/resistor-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/web-components'; 2 | import { html } from 'lit'; 3 | import { withKnobs, number } from '@storybook/addon-knobs'; 4 | 5 | import './resistor-element'; 6 | 7 | storiesOf('Resistor', module) 8 | .addParameters({ component: 'wokwi-resistor' }) 9 | .addDecorator(withKnobs()) 10 | .add('1Ω', () => html``) 11 | .add('470Ω', () => html``); 12 | -------------------------------------------------------------------------------- /src/resistor-element.ts: -------------------------------------------------------------------------------- 1 | import { css, html, LitElement } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin } from './pin'; 4 | 5 | const bandColors: { [key: number]: string } = { 6 | [-2]: '#C3C7C0', // Silver 7 | [-1]: '#F1D863', // Gold 8 | 0: '#000000', // Black 9 | 1: '#8F4814', // Brown 10 | 2: '#FB0000', // Red 11 | 3: '#FC9700', // Orange 12 | 4: '#FCF800', // Yellow 13 | 5: '#00B800', // Green 14 | 6: '#0000FF', // Blue 15 | 7: '#A803D6', // Violet 16 | 8: '#808080', // Gray 17 | 9: '#FCFCFC', // White 18 | }; 19 | 20 | /** 21 | * Renders an axial-lead resistor with 4 color bands. 22 | */ 23 | @customElement('wokwi-resistor') 24 | export class ResistorElement extends LitElement { 25 | /** 26 | * Resitance value, in ohms. The value is reflected in the color of the bands, according to 27 | * standard [electronic color code](https://en.wikipedia.org/wiki/Electronic_color_code#Resistors). 28 | */ 29 | @property() value = '1000'; 30 | 31 | readonly pinInfo: ElementPin[] = [ 32 | { name: '1', x: 0, y: 5.65, signals: [] }, 33 | { name: '2', x: 58.8, y: 5.65, signals: [] }, 34 | ]; 35 | 36 | static get styles() { 37 | return css` 38 | :host { 39 | display: flex; 40 | } 41 | `; 42 | } 43 | 44 | private breakValue(value: number) { 45 | const exponent = 46 | value >= 1e10 47 | ? 9 48 | : value >= 1e9 49 | ? 8 50 | : value >= 1e8 51 | ? 7 52 | : value >= 1e7 53 | ? 6 54 | : value >= 1e6 55 | ? 5 56 | : value >= 1e5 57 | ? 4 58 | : value >= 1e4 59 | ? 3 60 | : value >= 1e3 61 | ? 2 62 | : value >= 1e2 63 | ? 1 64 | : value >= 1e1 65 | ? 0 66 | : value >= 1 67 | ? -1 68 | : -2; 69 | const base = Math.round(value / 10 ** exponent); 70 | if (value === 0) { 71 | return [0, 0]; 72 | } 73 | return [Math.round(base % 100), exponent]; 74 | } 75 | 76 | render() { 77 | const { value } = this; 78 | const numValue = parseFloat(value); 79 | const [base, exponent] = this.breakValue(numValue); 80 | const band1Color = bandColors[Math.floor(base / 10)]; 81 | const band2Color = bandColors[base % 10]; 82 | const band3Color = bandColors[exponent]; 83 | return html` 84 | 92 | 93 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | `; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/rgb-led-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './rgb-led-element'; 3 | 4 | export default { 5 | title: 'RGB Led', 6 | component: 'wokwi-rgb-led', 7 | argTypes: { 8 | ledRed: { control: { type: 'range', min: 0, max: 1, step: 0.01 } }, 9 | ledGreen: { control: { type: 'range', min: 0, max: 1, step: 0.01 } }, 10 | ledBlue: { control: { type: 'range', min: 0, max: 1, step: 0.01 } }, 11 | background: { control: { type: 'color' } }, 12 | }, 13 | args: { 14 | ledRed: 0, 15 | ledGreen: 0, 16 | ledBlue: 0, 17 | background: '', 18 | }, 19 | }; 20 | 21 | const Template = ({ ledRed, ledGreen, ledBlue, background }) => 22 | html`
23 | 24 |
`; 25 | 26 | export const Default = Template.bind({}); 27 | 28 | export const DarkMode = Template.bind({}); 29 | DarkMode.args = { background: '#333' }; 30 | 31 | export const Red = Template.bind({}); 32 | Red.args = { ledRed: 1 }; 33 | 34 | export const Green = Template.bind({}); 35 | Green.args = { ledGreen: 1 }; 36 | 37 | export const Blue = Template.bind({}); 38 | Blue.args = { ledBlue: 1 }; 39 | 40 | export const Yellow = Template.bind({}); 41 | Yellow.args = { ledRed: 1, ledGreen: 1, ledBlue: 0 }; 42 | 43 | export const White = Template.bind({}); 44 | White.args = { ledRed: 1, ledGreen: 1, ledBlue: 1 }; 45 | 46 | export const Cyan = Template.bind({}); 47 | Cyan.args = { ledRed: 0, ledGreen: 1, ledBlue: 1 }; 48 | -------------------------------------------------------------------------------- /src/rgb-led-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin } from '.'; 4 | 5 | @customElement('wokwi-rgb-led') 6 | export class RGBLedElement extends LitElement { 7 | @property() ledRed = 0; 8 | @property() ledGreen = 0; 9 | @property() ledBlue = 0; 10 | 11 | readonly pinInfo: ElementPin[] = [ 12 | { name: 'R', x: 8.5, y: 44, signals: [] }, 13 | { name: 'COM', x: 18, y: 54, signals: [] }, 14 | { name: 'G', x: 26.4, y: 44, signals: [] }, 15 | { name: 'B', x: 35.7, y: 44, signals: [] }, 16 | ]; 17 | 18 | render() { 19 | const { ledRed, ledGreen, ledBlue } = this; 20 | const brightness = Math.max(ledRed, ledGreen, ledBlue); 21 | const opacity = brightness ? 0.2 + brightness * 0.6 : 0; 22 | 23 | return html` 24 | 25 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 50 | 55 | 56 | 60 | 64 | 65 | 66 | 71 | 76 | 81 | 82 | 86 | 90 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 117 | 125 | 133 | 134 | 142 | 143 | 144 | 154 | 155 | `; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/rotary-dailer-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@storybook/addon-actions'; 2 | import { html } from 'lit'; 3 | import './rotary-dialer-element'; 4 | 5 | export default { 6 | title: 'Rotary Dialer', 7 | component: 'wokwi-rotary-dialer', 8 | }; 9 | 10 | export const Default = () => 11 | html` 12 | 17 | `; 18 | -------------------------------------------------------------------------------- /src/servo-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { number, withKnobs } from '@storybook/addon-knobs'; 2 | import { storiesOf, forceReRender } from '@storybook/web-components'; 3 | import { html } from 'lit'; 4 | import './servo-element'; 5 | 6 | class SweepAnimation { 7 | angle = 0; 8 | goingUp = true; 9 | 10 | step() { 11 | if (this.goingUp) { 12 | this.angle++; 13 | } else { 14 | this.angle--; 15 | } 16 | if (this.angle === 180) { 17 | this.goingUp = false; 18 | } 19 | if (this.angle === 0) { 20 | this.goingUp = true; 21 | } 22 | } 23 | } 24 | 25 | const sweepAnimation = new SweepAnimation(); 26 | setInterval(() => { 27 | sweepAnimation.step(); 28 | forceReRender(); 29 | }, 20); 30 | 31 | storiesOf('Servo', module) 32 | .addParameters({ component: 'wokwi-servo' }) 33 | .addDecorator(withKnobs) 34 | .add( 35 | 'Default', 36 | () => html` ` 37 | ) 38 | .add('Animated: sweep', () => { 39 | return html` `; 40 | }) 41 | .add( 42 | 'Horn: double', 43 | () => 44 | html` 45 | 46 | ` 47 | ) 48 | .add( 49 | 'Horn: cross', 50 | () => 51 | html` 52 | 53 | ` 54 | ); 55 | -------------------------------------------------------------------------------- /src/servo-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin } from './pin'; 4 | 5 | @customElement('wokwi-servo') 6 | export class ServoElement extends LitElement { 7 | /** 8 | * The angle of the servo's horn 9 | */ 10 | @property() angle = 0; 11 | 12 | /** 13 | * Servo horn (arm) type. The horn is attached to the 14 | * servo's output shaft, and they rotate together. 15 | */ 16 | @property() horn: 'single' | 'double' | 'cross' = 'single'; 17 | 18 | /** Servo horn color (as an HTML color) */ 19 | @property() hornColor = '#ccc'; 20 | 21 | readonly pinInfo: ElementPin[] = [ 22 | { name: 'GND', x: 0, y: 50, signals: [{ type: 'power', signal: 'GND' }] }, 23 | { name: 'V+', x: 0, y: 59.5, signals: [{ type: 'power', signal: 'VCC' }] }, 24 | { name: 'PWM', x: 0, y: 69, signals: [{ type: 'pwm' }] }, 25 | ]; 26 | 27 | hornPath() { 28 | switch (this.horn) { 29 | case 'cross': 30 | return 'm119.54 50.354h-18.653v-18.653a8.4427 8.4427 0 0 0-8.4427-8.4427h-1.9537a8.4427 8.4427 0 0 0-8.4427 8.4427v18.653h-18.653a8.4427 8.4427 0 0 0-8.4427 8.4427v1.9537a8.4427 8.4427 0 0 0 8.4427 8.4427h18.653v18.653a8.4427 8.4427 0 0 0 8.4427 8.4427h1.9537a8.4427 8.4427 0 0 0 8.4427-8.4427v-18.653h18.653a8.4427 8.4427 0 0 0 8.4426-8.4427v-1.9537a8.4427 8.4427 0 0 0-8.4426-8.4427zm-57.447 12.136a2.7165 2.7165 0 1 1 2.7119-2.7165 2.7165 2.7165 0 0 1-2.7165 2.7165zm8.7543 0a2.7165 2.7165 0 1 1 2.7119-2.7165 2.7165 2.7165 0 0 1-2.7165 2.7165zm20.621-34.813a2.7165 2.7165 0 1 1-2.7165 2.7165 2.7165 2.7165 0 0 1 2.7165-2.7165zm0 8.7543a2.7165 2.7165 0 1 1-2.7165 2.7165 2.7165 2.7165 0 0 1 2.7165-2.7165zm0 55.438a2.7165 2.7165 0 1 1 2.7165-2.7165 2.7165 2.7165 0 0 1-2.7165 2.7165zm0-8.7543a2.7165 2.7165 0 1 1 2.7165-2.7165 2.7165 2.7165 0 0 1-2.7165 2.7165zm5.9215-17.42a8.3729 8.3729 0 1 1 0-11.843 8.3729 8.3729 0 0 1 0 11.843zm14.704-3.205a2.7165 2.7165 0 1 1 2.7165-2.7165 2.7165 2.7165 0 0 1-2.7165 2.7165zm8.7543 0a2.7165 2.7165 0 1 1 2.7165-2.7165 2.7165 2.7165 0 0 1-2.7165 2.7165z'; 31 | case 'double': 32 | return 'm101.63 57.808c-0.0768-0.48377-0.16978-0.8838-0.23258-1.1629l-4.112-51.454c0-2.8654-2.6026-5.1912-5.8145-5.1912s-5.8145 2.3258-5.8145 5.1912l-4.1004 51.447c-0.07443 0.28607-0.16746 0.69774-0.24421 1.1629a12.473 12.473 0 0 0 0 3.9306c0.07675 0.48377 0.16978 0.8838 0.24421 1.1629l4.1004 51.461c0 2.8654 2.6026 5.1912 5.8145 5.1912s5.8145-2.3258 5.8145-5.1912l4.1004-51.447c0.0744-0.28607 0.16746-0.69774 0.23258-1.1629a12.473 12.473 0 0 0 0.0116-3.9376zm-4.2376 7.8868a8.3426 8.3426 0 0 1-3.5375 2.1072c-0.25816 0.07443-0.52098 0.13955-0.7838 0.19072a8.7217 8.7217 0 0 1-1.1978 0.1442c-0.26747 0.01163-0.53726 0.01163-0.80473 0a8.7217 8.7217 0 0 1-1.1978-0.1442c-0.26282-0.05117-0.52563-0.11629-0.78379-0.19072a8.3729 8.3729 0 0 1 0-16.048c0.25816-0.07675 0.52098-0.13955 0.78379-0.19072a8.7217 8.7217 0 0 1 1.1978-0.1442c0.26747-0.01163 0.53726-0.01163 0.80473 0a8.7217 8.7217 0 0 1 1.1978 0.1442c0.26282 0.05117 0.52563 0.11396 0.7838 0.19072a8.3729 8.3729 0 0 1 3.5375 13.955zm-5.9215-54.996a2.791 2.791 0 1 1-2.791 2.791 2.791 2.791 0 0 1 2.791-2.791zm0 8.6055a2.791 2.791 0 1 1-2.791 2.791 2.791 2.791 0 0 1 2.791-2.791zm0 8.3729a2.791 2.791 0 1 1-2.791 2.791 2.791 2.791 0 0 1 2.791-2.791zm0 8.6055a2.791 2.791 0 1 1-2.791 2.791 2.791 2.791 0 0 1 2.791-2.791zm0 72.565a2.791 2.791 0 1 1 2.791-2.791 2.791 2.791 0 0 1-2.791 2.791zm0-8.6055a2.791 2.791 0 1 1 2.791-2.791 2.791 2.791 0 0 1-2.791 2.791zm0-8.3729a2.791 2.791 0 1 1 2.791-2.791 2.791 2.791 0 0 1-2.791 2.791zm0-8.6055a2.791 2.791 0 1 1 2.791-2.791 2.791 2.791 0 0 1-2.791 2.791z'; 33 | case 'single': 34 | default: 35 | return 'm101.6 59.589-4.3167-54.166c0-2.8654-2.6026-5.1912-5.8145-5.1912s-5.8145 2.3258-5.8145 5.1912l-4.3167 54.166a8.3264 8.3264 0 0 0-0.10234 1.2792c0 5.047 4.5818 9.1381 10.234 9.1381s10.234-4.0911 10.234-9.1381a8.3264 8.3264 0 0 0-0.10233-1.2792zm-10.131-48.658a2.791 2.791 0 1 1-2.791 2.791 2.791 2.791 0 0 1 2.791-2.791zm0 8.6055a2.791 2.791 0 1 1-2.791 2.791 2.791 2.791 0 0 1 2.791-2.791zm0 8.3729a2.791 2.791 0 1 1-2.791 2.791 2.791 2.791 0 0 1 2.791-2.791zm0 8.6055a2.791 2.791 0 1 1-2.791 2.791 2.791 2.791 0 0 1 2.791-2.791zm5.9215 29.412a8.3729 8.3729 0 1 1 0-11.843 8.3729 8.3729 0 0 1 0 11.843z'; 36 | } 37 | } 38 | 39 | render() { 40 | return html` 41 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 61 | 62 | 66 | 67 | 68 | 69 | 70 | 71 | 75 | 79 | 80 | 81 | 82 | 87 | 88 | 89 | 93 | 94 | `; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/slide-potentiometer-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import { action } from '@storybook/addon-actions'; 3 | import './slide-potentiometer-element'; 4 | 5 | export default { 6 | title: 'Slide Potentiometer', 7 | component: 'wokwi-slide-potentiometer', 8 | argTypes: { 9 | travelLength: { control: { type: 'range', min: 15, max: 100 } }, 10 | }, 11 | args: { 12 | travelLength: 30, 13 | }, 14 | }; 15 | 16 | const Template = ({ travelLength, degrees = 0 }) => html` 17 |
18 | 19 |
20 | `; 21 | 22 | export const Default = Template.bind({}); 23 | Default.args = {}; 24 | 25 | export const Rotated = Template.bind({}); 26 | Rotated.args = { ...Default.args, degrees: 90 }; 27 | 28 | export const Short = Template.bind({}); 29 | Short.args = { travelLength: 15 }; 30 | 31 | export const Long = Template.bind({}); 32 | Long.args = { travelLength: 60 }; 33 | 34 | export const ExtraLong = Template.bind({}); 35 | ExtraLong.args = { travelLength: 100 }; 36 | -------------------------------------------------------------------------------- /src/slide-switch-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './slide-switch-element'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | export default { 6 | title: 'Slide Switch', 7 | component: 'wokwi-slide-switch', 8 | }; 9 | 10 | export const SlideSwitch = () => 11 | html``; 12 | -------------------------------------------------------------------------------- /src/slide-switch-element.ts: -------------------------------------------------------------------------------- 1 | import { css, html, LitElement } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { ElementPin } from './pin'; 4 | 5 | @customElement('wokwi-slide-switch') 6 | export class SlideSwitchElement extends LitElement { 7 | @property() value = 0; 8 | 9 | readonly pinInfo: ElementPin[] = [ 10 | { name: '1', number: 1, y: 34, x: 6.5, signals: [] }, 11 | { name: '2', number: 2, y: 34, x: 16, signals: [] }, 12 | { name: '3', number: 3, y: 34, x: 25.5, signals: [] }, 13 | ]; 14 | 15 | static get styles() { 16 | return css` 17 | .hide-input { 18 | position: absolute; 19 | clip: rect(0 0 0 0); 20 | width: 1px; 21 | height: 1px; 22 | margin: -1px; 23 | } 24 | svg #handle { 25 | transition: transform 0.2s linear; 26 | } 27 | input:checked + svg #handle { 28 | transform: translate(2px, 0); 29 | } 30 | input:focus + svg #handle { 31 | stroke-width: 0.4px; 32 | stroke: #8080ff; 33 | } 34 | `; 35 | } 36 | 37 | private onClick() { 38 | const inputEl = this.shadowRoot?.querySelector('.hide-input'); 39 | if (inputEl) { 40 | inputEl.checked = !inputEl.checked; 41 | this.onValueChange(inputEl); 42 | inputEl?.focus(); 43 | } 44 | } 45 | 46 | private onValueChange(target: HTMLInputElement) { 47 | this.value = target.checked ? 1 : 0; 48 | this.dispatchEvent(new InputEvent('input', { detail: this.value })); 49 | } 50 | 51 | render() { 52 | const { value } = this; 53 | return html` 54 | 61 | 70 | 71 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 93 | 94 | 95 | 96 | 97 | 98 | `; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/small-sound-sensor-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './small-sound-sensor-element'; 3 | 4 | export default { 5 | title: 'Small Sound Sensor', 6 | component: 'wokwi-small-sound-sensor', 7 | argTypes: { 8 | ledPower: { control: { type: 'boolean' } }, 9 | ledSignal: { control: { type: 'boolean' } }, 10 | }, 11 | args: { 12 | ledPower: false, 13 | ledSignal: false, 14 | }, 15 | }; 16 | 17 | const Template = ({ ledPower, ledSignal }) => 18 | html``; 22 | 23 | export const Default = Template.bind({}); 24 | -------------------------------------------------------------------------------- /src/ssd1306-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { withKnobs } from '@storybook/addon-knobs'; 2 | import { storiesOf } from '@storybook/web-components'; 3 | import { html } from 'lit'; 4 | import './ssd1306-element'; 5 | 6 | const logoBitmap = new Uint8Array([ 7 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 8 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 9 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 10 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 11 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 12 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 13 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 14 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 15 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 252, 63, 255, 255, 255, 255, 255, 255, 255, 16 | 255, 255, 255, 255, 255, 255, 255, 248, 31, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 143, 17 | 255, 255, 159, 240, 31, 252, 63, 255, 127, 255, 255, 255, 255, 255, 191, 7, 231, 254, 7, 240, 15, 18 | 240, 7, 254, 63, 243, 195, 255, 207, 255, 14, 3, 195, 254, 7, 241, 143, 192, 3, 254, 31, 225, 195, 19 | 255, 143, 254, 14, 35, 129, 252, 3, 241, 143, 128, 1, 254, 31, 225, 195, 255, 135, 254, 14, 51, 0, 20 | 252, 99, 241, 143, 0, 1, 254, 31, 195, 225, 255, 7, 254, 30, 51, 24, 252, 99, 240, 15, 3, 224, 21 | 254, 31, 131, 225, 255, 7, 254, 62, 35, 24, 252, 99, 248, 30, 15, 240, 254, 31, 131, 225, 255, 7, 22 | 252, 62, 3, 24, 252, 3, 248, 30, 31, 248, 254, 63, 7, 225, 254, 7, 252, 63, 3, 8, 254, 3, 248, 60, 23 | 31, 248, 126, 63, 7, 241, 254, 7, 252, 63, 7, 129, 255, 7, 240, 252, 127, 252, 126, 62, 31, 240, 24 | 254, 7, 252, 127, 255, 193, 255, 135, 240, 252, 127, 252, 126, 60, 31, 240, 254, 3, 252, 127, 223, 25 | 225, 255, 199, 241, 248, 127, 252, 126, 56, 63, 240, 252, 3, 248, 127, 159, 241, 255, 195, 241, 26 | 248, 255, 252, 126, 48, 127, 248, 252, 3, 248, 127, 31, 241, 255, 227, 241, 248, 255, 252, 126, 27 | 16, 127, 248, 124, 35, 248, 255, 31, 248, 255, 227, 227, 240, 255, 252, 126, 0, 255, 248, 124, 99, 28 | 240, 254, 31, 248, 255, 227, 227, 240, 255, 252, 126, 1, 255, 248, 120, 99, 240, 254, 31, 248, 29 | 255, 243, 227, 240, 255, 252, 126, 3, 255, 252, 120, 99, 241, 254, 31, 248, 255, 225, 227, 241, 30 | 255, 252, 124, 99, 255, 252, 120, 99, 241, 254, 31, 248, 255, 225, 199, 241, 255, 252, 124, 99, 31 | 255, 252, 120, 99, 225, 254, 31, 252, 127, 193, 199, 241, 255, 252, 124, 99, 255, 252, 56, 225, 32 | 227, 254, 31, 252, 127, 129, 199, 241, 255, 252, 124, 3, 255, 252, 48, 225, 227, 254, 63, 252, 33 | 127, 0, 199, 241, 255, 252, 126, 3, 255, 252, 48, 241, 227, 254, 63, 252, 126, 8, 143, 241, 255, 34 | 252, 126, 1, 255, 254, 48, 241, 195, 254, 63, 254, 60, 24, 143, 241, 255, 252, 126, 1, 255, 254, 35 | 49, 241, 199, 254, 63, 254, 60, 56, 15, 241, 255, 248, 126, 32, 255, 254, 33, 241, 199, 254, 63, 36 | 254, 56, 124, 15, 240, 255, 248, 126, 48, 127, 254, 1, 241, 135, 254, 63, 254, 16, 252, 31, 240, 37 | 255, 248, 254, 56, 63, 254, 3, 240, 143, 254, 63, 254, 1, 248, 15, 248, 255, 240, 254, 56, 63, 38 | 254, 3, 240, 143, 254, 63, 254, 1, 248, 15, 248, 127, 225, 254, 60, 31, 254, 3, 240, 15, 254, 63, 39 | 254, 3, 248, 15, 248, 63, 225, 254, 62, 15, 255, 7, 248, 15, 254, 63, 254, 3, 249, 143, 252, 31, 40 | 193, 254, 63, 7, 255, 7, 248, 31, 254, 63, 252, 3, 249, 207, 252, 15, 131, 254, 63, 3, 255, 7, 41 | 248, 31, 254, 63, 252, 99, 248, 143, 254, 0, 3, 254, 63, 129, 255, 7, 248, 31, 254, 63, 252, 99, 42 | 248, 15, 254, 0, 7, 254, 63, 192, 255, 7, 248, 63, 254, 63, 252, 35, 248, 15, 255, 0, 15, 254, 63, 43 | 224, 255, 7, 248, 63, 254, 63, 252, 3, 252, 31, 255, 128, 31, 254, 63, 240, 255, 135, 252, 63, 44 | 254, 63, 254, 7, 254, 63, 255, 224, 127, 254, 63, 253, 255, 143, 252, 63, 254, 63, 254, 7, 255, 45 | 255, 255, 255, 255, 255, 63, 255, 255, 255, 254, 127, 254, 63, 255, 31, 255, 255, 255, 255, 255, 46 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 48 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 49 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 50 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 51 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 52 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 53 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 54 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 55 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 56 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 57 | ]); 58 | 59 | function toImageData(bitmap: Uint8Array, width: number, height: number) { 60 | const result = new ImageData(width, height); 61 | for (let y = 0; y < height; y++) { 62 | for (let x = 0; x < width; x++) { 63 | const pixIndex = Math.floor((y * width + x) / 8); 64 | const pixValue = bitmap[pixIndex] & (1 << (7 - (x % 8))) ? 0xff : 0; 65 | const pixOffset = (y * width + x) * 4; 66 | result.data.fill(pixValue, pixOffset, pixOffset + 3); 67 | result.data[pixOffset + 3] = 0xff; 68 | } 69 | } 70 | return result; 71 | } 72 | 73 | storiesOf('SSD1306', module) 74 | .addParameters({ component: 'wokwi-ssd1306' }) 75 | .addDecorator(withKnobs) 76 | .add('Default', () => html``) 77 | .add( 78 | 'Wokwi logo', 79 | () => html`` 80 | ); 81 | -------------------------------------------------------------------------------- /src/ssd1306-element.ts: -------------------------------------------------------------------------------- 1 | // Reference: https://cdn-learn.adafruit.com/assets/assets/000/036/494/original/lcds___displays_fabprint.png?1476374574 2 | import { css, html, LitElement } from 'lit'; 3 | import { customElement, property } from 'lit/decorators.js'; 4 | import { ElementPin, i2c } from './pin'; 5 | 6 | type CanvasContext = CanvasRenderingContext2D | null | undefined; 7 | @customElement('wokwi-ssd1306') 8 | export class SSD1306Element extends LitElement { 9 | /** 10 | * The pixel data to draw on the element's internal <canvas>. 11 | * If you change the underlaying pixel data without updating the 12 | * `imageData` reference, call the `redraw()` method to update the 13 | * screen with your changes. 14 | */ 15 | @property() imageData: ImageData; 16 | 17 | readonly width = 150; 18 | readonly height = 116; 19 | private screenWidth = 128; 20 | private screenHeight = 64; 21 | private canvas: HTMLCanvasElement | null | undefined = void 0; 22 | private ctx: CanvasContext = null; 23 | 24 | readonly pinInfo: ElementPin[] = [ 25 | { name: 'DATA', x: 36.5, y: 12.5, signals: [i2c('SDA')] }, 26 | { name: 'CLK', x: 45.5, y: 12.5, signals: [i2c('SCL')] }, 27 | { name: 'DC', x: 54.5, y: 12.5, signals: [] }, 28 | { name: 'RST', x: 64.5, y: 12.5, signals: [] }, 29 | { name: 'CS', x: 74.5, y: 12.5, signals: [] }, 30 | { name: '3V3', x: 83.5, y: 12.5, signals: [{ type: 'power', signal: 'VCC', voltage: 3.3 }] }, 31 | { name: 'VIN', x: 93.5, y: 12.5, signals: [{ type: 'power', signal: 'VCC' }] }, 32 | { name: 'GND', x: 103.5, y: 12, signals: [{ type: 'power', signal: 'GND' }] }, 33 | ]; 34 | 35 | static get styles() { 36 | return css` 37 | .container { 38 | position: relative; 39 | } 40 | 41 | .container > canvas { 42 | position: absolute; 43 | left: 11.5px; 44 | top: 27px; 45 | } 46 | 47 | .pixelated { 48 | image-rendering: crisp-edges; /* firefox */ 49 | image-rendering: pixelated; /* chrome/webkit */ 50 | } 51 | `; 52 | } 53 | 54 | constructor() { 55 | super(); 56 | this.imageData = new ImageData(this.screenWidth, this.screenHeight); 57 | } 58 | 59 | /** 60 | * Used for initiating update of an imageData data which its reference wasn't changed 61 | */ 62 | public redraw() { 63 | this.ctx?.putImageData(this.imageData, 0, 0); 64 | } 65 | 66 | private initContext() { 67 | this.canvas = this.shadowRoot?.querySelector('canvas'); 68 | // No need to clear canvas rect - all images will have full opacity 69 | this.ctx = this.canvas?.getContext('2d'); 70 | } 71 | 72 | firstUpdated() { 73 | this.initContext(); 74 | this.ctx?.putImageData(this.imageData, 0, 0); 75 | } 76 | 77 | updated() { 78 | if (this.imageData) { 79 | this.redraw(); 80 | } 81 | } 82 | 83 | render() { 84 | const { width, height, screenWidth, screenHeight } = this; 85 | return html`
86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 107 | Data 108 | SA0 109 | CS 110 | Vin 111 | C1k 112 | DC 113 | Rst 114 | 3v3 115 | Gnd 116 | 117 | 118 | 119 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 |
`; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/stepper-motor-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './stepper-motor-element'; 3 | 4 | export default { 5 | title: 'Stepper Motor', 6 | component: 'wokwi-stepper-motor', 7 | argTypes: { 8 | angle: { control: { type: 'range', min: 0, max: 360 } }, 9 | size: { control: { type: 'select', options: [8, 11, 14, 17, 23, 34] } }, 10 | arrow: { control: { type: 'color' } }, 11 | }, 12 | args: { 13 | angle: 0, 14 | arrow: '', 15 | units: '', 16 | value: '', 17 | size: 23, 18 | }, 19 | }; 20 | 21 | const Template = ({ angle, arrow, units, value, size }) => 22 | html``; 29 | 30 | export const Default = Template.bind({}); 31 | Default.args = {}; 32 | 33 | export const Rotated90 = Template.bind({}); 34 | Rotated90.args = { angle: 90, units: 'degrees', value: '90', size: 14 }; 35 | 36 | export const Steps = Template.bind({}); 37 | Steps.args = { angle: 180, value: '52,500', units: 'steps', size: 14 }; 38 | 39 | export const Degrees = Template.bind({}); 40 | Degrees.args = { angle: 180, value: '180', units: 'degrees', size: 14 }; 41 | 42 | export const PurpleArrow = Template.bind({}); 43 | PurpleArrow.args = { angle: 350, arrow: '#4a36ba', size: 14 }; 44 | 45 | export const Nema17 = Template.bind({}); 46 | Nema17.args = { angle: 70, arrow: '#4a36ba', size: 17 }; 47 | 48 | export const Nema23 = Template.bind({}); 49 | Nema23.args = { angle: 70, arrow: '#4a36ba', value: '1234', units: 'steps', size: 23 }; 50 | 51 | export const Nema34 = Template.bind({}); 52 | Nema34.args = { angle: 70, arrow: '#4a36ba', value: '180', units: 'degrees', size: 34 }; 53 | -------------------------------------------------------------------------------- /src/storybook-events-logger.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'storybook-events-logger' { 2 | export function logEvent(e: Event): void; 3 | } 4 | -------------------------------------------------------------------------------- /src/tilt-switch-element.stories.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import './tilt-switch-element'; 3 | 4 | export default { 5 | title: 'Tilt Switch', 6 | component: 'wokwi-tilt-switch', 7 | }; 8 | 9 | const Template = () => html` `; 10 | 11 | export const Default = Template.bind({}); 12 | -------------------------------------------------------------------------------- /src/types/rgb.ts: -------------------------------------------------------------------------------- 1 | export interface RGB { 2 | r: number; 3 | g: number; 4 | b: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/clamp.ts: -------------------------------------------------------------------------------- 1 | export const clamp = (min: number, max: number, value: number): number => { 2 | const clampedValue = Math.min(value, max); 3 | return Math.max(clampedValue, min); 4 | }; 5 | -------------------------------------------------------------------------------- /src/utils/ctm-workaround.ts: -------------------------------------------------------------------------------- 1 | import { calculateBoundingRect } from './geometry'; 2 | 3 | export function getScreenCTM( 4 | target: SVGGraphicsElement, 5 | workaroundElement: SVGGraphicsElement, 6 | workaroundRect: DOMRectReadOnly 7 | ) { 8 | const { userAgent } = navigator; 9 | const workaroundNeeded = 10 | userAgent.indexOf('Firefox') >= 0 || 11 | userAgent.indexOf('Epiphany') >= 0 || 12 | userAgent.indexOf('Safari') >= 0; 13 | 14 | if (workaroundNeeded) { 15 | // Firefox's getScreenCTM() is broken: https://bugzilla.mozilla.org/show_bug.cgi?id=1610093 16 | const targetCTM = target.getCTM(); 17 | const workaroundCTM = workaroundElement?.getCTM(); 18 | const boundingRect = workaroundElement?.getBoundingClientRect(); 19 | const svgRect = workaroundElement?.ownerSVGElement?.getBoundingClientRect(); 20 | if (!boundingRect || !svgRect || !workaroundCTM || !targetCTM) { 21 | return null; 22 | } 23 | 24 | const centerX = svgRect.x + svgRect.width / 2; 25 | const centerY = svgRect.y + svgRect.height / 2; 26 | const deltaX = centerX - (boundingRect.x + boundingRect.width / 2); 27 | const deltaY = centerY - (boundingRect.y + boundingRect.height / 2); 28 | const angle = (Math.atan2(deltaY, deltaX) / Math.PI) * 180; 29 | const rotation = new DOMMatrix().rotate(angle); 30 | const rotatedRect = calculateBoundingRect(workaroundRect, rotation); 31 | const scaleX = rotatedRect.width / boundingRect.width; 32 | const scaleY = rotatedRect.height / boundingRect.height; 33 | const localCTM = workaroundCTM.inverse().multiply(targetCTM); 34 | return rotation 35 | .inverse() 36 | .translate(rotatedRect.left, rotatedRect.top) 37 | .multiply(localCTM.inverse()) 38 | .scale(scaleX, scaleY) 39 | .translate(-boundingRect.left, -boundingRect.top); 40 | } else { 41 | return target.getScreenCTM()?.inverse() || null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/geometry.ts: -------------------------------------------------------------------------------- 1 | export function calculateBoundingRect(rect: DOMRect, transform: DOMMatrix) { 2 | const topLeft = transform.transformPoint({ x: rect.left, y: rect.top }); 3 | const topRight = transform.transformPoint({ x: rect.right, y: rect.top }); 4 | const bottomLeft = transform.transformPoint({ x: rect.left, y: rect.bottom }); 5 | const bottomRight = transform.transformPoint({ x: rect.right, y: rect.bottom }); 6 | const minX = Math.min(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x); 7 | const minY = Math.min(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y); 8 | const maxX = Math.max(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x); 9 | const maxY = Math.max(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y); 10 | return new DOMRect(minX, minY, maxX - minX, maxY - minY); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/keys.ts: -------------------------------------------------------------------------------- 1 | export const SPACE_KEYS = [' ', 'Spacebar']; 2 | 3 | export function getUserAgent() { 4 | return typeof navigator === 'object' ? navigator.userAgent : ''; 5 | } 6 | 7 | function isMac() { 8 | return getUserAgent().indexOf('Macintosh') >= 0; 9 | } 10 | 11 | export function ctrlCmdPressed(e: KeyboardEvent | MouseEvent) { 12 | return isMac() ? e.metaKey : e.ctrlKey; 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/logo.ts: -------------------------------------------------------------------------------- 1 | export function drawWokwiW(ctx: CanvasRenderingContext2D, scale = 1, x = 0, y = 0) { 2 | ctx.save(); 3 | ctx.transform(scale, 0, 0, scale, x, y); 4 | 5 | ctx.fillStyle = '#000'; 6 | ctx.beginPath(); 7 | ctx.moveTo(4.19, 15.53); 8 | ctx.bezierCurveTo(3.56, 14.79, 3.43, 13.67, 3.87, 12.85); 9 | ctx.bezierCurveTo(4.28, 12.1, 4.22, 11.71, 3.29, 9); 10 | ctx.bezierCurveTo(2.35, 6.27, 2.17, 5.96, 1.35, 5.55); 11 | ctx.bezierCurveTo(-0.56, 4.6, -0.41, 2.1, 1.6, 1.55); 12 | ctx.bezierCurveTo(3.49, 1.03, 4.97, 2.94, 4.01, 4.65); 13 | ctx.bezierCurveTo(3.6, 5.4, 3.65, 5.77, 4.61, 8.52); 14 | ctx.bezierCurveTo(5.58, 11.32, 5.73, 11.59, 6.57, 11.98); 15 | ctx.bezierCurveTo(7.76, 12.52, 8.19, 13.86, 7.54, 15); 16 | ctx.bezierCurveTo(6.89, 16.14, 4.97, 16.44, 4.19, 15.53); 17 | ctx.moveTo(6.1, 14.53); 18 | ctx.bezierCurveTo(6.57, 14.15, 6.53, 13.73, 6, 13.43); 19 | ctx.bezierCurveTo(5.76, 13.29, 5.46, 13.24, 5.32, 13.32); 20 | ctx.bezierCurveTo(4.9, 13.55, 4.91, 14.33, 5.34, 14.58); 21 | ctx.bezierCurveTo(5.57, 14.71, 5.91, 14.68, 6.1, 14.53); 22 | ctx.moveTo(2.61, 4.17); 23 | ctx.bezierCurveTo(2.96, 3.98, 2.84, 3.09, 2.43, 2.86); 24 | ctx.bezierCurveTo(2.09, 2.67, 1.44, 3.13, 1.45, 3.55); 25 | ctx.bezierCurveTo(1.47, 3.93, 2.26, 4.36, 2.61, 4.17); 26 | ctx.moveTo(11.94, 14.99); 27 | ctx.bezierCurveTo(11.31, 14.26, 11.18, 13.13, 11.62, 12.31); 28 | ctx.bezierCurveTo(12.03, 11.56, 11.97, 11.17, 11.04, 8.47); 29 | ctx.bezierCurveTo(10.1, 5.74, 9.92, 5.42, 9.11, 5.02); 30 | ctx.bezierCurveTo(7.19, 4.07, 7.34, 1.56, 9.35, 1.01); 31 | ctx.bezierCurveTo(11.24, 0.5, 12.72, 2.4, 11.76, 4.12); 32 | ctx.bezierCurveTo(11.35, 4.87, 11.4, 5.23, 12.36, 7.99); 33 | ctx.bezierCurveTo(13.33, 10.78, 13.48, 11.06, 14.32, 11.44); 34 | ctx.bezierCurveTo(15.51, 11.99, 15.94, 13.33, 15.29, 14.47); 35 | ctx.bezierCurveTo(14.64, 15.61, 12.72, 15.91, 11.94, 14.99); 36 | ctx.moveTo(13.86, 13.99); 37 | ctx.bezierCurveTo(14.32, 13.61, 14.28, 13.2, 13.75, 12.89); 38 | ctx.bezierCurveTo(13.51, 12.76, 13.21, 12.71, 13.07, 12.78); 39 | ctx.bezierCurveTo(12.65, 13.01, 12.66, 13.8, 13.1, 14.04); 40 | ctx.bezierCurveTo(13.32, 14.17, 13.66, 14.15, 13.86, 13.99); 41 | ctx.moveTo(10.36, 3.63); 42 | ctx.bezierCurveTo(10.71, 3.45, 10.59, 2.56, 10.18, 2.33); 43 | ctx.bezierCurveTo(9.85, 2.13, 9.19, 2.59, 9.2, 3.01); 44 | ctx.bezierCurveTo(9.22, 3.4, 10.01, 3.83, 10.36, 3.63); 45 | ctx.moveTo(4.54, 16.22); 46 | ctx.bezierCurveTo(2.52, 15.03, 3.24, 12.04, 5.56, 12.04); 47 | ctx.bezierCurveTo(6.31, 12.04, 6.38, 11.86, 7.42, 10.87); 48 | ctx.bezierCurveTo(8.47, 9.88, 10.26, 8.24, 11.46, 7.65); 49 | ctx.bezierCurveTo(11.78, 8.1, 12.03, 9.26, 12.07, 9.39); 50 | ctx.bezierCurveTo(11.29, 9.39, 9.31, 10.91, 8.48, 11.93); 51 | ctx.bezierCurveTo(7.67, 12.92, 7.49, 13.33, 7.55, 14.12); 52 | ctx.bezierCurveTo(7.6, 14.85, 7.47, 15.23, 7.01, 15.66); 53 | ctx.bezierCurveTo(6.23, 16.39, 5.22, 16.62, 4.54, 16.22); 54 | ctx.moveTo(6.1, 14.53); 55 | ctx.bezierCurveTo(6.32, 13.97, 6.08, 13.62, 5.47, 13.62); 56 | ctx.bezierCurveTo(4.88, 13.62, 4.57, 14.03, 4.78, 14.56); 57 | ctx.bezierCurveTo(4.97, 15.08, 5.9, 15.05, 6.1, 14.53); 58 | ctx.moveTo(13.19, 12.05); 59 | ctx.bezierCurveTo(13.19, 12.05, 13.47, 11.49, 14.58, 8.34); 60 | ctx.bezierCurveTo(16.02, 4.26, 16.09, 3.93, 15.67, 3.07); 61 | ctx.bezierCurveTo(14.79, 1.23, 16.51, -0.61, 18.37, 0.19); 62 | ctx.bezierCurveTo(19.06, 0.49, 19.3, 0.78, 19.6, 1.68); 63 | ctx.bezierCurveTo(19.93, 2.67, 19.9, 2.87, 19.36, 3.4); 64 | ctx.bezierCurveTo(19.02, 3.73, 18.46, 4.12, 18.13, 4.26); 65 | ctx.bezierCurveTo(17.66, 4.46, 17.15, 5.51, 15.94, 8.88); 66 | ctx.bezierCurveTo(14.5, 12.87, 14.88, 11.69, 15.27, 12.42); 67 | ctx.bezierCurveTo(16.23, 14.21, 15.01, 12.91, 13.19, 12.05); 68 | ctx.moveTo(18.25, 2.2); 69 | ctx.bezierCurveTo(18.25, 1.6, 17.9, 1.37, 17.33, 1.6); 70 | ctx.bezierCurveTo(17.08, 1.7, 16.85, 1.91, 16.82, 2.06); 71 | ctx.bezierCurveTo(16.73, 2.53, 17.36, 3.01, 17.82, 2.83); 72 | ctx.bezierCurveTo(18.06, 2.73, 18.26, 2.45, 18.25, 2.2); 73 | ctx.fill(); 74 | ctx.restore(); 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/show-pins-element.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement, svg } from 'lit'; 2 | import { customElement, property, query } from 'lit/decorators.js'; 3 | import { ElementPin } from '../pin'; 4 | 5 | export interface ElementWithPinInfo extends Element { 6 | pinInfo?: ElementPin[]; 7 | } 8 | 9 | /** 10 | * Utility element to debug the pinInfo property. Pin locations are displayed using red dots. 11 | * Moving your mouse over one of the red dots shows a tooltip with the pin's name. 12 | * 13 | * 14 | * Usage: 15 | * 1. Import this file in your element's story file, e.g. 16 | * ``` 17 | * import './utils/show-pins-element'; 18 | * ``` 19 | * 2. Wrap your element with the element, e.g. 20 | * ``` 21 | * export const HCSR04 = () => html` 22 | * 23 | * 24 | * 25 | * `; 26 | * ``` 27 | */ 28 | @customElement('wokwi-show-pins') 29 | export class ShowPinsElement extends LitElement { 30 | @property() pinColor = 'red'; 31 | @query('#content') elementSlot!: HTMLSlotElement; 32 | 33 | previousSlotChild?: ElementWithPinInfo; 34 | 35 | get slotChild() { 36 | return this.elementSlot?.assignedElements()[0] as ElementWithPinInfo | undefined; 37 | } 38 | 39 | private pinInfoChangeCallback = () => { 40 | this.requestUpdate(); 41 | }; 42 | 43 | handleSlotChange() { 44 | const slotChild = this.slotChild; 45 | if (slotChild !== this.previousSlotChild) { 46 | this.previousSlotChild?.removeEventListener('pininfo-change', this.pinInfoChangeCallback); 47 | slotChild?.addEventListener('pininfo-change', this.pinInfoChangeCallback); 48 | this.previousSlotChild = slotChild; 49 | } 50 | this.requestUpdate(); 51 | } 52 | 53 | render() { 54 | const pinInfo = this.slotChild?.pinInfo ?? []; 55 | const { pinColor } = this; 56 | return html`
57 | this.handleSlotChange()}> 58 | 59 | 60 | ${pinInfo.map( 61 | (pin) => svg`${pin.name}` 62 | )} 63 | 64 |
`; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/units.ts: -------------------------------------------------------------------------------- 1 | export const mmToPix = 3.78; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noImplicitAny": true, 7 | "removeComments": false, 8 | "experimentalDecorators": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "esModuleInterop": true, 11 | "sourceMap": true, 12 | "declaration": true, 13 | "noEmitOnError": true, 14 | "strict": true, 15 | "rootDir": "src", 16 | "outDir": "dist/cjs", 17 | "lib": ["es2017", "dom"] 18 | }, 19 | "exclude": ["src/**/*.stories.ts"], 20 | "include": ["src/**/*.ts"] 21 | } 22 | --------------------------------------------------------------------------------