class JoyWidget extends BaseWidget { $el; #center = 0; #posX = null; #posY = null; #pressed = 0; #_onTouchStart; #_onTouchMove; #_onTouchEnd; #_onMouseDown; #_onMouseMove; #_onMouseUp; constructor(data, renderer) { super(data, renderer); this.makeLayout({ type: 'canvas', name: 'el' }); if (!this.data.color) this.data.color = colToInt(window.getComputedStyle(document.body).getPropertyValue('--prim')); if ("ontouchstart" in document.documentElement) { this.#_onTouchStart = this.#onTouchStart.bind(this); this.#_onTouchMove = this.#onTouchMove.bind(this); this.#_onTouchEnd = this.#onTouchEnd.bind(this); this.$el.addEventListener("touchstart", this.#_onTouchStart, { passive: false }); document.addEventListener("touchmove", this.#_onTouchMove, { passive: false }); document.addEventListener("touchend", this.#_onTouchEnd); } else { this.#_onMouseDown = this.#onMouseDown.bind(this); this.#_onMouseMove = this.#onMouseMove.bind(this); this.#_onMouseUp = this.#onMouseUp.bind(this); this.$el.addEventListener("mousedown", this.#_onMouseDown); document.addEventListener("mousemove", this.#_onMouseMove); document.addEventListener("mouseup", this.#_onMouseUp); } this.$el.parentNode.addEventListener('resize', () => { this.#reset(); this.#redraw(); }) this.update(data); this.disable(this.$el, data.disable); waitFrame().then(() => { this.#redraw(false); }); } close() { if ("ontouchstart" in document.documentElement) { this.$el.removeEventListener("touchstart", this.#_onTouchStart, { passive: false }); document.removeEventListener("touchmove", this.#_onTouchMove, { passive: false }); document.removeEventListener("touchend", this.#_onTouchEnd); } else { this.$el.removeEventListener("mousedown", this.#_onMouseDown); document.removeEventListener("mousemove", this.#_onMouseMove); document.removeEventListener("mouseup", this.#_onMouseUp); } } #reset() { this.#posX = null; this.#posY = null; } #redraw(send = true) { const cv = this.$el; let size = cv.parentNode.clientWidth; if (!size) return; cv.style.width = size + 'px'; cv.style.height = size + 'px'; size *= window.devicePixelRatio; cv.width = size; cv.height = size; cv.style.cursor = 'pointer'; const r = size * 0.23; const R = size * 0.4; this.#center = size / 2; if (this.#posX === null) this.#posX = this.#center; if (this.#posY === null) this.#posY = this.#center; this.#posX = constrain(this.#posX, r, size - r); this.#posY = constrain(this.#posY, r, size - r); let x = Math.round((this.#posX - this.#center) / (size / 2 - r) * 255); let y = -Math.round((this.#posY - this.#center) / (size / 2 - r) * 255); const cx = cv.getContext("2d"); cx.clearRect(0, 0, size, size); cx.beginPath(); cx.arc(this.#center, this.#center, R, 0, 2 * Math.PI, false); let grd = cx.createRadialGradient(this.#center, this.#center, R * 2 / 3, this.#center, this.#center, R); grd.addColorStop(0, '#00000005'); grd.addColorStop(1, '#00000030'); cx.fillStyle = grd; cx.fill(); cx.beginPath(); cx.arc(this.#posX, this.#posY, r, 0, 2 * Math.PI, false); grd = cx.createRadialGradient(this.#posX, this.#posY, 0, this.#posX, this.#posY, r); grd.addColorStop(0, intToCol(adjustColor(this.data.color, 0.7))); grd.addColorStop(1, intToCol(adjustColor(this.data.color, this.#pressed ? 1.3 : 1))); cx.fillStyle = grd; cx.fill(); if (this.data.exp) { x = ((x * x + 255) >> 8) * (x > 0 ? 1 : -1); y = ((y * y + 255) >> 8) * (y > 0 ? 1 : -1); } if (send) { this.set((x + 255) << 16) | (y + 255); this.setSuffix('[' + x + ',' + y + ']'); } } #onTouchStart(event) { if (this.data.disabled) return; event.preventDefault(); this.#pressed = true; } #onTouchMove(event) { if (!this.#pressed) return; event.preventDefault(); let target = null; for (const t of event.changedTouches) { if (t.target === this.$el) target = t; } if (!target) return; this.#posX = target.pageX; this.#posY = target.pageY; if (this.$el.offsetParent.tagName.toUpperCase() === "BODY") { this.#posX -= this.$el.offsetLeft; this.#posY -= this.$el.offsetTop; } else { this.#posX -= this.$el.offsetParent.offsetLeft; this.#posY -= this.$el.offsetParent.offsetTop; } const ratio = window.devicePixelRatio; this.#posX *= ratio; this.#posY *= ratio; this.#redraw(); } #onTouchEnd(event) { if (!this.#pressed) return; let target = null; for (const t of event.changedTouches) { if (t.target === this.$el) target = t; } if (!target) return; this.#pressed = false; if (!this.data.keep) { this.#posX = this.#center; this.#posY = this.#center; } this.#redraw(); } #onMouseDown() { if (this.data.disabled) return; this.#pressed = true; document.body.style.userSelect = 'none'; } #onMouseMove(event) { if (!this.#pressed) return; this.#posX = event.pageX; this.#posY = event.pageY; if (this.$el.offsetParent.tagName.toUpperCase() === "BODY") { this.#posX -= this.$el.offsetLeft; this.#posY -= this.$el.offsetTop; } else { this.#posX -= this.$el.offsetParent.offsetLeft; this.#posY -= this.$el.offsetParent.offsetTop; } const ratio = window.devicePixelRatio; this.#posX *= ratio; this.#posY *= ratio; this.#redraw(); } #onMouseUp() { if (!this.#pressed) return; this.#pressed = false; if (!this.data.keep) { this.#posX = this.#center; this.#posY = this.#center; } this.#redraw(); document.body.style.userSelect = ''; } } Renderer.register('joy', JoyWidget); function constrain(val, min, max) { return val < min ? min : (val > max ? max : val); }