Implement CSS grid click effect

This commit is contained in:
aronmal 2022-10-24 00:35:28 +02:00
parent 1cfde0e232
commit 09a9981192
Signed by: aronmal
GPG key ID: 816B7707426FC612
5 changed files with 231 additions and 115 deletions

View file

@ -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>
); );

View 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

View 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

View 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;
}
}