Skip to content

Commit

Permalink
feat(vue): add joystick support
Browse files Browse the repository at this point in the history
  • Loading branch information
verekia committed Apr 4, 2024
1 parent 2d41431 commit 5e99a31
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 13 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ useAnimationFrame(

## Virtual joysticks

⚠️ React and vanilla-only for now ⚠️
⚠️ Svelte not supported yet ⚠️

Mana Potion includes **🗿 non-reactive** and **headless** virtual joysticks for mobile controls. Each virtual joystick is associated with a single `<JoystickArea />`. You can create your own Joystick objects with `createJoystick()` or use one of the two default ones that are already available on the joysticks store. The default ones are called `movement` and `rotation` joysticks.

Expand All @@ -374,6 +374,8 @@ const MobileUI = () => (
)
```

With vanilla JS, use `mountJoystickArea` instead.

In follow mode, the joystick will follow the user's finger, which is good for player movement.

Here are the properties that will be updated on your joystick object:
Expand Down
2 changes: 1 addition & 1 deletion examples/react/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ const UI = ({
<div className="absolute left-[58px] top-[75px] max-w-36 text-center mobile:hidden">
Switch to 👆 mobile mode in devtools
</div>
<MobileJoystick className="mt-3" mode={joystickMode} />
<MobileJoystick mode={joystickMode} />
<div className="mt-3 flex items-center justify-center gap-3">
Mode{' '}
<button
Expand Down
10 changes: 2 additions & 8 deletions examples/react/src/components/MobileJoystick.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@ import { useRef } from 'react'

import { getJoysticks, Joystick, JoystickArea } from '@manapotion/r3f'

const MobileJoystick = ({
className = '',
mode = 'follow',
}: {
className?: string
mode?: 'follow' | 'origin'
}) => {
const MobileJoystick = ({ mode = 'follow' }: { mode?: 'follow' | 'origin' }) => {
const joystickCurrentRef = useRef<HTMLDivElement>(null)
const joystickOriginRef = useRef<HTMLDivElement>(null)
const joystickFollowRef = useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -39,7 +33,7 @@ const MobileJoystick = ({
joystick={getJoysticks().movement}
mode={mode}
containerProps={{
className: `relative z-10 h-48 w-64 rounded-md border border-slate-500 ${className}`,
className: `relative z-10 h-48 w-64 rounded-md border border-slate-500 mt-3`,
}}
onStart={handleStart}
onMove={handleMove}
Expand Down
23 changes: 20 additions & 3 deletions examples/vue/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import LockedLabel from './components/labels/LockedLabel.vue'
import LockMouseButton from './components/labels/LockMouseButton.vue'
import MiddleMouseButtonLabel from './components/labels/MiddleMouseButtonLabel.vue'
import RightMouseButtonLabel from './components/labels/RightMouseButtonLabel.vue'
import MobileJoystick from './components/MobileJoystick.vue'
import TwitterIcon from './components/TwitterIcon.vue'
const animationFrameEl = ref<HTMLSpanElement>()
Expand All @@ -46,6 +47,8 @@ useAnimationFrame(
},
{ throttle: 100 },
)
const joystickMode = ref<'follow' | 'origin'>('follow')
</script>

<template>
Expand Down Expand Up @@ -132,13 +135,13 @@ useAnimationFrame(
<template #label>
<IsPortraitLabel />
</template>
<template #extra><span className="text-sm">Ratio-based</span></template>
<template #extra><span class="text-sm">Ratio-based</span></template>
</Item>
<Item name="isLandscape">
<template #label>
<IsLandscapeLabel />
</template>
<template #extra><span className="text-sm">Ratio-based</span></template>
<template #extra><span class="text-sm">Ratio-based</span></template>
</Item>

<Item :name="'width,height'" :is-reactive="false">
Expand Down Expand Up @@ -212,7 +215,21 @@ useAnimationFrame(
</section>
<section>
<h2 class="section-heading">🕹️ Virtual joysticks</h2>
<div>Vue support coming soon.</div>
<div class="relative w-max">
<div class="absolute left-[58px] top-[75px] max-w-36 text-center mobile:hidden">
Switch to 👆 mobile mode in devtools
</div>
<MobileJoystick :mode="joystickMode" />
<div class="mt-3 flex items-center justify-center gap-3">
Mode
<button
class="btn capitalize"
@click="() => (joystickMode = joystickMode === 'follow' ? 'origin' : 'follow')"
>
{{ joystickMode === 'follow' ? 'Follow' : 'Origin' }}
</button>
</div>
</div>
</section>
<section>
<h2 class="section-heading">🔄 Animation loops</h2>
Expand Down
56 changes: 56 additions & 0 deletions examples/vue/src/components/MobileJoystick.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<script setup lang="ts">
import { ref } from 'vue'
import { getJoysticks, Joystick, JoystickArea } from '@manapotion/vue'
const props = defineProps<{ mode: 'follow' | 'origin' }>()
const joystickCurrent = ref<HTMLDivElement | null>(null)
const joystickOrigin = ref<HTMLDivElement | null>(null)
const joystickFollow = ref<HTMLDivElement | null>(null)
const handleStart = (joystick: Joystick) => {
joystickCurrent.value!.style.transform = `translate(${joystick.current.x}px, ${-joystick.current.y!}px)`
joystickOrigin.value!.style.transform = `translate(${joystick.origin.x}px, ${-joystick.origin.y!}px)`
joystickFollow.value!.style.transform = `translate(${joystick.follow.x}px, ${-joystick.follow.y!}px)`
joystickCurrent.value!.style.opacity = '1'
props.mode === 'follow' && (joystickFollow.value!.style.opacity = '1')
joystickOrigin.value!.style.opacity = '1'
}
const handleEnd = () => {
joystickCurrent.value!.style.opacity = '0'
joystickFollow.value!.style.opacity = '0'
joystickOrigin.value!.style.opacity = '0'
}
const handleMove = (joystick: Joystick) => {
joystickCurrent.value!.style.transform = `translate(${joystick.current.x}px, ${-joystick.current.y!}px)`
joystickOrigin.value!.style.transform = `translate(${joystick.origin.x}px, ${-joystick.origin.y!}px)`
joystickFollow.value!.style.transform = `translate(${joystick.follow.x}px, ${-joystick.follow.y!}px)`
}
</script>

<template>
<JoystickArea
:joystick="getJoysticks().movement"
:mode="props.mode"
:container-props="{ class: `relative z-10 h-48 w-64 rounded-md border border-slate-500 ` }"
@start="handleStart"
@move="handleMove"
@end="handleEnd"
>
<div
ref="joystickCurrent"
class="pointer-events-none absolute -bottom-6 -left-6 size-12 rounded-full bg-red-500 opacity-0 transition-opacity"
/>
<div
ref="joystickOrigin"
class="pointer-events-none absolute -bottom-6 -left-6 size-12 rounded-full bg-blue-500 opacity-0 transition-opacity"
/>
<div
ref="joystickFollow"
class="pointer-events-none absolute -bottom-6 -left-6 size-12 rounded-full bg-green-500 opacity-0 transition-opacity"
/>
</JoystickArea>
</template>
76 changes: 76 additions & 0 deletions packages/vue/src/JoystickArea.vue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { defineComponent, h, onMounted, onUnmounted, ref, watch } from 'vue'

import { Joystick, mountJoystickArea } from '@manapotion/core'

export const JoystickArea = defineComponent({
name: 'JoystickArea',
props: {
mode: {
type: String as () => 'follow' | 'origin',
default: 'follow',
},
joystick: {
type: Object as () => Joystick,
required: true,
},
// eslint-disable-next-line vue/require-default-prop
maxFollowDistance: { type: Number },
// eslint-disable-next-line vue/require-default-prop
maxOriginDistance: { type: Number },
// eslint-disable-next-line vue/require-default-prop
onEnd: { type: Function },
// eslint-disable-next-line vue/require-default-prop
onMove: { type: Function },
// eslint-disable-next-line vue/require-default-prop
onStart: { type: Function },
// eslint-disable-next-line vue/require-default-prop
containerProps: { type: Object },
},
setup(props, { slots, attrs }) {
const localRef = ref<HTMLDivElement | null>(null)

onMounted(() => {
let unsub = mountJoystickArea({
mode: props.mode,
joystick: props.joystick,
maxFollowDistance: props.maxFollowDistance,
maxOriginDistance: props.maxOriginDistance,
onEnd: props.onEnd as (joystick: Joystick) => void,
onMove: props.onMove as (joystick: Joystick) => void,
onStart: props.onStart as (joystick: Joystick) => void,
element: localRef.value!,
})
// TODO: watch for changes in other props? joystick, maxFollowDistance, maxOriginDistance
watch(
() => props.mode,
newMode => {
unsub()
unsub = mountJoystickArea({
mode: newMode,
joystick: props.joystick,
maxFollowDistance: props.maxFollowDistance,
maxOriginDistance: props.maxOriginDistance,
onEnd: props.onEnd as (joystick: Joystick) => void,
onMove: props.onMove as (joystick: Joystick) => void,
onStart: props.onStart as (joystick: Joystick) => void,
element: localRef.value!,
})
},
)
onUnmounted(() => {
unsub()
})
})

return () =>
h(
'div',
{
...props.containerProps,
ref: localRef,
...attrs,
},
slots.default ? slots.default() : [],
)
},
})
1 change: 1 addition & 0 deletions packages/vue/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './Listeners.vue'
export * from './vue-store'
export * from './vue-loops'
export * from './JoystickArea.vue'

export * from './listeners/DeviceTypeListener.vue'
export * from './listeners/KeyboardListener.vue'
Expand Down

0 comments on commit 5e99a31

Please sign in to comment.