|
| 1 | +import { css, customElement, html, LitElement, property } from 'lit-element'; |
| 2 | +import { analog, ElementPin, GND, VCC } from './pin'; |
| 3 | +import { SPACE_KEYS } from './utils/keys'; |
| 4 | + |
| 5 | +@customElement('wokwi-analog-joystick') |
| 6 | +export class AnalogJoystickElement extends LitElement { |
| 7 | + @property({ type: Number }) xValue = 0; |
| 8 | + @property({ type: Number }) yValue = 0; |
| 9 | + @property() pressed = false; |
| 10 | + |
| 11 | + readonly pinInfo: ElementPin[] = [ |
| 12 | + { name: 'VCC', x: 33, y: 115.8, signals: [VCC()] }, |
| 13 | + { name: 'VERT', x: 42.6012, y: 115.8, signals: [analog(0)] }, |
| 14 | + { name: 'HORZ', x: 52.2024, y: 115.8, signals: [analog(1)] }, |
| 15 | + { name: 'SEL', x: 61.8036, y: 115.8, signals: [] }, |
| 16 | + { name: 'GND', x: 71.4048, y: 115.8, signals: [GND()] }, |
| 17 | + ]; |
| 18 | + |
| 19 | + static get styles() { |
| 20 | + return css` |
| 21 | + #knob { |
| 22 | + transition: transform 0.3s; |
| 23 | + } |
| 24 | +
|
| 25 | + #knob:hover { |
| 26 | + fill: #222; |
| 27 | + } |
| 28 | +
|
| 29 | + #knob:focus { |
| 30 | + outline: none; |
| 31 | + stroke: #4d90fe; |
| 32 | + stroke-width: 0.5; |
| 33 | + } |
| 34 | +
|
| 35 | + .controls { |
| 36 | + opacity: 0; |
| 37 | + transition: opacity 0.3s; |
| 38 | + cursor: pointer; |
| 39 | + } |
| 40 | +
|
| 41 | + #knob:hover ~ .controls { |
| 42 | + opacity: 1; |
| 43 | + } |
| 44 | +
|
| 45 | + .controls:hover { |
| 46 | + opacity: 1; |
| 47 | + } |
| 48 | +
|
| 49 | + .controls path { |
| 50 | + pointer-events: none; |
| 51 | + } |
| 52 | +
|
| 53 | + .controls .region { |
| 54 | + pointer-events: bounding-box; |
| 55 | + fill: none; |
| 56 | + } |
| 57 | +
|
| 58 | + .controls .region:hover + path { |
| 59 | + fill: #fff; |
| 60 | + } |
| 61 | +
|
| 62 | + .controls circle:hover { |
| 63 | + stroke: #fff; |
| 64 | + } |
| 65 | +
|
| 66 | + .controls circle.pressed { |
| 67 | + fill: #fff; |
| 68 | + } |
| 69 | + `; |
| 70 | + } |
| 71 | + |
| 72 | + render() { |
| 73 | + const { xValue, yValue } = this; |
| 74 | + return html` |
| 75 | + <svg |
| 76 | + width="27.2mm" |
| 77 | + height="31.8mm" |
| 78 | + viewBox="0 0 27.2 31.8" |
| 79 | + xmlns="http://www.w3.org/2000/svg" |
| 80 | + xmlns:xlink="http://www.w3.org/1999/xlink" |
| 81 | + > |
| 82 | + <defs> |
| 83 | + <filter id="noise" primitiveUnits="objectBoundingBox"> |
| 84 | + <feTurbulence baseFrequency="2 2" type="fractalNoise" /> |
| 85 | + <feColorMatrix |
| 86 | + values=".1 0 0 0 .1 |
| 87 | + .1 0 0 0 .1 |
| 88 | + .1 0 0 0 .1 |
| 89 | + 0 0 0 0 1" |
| 90 | + /> |
| 91 | + <feComposite in2="SourceGraphic" operator="lighter" /> |
| 92 | + <feComposite result="body" in2="SourceAlpha" operator="in" /> |
| 93 | + </filter> |
| 94 | + <radialGradient id="g-knob" cx="13.6" cy="13.6" r="10.6" gradientUnits="userSpaceOnUse"> |
| 95 | + <stop offset="0" /> |
| 96 | + <stop offset="0.9" /> |
| 97 | + <stop stop-color="#777" offset="1" /> |
| 98 | + </radialGradient> |
| 99 | + <radialGradient |
| 100 | + id="g-knob-base" |
| 101 | + cx="13.6" |
| 102 | + cy="13.6" |
| 103 | + r="13.6" |
| 104 | + gradientUnits="userSpaceOnUse" |
| 105 | + > |
| 106 | + <stop offset="0" /> |
| 107 | + <stop stop-color="#444" offset=".8" /> |
| 108 | + <stop stop-color="#555" offset=".9" /> |
| 109 | + <stop offset="1" /> |
| 110 | + </radialGradient> |
| 111 | + <path |
| 112 | + id="pin" |
| 113 | + fill="silver" |
| 114 | + stroke="#a2a2a2" |
| 115 | + stroke-width=".024" |
| 116 | + d="M8.726 29.801a.828.828 0 00-.828.829.828.828 0 00.828.828.828.828 0 00.829-.828.828.828 0 00-.829-.829zm-.004.34a.49.49 0 01.004 0 .49.49 0 01.49.489.49.49 0 01-.49.49.49.49 0 01-.489-.49.49.49 0 01.485-.49z" |
| 117 | + /> |
| 118 | + </defs> |
| 119 | + <path |
| 120 | + d="M1.3 0v31.7h25.5V0zm2.33.683a1.87 1.87 0 01.009 0 1.87 1.87 0 011.87 1.87 1.87 1.87 0 01-1.87 1.87 1.87 1.87 0 01-1.87-1.87 1.87 1.87 0 011.87-1.87zm20.5 0a1.87 1.87 0 01.009 0 1.87 1.87 0 011.87 1.87 1.87 1.87 0 01-1.87 1.87 1.87 1.87 0 01-1.87-1.87 1.87 1.87 0 011.87-1.87zm-20.5 26.8a1.87 1.87 0 01.009 0 1.87 1.87 0 011.87 1.87 1.87 1.87 0 01-1.87 1.87 1.87 1.87 0 01-1.87-1.87 1.87 1.87 0 011.87-1.87zm20.4 0a1.87 1.87 0 01.009 0 1.87 1.87 0 011.87 1.87 1.87 1.87 0 01-1.87 1.87 1.87 1.87 0 01-1.87-1.87 1.87 1.87 0 011.87-1.87zm-12.7 2.66a.489.489 0 01.004 0 .489.489 0 01.489.489.489.489 0 01-.489.489.489.489 0 01-.489-.489.489.489 0 01.485-.489zm2.57 0a.489.489 0 01.004 0 .489.489 0 01.489.489.489.489 0 01-.489.489.489.489 0 01-.489-.489.489.489 0 01.485-.489zm2.49.013a.489.489 0 01.004 0 .489.489 0 01.489.489.489.489 0 01-.489.489.489.489 0 01-.489-.489.489.489 0 01.485-.489zm-7.62.007a.489.489 0 01.004 0 .489.489 0 01.489.489.489.489 0 01-.489.489.489.489 0 01-.489-.49.489.489 0 01.485-.488zm10.2.013a.489.489 0 01.004 0 .489.489 0 01.489.489.489.489 0 01-.489.489.489.489 0 01-.489-.49.489.489 0 01.485-.488z" |
| 121 | + fill="#bd1e34" |
| 122 | + /> |
| 123 | + <g |
| 124 | + fill="#fff" |
| 125 | + font-family="sans-serif" |
| 126 | + text-anchor="middle" |
| 127 | + stroke-width=".03" |
| 128 | + font-size="1.2" |
| 129 | + > |
| 130 | + <text letter-spacing=".053"> |
| 131 | + <tspan x="4.034" y="25.643">Analog</tspan> |
| 132 | + <tspan x="4.061" y="27.159">Joystick</tspan> |
| 133 | + </text> |
| 134 | + <text transform="rotate(-90)" text-anchor="start" font-size="1.2"> |
| 135 | + <tspan x="-29.2" y="9.2">VCC</tspan> |
| 136 | + <tspan x="-29.2" y="11.74">VERT</tspan> |
| 137 | + <tspan x="-29.2" y="14.28">HORZ</tspan> |
| 138 | + <tspan x="-29.2" y="16.82">SEL</tspan> |
| 139 | + <tspan x="-29.2" y="19.36">GND</tspan> |
| 140 | + </text> |
| 141 | + </g> |
| 142 | + <ellipse cx="13.6" cy="13.7" rx="13.6" ry="13.7" fill="url(#g-knob-base)" /> |
| 143 | + <path |
| 144 | + d="M48.2 65.5s.042.179-.093.204c-.094.017-.246-.077-.322-.17-.094-.115-.082-.205-.009-.285.11-.122.299-.075.299-.075s-.345-.303-.705-.054c-.32.22-.228.52.06.783.262.237.053.497-.21.463-.18-.023-.252-.167-.21-.256.038-.076.167-.122.167-.122s-.149-.06-.324.005c-.157.06-.286.19-.276.513v1.51s.162-.2.352-.403c.214-.229.311-.384.53-.366.415.026.714-.159.918-.454.391-.569.085-1.2-.178-1.29" |
| 145 | + fill="#fff" |
| 146 | + /> |
| 147 | + <circle |
| 148 | + id="knob" |
| 149 | + cx="13.6" |
| 150 | + cy="13.6" |
| 151 | + transform="translate(${2.5 * xValue}, ${2.5 * yValue})" |
| 152 | + r="10.6" |
| 153 | + fill="url(#g-knob)" |
| 154 | + filter="url(#noise)" |
| 155 | + tabindex="0" |
| 156 | + @keyup=${(e: KeyboardEvent) => this.keyup(e)} |
| 157 | + @keydown=${(e: KeyboardEvent) => this.keydown(e)} |
| 158 | + /> |
| 159 | + <g fill="none" stroke="#fff" stroke-width=".142"> |
| 160 | + <path |
| 161 | + d="M7.8 31.7l-.383-.351v-1.31l.617-.656h1.19l.721.656.675-.656h1.18l.708.656.662-.656h1.25l.643.656.63-.656h1.21l.695.656.636-.656h1.17l.753.656v1.3l-.416.39" |
| 162 | + /> |
| 163 | + <path |
| 164 | + d="M9.5 31.7l.381-.344.381.331M12.1 31.7l.381-.344.381.331M14.7 31.7l.381-.344.381.331M17.2 31.7l.381-.344.381.331" |
| 165 | + stroke-linecap="square" |
| 166 | + stroke-linejoin="bevel" |
| 167 | + /> |
| 168 | + </g> |
| 169 | + <g class="controls" stroke-width="0.6" stroke-linejoin="bevel" fill="#aaa"> |
| 170 | + <rect |
| 171 | + class="region" |
| 172 | + y="8.5" |
| 173 | + x="1" |
| 174 | + height="10" |
| 175 | + width="7" |
| 176 | + @mousedown=${() => this.mousedown(-1, 0)} |
| 177 | + @mouseup=${() => this.mouseup(true, false)} |
| 178 | + /> |
| 179 | + <path d="m 7.022,11.459 -3.202,2.497 3.202,2.497" /> |
| 180 | +
|
| 181 | + <rect |
| 182 | + class="region" |
| 183 | + y="1.38" |
| 184 | + x="7.9" |
| 185 | + height="7" |
| 186 | + width="10" |
| 187 | + @mousedown=${() => this.mousedown(0, -1)} |
| 188 | + @mouseup=${() => this.mouseup(false, true)} |
| 189 | + /> |
| 190 | + <path d="m 16.615,7.095 -2.497,-3.202 -2.497,3.202" /> |
| 191 | +
|
| 192 | + <rect |
| 193 | + class="region" |
| 194 | + y="8.5" |
| 195 | + x="18" |
| 196 | + height="10" |
| 197 | + width="7" |
| 198 | + @mousedown=${() => this.mousedown(1, 0)} |
| 199 | + @mouseup=${() => this.mouseup(true, false)} |
| 200 | + /> |
| 201 | + <path d="m 19.980,16.101 3.202,-2.497 -3.202,-2.497" /> |
| 202 | +
|
| 203 | + <rect |
| 204 | + class="region" |
| 205 | + y="17" |
| 206 | + x="7.9" |
| 207 | + height="7" |
| 208 | + width="10" |
| 209 | + @mousedown=${() => this.mousedown(0, 1)} |
| 210 | + @mouseup=${() => this.mouseup(false, true)} |
| 211 | + /> |
| 212 | + <path d="m 11.620,20.112 2.497,3.202 2.497,-3.202" /> |
| 213 | +
|
| 214 | + <circle |
| 215 | + cx="13.6" |
| 216 | + cy="13.6" |
| 217 | + r="3" |
| 218 | + stroke="#aaa" |
| 219 | + class=${this.pressed ? 'pressed' : ''} |
| 220 | + @mousedown=${() => this.press()} |
| 221 | + @mouseup=${() => this.release()} |
| 222 | + /> |
| 223 | + </g> |
| 224 | + <use xlink:href="#pin" x="0" /> |
| 225 | + <use xlink:href="#pin" x="2.54" /> |
| 226 | + <use xlink:href="#pin" x="5.08" /> |
| 227 | + <use xlink:href="#pin" x="7.62" /> |
| 228 | + <use xlink:href="#pin" x="10.16" /> |
| 229 | + </svg> |
| 230 | + `; |
| 231 | + } |
| 232 | + |
| 233 | + private keydown(e: KeyboardEvent) { |
| 234 | + switch (e.key) { |
| 235 | + case 'ArrowUp': |
| 236 | + this.yValue = -1; |
| 237 | + this.valueChanged(); |
| 238 | + break; |
| 239 | + case 'ArrowDown': |
| 240 | + this.yValue = 1; |
| 241 | + this.valueChanged(); |
| 242 | + break; |
| 243 | + case 'ArrowLeft': |
| 244 | + this.xValue = -1; |
| 245 | + this.valueChanged(); |
| 246 | + break; |
| 247 | + case 'ArrowRight': |
| 248 | + this.xValue = 1; |
| 249 | + this.valueChanged(); |
| 250 | + break; |
| 251 | + } |
| 252 | + if (SPACE_KEYS.includes(e.key)) { |
| 253 | + this.pressed = true; |
| 254 | + this.valueChanged(); |
| 255 | + } |
| 256 | + } |
| 257 | + |
| 258 | + private keyup(e: KeyboardEvent) { |
| 259 | + switch (e.key) { |
| 260 | + case 'ArrowUp': |
| 261 | + case 'ArrowDown': |
| 262 | + this.yValue = 0; |
| 263 | + this.valueChanged(); |
| 264 | + break; |
| 265 | + case 'ArrowLeft': |
| 266 | + case 'ArrowRight': |
| 267 | + this.xValue = 0; |
| 268 | + this.valueChanged(); |
| 269 | + break; |
| 270 | + } |
| 271 | + if (SPACE_KEYS.includes(e.key)) { |
| 272 | + this.pressed = false; |
| 273 | + this.valueChanged(); |
| 274 | + } |
| 275 | + } |
| 276 | + |
| 277 | + private mousedown(dx: number, dy: number) { |
| 278 | + if (dx) { |
| 279 | + this.xValue = dx; |
| 280 | + } |
| 281 | + if (dy) { |
| 282 | + this.yValue = dy; |
| 283 | + } |
| 284 | + this.valueChanged(); |
| 285 | + } |
| 286 | + |
| 287 | + private mouseup(x: boolean, y: boolean) { |
| 288 | + if (x) { |
| 289 | + this.xValue = 0; |
| 290 | + } |
| 291 | + if (y) { |
| 292 | + this.yValue = 0; |
| 293 | + } |
| 294 | + this.valueChanged(); |
| 295 | + } |
| 296 | + |
| 297 | + private press() { |
| 298 | + this.pressed = true; |
| 299 | + this.dispatchEvent(new InputEvent('button-press')); |
| 300 | + } |
| 301 | + |
| 302 | + private release() { |
| 303 | + this.pressed = false; |
| 304 | + this.dispatchEvent(new InputEvent('button-release')); |
| 305 | + } |
| 306 | + |
| 307 | + private valueChanged() { |
| 308 | + this.dispatchEvent(new InputEvent('input')); |
| 309 | + } |
| 310 | +} |
0 commit comments