Improve control panel, add zoom and fps controls

This commit is contained in:
2019-08-28 10:03:28 +02:00
parent aa1bf44f7c
commit 9d723e4470
10 changed files with 225 additions and 45 deletions

View File

@@ -4,6 +4,18 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Redux Demo</title> <title>Redux Demo</title>
<link href="https://fonts.googleapis.com/css?family=Lato:100,400|Raleway:100,400,700|Rubik:500" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css?family=Lato:100,400|Raleway:100,400,700|Rubik:500" rel="stylesheet" />
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.9.0/css/solid.css"
integrity="sha384-ypqxM+6jj5ropInEPawU1UEhbuOuBkkz59KyIbbsTu4Sw62PfV3KUnQadMbIoAzq"
crossorigin="anonymous"
/>
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.9.0/css/fontawesome.css"
integrity="sha384-NnhYAEceBbm5rQuNvCv6o4iIoPZlkaWfvuXVh4XkRNvHWKgu/Mk2nEjFZpPQdwiz"
crossorigin="anonymous"
/>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -1,15 +1,21 @@
import styled from "styled-components"; import styled from "styled-components";
export const Apple = styled.div.attrs({ role: "img", "aria-label": "apple" })` const Apple = styled.div.attrs(({ theme, zoom }) => ({
width: ${({ theme }) => theme.snake.cell.size}; style: {
height: ${({ theme }) => theme.snake.cell.size}; height: theme.snake.cell.size * (zoom || 1) + "rem",
width: theme.snake.cell.size * (zoom || 1) + "rem",
border: theme.snake.cell.border,
fontSize: (zoom || 1) * 1.3 + "rem",
lineHeight: (zoom || 1) * 1.7 + "rem",
paddingLeft: (zoom || 1) * 0.0625 + "rem"
}
}))`
display: inline-block; display: inline-block;
font-size: 1.3rem;
vertical-align: top; vertical-align: top;
line-height: 1.8rem;
padding-left: 0.0625rem;
&:after { &:after {
content: "${props => (!props.died ? "🍎" : "🐛")}"; // content: "${props => (!props.died ? "🍎" : "🐛")}";
content: "🍎";
display: inline-block; display: inline-block;
} }
`; `;

View File

@@ -54,6 +54,16 @@ export const Button = (props, refs) => {
return StyledNavLink.render(updatedProps); return StyledNavLink.render(updatedProps);
} }
if (props.icon) {
return (
<i
onClick={updatedProps.onClick}
className={`fas fa-${props.icon} ${props.className}`}
style={{ display: "inline-block", fontSize: `${props.size || 1}rem` }}
/>
);
}
return StyledButton.render(updatedProps); return StyledButton.render(updatedProps);
}; };
@@ -61,4 +71,21 @@ export const ToggleButton = styled(Button)`
background-color: ${props => (props.toggle ? "silver" : null)}; background-color: ${props => (props.toggle ? "silver" : null)};
`; `;
export const IconButton = styled(Button)`
margin: 0 0.2rem;
display: inline-block;
cursor: pointer;
&:before {
text-shadow: 0 0 1px black, 1px 1px 3px #d0bfa3;
}
`;
export const ToggleIconButton = styled(IconButton)`
color: ${props => (props.toggle ? "#e6b4c4" : null)};
&:before {
text-shadow: 0 0 1px gray, 0 0 1px #d0bfa3;
}
`;
export default Button; export default Button;

View File

@@ -1,21 +1,76 @@
import React from "react"; import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import Button, { ToggleButton } from "../components/Button.js"; import Button, { IconButton, ToggleIconButton } from "../components/Button.js";
const buttonSize = 2.5;
const Layout = styled.div` const Layout = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
`; `;
export const ControlPanel = ({ updateFrameSnake, paused, pauseSnake, stopSnake, startSnake, fps }) => ( const ButtonRow = styled.div`
display: flex;
place-content: center;
align-content: space-around;
`;
const HorizontalStack = styled.div`
display: flex;
width: 100%;
// place-content: center;
align-content: center;
place-content: space-between;
`;
const VerticalStack = styled.div`
display: flex;
flex-direction: column;
place-content: center;
align-content: space-around;
`;
export const ControlPanel = ({
updateFrameSnake,
paused,
started,
pauseSnake,
stopSnake,
startSnake,
fps,
zoom,
setFps,
zoomIn,
zoomOut
}) => (
<Layout> <Layout>
<h1>Control Panel</h1> {
<ToggleButton toggle={paused} onClick={pauseSnake}> // <h1>Control Panel</h1>
{paused ? "Resume" : "Pause"} // <ToggleButton toggle={paused} onClick={pauseSnake}>
</ToggleButton> // {paused ? "Resume" : "Pause"}
<Button onClick={stopSnake}>Stop</Button> // </ToggleButton>
<Button onClick={startSnake}>Reset</Button> }
<span>FPS: {fps}</span>
<HorizontalStack style={{ minHeight: "2rem" }}>
<span>Zoom: {zoom}</span>
<div style={{ display: "inline-block" }}>
<IconButton icon="minus-circle" size={1} onClick={zoomOut} />
<IconButton icon="plus-circle" size={1} onClick={zoomIn} />
</div>
</HorizontalStack>
<HorizontalStack style={{ minHeight: "2rem" }}>
<span>FPS: {fps}</span>
<div style={{ display: "inline-block" }}>
<IconButton icon="minus-circle" size={1} onClick={() => setFps(fps - 1)} />
<IconButton icon="plus-circle" size={1} onClick={() => setFps(fps + 1)} />
</div>
</HorizontalStack>
<ButtonRow>
<ToggleIconButton toggle={!started} icon="stop-circle" size={buttonSize} onClick={stopSnake} />
<ToggleIconButton toggle={started && !paused} icon="play-circle" size={buttonSize} onClick={startSnake} />
<ToggleIconButton toggle={paused || !started} icon="pause-circle" size={buttonSize} onClick={pauseSnake} />
<ToggleIconButton toggle={!started} icon="dot-circle" size={buttonSize} onClick={startSnake} />
</ButtonRow>
</Layout> </Layout>
); );

View File

@@ -76,7 +76,7 @@ const CardCSSTransition = styled(CSSTransition)`
&.card-appear-active { &.card-appear-active {
${Card} { ${Card} {
transition: transform ease-out 250ms, opacity ease-out 100ms; transition: transform ease-in 250ms, opacity ease-in 100ms;
transform: rotateX(0deg); transform: rotateX(0deg);
opacity: 1; opacity: 1;
} }
@@ -84,7 +84,7 @@ const CardCSSTransition = styled(CSSTransition)`
&.card-enter-active { &.card-enter-active {
${Card} { ${Card} {
transition: transform ease-out 250ms 250ms, opacity ease-out 100ms; transition: transform ease-in 250ms 250ms, opacity ease-in 100ms;
transform: rotateX(0deg); transform: rotateX(0deg);
opacity: 1; opacity: 1;
} }
@@ -94,13 +94,13 @@ const CardCSSTransition = styled(CSSTransition)`
&.card-exit { &.card-exit {
${Card} { ${Card} {
transform: rotateX(0deg); transform: rotateX(0deg);
transition: transform ease-out 250ms, opacity ease-out 100ms 150ms; transition: transform ease-in 250ms, opacity ease-in 100ms 150ms;
opacity: 1; opacity: 1;
} }
&.card-exit-active { &.card-exit-active {
${Card} { ${Card} {
transition: transform ease-out 250ms, opacity ease-out 100ms 150ms; transition: transform ease-in 250ms, opacity ease-in 100ms 150ms;
transform: rotateX(90deg); transform: rotateX(90deg);
opacity: 0; opacity: 0;
} }

View File

@@ -1,13 +1,13 @@
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
export const SnakePart = styled.div` export const SnakePart = styled.div`
width: ${({ theme }) => theme.snake.cell.size}; width: ${({ theme, zoom }) => theme.snake.cell.size * (zoom || 1)}rem;
height: ${({ theme }) => theme.snake.cell.size}; height: ${({ theme, zoom }) => theme.snake.cell.size * (zoom || 1)}rem;
display: inline-block; display: inline-block;
font-size: 1.3rem; font-size: ${props => (props.zoom || 1) * 1.3}rem;
vertical-align: top; vertical-align: top;
line-height: 1.8rem; line-height: ${props => (props.zoom || 1) * 1.7}rem;
padding-left: 0.0625rem; padding-left: ${props => (props.zoom || 1) * 0.0625}rem;
${props => ${props =>
!props.died !props.died

View File

@@ -1,26 +1,35 @@
import React from "react"; import React from "react";
import styled from "styled-components"; import styled from "styled-components";
const Row = styled.div` const Row = styled.div.attrs(({ theme, zoom }) => ({
style: {
height: theme.snake.cell.size * (zoom || 1) + "rem"
}
}))`
white-space: nowrap; white-space: nowrap;
`; `;
const Cell = styled.div` const Cell = styled.div.attrs(({ theme, zoom }) => ({
width: ${({ theme }) => theme.snake.cell.size}; style: {
height: ${({ theme }) => theme.snake.cell.size}; height: theme.snake.cell.size * (zoom || 1) + "rem",
border: ${({ theme }) => theme.snake.cell.border}; width: theme.snake.cell.size * (zoom || 1) + "rem",
border: theme.snake.cell.border
}
}))`
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
text-align: center; text-align: center;
box-shadow: 1px 1px 4px gray inset, -1px -1px 4px #fff inset; box-shadow: 1px 1px 4px gray inset, -1px -1px 4px #fff inset;
`; `;
export const Stage = ({ data, children }) => ( export const Stage = ({ data, children, zoom = 1 }) => (
<div> <div>
{data.map((r, y) => ( {data.map((r, y) => (
<Row key={`y${y}`}> <Row key={`y${y}`} zoom={zoom}>
{r.map((c, x) => ( {r.map((c, x) => (
<Cell key={`x${x}y${y}`}>{children(c)}</Cell> <Cell key={`x${x}y${y}`} zoom={zoom}>
{children(c, zoom)}
</Cell>
))} ))}
</Row> </Row>
))} ))}

View File

@@ -3,7 +3,7 @@ import { connect } from "react-redux";
import styled from "styled-components"; import styled from "styled-components";
/* Redux Modules */ /* Redux Modules */
import { module as gameModule, keyPress } from "../redux/game.js"; import { module as gameModule, keyPress, setFps } from "../redux/game.js";
import { import {
module as snakeModule, module as snakeModule,
startSnake, startSnake,
@@ -11,7 +11,9 @@ import {
pauseSnake, pauseSnake,
updateFrameSnake, updateFrameSnake,
keyPressedSnake, keyPressedSnake,
keys keys,
zoomIn,
zoomOut
} from "../redux/snake.js"; } from "../redux/snake.js";
/* Components */ /* Components */
@@ -24,12 +26,15 @@ import Apple from "../components/Apple.js";
const Layout = styled.div` const Layout = styled.div`
display: flex; display: flex;
width: 100%; width: 100%;
place-content: space-between; place-content: center;
margin-top: 2rem;
`; `;
const SidePanel = styled.div` const SidePanel = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-left: 2rem;
place-content: space-between;
`; `;
class Snake extends React.Component { class Snake extends React.Component {
@@ -61,27 +66,36 @@ class Snake extends React.Component {
snake, snake,
died, died,
fps, fps,
score setFps,
score,
zoom,
zoomIn,
zoomOut
} = this.props; } = this.props;
return ( return (
<React.Fragment> <React.Fragment>
<Layout> <Layout>
<Stage data={grid}> <Stage data={grid} zoom={zoom}>
{cell => {cell =>
(cell.type === "apple" && <Apple died={died} />) || (cell.type === "apple" && <Apple zoom={zoom} died={died} />) ||
(cell.type === "snake" && <SnakePart value={cell} died={died} />) (cell.type === "snake" && <SnakePart zoom={zoom} value={cell} died={died} />)
} }
</Stage> </Stage>
<SidePanel> <SidePanel>
<Scoreboard score={score} zoom={2} /> <Scoreboard score={score} zoom={2} />
<ControlPanel <ControlPanel
updateFrameSnake={updateFrameSnake} updateFrameSnake={updateFrameSnake}
started={started}
paused={paused} paused={paused}
pauseSnake={pauseSnake} pauseSnake={pauseSnake}
stopSnake={stopSnake} stopSnake={stopSnake}
startSnake={startSnake} startSnake={startSnake}
fps={fps} fps={fps}
setFps={setFps}
zoom={zoom}
zoomIn={zoomIn}
zoomOut={zoomOut}
/> />
</SidePanel> </SidePanel>
</Layout> </Layout>
@@ -105,7 +119,10 @@ const mapActionsToProps = {
pauseSnake, pauseSnake,
updateFrameSnake, updateFrameSnake,
keyPressedSnake, keyPressedSnake,
keyPress keyPress,
setFps,
zoomIn,
zoomOut
}; };
export default connect( export default connect(

View File

@@ -10,9 +10,11 @@ import {
const MODULE_NAME = "SNAKE"; const MODULE_NAME = "SNAKE";
const DEFAULT_GRID_SIZE = 16; const DEFAULT_GRID_SIZE = 16;
const DEFAUL_ZOOM_STEP = 0.1;
const initialState = { const initialState = {
grid: createGrid(DEFAULT_GRID_SIZE), grid: createGrid(DEFAULT_GRID_SIZE),
zoom: 1.5,
size: DEFAULT_GRID_SIZE, size: DEFAULT_GRID_SIZE,
paused: false, paused: false,
vX: 1, vX: 1,
@@ -46,7 +48,18 @@ export const keys = {
snakePart: "snakePart", snakePart: "snakePart",
s: "s", s: "s",
r: "r" r: "r",
1: "1",
2: "2",
3: "3",
4: "4",
5: "5",
6: "6",
7: "7",
8: "8",
9: "9",
0: "0"
}; };
function randomPosition(size) { function randomPosition(size) {
@@ -106,6 +119,9 @@ export const [START_SNAKE, startSnake] = module.action("START_SNAKE");
export const [RESET_SNAKE, resetSnake] = module.action("RESET_SNAKE", options => ({ ...options })); export const [RESET_SNAKE, resetSnake] = module.action("RESET_SNAKE", options => ({ ...options }));
export const [STOP_SNAKE, stopSnake] = module.action("STOP_SNAKE"); export const [STOP_SNAKE, stopSnake] = module.action("STOP_SNAKE");
export const [PAUSE_SNAKE, pauseSnake] = module.action("PAUSE_SNAKE"); export const [PAUSE_SNAKE, pauseSnake] = module.action("PAUSE_SNAKE");
export const [ZOOM_IN, zoomIn] = module.action("ZOOM_IN", step => ({ step }));
export const [SET_ZOOM_LEVEL, setZoomLevel] = module.action("SET_ZOOM_LEVEL", level => ({ level }));
export const [ZOOM_OUT, zoomOut] = module.action("ZOOM_OUT", step => ({ step }));
export const [UPDATE_FRAME_SNAKE, updateFrameSnake] = module.action("UPDATE_FRAME_SNAKE"); export const [UPDATE_FRAME_SNAKE, updateFrameSnake] = module.action("UPDATE_FRAME_SNAKE");
export const [CHANGE_DIRECTION, changeDirection] = module.action("CHANGE_DIRECTION", direction => ({ direction })); export const [CHANGE_DIRECTION, changeDirection] = module.action("CHANGE_DIRECTION", direction => ({ direction }));
export const [KEY_PRESSED_SNAKE, keyPressedSnake] = module.action("KEY_PRESSED_SNAKE", key => ({ key })); export const [KEY_PRESSED_SNAKE, keyPressedSnake] = module.action("KEY_PRESSED_SNAKE", key => ({ key }));
@@ -117,8 +133,18 @@ export const [UPDATE_STATE, updateState] = module.action("UPDATE_STATE", (newSta
/* === Reducers ================================================================================= */ /* === Reducers ================================================================================= */
module.reducer(UPDATE_STATE, (state, { newState, fullUpdate }) => (fullUpdate ? newState : { ...state, ...newState })); module.reducer(UPDATE_STATE, (state, { newState, fullUpdate }) => (fullUpdate ? newState : { ...state, ...newState }));
module.reducer(ZOOM_IN, (state, { step = DEFAUL_ZOOM_STEP }) => ({
...state,
zoom: Math.round((state.zoom + step) * 10) / 10
}));
module.reducer(SET_ZOOM_LEVEL, (state, { level }) => ({ ...state, zoom: level }));
module.reducer(ZOOM_OUT, (state, { step = DEFAUL_ZOOM_STEP }) =>
state.zoom - step >= 0.5 ? { ...state, zoom: Math.round((state.zoom - step) * 10) / 10 } : { ...state, zoom: 1 }
);
module.reducer(RESET_SNAKE, (state, options) => { module.reducer(RESET_SNAKE, (state, options) => {
const grid = createGrid(state.size); const grid = createGrid(state.size);
const zoom = state.zoom;
const apple = options.started ? randomPosition(state.size) : initialState.apple; const apple = options.started ? randomPosition(state.size) : initialState.apple;
const snake = options.started ? [[0, 0]] : initialState.snake; const snake = options.started ? [[0, 0]] : initialState.snake;
@@ -126,7 +152,7 @@ module.reducer(RESET_SNAKE, (state, options) => {
snake.forEach((snakePart, i) => snake.forEach((snakePart, i) =>
markCell(grid, snakePart, { type: "snake", index: snake.length - 1 - i, brightness: brightness(snake.length, i) }) markCell(grid, snakePart, { type: "snake", index: snake.length - 1 - i, brightness: brightness(snake.length, i) })
); );
return { ...state, ...initialState, grid, snake, apple, ...options }; return { ...state, ...initialState, grid, zoom, snake, apple, ...options };
}); });
module.reducer(CHANGE_DIRECTION, (state, { direction }) => { module.reducer(CHANGE_DIRECTION, (state, { direction }) => {
@@ -223,7 +249,7 @@ module.middleware(START_SNAKE, (dispatch, { started }) => {
module.middleware(STOP_SNAKE, (dispatch, { started }) => { module.middleware(STOP_SNAKE, (dispatch, { started }) => {
dispatch(unregisterCaller(UPDATE_FRAME_SNAKE)); dispatch(unregisterCaller(UPDATE_FRAME_SNAKE));
dispatch(unsubscribeKeyPressed(KEY_PRESSED_SNAKE)); // dispatch(unsubscribeKeyPressed(KEY_PRESSED_SNAKE));
dispatch(stopLoop()); dispatch(stopLoop());
dispatch(resetSnake({ started: false })); dispatch(resetSnake({ started: false }));
}); });
@@ -237,6 +263,7 @@ module.middleware(PAUSE_SNAKE, (dispatch, { paused }) => {
} }
}); });
// eslint-disable-next-line complexity
module.middleware(KEY_PRESSED_SNAKE, (dispatch, _, { key }) => { module.middleware(KEY_PRESSED_SNAKE, (dispatch, _, { key }) => {
if (key === keys.UP || key === keys.k) { if (key === keys.UP || key === keys.k) {
dispatch(changeDirection(directions.UP)); dispatch(changeDirection(directions.UP));
@@ -259,4 +286,31 @@ module.middleware(KEY_PRESSED_SNAKE, (dispatch, _, { key }) => {
if (key === keys.r) { if (key === keys.r) {
dispatch(startSnake()); dispatch(startSnake());
} }
if (key === keys[1]) {
dispatch(setZoomLevel(0.6));
}
if (key === keys[2]) {
dispatch(setZoomLevel(0.8));
}
if (key === keys[3]) {
dispatch(setZoomLevel(1));
}
if (key === keys[4]) {
dispatch(setZoomLevel(1.2));
}
if (key === keys[5]) {
dispatch(setZoomLevel(1.4));
}
if (key === keys[6]) {
dispatch(setZoomLevel(1.6));
}
if (key === keys[7]) {
dispatch(setZoomLevel(1.8));
}
if (key === keys[8]) {
dispatch(setZoomLevel(2));
}
if (key === keys[9]) {
dispatch(setZoomLevel(2.2));
}
}); });

View File

@@ -24,7 +24,7 @@ const themes = {
snake: { snake: {
cell: { cell: {
border: "0.0625rem solid papayawhip", border: "0.0625rem solid papayawhip",
size: "1.5rem" size: 1.5
}, },
cellColors: { cellColors: {
" ": "", " ": "",