Skip to content

Getting Started with ArcanePad on Web

Welcome to ArcanePad! This guide will help you get started with integrating ArcanePad into your web projects.

If you haven't installed the app yet, please refer to this guide and follow the instructions to install ArcanePad desktop and mobile apps before continuing.

We are going to use Quasar.js, a framework on top of Vue.js. The reason for this is that Quasar comes with a lot of tools out of the box that are going to make our development journey a lot easier. For more information, visit quasar.dev to get started.

Things you need to have installed

  1. Visual Studio Code
  2. Node.js
  3. NPM
  4. Vue
  5. Quasar

Installing the ArcanePad Web SDK

bash
npm install arcanepad-web-sdk
npm install arcanepad-web-sdk

Basic Tutorial

Getting Sensors Data Tutorial

Starter Template Repo

https://github.com/imvenx/arcanepad-web-template

vue
<template>
  <router-view v-if="isInitialized" />
</template>

<script setup lang="ts">
import { Arcane } from 'arcanepad-web-sdk';
import { ArcaneInitParams } from 'arcanepad-web-sdk/src/models/Models';
import { onMounted, ref } from 'vue';


const isInitialized = ref(false)

onMounted(async () => {
  Arcane.init(new ArcaneInitParams({ padOrientation: 'Portrait', hideMouse: false }))

  await Arcane.arcaneClientInitialized()
  isInitialized.value = true
})

</script>
<template>
  <router-view v-if="isInitialized" />
</template>

<script setup lang="ts">
import { Arcane } from 'arcanepad-web-sdk';
import { ArcaneInitParams } from 'arcanepad-web-sdk/src/models/Models';
import { onMounted, ref } from 'vue';


const isInitialized = ref(false)

onMounted(async () => {
  Arcane.init(new ArcaneInitParams({ padOrientation: 'Portrait', hideMouse: false }))

  await Arcane.arcaneClientInitialized()
  isInitialized.value = true
})

</script>
vue
<template>
  <h1>Viewer</h1>
  <div>Is app paused: {{ isAppPaused }}</div>
  <div style="display: grid; grid-template-columns: 50% 50%;">
    <div v-for="pad in pads">
      <Player :pad="pad" />
    </div>
  </div>
</template>

<script setup lang="ts">
import { AEventName, Arcane, ArcanePad, IframePadConnectEvent, IframePadDisconnectEvent } from 'arcanepad-web-sdk';
import Player from 'src/components/Player.vue';
import { Ref, onMounted, ref } from 'vue';


const pads: Ref<ArcanePad[]> = ref([])
const isAppPaused = ref(false)

onMounted(() => {
  init()
})

async function init() {
  let initialState = await Arcane.arcaneClientInitialized()
  pads.value = initialState.pads

  Arcane.msg.on(AEventName.IframePadConnect, (e: IframePadConnectEvent) => {
    const padExists = pads.value.some(p => p.iframeId === e.iframeId)
    if (padExists) return

    const padToAdd = new ArcanePad({ deviceId: e.deviceId, internalId: e.internalId, iframeId: e.iframeId, isConnected: true, user: e.user })
    pads.value.push(padToAdd)
  })

  Arcane.msg.on(AEventName.IframePadDisconnect, (e: IframePadDisconnectEvent) => {
    const padToRemove = pads.value.find(p => p.iframeId === e.iframeId)
    if (!padToRemove) return

    pads.value.splice(pads.value.indexOf(padToRemove), 1)
  })

  Arcane.msg.on(AEventName.PauseApp, (e) => isAppPaused.value = true)
  Arcane.msg.on(AEventName.ResumeApp, (e) => isAppPaused.value = false)
}
</script>
<template>
  <h1>Viewer</h1>
  <div>Is app paused: {{ isAppPaused }}</div>
  <div style="display: grid; grid-template-columns: 50% 50%;">
    <div v-for="pad in pads">
      <Player :pad="pad" />
    </div>
  </div>
</template>

<script setup lang="ts">
import { AEventName, Arcane, ArcanePad, IframePadConnectEvent, IframePadDisconnectEvent } from 'arcanepad-web-sdk';
import Player from 'src/components/Player.vue';
import { Ref, onMounted, ref } from 'vue';


const pads: Ref<ArcanePad[]> = ref([])
const isAppPaused = ref(false)

onMounted(() => {
  init()
})

async function init() {
  let initialState = await Arcane.arcaneClientInitialized()
  pads.value = initialState.pads

  Arcane.msg.on(AEventName.IframePadConnect, (e: IframePadConnectEvent) => {
    const padExists = pads.value.some(p => p.iframeId === e.iframeId)
    if (padExists) return

    const padToAdd = new ArcanePad({ deviceId: e.deviceId, internalId: e.internalId, iframeId: e.iframeId, isConnected: true, user: e.user })
    pads.value.push(padToAdd)
  })

  Arcane.msg.on(AEventName.IframePadDisconnect, (e: IframePadDisconnectEvent) => {
    const padToRemove = pads.value.find(p => p.iframeId === e.iframeId)
    if (!padToRemove) return

    pads.value.splice(pads.value.indexOf(padToRemove), 1)
  })

  Arcane.msg.on(AEventName.PauseApp, (e) => isAppPaused.value = true)
  Arcane.msg.on(AEventName.ResumeApp, (e) => isAppPaused.value = false)
}
</script>
vue
<template>
  <router-view />
</template>

<script setup lang="ts">
</script>
<template>
  <router-view />
</template>

<script setup lang="ts">
</script>
ts
import { ArcaneBaseEvent } from "arcanepad-web-sdk"

export class AttackedEvent extends ArcaneBaseEvent {
  damage: number
  constructor(damage: number) {
    super('Attacked')
    this.damage = damage
  }
}
import { ArcaneBaseEvent } from "arcanepad-web-sdk"

export class AttackedEvent extends ArcaneBaseEvent {
  damage: number
  constructor(damage: number) {
    super('Attacked')
    this.damage = damage
  }
}
vue
<template>
  <h1>Gamepad</h1>
  <h5> User Name:
    {{ Arcane.pad?.user?.name }}
  </h5>
  <q-btn @click="Arcane.msg.emitToViews(new ArcaneBaseEvent('Jump'))" size="xl" outline>Jump</q-btn>
  <div>
    <q-btn @click="Arcane.pad?.calibratePointer(true)" size="xl" outline>Calibrate Pointer Top Left</q-btn>
    <q-btn @click="Arcane.pad?.calibratePointer(false)" size="xl" outline>Calibrate Pointer Bottom Right</q-btn>
    <q-btn @click="Arcane.pad?.calibrateQuaternion()" size="xl" outline>Calibrate Quaternion</q-btn>
  </div>
</template>

<script setup lang="ts">
import { Arcane, ArcaneBaseEvent } from 'arcanepad-web-sdk';
import { AttackedEvent } from 'src/models';
import { onMounted } from 'vue';

onMounted(() => {
  Arcane.msg.on('Attacked', ({ damage }: AttackedEvent) => { alert('taken damage: ' + damage) })
})

</script>
<template>
  <h1>Gamepad</h1>
  <h5> User Name:
    {{ Arcane.pad?.user?.name }}
  </h5>
  <q-btn @click="Arcane.msg.emitToViews(new ArcaneBaseEvent('Jump'))" size="xl" outline>Jump</q-btn>
  <div>
    <q-btn @click="Arcane.pad?.calibratePointer(true)" size="xl" outline>Calibrate Pointer Top Left</q-btn>
    <q-btn @click="Arcane.pad?.calibratePointer(false)" size="xl" outline>Calibrate Pointer Bottom Right</q-btn>
    <q-btn @click="Arcane.pad?.calibrateQuaternion()" size="xl" outline>Calibrate Quaternion</q-btn>
  </div>
</template>

<script setup lang="ts">
import { Arcane, ArcaneBaseEvent } from 'arcanepad-web-sdk';
import { AttackedEvent } from 'src/models';
import { onMounted } from 'vue';

onMounted(() => {
  Arcane.msg.on('Attacked', ({ damage }: AttackedEvent) => { alert('taken damage: ' + damage) })
})

</script>
vue
<template>
  <div :style="`border: 2px solid #${pad.user?.color}`">
    {{ pad.user?.name }}
  </div>
  <div>
    Jump count: {{ jumpCount }}
  </div>
  <q-btn @click="onAttacked" size="xl" outline>Be Attacked</q-btn>
  <div>
    {{ pointerData.x.toFixed(0) }} |
    {{ pointerData.y.toFixed(0) }}
  </div>

  <q-btn @click="pad.startGetPointer()" size="xl" outline>Start Get Pointer</q-btn>
  <q-btn @click="pad.stopGetPointer()" size="xl" outline>Stop Get Pointer</q-btn>
  <div style="width: 10px; height: 10px; border-radius: 100px; border: 2px solid red; position: absolute;"
    :style="`left:${pointerData.x}%; top: ${pointerData.y}%`">
  </div>

  <div>
    Euler Data: {{ eulerData }}
  </div>
  <div>
    <q-btn @click="pad.startGetRotationEuler()" size="xl" outline>Start Get Euler</q-btn>
    <q-btn @click="pad.stopGetRotationEuler()" size="xl" outline>Stop Get Euler</q-btn>
  </div>

  <div>
    Quaternion Data: {{ quaternionData }}
  </div>
  <div>
    <q-btn @click="pad.startGetQuaternion()" size="xl" outline>Start Get Quaternion</q-btn>
    <q-btn @click="pad.stopGetQuaternion()" size="xl" outline>Stop Get Quaternion</q-btn>
  </div>

  <div>
    Linear acceleration Data: {{ linearAcceleration }}
  </div>
  <div>
    <q-btn @click="pad.startGetLinearAcceleration()" size="xl" outline>Start Get linear acceleration</q-btn>
    <q-btn @click="pad.stopGetLinearAcceleration()" size="xl" outline>Stop Get linear acceleration</q-btn>
  </div>
</template>

<script lang="ts" setup>
import { ArcaneBaseEvent, ArcanePad, GetLinearAccelerationEvent, GetPointerEvent, GetQuaternionEvent, GetRotationEulerEvent } from 'arcanepad-web-sdk';
import { AttackedEvent } from 'src/models';
import { onMounted, ref } from 'vue';

const { pad } = defineProps<{ pad: ArcanePad }>()

const jumpCount = ref(0)
const pointerData = ref({ x: 0, y: 0 })
const eulerData = ref({ azimuth: 0, pitch: 0, roll: 0 })
const quaternionData = ref({ x: 0, y: 0, z: 0, w: 0 })
const linearAcceleration = ref({ x: 0, y: 0, z: 0 })

onMounted(() => {
  pad.on('Jump', () => jumpCount.value++)

  pad.onGetPointer(({ x, y }: GetPointerEvent) => {
    pointerData.value = { x, y }
  })

  pad.onGetRotationEuler(({ azimuth, pitch, roll }: GetRotationEulerEvent) => {
    eulerData.value = { azimuth, pitch, roll }
  })


  pad.onGetQuaternion(({ w, x, y, z, }: GetQuaternionEvent) => {
    quaternionData.value = { w, x, y, z }
  })

  pad.onGetLinearAcceleration(({ x, y, z, }: GetLinearAccelerationEvent) => {
    linearAcceleration.value = { x, y, z }
  })

})

function onAttacked() {
  pad.vibrate(1000)
  pad.emit(new AttackedEvent(5))
}



</script>
<template>
  <div :style="`border: 2px solid #${pad.user?.color}`">
    {{ pad.user?.name }}
  </div>
  <div>
    Jump count: {{ jumpCount }}
  </div>
  <q-btn @click="onAttacked" size="xl" outline>Be Attacked</q-btn>
  <div>
    {{ pointerData.x.toFixed(0) }} |
    {{ pointerData.y.toFixed(0) }}
  </div>

  <q-btn @click="pad.startGetPointer()" size="xl" outline>Start Get Pointer</q-btn>
  <q-btn @click="pad.stopGetPointer()" size="xl" outline>Stop Get Pointer</q-btn>
  <div style="width: 10px; height: 10px; border-radius: 100px; border: 2px solid red; position: absolute;"
    :style="`left:${pointerData.x}%; top: ${pointerData.y}%`">
  </div>

  <div>
    Euler Data: {{ eulerData }}
  </div>
  <div>
    <q-btn @click="pad.startGetRotationEuler()" size="xl" outline>Start Get Euler</q-btn>
    <q-btn @click="pad.stopGetRotationEuler()" size="xl" outline>Stop Get Euler</q-btn>
  </div>

  <div>
    Quaternion Data: {{ quaternionData }}
  </div>
  <div>
    <q-btn @click="pad.startGetQuaternion()" size="xl" outline>Start Get Quaternion</q-btn>
    <q-btn @click="pad.stopGetQuaternion()" size="xl" outline>Stop Get Quaternion</q-btn>
  </div>

  <div>
    Linear acceleration Data: {{ linearAcceleration }}
  </div>
  <div>
    <q-btn @click="pad.startGetLinearAcceleration()" size="xl" outline>Start Get linear acceleration</q-btn>
    <q-btn @click="pad.stopGetLinearAcceleration()" size="xl" outline>Stop Get linear acceleration</q-btn>
  </div>
</template>

<script lang="ts" setup>
import { ArcaneBaseEvent, ArcanePad, GetLinearAccelerationEvent, GetPointerEvent, GetQuaternionEvent, GetRotationEulerEvent } from 'arcanepad-web-sdk';
import { AttackedEvent } from 'src/models';
import { onMounted, ref } from 'vue';

const { pad } = defineProps<{ pad: ArcanePad }>()

const jumpCount = ref(0)
const pointerData = ref({ x: 0, y: 0 })
const eulerData = ref({ azimuth: 0, pitch: 0, roll: 0 })
const quaternionData = ref({ x: 0, y: 0, z: 0, w: 0 })
const linearAcceleration = ref({ x: 0, y: 0, z: 0 })

onMounted(() => {
  pad.on('Jump', () => jumpCount.value++)

  pad.onGetPointer(({ x, y }: GetPointerEvent) => {
    pointerData.value = { x, y }
  })

  pad.onGetRotationEuler(({ azimuth, pitch, roll }: GetRotationEulerEvent) => {
    eulerData.value = { azimuth, pitch, roll }
  })


  pad.onGetQuaternion(({ w, x, y, z, }: GetQuaternionEvent) => {
    quaternionData.value = { w, x, y, z }
  })

  pad.onGetLinearAcceleration(({ x, y, z, }: GetLinearAccelerationEvent) => {
    linearAcceleration.value = { x, y, z }
  })

})

function onAttacked() {
  pad.vibrate(1000)
  pad.emit(new AttackedEvent(5))
}



</script>
ts
import { RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    component: () => import('layouts/MainLayout.vue'),
    children: [
      { path: '', component: () => import('pages/IndexPage.vue') },
      { path: '/Pad', component: () => import('pages/PadPage.vue') },
    ],
  },

  // Always leave this as last one,
  // but you can also remove it
  {
    path: '/:catchAll(.*)*',
    component: () => import('pages/ErrorNotFound.vue'),
  },
];

export default routes;
import { RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    component: () => import('layouts/MainLayout.vue'),
    children: [
      { path: '', component: () => import('pages/IndexPage.vue') },
      { path: '/Pad', component: () => import('pages/PadPage.vue') },
    ],
  },

  // Always leave this as last one,
  // but you can also remove it
  {
    path: '/:catchAll(.*)*',
    component: () => import('pages/ErrorNotFound.vue'),
  },
];

export default routes;
js
/* eslint-env node */

/*
 * This file runs in a Node context (it's NOT transpiled by Babel), so use only
 * the ES6 features that are supported by your Node version. https://node.green/
 */

// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js


const { configure } = require('quasar/wrappers');


module.exports = configure(function (/* ctx */) {
  return {


    // https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
    // preFetch: true,

    // app boot file (/src/boot)
    // --> boot files are part of "main.js"
    // https://v2.quasar.dev/quasar-cli-vite/boot-files
    boot: [


    ],

    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
    css: [
      'app.css'
    ],

    // https://github.com/quasarframework/quasar/tree/dev/extras
    extras: [
      // 'ionicons-v4',
      // 'mdi-v5',
      // 'fontawesome-v6',
      // 'eva-icons',
      // 'themify',
      // 'line-awesome',
      // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!

      'roboto-font', // optional, you are not bound to it
      'material-icons', // optional, you are not bound to it
    ],

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
    build: {
      target: {
        browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
        node: 'node16'
      },

      vueRouterMode: 'hash', // available values: 'hash', 'history'
      // vueRouterBase,
      // vueDevtools,
      // vueOptionsAPI: false,

      // rebuildCache: true, // rebuilds Vite/linter/etc cache on startup

      // publicPath: '/',
      // analyze: true,
      // env: {},
      // rawDefine: {}
      // ignorePublicFolder: true,
      // minify: false,
      // polyfillModulePreload: true,
      // distDir

      // extendViteConf (viteConf) {},
      // viteVuePluginOptions: {},


      // vitePlugins: [
      //   [ 'package-name', { ..options.. } ]
      // ]
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
    devServer: {
      https: true,
      open: false // opens browser window automatically
    },

    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
    framework: {
      config: { dark: true },

      // iconSet: 'material-icons', // Quasar icon set
      // lang: 'en-US', // Quasar language pack

      // For special cases outside of where the auto-import strategy can have an impact
      // (like functional components as one of the examples),
      // you can manually specify Quasar components/directives to be available everywhere:
      //
      // components: [],
      // directives: [],

      // Quasar plugins
      plugins: []
    },

    // animations: 'all', // --- includes all animations
    // https://v2.quasar.dev/options/animations
    animations: [],

    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#sourcefiles
    // sourceFiles: {
    //   rootComponent: 'src/App.vue',
    //   router: 'src/router/index',
    //   store: 'src/store/index',
    //   registerServiceWorker: 'src-pwa/register-service-worker',
    //   serviceWorker: 'src-pwa/custom-service-worker',
    //   pwaManifestFile: 'src-pwa/manifest.json',
    //   electronMain: 'src-electron/electron-main',
    //   electronPreload: 'src-electron/electron-preload'
    // },

    // https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
    ssr: {
      // ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
      // will mess up SSR

      // extendSSRWebserverConf (esbuildConf) {},
      // extendPackageJson (json) {},

      pwa: false,

      // manualStoreHydration: true,
      // manualPostHydrationTrigger: true,

      prodPort: 3000, // The default port that the production server should use
      // (gets superseded if process.env.PORT is specified at runtime)

      middlewares: [
        'render' // keep this as last one
      ]
    },

    // https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
    pwa: {
      workboxMode: 'generateSW', // or 'injectManifest'
      injectPwaMetaTags: true,
      swFilename: 'sw.js',
      manifestFilename: 'manifest.json',
      useCredentialsForManifestTag: false,
      // useFilenameHashes: true,
      // extendGenerateSWOptions (cfg) {}
      // extendInjectManifestOptions (cfg) {},
      // extendManifestJson (json) {}
      // extendPWACustomSWConf (esbuildConf) {}
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
    cordova: {
      // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
    capacitor: {
      hideSplashscreen: true
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
    electron: {
      // extendElectronMainConf (esbuildConf)
      // extendElectronPreloadConf (esbuildConf)

      inspectPort: 5858,

      bundler: 'packager', // 'packager' or 'builder'

      packager: {
        // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options

        // OS X / Mac App Store
        // appBundleId: '',
        // appCategoryType: '',
        // osxSign: '',
        // protocol: 'myapp://path',

        // Windows only
        // win32metadata: { ... }
      },

      builder: {
        // https://www.electron.build/configuration/configuration

        appId: 'arcanepad-web-template'
      }
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
    bex: {
      contentScripts: [
        'my-content-script'
      ],

      // extendBexScriptsConf (esbuildConf) {}
      // extendBexManifestJson (json) {}
    }
  }
});
/* eslint-env node */

/*
 * This file runs in a Node context (it's NOT transpiled by Babel), so use only
 * the ES6 features that are supported by your Node version. https://node.green/
 */

// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js


const { configure } = require('quasar/wrappers');


module.exports = configure(function (/* ctx */) {
  return {


    // https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
    // preFetch: true,

    // app boot file (/src/boot)
    // --> boot files are part of "main.js"
    // https://v2.quasar.dev/quasar-cli-vite/boot-files
    boot: [


    ],

    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
    css: [
      'app.css'
    ],

    // https://github.com/quasarframework/quasar/tree/dev/extras
    extras: [
      // 'ionicons-v4',
      // 'mdi-v5',
      // 'fontawesome-v6',
      // 'eva-icons',
      // 'themify',
      // 'line-awesome',
      // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!

      'roboto-font', // optional, you are not bound to it
      'material-icons', // optional, you are not bound to it
    ],

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
    build: {
      target: {
        browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
        node: 'node16'
      },

      vueRouterMode: 'hash', // available values: 'hash', 'history'
      // vueRouterBase,
      // vueDevtools,
      // vueOptionsAPI: false,

      // rebuildCache: true, // rebuilds Vite/linter/etc cache on startup

      // publicPath: '/',
      // analyze: true,
      // env: {},
      // rawDefine: {}
      // ignorePublicFolder: true,
      // minify: false,
      // polyfillModulePreload: true,
      // distDir

      // extendViteConf (viteConf) {},
      // viteVuePluginOptions: {},


      // vitePlugins: [
      //   [ 'package-name', { ..options.. } ]
      // ]
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
    devServer: {
      https: true,
      open: false // opens browser window automatically
    },

    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
    framework: {
      config: { dark: true },

      // iconSet: 'material-icons', // Quasar icon set
      // lang: 'en-US', // Quasar language pack

      // For special cases outside of where the auto-import strategy can have an impact
      // (like functional components as one of the examples),
      // you can manually specify Quasar components/directives to be available everywhere:
      //
      // components: [],
      // directives: [],

      // Quasar plugins
      plugins: []
    },

    // animations: 'all', // --- includes all animations
    // https://v2.quasar.dev/options/animations
    animations: [],

    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#sourcefiles
    // sourceFiles: {
    //   rootComponent: 'src/App.vue',
    //   router: 'src/router/index',
    //   store: 'src/store/index',
    //   registerServiceWorker: 'src-pwa/register-service-worker',
    //   serviceWorker: 'src-pwa/custom-service-worker',
    //   pwaManifestFile: 'src-pwa/manifest.json',
    //   electronMain: 'src-electron/electron-main',
    //   electronPreload: 'src-electron/electron-preload'
    // },

    // https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
    ssr: {
      // ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
      // will mess up SSR

      // extendSSRWebserverConf (esbuildConf) {},
      // extendPackageJson (json) {},

      pwa: false,

      // manualStoreHydration: true,
      // manualPostHydrationTrigger: true,

      prodPort: 3000, // The default port that the production server should use
      // (gets superseded if process.env.PORT is specified at runtime)

      middlewares: [
        'render' // keep this as last one
      ]
    },

    // https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
    pwa: {
      workboxMode: 'generateSW', // or 'injectManifest'
      injectPwaMetaTags: true,
      swFilename: 'sw.js',
      manifestFilename: 'manifest.json',
      useCredentialsForManifestTag: false,
      // useFilenameHashes: true,
      // extendGenerateSWOptions (cfg) {}
      // extendInjectManifestOptions (cfg) {},
      // extendManifestJson (json) {}
      // extendPWACustomSWConf (esbuildConf) {}
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
    cordova: {
      // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
    capacitor: {
      hideSplashscreen: true
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
    electron: {
      // extendElectronMainConf (esbuildConf)
      // extendElectronPreloadConf (esbuildConf)

      inspectPort: 5858,

      bundler: 'packager', // 'packager' or 'builder'

      packager: {
        // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options

        // OS X / Mac App Store
        // appBundleId: '',
        // appCategoryType: '',
        // osxSign: '',
        // protocol: 'myapp://path',

        // Windows only
        // win32metadata: { ... }
      },

      builder: {
        // https://www.electron.build/configuration/configuration

        appId: 'arcanepad-web-template'
      }
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
    bex: {
      contentScripts: [
        'my-content-script'
      ],

      // extendBexScriptsConf (esbuildConf) {}
      // extendBexManifestJson (json) {}
    }
  }
});

Upload your game to Arcanepad

Go to https://dev.arcanepad.com, create an account and after you are verified you can upload your game. The app folder has to be compressed on .zip format.