Working Eventbar

This commit is contained in:
aronmal 2023-06-07 11:51:33 +02:00
parent 787a85e425
commit 9895a286a3
Signed by: aronmal
GPG key ID: 816B7707426FC612
10 changed files with 403 additions and 77 deletions

View file

@ -1,35 +1,225 @@
import { Items, Target } from "../../interfaces/frontend" import { EventBarModes, Target } from "../../interfaces/frontend"
import Item from "./Item" import Item from "./Item"
import React, { Dispatch, SetStateAction } from "react" import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
import {
faArrowRightFromBracket,
faBroomWide,
faComments,
faEye,
faEyeSlash,
faGlasses,
faPalette,
faReply,
faScribble,
faSparkles,
faSwords,
} from "@fortawesome/pro-solid-svg-icons"
import { useDrawProps } from "@hooks/useDrawProps"
import { useGameProps } from "@hooks/useGameProps"
import { socket } from "@lib/socket"
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from "react"
function EventBar({ function EventBar({
props: { setMode, setTarget }, props: { setMode, setTarget, clear },
}: { }: {
props: { props: {
setMode: Dispatch<SetStateAction<number>> setMode: Dispatch<SetStateAction<number>>
setTarget: Dispatch<SetStateAction<Target>> setTarget: Dispatch<SetStateAction<Target>>
clear: () => void
} }
}) { }) {
const items: Items[] = [ const [menu, setMenu] = useState<keyof EventBarModes>("main")
{ icon: "burger-menu", text: "Menu" }, const { shouldHide, color } = useDrawProps()
{ icon: "radar", text: "Radar scan", mode: 0, amount: 1 }, const { payload, setSetting, full } = useGameProps()
{ icon: "torpedo", text: "Fire torpedo", mode: 1, amount: 1 },
{ icon: "scope", text: "Fire missile", mode: 3 }, const gameSetting = useCallback(
{ icon: "gear", text: "Settings" }, (payload: GameSettings) => {
] const hash = setSetting(payload)
socket.emit("gameSetting", payload, (newHash) => {
if (newHash === hash) return
console.log("hash", hash, newHash)
socket.emit("update", full)
})
},
[full, setSetting]
)
const items = useMemo<EventBarModes>(
() => ({
main: [
{
icon: "burger-menu",
text: "Menu",
callback: () => {
setMenu("menu")
},
},
{
icon: faSwords,
text: "Attack",
callback: () => {
setMenu("attack")
},
},
{
icon: "pen",
text: "Draw",
callback: () => {
setMenu("draw")
},
},
{
icon: "gear",
text: "Settings",
callback: () => {
setMenu("settings")
},
},
],
menu: [
{
icon: faReply,
text: "Return",
iconColor: "#555",
callback: () => {
setMenu("main")
},
},
{
icon: faArrowRightFromBracket,
text: "Leave",
iconColor: "darkred",
callback: () => {
// router.push()
},
},
],
attack: [
{
icon: faReply,
text: "Return",
iconColor: "#555",
callback: () => {
setMenu("main")
},
},
{
icon: "radar",
text: "Radar scan",
amount: 1,
callback: () => {
setMode(0)
setTarget((e) => ({ ...e, show: false }))
},
},
{
icon: "torpedo",
text: "Fire torpedo",
amount: 1,
callback: () => {
setMode(1)
setTarget((e) => ({ ...e, show: false }))
},
},
{
icon: "scope",
text: "Fire missile",
callback: () => {
setMode(3)
setTarget((e) => ({ ...e, show: false }))
},
},
],
draw: [
{
icon: faReply,
text: "Return",
iconColor: "#555",
callback: () => {
setMenu("main")
},
},
{ icon: faBroomWide, text: "Clear", callback: clear },
{ icon: faPalette, text: "Color", iconColor: color },
{
icon: shouldHide ? faEye : faEyeSlash,
text: shouldHide ? "Show" : "Hide",
callback: () => {
useDrawProps.setState({ shouldHide: !shouldHide })
},
},
],
settings: [
{
icon: faReply,
text: "Return",
iconColor: "#555",
callback: () => {
setMenu("main")
},
},
{
icon: faGlasses,
text: "Spectators",
disabled: !payload?.game?.allowSpectators,
callback: () => {
gameSetting({ allowSpectators: !payload?.game?.allowSpectators })
},
},
{
icon: faSparkles,
text: "Specials",
disabled: !payload?.game?.allowSpecials,
callback: () => {
gameSetting({ allowSpecials: !payload?.game?.allowSpecials })
},
},
{
icon: faComments,
text: "Chat",
disabled: !payload?.game?.allowChat,
callback: () => {
gameSetting({ allowChat: !payload?.game?.allowChat })
},
},
{
icon: faScribble,
text: "Mark/Draw",
disabled: !payload?.game?.allowMarkDraw,
callback: () => {
gameSetting({ allowMarkDraw: !payload?.game?.allowMarkDraw })
},
},
],
}),
[
clear,
color,
gameSetting,
payload?.game?.allowChat,
payload?.game?.allowMarkDraw,
payload?.game?.allowSpecials,
payload?.game?.allowSpectators,
setMode,
setTarget,
shouldHide,
]
)
useEffect(() => {
useDrawProps.setState({ enable: menu === "draw" })
}, [menu])
return ( return (
<div className="event-bar"> <div className="event-bar">
{items.map((e, i) => ( {items[menu].map((e, i) => (
<Item <Item key={i} props={e} />
key={i}
props={{
...e,
callback: () => {
if (e.mode !== undefined) setMode(e.mode)
setTarget((e) => ({ ...e, show: false }))
},
}}
/>
))} ))}
</div> </div>
) )

View file

@ -1,4 +1,4 @@
import { Draw, Hit, MouseCursor, Target } from "../../interfaces/frontend" import { Hit, MouseCursor, Target } from "../../interfaces/frontend"
// import Bluetooth from "./Bluetooth" // import Bluetooth from "./Bluetooth"
// import FogImages from "./FogImages" // import FogImages from "./FogImages"
import Labeling from "./Labeling" import Labeling from "./Labeling"
@ -8,6 +8,7 @@ import EventBar from "@components/Gamefield/EventBar"
import HitElems from "@components/Gamefield/HitElems" import HitElems from "@components/Gamefield/HitElems"
import Targets from "@components/Gamefield/Targets" import Targets from "@components/Gamefield/Targets"
import { useDraw } from "@hooks/useDraw" import { useDraw } from "@hooks/useDraw"
import { useDrawProps } from "@hooks/useDrawProps"
import { import {
hitReducer, hitReducer,
initlialTarget, initlialTarget,
@ -69,28 +70,8 @@ function Gamefield() {
} }
}, [mode, mouseCursor, target]) }, [mode, mouseCursor, target])
const [color, setColor] = useState<string>("#f00") const { canvasRef, onMouseDown, clear } = useDraw()
const [disable, setDisable] = useState(false) const { enable, color, shouldHide } = useDrawProps()
const { canvasRef, onMouseDown, clear } = useDraw(drawLine)
function drawLine({ prevPoint, currentPoint, ctx }: Draw) {
const { x: currX, y: currY } = currentPoint
const lineColor = color
const lineWidth = 5
let startPoint = prevPoint ?? currentPoint
ctx.beginPath()
ctx.lineWidth = lineWidth
ctx.strokeStyle = lineColor
ctx.moveTo(startPoint.x, startPoint.y)
ctx.lineTo(currX, currY)
ctx.stroke()
ctx.fillStyle = lineColor
ctx.beginPath()
ctx.arc(startPoint.x, startPoint.y, 2, 0, 2 * Math.PI)
ctx.fill()
}
return ( return (
<div id="gamefield"> <div id="gamefield">
@ -117,10 +98,13 @@ function Gamefield() {
<Targets props={{ target, targetPreview, mode, hits }} /> <Targets props={{ target, targetPreview, mode, hits }} />
{/* <span id='dev-debug' style={{gridArea: '1 / 12 / 1 / 15', backgroundColor: 'red', zIndex: 3} as CSSProperties}>Debug</span> */} {/* <span id='dev-debug' style={{gridArea: '1 / 12 / 1 / 15', backgroundColor: 'red', zIndex: 3} as CSSProperties}>Debug</span> */}
<canvas <canvas
style={ style={
{ {
pointerEvents: !disable ? "auto" : "none", opacity: shouldHide ? 0 : 1,
boxShadow: enable ? "inset 0 0 0 2px " + color : "none",
pointerEvents: enable && !shouldHide ? "auto" : "none",
} as CSSProperties } as CSSProperties
} }
ref={canvasRef} ref={canvasRef}
@ -129,9 +113,7 @@ function Gamefield() {
height="648" height="648"
/> />
</div> </div>
<button onClick={() => setDisable((e) => !e)}>toggle disable</button> <EventBar props={{ setMode, setTarget, clear }} />
<button onClick={clear}>Clear</button>
<EventBar props={{ setMode, setTarget }} />
</div> </div>
) )
} }

View file

@ -1,20 +1,53 @@
import { ItemProps } from "../../interfaces/frontend"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { useDrawProps } from "@hooks/useDrawProps"
import classNames from "classnames" import classNames from "classnames"
import React, { CSSProperties } from "react" import React, { CSSProperties, useEffect, useRef, useState } from "react"
import { HexColorPicker } from "react-colorful"
function Item({ function Item({
props: { icon, text, amount, callback }, props: { icon, text, amount, iconColor, disabled, callback },
}: { }: {
props: { props: ItemProps
icon: string
text: string
amount?: number
callback: () => void
}
}) { }) {
const isColor = text === "Color"
const { color, setColor } = useDrawProps()
const [active, setActive] = useState(false)
const cpRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const inActive = (e: MouseEvent) => {
if (cpRef.current && !cpRef.current.contains(e.target as Node))
setActive(false)
}
// Add event listeners
if (!isColor) return
setTimeout(() => window.addEventListener("click", inActive), 200)
// Remove event listeners
return () => {
window.removeEventListener("click", inActive)
}
}, [active, isColor])
return ( return (
<div className="item" onClick={callback}> <div className="item" onClick={isColor ? () => setActive(true) : callback}>
{isColor ? (
<div
ref={cpRef}
className={classNames("react-colorful-wrapper", { active: active })}
>
<HexColorPicker color={color} onChange={setColor} />
</div>
) : (
<></>
)}
<div <div
className={classNames("container", { amount: amount })} className={classNames("container", {
amount: amount,
disabled: disabled,
})}
style={ style={
amount amount
? ({ ? ({
@ -23,7 +56,15 @@ function Item({
: {} : {}
} }
> >
<img src={`/assets/${icon}.png`} alt={`${icon}.png`} /> {typeof icon === "string" ? (
<img
src={`/assets/${icon}.png`}
alt={`${icon}.png`}
className="pixelart"
/>
) : (
<FontAwesomeIcon icon={icon} color={iconColor ?? "#444"} />
)}
</div> </div>
<span>{text}</span> <span>{text}</span>
</div> </div>

View file

@ -88,7 +88,6 @@ function Player({
isLatched={!!isReady} isLatched={!!isReady}
onClick={() => { onClick={() => {
if (!player) return if (!player) return
console.log(i, !isReady)
setIsReady({ setIsReady({
i, i,
isReady: !isReady, isReady: !isReady,

View file

@ -1,14 +1,34 @@
import { Draw, Point } from "../interfaces/frontend" import { Draw, Point } from "../interfaces/frontend"
import { useDrawProps } from "./useDrawProps"
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react"
export const useDraw = ( function onDraw({ prevPoint, currentPoint, ctx, color }: Draw) {
onDraw: ({ ctx, currentPoint, prevPoint }: Draw) => void const { x: currX, y: currY } = currentPoint
) => { const lineColor = color
const lineWidth = 5
let startPoint = prevPoint ?? currentPoint
ctx.beginPath()
ctx.lineWidth = lineWidth
ctx.strokeStyle = lineColor
ctx.moveTo(startPoint.x, startPoint.y)
ctx.lineTo(currX, currY)
ctx.stroke()
ctx.fillStyle = lineColor
ctx.beginPath()
ctx.arc(startPoint.x, startPoint.y, 2, 0, 2 * Math.PI)
ctx.fill()
}
export const useDraw = () => {
const [mouseDown, setMouseDown] = useState(false) const [mouseDown, setMouseDown] = useState(false)
const canvasRef = useRef<HTMLCanvasElement>(null) const canvasRef = useRef<HTMLCanvasElement>(null)
const prevPoint = useRef<null | Point>(null) const prevPoint = useRef<null | Point>(null)
const { color } = useDrawProps()
const onMouseDown = () => setMouseDown(true) const onMouseDown = () => setMouseDown(true)
const clear = () => { const clear = () => {
@ -22,6 +42,9 @@ export const useDraw = (
} }
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const handler = (e: MouseEvent) => { const handler = (e: MouseEvent) => {
if (!mouseDown) return if (!mouseDown) return
const currentPoint = computePointInCanvas(e) const currentPoint = computePointInCanvas(e)
@ -29,14 +52,11 @@ export const useDraw = (
const ctx = canvasRef.current?.getContext("2d") const ctx = canvasRef.current?.getContext("2d")
if (!ctx || !currentPoint) return if (!ctx || !currentPoint) return
onDraw({ ctx, currentPoint, prevPoint: prevPoint.current }) onDraw({ ctx, currentPoint, prevPoint: prevPoint.current, color })
prevPoint.current = currentPoint prevPoint.current = currentPoint
} }
const computePointInCanvas = (e: MouseEvent) => { const computePointInCanvas = (e: MouseEvent) => {
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect() const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left const x = e.clientX - rect.left
const y = e.clientY - rect.top const y = e.clientY - rect.top
@ -50,15 +70,15 @@ export const useDraw = (
} }
// Add event listeners // Add event listeners
canvasRef.current?.addEventListener("mousemove", handler) canvas.addEventListener("mousemove", handler)
window.addEventListener("mouseup", mouseUpHandler) window.addEventListener("mouseup", mouseUpHandler)
// Remove event listeners // Remove event listeners
return () => { return () => {
canvasRef.current?.removeEventListener("mousemove", handler) canvas.removeEventListener("mousemove", handler)
window.removeEventListener("mouseup", mouseUpHandler) window.removeEventListener("mouseup", mouseUpHandler)
} }
}, [onDraw]) }, [color, mouseDown])
return { canvasRef, onMouseDown, clear } return { canvasRef, onMouseDown, clear }
} }

View file

@ -0,0 +1,40 @@
import { produce } from "immer"
import { create } from "zustand"
import { devtools } from "zustand/middleware"
const initialState: {
enable: boolean
shouldHide: boolean
color: string
} = {
enable: false,
shouldHide: false,
color: "#b32aa9",
}
export type State = typeof initialState
export type Action = {
setColor: (color: string) => void
reset: () => void
}
export const useDrawProps = create<State & Action>()(
devtools(
(set) => ({
...initialState,
setColor: (color) =>
set(
produce((state) => {
state.color = color
})
),
reset: () => {
set(initialState)
},
}),
{
name: "gameState",
}
)
)

View file

@ -1,3 +1,5 @@
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons"
export interface Position { export interface Position {
x: number x: number
y: number y: number
@ -16,11 +18,20 @@ export interface Mode {
pointerGrid: any[][] pointerGrid: any[][]
type: string type: string
} }
export interface Items { export interface ItemProps {
icon: string icon: string | IconDefinition
text: string text: string
mode?: number
amount?: number amount?: number
iconColor?: string
disabled?: boolean
callback?: () => void
}
export interface EventBarModes {
main: ItemProps[]
menu: ItemProps[]
attack: ItemProps[]
draw: ItemProps[]
settings: ItemProps[]
} }
export interface Field extends Position { export interface Field extends Position {
field: string field: string
@ -57,4 +68,5 @@ export interface Draw {
ctx: CanvasRenderingContext2D ctx: CanvasRenderingContext2D
currentPoint: Point currentPoint: Point
prevPoint: Point | null prevPoint: Point | null
color: string
} }

View file

@ -31,6 +31,7 @@
"nodemailer": "^6.9.3", "nodemailer": "^6.9.3",
"prisma": "^4.15.0", "prisma": "^4.15.0",
"react": "18.2.0", "react": "18.2.0",
"react-colorful": "^5.6.1",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-otp-input": "^3.0.2", "react-otp-input": "^3.0.2",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",

View file

@ -71,6 +71,9 @@ dependencies:
react: react:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0 version: 18.2.0
react-colorful:
specifier: ^5.6.1
version: 5.6.1(react-dom@18.2.0)(react@18.2.0)
react-dom: react-dom:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
@ -2877,6 +2880,16 @@ packages:
/queue-microtask@1.2.3: /queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
/react-colorful@5.6.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-dom@18.2.0(react@18.2.0): /react-dom@18.2.0(react@18.2.0):
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
peerDependencies: peerDependencies:

View file

@ -337,7 +337,8 @@ body {
canvas { canvas {
grid-area: 1 / 1 / -1 / -1; grid-area: 1 / 1 / -1 / -1;
border: 1px solid #000; border: 1px solid #000;
display: absolute; border-radius: 0.5rem;
z-index: 1;
} }
} }
@ -352,10 +353,35 @@ body {
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
width: 128px; width: 128px;
position: relative;
.react-colorful-wrapper {
pointer-events: none;
opacity: 0;
transition: 0.2s;
position: absolute;
top: -225px;
left: 50%;
transform: translateX(-50%);
z-index: 1;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
&.active {
pointer-events: auto;
opacity: 1;
}
}
.container { .container {
width: initial; width: initial;
position: relative; position: relative;
padding: 8px;
background-color: white;
border-radius: 1rem;
&.disabled {
box-shadow: inset 0 0 1rem 1rem #888;
}
&.amount::after { &.amount::after {
content: var(--amount); content: var(--amount);
@ -372,10 +398,12 @@ body {
img { img {
width: 64px; width: 64px;
padding: 8px;
@include pixelart; @include pixelart;
background-color: white; }
border-radius: 1rem; svg {
margin: 8px;
height: 48px;
display: inherit;
} }
} }