Implement CSS grid click effect
This commit is contained in:
parent
1cfde0e232
commit
09a9981192
5 changed files with 231 additions and 115 deletions
|
@ -1,126 +1,15 @@
|
||||||
import { faCrosshairs } from '@fortawesome/free-solid-svg-icons';
|
// import Gamefield from './components/Gamefield';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import Homepage from './components/Homepage';
|
||||||
import { CSSProperties, useEffect, useReducer, useState } from 'react';
|
|
||||||
import Bluetooth from './Bluetooth';
|
|
||||||
import BorderTiles from './components/BorderTiles';
|
|
||||||
import FogImages from './components/FogImages';
|
|
||||||
import HitElems from './components/HitElems';
|
|
||||||
import Labeling from './components/Labeling';
|
|
||||||
import Ships from './components/Ships';
|
|
||||||
import { hitReducer, initlialTarget, initlialTargetPreview, isHit } from './helpers';
|
|
||||||
import { HitType, TargetPreviewType, TargetType } from './interfaces';
|
|
||||||
import './styles/App.scss';
|
import './styles/App.scss';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
const count = 12;
|
|
||||||
const [target, setTarget] = useState<TargetType>(initlialTarget);
|
|
||||||
const [targetPreview, setTargetPreview] = useState<TargetPreviewType>(initlialTargetPreview);
|
|
||||||
const [hits, DispatchHits] = useReducer(hitReducer, [] as HitType[]);
|
|
||||||
|
|
||||||
// handle visibility and position change of targetPreview
|
|
||||||
useEffect(() => {
|
|
||||||
const { newX, newY, shouldShow, appearOK, eventReady, show, x, y } = targetPreview;
|
|
||||||
const positionChange = !(x === newX && y === newY);
|
|
||||||
// if not ready or no new position
|
|
||||||
if (!eventReady || (!positionChange && show))
|
|
||||||
return;
|
|
||||||
if (show) {
|
|
||||||
// hide preview to change position when hidden
|
|
||||||
setTargetPreview(e => ({ ...e, appearOK: false, eventReady: false, show: false }));
|
|
||||||
} else if (shouldShow && appearOK && !isHit(hits, newX, newY).length) {
|
|
||||||
// BUT only appear again if it's supposed to (in case the mouse left over the edge) and ()
|
|
||||||
setTargetPreview(e => ({ ...e, appearOK: false, eventReady: false, show: true, x: newX, y: newY }));
|
|
||||||
}
|
|
||||||
}, [targetPreview, hits])
|
|
||||||
|
|
||||||
// enable targetPreview event again after 200 mil. sec.
|
|
||||||
useEffect(() => {
|
|
||||||
if (targetPreview.eventReady)
|
|
||||||
return;
|
|
||||||
const autoTimeout = setTimeout(() => {
|
|
||||||
setTargetPreview(e => ({ ...e, eventReady: true }));
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
// or abort if state has changed early
|
|
||||||
return () => {
|
|
||||||
clearTimeout(autoTimeout);
|
|
||||||
}
|
|
||||||
}, [targetPreview.eventReady]);
|
|
||||||
|
|
||||||
// approve targetPreview new position after 200 mil. sec.
|
|
||||||
useEffect(() => {
|
|
||||||
// early return to start cooldown only when about to show up
|
|
||||||
if (!targetPreview.shouldShow)
|
|
||||||
return;
|
|
||||||
const autoTimeout = setTimeout(() => {
|
|
||||||
setTargetPreview(e => ({ ...e, appearOK: true }));
|
|
||||||
}, 350);
|
|
||||||
|
|
||||||
// or abort if movement is repeated early
|
|
||||||
return () => {
|
|
||||||
clearTimeout(autoTimeout);
|
|
||||||
}
|
|
||||||
}, [targetPreview.shouldShow, targetPreview.newX, targetPreview.newY]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<header className="App-header">
|
<header className="App-header">
|
||||||
<Bluetooth />
|
<Homepage/>
|
||||||
<p>
|
{/* <Gamefield/> */}
|
||||||
<span
|
|
||||||
className="App-link"
|
|
||||||
onClick={() => {navigator.clipboard.writeText("chrome://flags/#enable-experimental-web-platform-features")}}
|
|
||||||
// target="_blank"
|
|
||||||
style={{"cursor": "pointer"}}
|
|
||||||
// rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Step 1
|
|
||||||
</span>
|
|
||||||
{" "}
|
|
||||||
<span
|
|
||||||
className="App-link"
|
|
||||||
onClick={() => {navigator.clipboard.writeText("chrome://flags/#enable-web-bluetooth-new-permissions-backend")}}
|
|
||||||
// target="_blank"
|
|
||||||
style={{"cursor": "pointer"}}
|
|
||||||
// rel="noopener noreferrer"
|
|
||||||
|
|
||||||
>
|
|
||||||
Step 2
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div id="game-frame" style={{ '--i': count } as CSSProperties}>
|
|
||||||
{/* Bordes */}
|
|
||||||
<BorderTiles count={count} actions={{ setTarget, setTargetPreview, hits, DispatchHits }} />
|
|
||||||
|
|
||||||
{/* Collumn lettes and row numbers */}
|
|
||||||
<Labeling count={count} />
|
|
||||||
|
|
||||||
{/* Ships */}
|
|
||||||
<Ships />
|
|
||||||
|
|
||||||
<HitElems hits={hits} />
|
|
||||||
|
|
||||||
{/* Fog images */}
|
|
||||||
{/* <FogImages /> */}
|
|
||||||
<div className={`hit-svg target ${target.show ? 'show' : ''}`} style={{ '--x': target.x, '--y': target.y } as CSSProperties}>
|
|
||||||
<FontAwesomeIcon icon={faCrosshairs} />
|
|
||||||
</div>
|
|
||||||
<div className={`hit-svg target-preview ${targetPreview.show && (target.x !== targetPreview.x || target.y !== targetPreview.y) ? 'show' : ''}`} style={{ '--x': targetPreview.x, '--y': targetPreview.y } as CSSProperties}>
|
|
||||||
<FontAwesomeIcon icon={faCrosshairs} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to reload.
|
|
||||||
</p> */}
|
|
||||||
<a
|
|
||||||
className="App-link"
|
|
||||||
href="https://www.freepik.com/free-vector/militaristic-ships-set-navy-ammunition-warship-submarine-nuclear-battleship-float-cruiser-trawler-gunboat-frigate-ferry_10704121.htm"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<p>Battleships designed by macrovector</p>
|
|
||||||
</a>
|
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
126
frontend/src/components/Gamefield.tsx
Normal file
126
frontend/src/components/Gamefield.tsx
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import { faCrosshairs } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { CSSProperties, useEffect, useReducer, useState } from 'react';
|
||||||
|
import Bluetooth from './Bluetooth';
|
||||||
|
import BorderTiles from './BorderTiles';
|
||||||
|
import FogImages from './FogImages';
|
||||||
|
import HitElems from './HitElems';
|
||||||
|
import Labeling from './Labeling';
|
||||||
|
import Ships from './Ships';
|
||||||
|
import { hitReducer, initlialTarget, initlialTargetPreview, isHit } from '../helpers';
|
||||||
|
import { HitType, TargetPreviewType, TargetType } from '../interfaces';
|
||||||
|
|
||||||
|
function Gamefield() {
|
||||||
|
|
||||||
|
const count = 12;
|
||||||
|
const [target, setTarget] = useState<TargetType>(initlialTarget);
|
||||||
|
const [targetPreview, setTargetPreview] = useState<TargetPreviewType>(initlialTargetPreview);
|
||||||
|
const [hits, DispatchHits] = useReducer(hitReducer, [] as HitType[]);
|
||||||
|
|
||||||
|
// handle visibility and position change of targetPreview
|
||||||
|
useEffect(() => {
|
||||||
|
const { newX, newY, shouldShow, appearOK, eventReady, show, x, y } = targetPreview;
|
||||||
|
const positionChange = !(x === newX && y === newY);
|
||||||
|
// if not ready or no new position
|
||||||
|
if (!eventReady || (!positionChange && show))
|
||||||
|
return;
|
||||||
|
if (show) {
|
||||||
|
// hide preview to change position when hidden
|
||||||
|
setTargetPreview(e => ({ ...e, appearOK: false, eventReady: false, show: false }));
|
||||||
|
} else if (shouldShow && appearOK && !isHit(hits, newX, newY).length) {
|
||||||
|
// BUT only appear again if it's supposed to (in case the mouse left over the edge) and ()
|
||||||
|
setTargetPreview(e => ({ ...e, appearOK: false, eventReady: false, show: true, x: newX, y: newY }));
|
||||||
|
}
|
||||||
|
}, [targetPreview, hits])
|
||||||
|
|
||||||
|
// enable targetPreview event again after 200 mil. sec.
|
||||||
|
useEffect(() => {
|
||||||
|
if (targetPreview.eventReady)
|
||||||
|
return;
|
||||||
|
const autoTimeout = setTimeout(() => {
|
||||||
|
setTargetPreview(e => ({ ...e, eventReady: true }));
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
// or abort if state has changed early
|
||||||
|
return () => {
|
||||||
|
clearTimeout(autoTimeout);
|
||||||
|
}
|
||||||
|
}, [targetPreview.eventReady]);
|
||||||
|
|
||||||
|
// approve targetPreview new position after 200 mil. sec.
|
||||||
|
useEffect(() => {
|
||||||
|
// early return to start cooldown only when about to show up
|
||||||
|
if (!targetPreview.shouldShow)
|
||||||
|
return;
|
||||||
|
const autoTimeout = setTimeout(() => {
|
||||||
|
setTargetPreview(e => ({ ...e, appearOK: true }));
|
||||||
|
}, 350);
|
||||||
|
|
||||||
|
// or abort if movement is repeated early
|
||||||
|
return () => {
|
||||||
|
clearTimeout(autoTimeout);
|
||||||
|
}
|
||||||
|
}, [targetPreview.shouldShow, targetPreview.newX, targetPreview.newY]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id='gamefield'>
|
||||||
|
<Bluetooth />
|
||||||
|
<p>
|
||||||
|
<span
|
||||||
|
className="App-link"
|
||||||
|
onClick={() => { navigator.clipboard.writeText("chrome://flags/#enable-experimental-web-platform-features") }}
|
||||||
|
// target="_blank"
|
||||||
|
style={{ "cursor": "pointer" }}
|
||||||
|
// rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Step 1
|
||||||
|
</span>
|
||||||
|
{" "}
|
||||||
|
<span
|
||||||
|
className="App-link"
|
||||||
|
onClick={() => { navigator.clipboard.writeText("chrome://flags/#enable-web-bluetooth-new-permissions-backend") }}
|
||||||
|
// target="_blank"
|
||||||
|
style={{ "cursor": "pointer" }}
|
||||||
|
// rel="noopener noreferrer"
|
||||||
|
|
||||||
|
>
|
||||||
|
Step 2
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div id="game-frame" style={{ '--i': count } as CSSProperties}>
|
||||||
|
{/* Bordes */}
|
||||||
|
<BorderTiles count={count} actions={{ setTarget, setTargetPreview, hits, DispatchHits }} />
|
||||||
|
|
||||||
|
{/* Collumn lettes and row numbers */}
|
||||||
|
<Labeling count={count} />
|
||||||
|
|
||||||
|
{/* Ships */}
|
||||||
|
<Ships />
|
||||||
|
|
||||||
|
<HitElems hits={hits} />
|
||||||
|
|
||||||
|
{/* Fog images */}
|
||||||
|
<FogImages />
|
||||||
|
<div className={`hit-svg target ${target.show ? 'show' : ''}`} style={{ '--x': target.x, '--y': target.y } as CSSProperties}>
|
||||||
|
<FontAwesomeIcon icon={faCrosshairs} />
|
||||||
|
</div>
|
||||||
|
<div className={`hit-svg target-preview ${targetPreview.show && (target.x !== targetPreview.x || target.y !== targetPreview.y) ? 'show' : ''}`} style={{ '--x': targetPreview.x, '--y': targetPreview.y } as CSSProperties}>
|
||||||
|
<FontAwesomeIcon icon={faCrosshairs} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <p>
|
||||||
|
Edit <code>src/App.tsx</code> and save to reload.
|
||||||
|
</p> */}
|
||||||
|
<a
|
||||||
|
className="App-link"
|
||||||
|
href="https://www.freepik.com/free-vector/militaristic-ships-set-navy-ammunition-warship-submarine-nuclear-battleship-float-cruiser-trawler-gunboat-frigate-ferry_10704121.htm"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<p>Battleships designed by macrovector</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Gamefield
|
65
frontend/src/components/Homepage.tsx
Normal file
65
frontend/src/components/Homepage.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { CSSProperties, useEffect, useMemo, useState } from 'react'
|
||||||
|
import '../styles/home.scss'
|
||||||
|
|
||||||
|
function Homepage() {
|
||||||
|
|
||||||
|
const [columns, setColumns] = useState(floorClient(document.body.clientWidth))
|
||||||
|
const [rows, setRows] = useState(floorClient(document.body.clientHeight))
|
||||||
|
const [quantity, setQuantity] = useState(columns * rows)
|
||||||
|
const [position, setPosition] = useState([0, 0])
|
||||||
|
const [active, setActve] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleResize() {
|
||||||
|
setColumns(floorClient(document.body.clientWidth))
|
||||||
|
setRows(floorClient(document.body.clientHeight))
|
||||||
|
}
|
||||||
|
handleResize()
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => setQuantity(columns * rows), 500)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}, [columns, rows])
|
||||||
|
|
||||||
|
|
||||||
|
function floorClient(number: number) {
|
||||||
|
return Math.floor(number / 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTile(index: number) {
|
||||||
|
const x = index % columns
|
||||||
|
const y = Math.floor(index / columns)
|
||||||
|
const xDiff = (x - position[0]) / 10
|
||||||
|
const yDiff = (y - position[1]) / 10
|
||||||
|
const pos = (Math.sqrt(xDiff * xDiff + yDiff * yDiff)).toFixed(2)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={"tile " + (active ? 'active' : '')}
|
||||||
|
style={{ '--delay': pos + 's' } as CSSProperties}
|
||||||
|
onClick={() => {
|
||||||
|
setPosition([x, y])
|
||||||
|
setActve(e => !e)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* {pos} */}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createTiles = useMemo(() => {
|
||||||
|
console.log(3, columns, rows, quantity)
|
||||||
|
// return <p>{quantity}</p>
|
||||||
|
return (
|
||||||
|
<div id='tiles' style={{ '--columns': columns, '--rows': rows } as CSSProperties}>
|
||||||
|
{Array.from(Array(quantity)).map((tile, index) => createTile(index))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}, [quantity, position])
|
||||||
|
|
||||||
|
return createTiles
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Homepage
|
36
frontend/src/styles/home.scss
Normal file
36
frontend/src/styles/home.scss
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
@use './mixins/effects' as *;
|
||||||
|
|
||||||
|
#tiles {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--columns), 1fr);
|
||||||
|
grid-template-rows: repeat(var(--rows), 1fr);
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
@include transition(1s);
|
||||||
|
outline: 1px solid white;
|
||||||
|
background-color: black;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
animation: bright .5s forwards;
|
||||||
|
animation-delay: var(--delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bright {
|
||||||
|
0% {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue