diff --git a/README.md b/README.md index 6e3d846..3bb5b7b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ マイコンカー操作などのための、スマホ用バーチャルコントローラーです。(もちろん PC も対応しています) - から使用できますが、自分用に作ったので汎用性は低いかもしれません。気に入らない場合は Fork して改造してください。Pull Request 大歓迎です。 + から HTML をダウンロードできますが、自分用に作ったので汎用性は低いかもしれません。気に入らない場合は Fork して改造してください。Pull Request 大歓迎です。 > [!TIP] > ジョイスティック部分は [nippleJS](https://github.com/yoannmoinet/nipplejs) を使っています。 @@ -17,20 +17,30 @@ ### サーバー側 (マイコン等) -1. [以下の規格](#Protocol) の形式の HTTP リクエストを処理できるプログラムを走らせます。(TODO: Arduino WiFi.h によるサンプル) -2. 対象のサーバーと通信できるか確認します。 +1. [以下の規格](#Protocol) の形式の HTTP リクエストを処理できるプログラムを走らせます。([参考: M5Atom によるマイコンカーのサンプル](example/m5atom-car/)) +2. 対象のサーバーのアドレスを確認します。 ### コントローラー側 -3. Web ページを開き、対象サーバー (マイコン等) の URL を host に入力します。 -4. ジョイスティックの送信間隔 (デフォルト $125~\mathrm{ms}$) を調整します。 +3. 対象サーバー (マイコン等) のアドレスをブラウザで開きます。 +4. ジョイスティックの送信間隔 (デフォルト $100~\mathrm{ms}$) を調整します。通信が詰まらない程度に小さくすることをおすすめします。 5. A, B ボタンを使わない場合は隠します。 6. 設定画面を閉じ、スワイプまたは WASD (+ Shift) で操作します。 ## Protocol > [!WARNING] -> このサイトのクローンを対象サーバー上でホスティングする場合などを除き、基本的にレスポンスヘッダーに `Access-Control-Allow-Origin: https://twosquirrels.github.io` 等を加えないとエラーが出て上手く動作しない可能性があります。 +> github.io 上など HTTPS のページから HTTP のホストに対して通信をするとセキュリティエラーが出ます。これはブラウザの設定で対処することもできますが、API と同じホストで HTML を配信してそちらからページにアクセスすることを推奨します。 + +### GET `/` + +#### Request + +基本的にブラウザからアクセスされます。 + +#### Response + +ゲームパッドの HTML を返してください。HTML は から最新の物をダウンロードできるようにすることをおすすめします。 ### POST `/joystick?p=aaff` @@ -57,6 +67,11 @@ body は常に空です。 デバッグコンソールに表示されることを除けば、無視されます。 +## Examples + +- [M5Atom によるマイコンカーのサンプル](example/m5atom-car/) +- その他のサンプル募集中! + ## Tech Stack **Site Builder:** [Vite](https://ja.vitejs.dev/) diff --git a/example/m5atom-car/.gitignore b/example/m5atom-car/.gitignore new file mode 100644 index 0000000..5603bfe --- /dev/null +++ b/example/m5atom-car/.gitignore @@ -0,0 +1,3 @@ +.env.h +index.html +index-html.h diff --git a/example/m5atom-car/README.md b/example/m5atom-car/README.md new file mode 100644 index 0000000..d7fb87d --- /dev/null +++ b/example/m5atom-car/README.md @@ -0,0 +1,24 @@ +# M5Atom WiFi Control Car Example + +M5Atom によるマイコンカーでの Virtual Gamepad 用サンプルプログラムです。 + +## How to RUN + +まずこのディレクトリ (フォルダー) をダウンロードしてください。 + +これをコンパイルするためには、`./index-html.h` というファイルを用意してその中で `index_html` 定数に の HTML を `const char[]` 型で設定する必要があります。 +この作業は Windows の場合は `./gen-index-html.bat` を、Mac 等 bash が動く環境の場合は `./gen-index-html.sh` を実行することで、自動で HTML ファイルをダウンロードしヘッダファイルを生成できるため、それを使うことを推奨します。 + +また WiFi の ssid と password は `./m5atom-car.ino` に直接書き込んでも良いですが、`./.env.h` を作りこのヘッダファイル内で以下のように定数を初期化することもできます。 + +```c +const char ssid[] = "YOUR_SSID"; +const char password[] = "YOUR_PASSWORD"; +``` + +シリアルモニタを開いた状態でスケッチを書き込むと、書き込みが完了しプログラムが実行されたタイミングで M5Atom のローカル IP アドレスが表示されるため、それをスマホ等のブラウザで開いてください。ゲームパッドの画面が開けたら成功です。 + +## Structure + +`./SimpleHTTPServer.h` は ChatGPT で作った Arduino 用の簡易的な HTTP サーバーライブラリです。 +`./m5atom-car.ino` ではジョイスティックのクエリからパラメータをパースし、`updateMoers` 関数でその値から左右のモーターの出力を設定しています。 diff --git a/example/m5atom-car/SimpleHTTPServer.h b/example/m5atom-car/SimpleHTTPServer.h new file mode 100644 index 0000000..1b90827 --- /dev/null +++ b/example/m5atom-car/SimpleHTTPServer.h @@ -0,0 +1,133 @@ +// SimpleHTTPServer written by ChatGPT + +#include + +#define MAX_HANDLERS (16) + +class SimpleHTTPServer { + private: + struct Handler { + const char* path; + void (*handler)(WiFiClient&, const String&); + }; + + const char* ssid; + const char* password; + WiFiServer server; + Handler getHandlers[MAX_HANDLERS]; + Handler postHandlers[MAX_HANDLERS]; + int getHandlerCount; + int postHandlerCount; + + public: + SimpleHTTPServer(const char* ssid, const char* password, int port = 80) + : ssid(ssid), password(password), server(port), getHandlerCount(0), postHandlerCount(0) {} + + void begin() { + Serial.begin(115200); + connectToWiFi(); + server.begin(); + Serial.println("Server started"); + Serial.print("IP Address: "); + Serial.println(WiFi.localIP()); + } + + void handleClient() { + WiFiClient client = server.available(); + if (client) { + Serial.println("New Client."); + String currentLine = ""; + String requestType = ""; + String path = ""; + String queryString = ""; + + while (client.connected()) { + if (client.available()) { + char c = client.read(); + Serial.write(c); + if (c == '\n') { + if (currentLine.length() == 0) { + if (requestType == "GET") { + handleRequest(path, queryString, client, getHandlers, getHandlerCount); + } else if (requestType == "POST") { + handleRequest(path, queryString, client, postHandlers, postHandlerCount); + } + break; + } else { + currentLine = ""; + } + } else if (c != '\r') { + currentLine += c; + if (currentLine.startsWith("GET ")) { + requestType = "GET"; + path = extractPathAndQuery(currentLine, queryString); + } else if (currentLine.startsWith("POST ")) { + requestType = "POST"; + path = extractPathAndQuery(currentLine, queryString); + } + } + } + } + client.stop(); + Serial.println("Client Disconnected."); + } + } + + void get(const char* path, void (*handler)(WiFiClient&, const String&)) { + if (getHandlerCount < MAX_HANDLERS) { + getHandlers[getHandlerCount++] = { path, handler }; + } + } + + void post(const char* path, void (*handler)(WiFiClient&, const String&)) { + if (postHandlerCount < MAX_HANDLERS) { + postHandlers[postHandlerCount++] = { path, handler }; + } + } + + private: + void connectToWiFi() { + Serial.print("Connecting to "); + Serial.println(ssid); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(1000); + Serial.print("."); + } + Serial.println(""); + Serial.println("WiFi connected."); + } + + String extractPathAndQuery(const String& requestLine, String& queryString) { + int firstSpace = requestLine.indexOf(' '); + int secondSpace = requestLine.indexOf(' ', firstSpace + 1); + String fullPath = requestLine.substring(firstSpace + 1, secondSpace); + + int queryIndex = fullPath.indexOf('?'); + if (queryIndex != -1) { + queryString = fullPath.substring(queryIndex + 1); + return fullPath.substring(0, queryIndex); + } else { + queryString = ""; + return fullPath; + } + } + + void handleRequest(const String& path, const String& queryString, WiFiClient& client, Handler* handlers, int handlerCount) { + for (int i = 0; i < handlerCount; i++) { + if (path == handlers[i].path) { + handlers[i].handler(client, queryString); + return; + } + } + defaultResponse(client); + } + + void defaultResponse(WiFiClient& client) { + client.println("HTTP/1.1 404 Not Found"); + client.println("Content-type:text/html"); + client.println(); + client.print("

404 Not Found

"); + client.println(); + } +}; diff --git a/example/m5atom-car/gen-index-html.bat b/example/m5atom-car/gen-index-html.bat new file mode 100644 index 0000000..cf43ef9 --- /dev/null +++ b/example/m5atom-car/gen-index-html.bat @@ -0,0 +1,12 @@ +@echo off +setlocal EnableDelayedExpansion + +powershell -Command "(New-Object Net.WebClient).DownloadFile('https://twosquirrels.github.io/virtual-gamepad/', 'index.html')" + +( + echo const char index_html[] = R"***( + type index.html + echo )***"; +) > index-html.h + +echo index-html.h has been created successfully. diff --git a/example/m5atom-car/gen-index-html.sh b/example/m5atom-car/gen-index-html.sh new file mode 100644 index 0000000..9e19d38 --- /dev/null +++ b/example/m5atom-car/gen-index-html.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +curl -o index.html https://twosquirrels.github.io/virtual-gamepad/ + +{ + echo 'const char index_html[] = R"***(' + cat index.html + echo ')***";' +} > index-html.h + +echo "index-html.h has been created successfully." diff --git a/example/m5atom-car/m5atom-car.ino b/example/m5atom-car/m5atom-car.ino new file mode 100644 index 0000000..4cb66cf --- /dev/null +++ b/example/m5atom-car/m5atom-car.ino @@ -0,0 +1,159 @@ +// WiFi Control Car Example + +#include "SimpleHTTPServer.h" +#include + +#define LEDright (22) +#define LEDleft (19) +#define MotorA (23) +#define MotorB (33) + +const double PWM_freq = 2000; // PWM 周波数. +const uint8_t PWM_res = 8; // PWM 分解能 16bit(0~256). +const uint8_t PWM_CH_A = 1; // チャンネル. +const uint8_t PWM_CH_B = 2; // チャンネル. + +#if __has_include(".env.h") +# include ".env.h" +#else +const char ssid[] = "YOUR_SSID"; +const char password[] = "YOUR_PASSWORD"; +#endif + +#if __has_include("index-html.h") +# include "index-html.h" +#else +const char index_html[] = "

There is not index-html.h

"; +#endif + +void updateMoters(double angle, double force); + +SimpleHTTPServer server(ssid, password); + +void handleRoot(WiFiClient& client, const String& query) { + client.println("HTTP/1.1 200 OK"); + client.println("Content-Type: text/html"); + client.println(); + client.println(index_html); +} + +void respondOk(WiFiClient& client) { + client.println("HTTP/1.1 200 OK"); + client.println("Content-Type: text/plain"); + client.println(); + client.println("OK"); +} + +void respondBad(WiFiClient& client) { + client.println("HTTP/1.1 200 OK"); + client.println("Content-Type: text/plain"); + client.println(); + client.println("OK"); +} + +void handlePostJoystick(WiFiClient& client, const String& query) { + if (query.length() != 6) { + respondBad(client); + return; + } + const int angle = strtol(query.substring(2, 4).c_str(), NULL, 16); + const int force = strtol(query.substring(4, 6).c_str(), NULL, 16); + updateMoters(angle * 360.0 / 256.0, force / 255.0); + Serial.print("======== Joystick (angle="); + Serial.print(angle); + Serial.print(", force="); + Serial.print(force); + Serial.println(") ========"); + respondOk(client); +} + +void handlePostA(WiFiClient& client, const String& query) { + M5.dis.drawpix(0, 0xFF0000); + Serial.println("======== Pushed A button! ========"); + respondOk(client); +} + +void handlePostB(WiFiClient& client, const String& query) { + M5.dis.drawpix(0, 0x00FF00); + Serial.println("======== Pushed B button! ========"); + respondOk(client); +} + +void setup() { + M5.begin(false, false, true); + Serial.begin(115200); + M5.dis.drawpix(0, 0x0000FF); + pinMode(LEDright, OUTPUT); + pinMode(LEDleft, OUTPUT); + pinMode(MotorA, OUTPUT); + pinMode(MotorB, OUTPUT); + // チャンネル1と周波数の分解能を設定. + ledcSetup(PWM_CH_A, PWM_freq, PWM_res); + // チャンネル2と周波数の分解能を設定. + ledcSetup(PWM_CH_B, PWM_freq, PWM_res); + + // LED とモータのピンとチャンネルの設定. + ledcAttachPin(LEDright, PWM_CH_A); + ledcAttachPin(LEDleft, PWM_CH_B); + ledcAttachPin(MotorA, PWM_CH_A); + ledcAttachPin(MotorB, PWM_CH_B); + + delay(10); + + server.begin(); + + server.get("/", handleRoot); + server.post("/joystick", handlePostJoystick); + server.post("/a", handlePostA); + server.post("/b", handlePostB); +} + +double moterRight = 0.0, moterLeft = 0.0; + +unsigned long prevTime = millis(); + +void loop() { + const unsigned long deltaTime = millis() - prevTime; + prevTime = millis(); + + if (moterRight >= 0.0) moterRight = max(moterRight - deltaTime / 1000.0, 0.0); + else moterRight = min(moterRight + deltaTime / 250.0, 0.0); + if (moterLeft >= 0.0) moterLeft = max(moterLeft - deltaTime / 1000.0, 0.0); + else moterLeft = min(moterLeft + deltaTime / 250.0, 0.0); + + server.handleClient(); + + ledcWrite(PWM_CH_A, (uint16_t)(sqrt(max(0.0, moterRight)) * 255)); + ledcWrite(PWM_CH_B, (uint16_t)(sqrt(max(0.0, moterLeft)) * 255)); + + M5.update(); +} + +void updateMoters(double angle, double force) { + if (angle < 15 || angle >= 345) { + moterRight = -0.5; + moterLeft = 0.5; + } else if (angle >= 165 && angle < 195) { + moterRight = 0.5; + moterLeft = -0.5; + } else if (angle < 90) { + moterRight = constrain(angle / 40 - 0.875, -0.5, 1.0); + moterLeft = constrain(angle / 40 + 0.125, 0.5, 1.0); + } else if (angle < 180) { + moterRight = constrain(-angle / 40 + 4.625, 0.5, 1.0); + moterLeft = constrain(-angle / 40 + 3.625, -0.5, 1.0); + } else if (angle < 270) { + //moterRight = constrain(-angle / 40 + 4.375, -1.0, -0.5); + //moterLeft = constrain(-angle / 40 + 5.375, -1.0, 0.5); + moterRight = angle < 255 ? 0.5 : -0.5; + moterLeft = -0.5; + } else { + //moterRight = constrain(angle / 40 - 8.125, -1.0, 0.5); + //moterLeft = constrain(angle / 40 - 9.125, -1.0, -0.5); + moterRight = -0.5; + moterLeft = angle < 285 ? -0.5 : 0.5; + } + + moterRight *= force; + moterLeft *= force; +} diff --git a/package.json b/package.json index b6a642b..5d91c66 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "preview": "vite preview" }, "devDependencies": { - "vite": "^5.3.1" + "vite": "^5.3.1", + "vite-plugin-singlefile": "^2.0.2" }, "dependencies": { "nipplejs": "^0.10.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28d7297..5bedae3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: vite: specifier: ^5.3.1 version: 5.3.3 + vite-plugin-singlefile: + specifier: ^2.0.2 + version: 2.0.2(rollup@4.18.1)(vite@5.3.3) packages: @@ -242,16 +245,32 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} hasBin: true + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -263,6 +282,10 @@ packages: picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + postcss@8.4.39: resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==} engines: {node: ^10 || ^12 || >=14} @@ -279,6 +302,17 @@ packages: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + vite-plugin-singlefile@2.0.2: + resolution: {integrity: sha512-Z2ou6HcvED5CF0hM+vcFSaFa+klyS8RyyLxW0PbMRLnMbvzTI6ueWyxdYNFhpuXZgz/aj6+E/dHFTdEcw6gb9w==} + engines: {node: '>18.0.0'} + peerDependencies: + rollup: ^4.18.0 + vite: ^5.3.1 + vite@5.3.3: resolution: {integrity: sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==} engines: {node: ^18.0.0 || >=20.0.0} @@ -428,6 +462,10 @@ snapshots: '@types/estree@1.0.5': {} + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -454,15 +492,28 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + fsevents@2.3.3: optional: true + is-number@7.0.0: {} + + micromatch@4.0.7: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + nanoid@3.3.7: {} nipplejs@0.10.2: {} picocolors@1.0.1: {} + picomatch@2.3.1: {} + postcss@8.4.39: dependencies: nanoid: 3.3.7 @@ -495,6 +546,16 @@ snapshots: source-map-js@1.2.0: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + vite-plugin-singlefile@2.0.2(rollup@4.18.1)(vite@5.3.3): + dependencies: + micromatch: 4.0.7 + rollup: 4.18.1 + vite: 5.3.3 + vite@5.3.3: dependencies: esbuild: 0.21.5 diff --git a/src/index.html b/src/index.html index 2581969..700c4d7 100644 --- a/src/index.html +++ b/src/index.html @@ -2,7 +2,7 @@ - + Virtual Gamepad @@ -25,11 +25,11 @@

Settings

- +

- ms + ms

@@ -44,15 +44,23 @@

Server Side (Microcontroller, etc.)

  1. Run a program that can handle HTTP requests in the format of the following protocol. - (TODO: Sample using Arduino WiFi.h) + (An example microcomputer car with M5Atom)
  2. -
  3. Confirm that you can communicate with the target server.
  4. +
  5. Check the address of the target server.

Controller Side

    -
  1. Open the web page and enter the URL of the target server (microcontroller, etc.) in the host field.
  2. -
  3. Adjust the joystick transmission interval (default 125 ms).
  4. +
  5. Open the address of the target server (e.g., microcomputer) in your browser.
  6. +
  7. + Adjust the joystick transmission interval (default 100 ms). It is recommended to keep it small enough + that it does not choke communication. +
  8. Hide the A, B buttons if you are not using them.
  9. Close the settings screen and operate with swipe or WASD (+ Shift).
@@ -62,11 +70,21 @@

Controller Side

Protocol

- Unless you are hosting a clone of this site on the target server, you will generally need to add - Access-Control-Allow-Origin: https://twosquirrels.github.io to the response headers to avoid - errors. + When communicating from an HTTPS page, such as on github.io, to an HTTP host, a security error occurs. This + can be handled by configuring the browser, but it is recommended to serve HTML on the same host as the API and + access the page from there.

+
+

GET /

+ +

Request

+

Basically, it is accessed through a browser.

+ +

Response

+

Please return the gamepad HTML, we recommend that you download the latest HTML from this page.

+
+

POST /joystick?p=aaff

diff --git a/src/main.js b/src/main.js index 2281333..c6db4d2 100644 --- a/src/main.js +++ b/src/main.js @@ -30,8 +30,8 @@ for (const burger of elms.hamburgers) { /// poster -let host = params.get("host") ?? "http://localhost:3000"; -let interval = Number(params.get("interval")) || 125; +let host = params.get("host") ?? window.location.origin; +let interval = Number(params.get("interval")) || 100; let showButtons = true; const joystickPoster = { @@ -85,12 +85,14 @@ const joystickPoster = { /// settings elms.settings.host.value = host; +elms.settings.host.placeholder = host; elms.settings.host.oninput = ({ target: input }) => { console.log(`Modify host ${host} -> ${input.value}`); host = input.value; }; elms.settings.interval.value = interval; +elms.settings.interval.placeholder = interval; elms.settings.interval.oninput = ({ target: input }) => { console.log(`Modify interval ${interval} -> ${input.value}`); interval = input.value; diff --git a/vite.config.js b/vite.config.js index 7309873..0110462 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,3 +1,5 @@ +import { viteSingleFile } from "vite-plugin-singlefile"; + export default { base: process.env.GITHUB_PAGES ? "REPOSITORY_NAME" : "./", root: "./src", @@ -6,4 +8,5 @@ export default { outDir: "../dist", emptyOutDir: true, }, + plugins: [viteSingleFile()], };