Implemented theming and game resuming via local storage
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"connected-react-router": "^6.5.2",
|
"connected-react-router": "^6.5.2",
|
||||||
|
"deepmerge": "^4.0.0",
|
||||||
"history": "^4.9.0",
|
"history": "^4.9.0",
|
||||||
"react": "^16.9.0",
|
"react": "^16.9.0",
|
||||||
"react-dom": "^16.9.0",
|
"react-dom": "^16.9.0",
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const Apple = styled.div.attrs(({ theme, zoom }) => ({
|
|||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: "${props => (!props.died ? "🍎" : "🐛")}";
|
content: "${props => (!props.died ? "🍎" : "🐛")}";
|
||||||
// content: "🍎";
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
animation: ${fade} 1s ease-out alternate infinite;
|
animation: ${fade} 1s ease-out alternate infinite;
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/components/Banner.js
Normal file
25
src/components/Banner.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const StyledBanner = styled.div`
|
||||||
|
border-width: ${({ theme }) => theme.banner.borderWidth};
|
||||||
|
border-style: solid;
|
||||||
|
border-color: ${({ theme }) => theme.banner.borderColor};
|
||||||
|
background-color: ${({ theme }) => theme.banner.background};
|
||||||
|
color: ${({ theme }) => theme.banner.color};
|
||||||
|
padding: ${({ theme }) => theme.banner.padding};
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: opacity 100ms ease-in;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 1px 1px 5px ${({ theme }) => theme.banner.shadowColor};
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Banner = ({ children }) => <StyledBanner>{children}</StyledBanner>;
|
||||||
|
|
||||||
|
export default Banner;
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { NavLink } from "react-router-dom";
|
import { NavLink } from "react-router-dom";
|
||||||
import styled, { css } from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import Tooltip from "./Tooltip.js";
|
||||||
|
|
||||||
const SharedStyle = () => `
|
const SharedStyle = () => `
|
||||||
border: 1px solid gray;
|
border-width: ${({ theme }) => theme.button.borderWidth};
|
||||||
border-radius: .1875rem;
|
border-style: solid;
|
||||||
background-color: palevioletred;
|
border-color: ${({ theme }) => theme.button.borderColor};
|
||||||
color: papayawhip;
|
border-radius: ${({ theme }) => theme.button.borderRadius};
|
||||||
|
background-color: ${({ theme }) => theme.button.background};
|
||||||
|
color: ${({ theme }) => theme.button.color};
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: .3125rem;
|
padding: .3125rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -17,7 +20,14 @@ const SharedStyle = () => `
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: blue;
|
background-color: ${({ theme }) => theme.button.hover.background};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&.active {
|
||||||
|
border-color: ${({ theme }) => theme.button.active.borderColor};
|
||||||
|
color: ${({ theme }) => theme.button.active.color};
|
||||||
|
background-color: ${({ theme }) => theme.button.active.background};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -37,31 +47,6 @@ const StyledAnchor = styled.a`
|
|||||||
|
|
||||||
const StyledNavLink = styled(NavLink)`
|
const StyledNavLink = styled(NavLink)`
|
||||||
${SharedStyle}
|
${SharedStyle}
|
||||||
&.active {
|
|
||||||
border-color: silver;
|
|
||||||
color: #828282;
|
|
||||||
background-color: #ecb1c5;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Tooltip = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
border: 1px solid gray;
|
|
||||||
background-color: palevioletred;
|
|
||||||
color: papayawhip;
|
|
||||||
padding: 5px;
|
|
||||||
box-shadow: 1px 1px 3px #d0bfa3;
|
|
||||||
${props =>
|
|
||||||
props.hover
|
|
||||||
? css`
|
|
||||||
transition: opacity 400ms 200ms ease-in;
|
|
||||||
`
|
|
||||||
: css`
|
|
||||||
transition: opacity 100ms ease-out;
|
|
||||||
`}
|
|
||||||
white-space: nowrap;
|
|
||||||
opacity: ${props => (props.hover ? 1 : 0)};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Button = (props, refs) => {
|
export const Button = (props, refs) => {
|
||||||
@@ -102,9 +87,10 @@ export const Button = (props, refs) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ToggleButton = styled(Button)`
|
export const ToggleButton = styled(Button)`
|
||||||
background-color: ${props => (props.toggle ? "silver" : null)};
|
background-color: ${({ theme, toggle }) => (toggle ? theme.button.toggled.background : null)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// TODO: extract text shadow colors
|
||||||
export const IconButton = styled(Button)`
|
export const IconButton = styled(Button)`
|
||||||
margin: 0 0.2rem;
|
margin: 0 0.2rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -116,7 +102,7 @@ export const IconButton = styled(Button)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const ToggleIconButton = styled(IconButton)`
|
export const ToggleIconButton = styled(IconButton)`
|
||||||
color: ${props => (props.toggle ? "#e6b4c4" : null)};
|
color: ${({ theme, toggle }) => (toggle ? theme.button.toggled.color : null)};
|
||||||
&:before {
|
&:before {
|
||||||
text-shadow: 0 0 1px gray, 0 0 1px #d0bfa3;
|
text-shadow: 0 0 1px gray, 0 0 1px #d0bfa3;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Button, { IconButton, ToggleIconButton } from "../components/Button.js";
|
import { IconButton, ToggleIconButton } from "../components/Button.js";
|
||||||
|
|
||||||
const buttonSize = 2.5;
|
const buttonSize = 2.5;
|
||||||
|
|
||||||
@@ -23,20 +23,40 @@ const HorizontalStack = styled.div`
|
|||||||
place-content: space-between;
|
place-content: space-between;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const VerticalStack = styled.div`
|
const ThemeSelector = styled.select`
|
||||||
display: flex;
|
appearance: none;
|
||||||
flex-direction: column;
|
border: 1px solid transparent;
|
||||||
place-content: center;
|
background: none;
|
||||||
align-content: space-around;
|
border-radius: 3px;
|
||||||
|
// padding: 5px;
|
||||||
|
line-height: 1.5em;
|
||||||
|
color: ${({ theme }) => theme.body.color};
|
||||||
|
font-size: ${({ theme }) => theme.body.fontSize};
|
||||||
|
font-family: ${({ theme }) => theme.body.fontFamily};
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
background-image: url('data:image/svg+xml;utf8,
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||||
|
<polygon points="0,3 16,3 8,13" fill="${({ theme }) => theme.select.color.replace("#", "%23")}" />
|
||||||
|
</svg>');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.625rem center;
|
||||||
|
background-size: 1rem 1rem;
|
||||||
|
outline-offset: 3px;
|
||||||
|
&:hover {
|
||||||
|
outline: 1px solid silver;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ControlPanel = ({
|
export const ControlPanel = ({
|
||||||
updateFrameSnake,
|
|
||||||
paused,
|
paused,
|
||||||
started,
|
started,
|
||||||
pauseSnake,
|
pauseSnake,
|
||||||
stopSnake,
|
stopSnake,
|
||||||
startSnake,
|
startSnake,
|
||||||
|
theme,
|
||||||
|
changeTheme,
|
||||||
fps,
|
fps,
|
||||||
zoom,
|
zoom,
|
||||||
setFps,
|
setFps,
|
||||||
@@ -44,13 +64,14 @@ export const ControlPanel = ({
|
|||||||
zoomOut
|
zoomOut
|
||||||
}) => (
|
}) => (
|
||||||
<Layout>
|
<Layout>
|
||||||
{
|
<HorizontalStack style={{ minHeight: "2rem" }}>
|
||||||
// <h1>Control Panel</h1>
|
<ThemeSelector onChange={e => changeTheme(e.target.value)} value={theme}>
|
||||||
// <ToggleButton toggle={paused} onClick={pauseSnake}>
|
<option value="light">Light</option>
|
||||||
// {paused ? "Resume" : "Pause"}
|
<option value="dark">Miami Night</option>
|
||||||
// </ToggleButton>
|
<option value="darkOcean">Dark Ocean</option>
|
||||||
}
|
<option value="default">Black / White</option>
|
||||||
|
</ThemeSelector>
|
||||||
|
</HorizontalStack>
|
||||||
<HorizontalStack style={{ minHeight: "2rem" }}>
|
<HorizontalStack style={{ minHeight: "2rem" }}>
|
||||||
<span>Zoom: {zoom}</span>
|
<span>Zoom: {zoom}</span>
|
||||||
<div style={{ display: "inline-block" }}>
|
<div style={{ display: "inline-block" }}>
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ const MenuButton = styled(NavLink)`
|
|||||||
const HeaderMenu = () => (
|
const HeaderMenu = () => (
|
||||||
<MenuPanel>
|
<MenuPanel>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<MenuButton to="/" exact>
|
<MenuButton to="/snake">Snake</MenuButton>
|
||||||
Home
|
|
||||||
</MenuButton>
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<MenuButton to="/snake">Snake</MenuButton>
|
<MenuButton to="/" exact>
|
||||||
|
About
|
||||||
|
</MenuButton>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuPanel>
|
</MenuPanel>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const Card = styled.div`
|
|||||||
font-size: ${props => (props.zoom || 1) * 1.5}rem;
|
font-size: ${props => (props.zoom || 1) * 1.5}rem;
|
||||||
font-family: Rubik,monospace;
|
font-family: Rubik,monospace;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
box-shadow: 1px 1px 3px #d0bfa3;
|
box-shadow: 1px 1px 3px ${({ theme }) => theme.card.shadow};
|
||||||
text-shadow: 0 0 1px black;
|
text-shadow: 0 0 1px black;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ const Card = styled.div`
|
|||||||
height: 50%;
|
height: 50%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
border-bottom: solid 2px #bdb19a;
|
border-bottom: solid 2px ${({ theme }) => theme.card.fold.highlight};
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
@@ -44,7 +44,7 @@ const Card = styled.div`
|
|||||||
top: 0;
|
top: 0;
|
||||||
height: 50%;
|
height: 50%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: dotted 2px #ad5a75;
|
border-bottom: dotted 2px ${({ theme }) => theme.card.fold.shadow};
|
||||||
z-index:1;
|
z-index:1;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,32 +1,23 @@
|
|||||||
import styled, { css } from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
export const SnakePart = styled.div.attrs(({ theme, zoom }) => ({
|
export const SnakePart = styled.div.attrs(({ theme, zoom, died, value: { index, length } }) => ({
|
||||||
style: {
|
style: {
|
||||||
height: theme.snake.cell.size * (zoom || 1) + "rem",
|
height: theme.snake.cell.size * (zoom || 1) + "rem",
|
||||||
width: theme.snake.cell.size * (zoom || 1) + "rem",
|
width: theme.snake.cell.size * (zoom || 1) + "rem",
|
||||||
fontSize: (zoom || 1) * 1.3 + "rem",
|
fontSize: (zoom || 1) * 1.3 + "rem",
|
||||||
lineHeight: (zoom || 1) * 1.7 + "rem",
|
lineHeight: (zoom || 1) * 1.7 + "rem",
|
||||||
paddingLeft: (zoom || 1) * 0.0625 + "rem"
|
paddingLeft: (zoom || 1) * 0.0625 + "rem",
|
||||||
|
backgroundColor: `${theme.snakePart.getColor(length, index, died)}`
|
||||||
}
|
}
|
||||||
}))`
|
}))`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
box-shadow: ${({ theme }) => theme.snakePart.boxShadow};
|
||||||
|
|
||||||
${props =>
|
&:after {
|
||||||
!props.died
|
content: "${({ value: { index }, died }) => index === 0 && died && "💀"}";
|
||||||
? css`
|
display: inline-block;
|
||||||
background-color: ${props => props.value && `hsl(340, ${props.value.brightness}%, 65%)`};
|
}
|
||||||
box-shadow: 1px 1px 2px #ddd inset, -1px -1px 2px gray inset;
|
|
||||||
`
|
|
||||||
: css`
|
|
||||||
background-color: ${props => props.value && `hsl(0, 0%, ${100 - props.value.brightness}% )`};
|
|
||||||
box-shadow: 1px 1px 2px #ddd inset, -1px -1px 2px gray inset;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: "${props => props.value && props.value.index === 0 && "💀"}";
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default SnakePart;
|
export default SnakePart;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const Cell = styled.div.attrs(({ theme, zoom }) => ({
|
|||||||
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: ${({ theme }) => theme.stage.cell.boxShadow};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Stage = ({ data, children, zoom = 1 }) => (
|
export const Stage = ({ data, children, zoom = 1 }) => (
|
||||||
|
|||||||
26
src/components/Tooltip.js
Normal file
26
src/components/Tooltip.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import styled, { css } from "styled-components";
|
||||||
|
|
||||||
|
export const Tooltip = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
border-width: ${({ theme }) => theme.tooltip.borderWidth};
|
||||||
|
border-style: solid;
|
||||||
|
border-color: ${({ theme }) => theme.tooltip.borderColor};
|
||||||
|
border-radius: ${({ theme }) => theme.tooltip.borderRadius};
|
||||||
|
background-color: ${({ theme }) => theme.tooltip.background};
|
||||||
|
color: ${({ theme }) => theme.tooltip.color};
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: ${({ theme }) => theme.tooltip.padding};
|
||||||
|
box-shadow: 1px 1px 3px ${({ theme }) => theme.tooltip.shadowColor};
|
||||||
|
${props =>
|
||||||
|
props.hover
|
||||||
|
? css`
|
||||||
|
transition: opacity 400ms 200ms ease-in;
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
transition: opacity 100ms ease-out;
|
||||||
|
`}
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: ${props => (props.hover ? 1 : 0)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Tooltip;
|
||||||
@@ -1,25 +1,27 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Provider } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { hot } from "react-hot-loader/root";
|
import { hot } from "react-hot-loader/root";
|
||||||
import { ConnectedRouter } from "connected-react-router";
|
import { MODULE_NAME as UI_MODULE_NAME } from "../redux/ui.js";
|
||||||
|
|
||||||
import { createStore } from "../redux/store";
|
|
||||||
|
|
||||||
import { ThemeProvider } from "styled-components";
|
import { ThemeProvider } from "styled-components";
|
||||||
import { themes } from "../theming/theme.js";
|
import { themes } from "../theming/theme.js";
|
||||||
import GlobalStyle from "../theming/GlobalStyle.js";
|
import GlobalStyle from "../theming/GlobalStyle.js";
|
||||||
|
|
||||||
const store = createStore();
|
const Document = ({ theme, children }) => (
|
||||||
|
<ThemeProvider theme={themes[theme]}>
|
||||||
const Document = ({ children }) => (
|
<React.Fragment>
|
||||||
<Provider store={store}>
|
<GlobalStyle />
|
||||||
<ThemeProvider theme={themes.main}>
|
{children}
|
||||||
<ConnectedRouter history={store.history}>
|
</React.Fragment>
|
||||||
<GlobalStyle />
|
</ThemeProvider>
|
||||||
{children}
|
|
||||||
</ConnectedRouter>
|
|
||||||
</ThemeProvider>
|
|
||||||
</Provider>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default (process.env.NODE_ENV === "development" ? hot(Document) : Document);
|
const ConnectedDocument = connect(
|
||||||
|
state => ({
|
||||||
|
theme: state[UI_MODULE_NAME].theme || "light"
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)(Document);
|
||||||
|
|
||||||
|
export default (process.env.NODE_ENV === "development" ? hot(ConnectedDocument) : ConnectedDocument);
|
||||||
|
// export default (process.env.NODE_ENV === "development" ? hot(Document) : Document);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { connect } from "react-redux";
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
/* Redux Modules */
|
/* Redux Modules */
|
||||||
|
import { module as uiModule, changeTheme } from "../redux/ui.js";
|
||||||
import { module as gameModule, keyPress, setFps } from "../redux/game.js";
|
import { module as gameModule, keyPress, setFps } from "../redux/game.js";
|
||||||
import {
|
import {
|
||||||
module as snakeModule,
|
module as snakeModule,
|
||||||
@@ -21,6 +22,7 @@ import Scoreboard from "../components/Scoreboard.js";
|
|||||||
import Stage from "../components/Stage.js";
|
import Stage from "../components/Stage.js";
|
||||||
import SnakePart from "../components/SnakePart.js";
|
import SnakePart from "../components/SnakePart.js";
|
||||||
import Apple from "../components/Apple.js";
|
import Apple from "../components/Apple.js";
|
||||||
|
import Banner from "../components/Banner.js";
|
||||||
|
|
||||||
const Layout = styled.div`
|
const Layout = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -29,6 +31,10 @@ const Layout = styled.div`
|
|||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StageContainer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
const SidePanel = styled.div`
|
const SidePanel = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -36,76 +42,61 @@ const SidePanel = styled.div`
|
|||||||
place-content: space-between;
|
place-content: space-between;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
class Snake extends React.Component {
|
const Snake = ({
|
||||||
constructor(props) {
|
startSnake,
|
||||||
super(props);
|
stopSnake,
|
||||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
pauseSnake,
|
||||||
window.me = this;
|
updateFrameSnake,
|
||||||
}
|
changeTheme,
|
||||||
|
theme,
|
||||||
handleKeyDown(e) {
|
grid,
|
||||||
this.props.keyPress(e.key);
|
started,
|
||||||
}
|
paused,
|
||||||
|
died,
|
||||||
componentDidMount() {
|
fps,
|
||||||
window.addEventListener("keydown", this.handleKeyDown, false);
|
setFps,
|
||||||
!this.props.started && this.props.startSnake();
|
score,
|
||||||
}
|
zoom,
|
||||||
|
zoomIn,
|
||||||
render() {
|
zoomOut
|
||||||
const {
|
}) => {
|
||||||
startSnake,
|
return (
|
||||||
stopSnake,
|
<React.Fragment>
|
||||||
pauseSnake,
|
<Layout>
|
||||||
updateFrameSnake,
|
<StageContainer>
|
||||||
grid,
|
|
||||||
started,
|
|
||||||
paused,
|
|
||||||
died,
|
|
||||||
fps,
|
|
||||||
setFps,
|
|
||||||
score,
|
|
||||||
zoom,
|
|
||||||
zoomIn,
|
|
||||||
zoomOut
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<Layout>
|
|
||||||
<Stage data={grid} zoom={zoom}>
|
<Stage data={grid} zoom={zoom}>
|
||||||
{cell =>
|
{cell =>
|
||||||
(cell.type === "apple" && <Apple zoom={zoom} died={died} />) ||
|
(cell.type === "apple" && <Apple zoom={zoom} died={died} />) ||
|
||||||
(cell.type === "snake" && <SnakePart zoom={zoom} value={cell} died={died} />)
|
(cell.type === "snake" && <SnakePart zoom={zoom} value={cell} died={died} />)
|
||||||
}
|
}
|
||||||
</Stage>
|
</Stage>
|
||||||
<SidePanel>
|
{!started && <Banner>Press 'r' to start the game</Banner>}
|
||||||
<Scoreboard score={score} zoom={2} />
|
</StageContainer>
|
||||||
<ControlPanel
|
<SidePanel>
|
||||||
updateFrameSnake={updateFrameSnake}
|
<Scoreboard score={score} zoom={2} />
|
||||||
started={started}
|
<ControlPanel
|
||||||
paused={paused}
|
updateFrameSnake={updateFrameSnake}
|
||||||
pauseSnake={pauseSnake}
|
started={started}
|
||||||
stopSnake={stopSnake}
|
paused={paused}
|
||||||
startSnake={startSnake}
|
pauseSnake={pauseSnake}
|
||||||
fps={fps}
|
stopSnake={stopSnake}
|
||||||
setFps={setFps}
|
startSnake={startSnake}
|
||||||
zoom={zoom}
|
theme={theme}
|
||||||
zoomIn={zoomIn}
|
changeTheme={changeTheme}
|
||||||
zoomOut={zoomOut}
|
fps={fps}
|
||||||
/>
|
setFps={setFps}
|
||||||
</SidePanel>
|
zoom={zoom}
|
||||||
</Layout>
|
zoomIn={zoomIn}
|
||||||
</React.Fragment>
|
zoomOut={zoomOut}
|
||||||
);
|
/>
|
||||||
}
|
</SidePanel>
|
||||||
|
</Layout>
|
||||||
componentWillUnmount() {
|
</React.Fragment>
|
||||||
window.removeEventListener("keydown", this.handleKeyDown);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
...state[uiModule.name],
|
||||||
...state[gameModule.name],
|
...state[gameModule.name],
|
||||||
...state[snakeModule.name]
|
...state[snakeModule.name]
|
||||||
});
|
});
|
||||||
@@ -119,7 +110,8 @@ const mapActionsToProps = {
|
|||||||
keyPress,
|
keyPress,
|
||||||
setFps,
|
setFps,
|
||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut
|
zoomOut,
|
||||||
|
changeTheme
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|||||||
28
src/index.js
28
src/index.js
@@ -1,12 +1,34 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { ConnectedRouter } from "connected-react-router";
|
||||||
|
|
||||||
|
import { createStore } from "./redux/store";
|
||||||
|
import { initSnake } from "./redux/snake";
|
||||||
|
import { keyPress } from "./redux/game";
|
||||||
|
|
||||||
import Document from "./containers/Document.js";
|
import Document from "./containers/Document.js";
|
||||||
import App from "./containers/App.js";
|
import App from "./containers/App.js";
|
||||||
|
|
||||||
|
const store = createStore();
|
||||||
|
window.store = store;
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Document>
|
<Provider store={store}>
|
||||||
<App />
|
<ConnectedRouter history={store.history}>
|
||||||
</Document>,
|
<Document>
|
||||||
|
<App />
|
||||||
|
</Document>
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>,
|
||||||
document.getElementById("root")
|
document.getElementById("root")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
store.dispatch(initSnake());
|
||||||
|
|
||||||
|
/* Register global key down handler */
|
||||||
|
if (window.__KEY_DOWN_LISTENER__) {
|
||||||
|
window.removeEventListener("keydown", window.__KEY_DOWN_LISTENER__);
|
||||||
|
}
|
||||||
|
window.__KEY_DOWN_LISTENER__ = e => store.dispatch(keyPress(e));
|
||||||
|
window.addEventListener("keydown", window.__KEY_DOWN_LISTENER__, false);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Module from "./module.js";
|
import Module from "./Module.js";
|
||||||
import keys from "../enums/keys.js";
|
import keys from "../enums/keys.js";
|
||||||
|
|
||||||
export const MODULE_NAME = "GAME";
|
export const MODULE_NAME = "GAME";
|
||||||
@@ -7,18 +7,25 @@ export const module = new Module(MODULE_NAME, {
|
|||||||
loopId: null,
|
loopId: null,
|
||||||
callers: [],
|
callers: [],
|
||||||
keyPressSubscribers: [],
|
keyPressSubscribers: [],
|
||||||
fps: 8
|
fps: 6
|
||||||
});
|
});
|
||||||
|
|
||||||
export const [START_LOOP, startLoop] = module.action("START_LOOP");
|
export const [START_LOOP, startLoop] = module.action("START_LOOP");
|
||||||
export const [STOP_LOOP, stopLoop] = module.action("STOP_LOOP");
|
export const [STOP_LOOP, stopLoop] = module.action("STOP_LOOP");
|
||||||
|
export const [INIT, initGame] = module.action("INIT");
|
||||||
export const [LOOP, loop] = module.action("LOOP", ({ recur, skip } = {}) => ({ recur, skip }));
|
export const [LOOP, loop] = module.action("LOOP", ({ recur, skip } = {}) => ({ recur, skip }));
|
||||||
export const [UPDATE_LOOP_ID, updateLoopId] = module.action("UPDATE_LOOP_ID", loopId => ({ loopId }));
|
export const [UPDATE_LOOP_ID, updateLoopId] = module.action("UPDATE_LOOP_ID", loopId => ({ loopId }));
|
||||||
export const [SET_FPS, setFps] = module.action("SET_FPS", fps => ({ fps }));
|
export const [SET_FPS, setFps] = module.action("SET_FPS", fps => ({ fps }));
|
||||||
export const [INVOKE_CALLERS, invokeCallers] = module.action("INVOKE_CALLERS");
|
export const [INVOKE_CALLERS, invokeCallers] = module.action("INVOKE_CALLERS");
|
||||||
export const [REGISTER_CALLER, registerCaller] = module.action("REGISTER_CALLER", name => ({ name }));
|
export const [REGISTER_CALLER, registerCaller] = module.action("REGISTER_CALLER", name => ({ name }));
|
||||||
export const [UNREGISTER_CALLER, unregisterCaller] = module.action("UNREGISTER_CALLER", name => ({ name }));
|
export const [UNREGISTER_CALLER, unregisterCaller] = module.action("UNREGISTER_CALLER", name => ({ name }));
|
||||||
export const [KEY_PRESS, keyPress] = module.action("KEY_PRESS", key => ({ key }));
|
export const [KEY_PRESS, keyPress] = module.action("KEY_PRESS", ({ key, altKey, ctrlKey, metaKey, shiftKey }) => ({
|
||||||
|
key,
|
||||||
|
altKey,
|
||||||
|
ctrlKey,
|
||||||
|
metaKey,
|
||||||
|
shiftKey
|
||||||
|
}));
|
||||||
export const [SUBSCRIBE_KEY_PRESSED, subscribeKeyPressed] = module.action("SUBSCRIBE_KEY_PRESSED", name => ({ name }));
|
export const [SUBSCRIBE_KEY_PRESSED, subscribeKeyPressed] = module.action("SUBSCRIBE_KEY_PRESSED", name => ({ name }));
|
||||||
export const [UNSUBSCRIBE_KEY_PRESSED, unsubscribeKeyPressed] = module.action("UNSUBSCRIBE_KEY_PRESSED", name => ({
|
export const [UNSUBSCRIBE_KEY_PRESSED, unsubscribeKeyPressed] = module.action("UNSUBSCRIBE_KEY_PRESSED", name => ({
|
||||||
name
|
name
|
||||||
@@ -84,16 +91,28 @@ module.reducer(UNSUBSCRIBE_KEY_PRESSED, (state, { name }) => {
|
|||||||
return { ...state, keyPressSubscribers: state.keyPressSubscribers.filter(subscriber => subscriber !== name) };
|
return { ...state, keyPressSubscribers: state.keyPressSubscribers.filter(subscriber => subscriber !== name) };
|
||||||
});
|
});
|
||||||
|
|
||||||
module.middleware(KEY_PRESS, (dispatch, { keyPressSubscribers = [], fps }, { key }) => {
|
module.middleware(INIT, (dispatch, { loopId }) => {
|
||||||
keyPressSubscribers.forEach(subscriber => dispatch({ type: subscriber, key }));
|
dispatch(updateLoopId(null));
|
||||||
if (key === keys.PLUS || key === keys.EQUAL) {
|
if (loopId) {
|
||||||
dispatch(setFps(fps + 1));
|
dispatch(startLoop());
|
||||||
}
|
|
||||||
if (key === keys.MINUS) {
|
|
||||||
dispatch(setFps(fps - 1));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.middleware(
|
||||||
|
KEY_PRESS,
|
||||||
|
(dispatch, { keyPressSubscribers = [], fps }, { key, altKey, ctrlKey, metaKey, shiftKey }) => {
|
||||||
|
keyPressSubscribers.forEach(subscriber => dispatch({ type: subscriber, key, altKey, ctrlKey, metaKey, shiftKey }));
|
||||||
|
|
||||||
|
if (key === keys.PLUS || key === keys.EQUAL) {
|
||||||
|
dispatch(setFps(fps + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === keys.MINUS) {
|
||||||
|
dispatch(setFps(fps - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.middleware(INVOKE_CALLERS, (dispatch, { callers = [] }) => {
|
module.middleware(INVOKE_CALLERS, (dispatch, { callers = [] }) => {
|
||||||
callers.forEach(caller => dispatch({ type: caller }));
|
callers.forEach(caller => dispatch({ type: caller }));
|
||||||
});
|
});
|
||||||
|
|||||||
23
src/redux/localStorage.js
Normal file
23
src/redux/localStorage.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export const loadState = () => {
|
||||||
|
try {
|
||||||
|
const serializedState = localStorage.getItem("state");
|
||||||
|
if (serializedState === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return JSON.parse(serializedState) || {};
|
||||||
|
} catch (err) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveState = state => {
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const serializedState = JSON.stringify(state);
|
||||||
|
localStorage.setItem("state", serializedState);
|
||||||
|
} catch {
|
||||||
|
// ignore write errors
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import Module from "./module.js";
|
import Module from "./Module.js";
|
||||||
import keys from "../enums/keys.js";
|
import keys from "../enums/keys.js";
|
||||||
import directions from "../enums/directions.js";
|
import directions from "../enums/directions.js";
|
||||||
import { registerCaller, unregisterCaller, startLoop, stopLoop, subscribeKeyPressed } from "./game.js";
|
import { registerCaller, unregisterCaller, startLoop, stopLoop, subscribeKeyPressed, initGame } from "./game.js";
|
||||||
|
|
||||||
const MODULE_NAME = "SNAKE";
|
const MODULE_NAME = "SNAKE";
|
||||||
const DEFAULT_GRID_SIZE = 16;
|
const DEFAULT_GRID_SIZE = 16;
|
||||||
@@ -28,8 +28,6 @@ function randomPosition(size) {
|
|||||||
}
|
}
|
||||||
const comparePositions = (pos1, pos2) => pos1[0] === pos2[0] && pos1[1] === pos2[1];
|
const comparePositions = (pos1, pos2) => pos1[0] === pos2[0] && pos1[1] === pos2[1];
|
||||||
|
|
||||||
const brightness = (l, i) => 20 + Math.round(0.8 * ((i + 1) / l) * 100);
|
|
||||||
|
|
||||||
function createGrid(size) {
|
function createGrid(size) {
|
||||||
const rows = [];
|
const rows = [];
|
||||||
for (let y = 0; y < size; y += 1) {
|
for (let y = 0; y < size; y += 1) {
|
||||||
@@ -76,6 +74,7 @@ export const module = new Module(MODULE_NAME, initialState);
|
|||||||
|
|
||||||
/* === Actions ================================================================================== */
|
/* === Actions ================================================================================== */
|
||||||
|
|
||||||
|
export const [INIT, initSnake] = module.action("INIT");
|
||||||
export const [START_SNAKE, startSnake] = module.action("START_SNAKE");
|
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");
|
||||||
@@ -99,7 +98,7 @@ module.reducer(ZOOM_IN, (state, { step = DEFAUL_ZOOM_STEP }) => ({
|
|||||||
...state,
|
...state,
|
||||||
zoom: Math.round((state.zoom + step) * 10) / 10
|
zoom: Math.round((state.zoom + step) * 10) / 10
|
||||||
}));
|
}));
|
||||||
module.reducer(SET_ZOOM_LEVEL, (state, { level }) => ({ ...state, zoom: level }));
|
module.reducer(SET_ZOOM_LEVEL, (state, { level }) => ({ ...state, zoom: Math.round(level * 10) / 10 }));
|
||||||
module.reducer(ZOOM_OUT, (state, { step = DEFAUL_ZOOM_STEP }) =>
|
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 }
|
state.zoom - step >= 0.5 ? { ...state, zoom: Math.round((state.zoom - step) * 10) / 10 } : { ...state, zoom: 1 }
|
||||||
);
|
);
|
||||||
@@ -111,7 +110,11 @@ module.reducer(RESET_SNAKE, (state, options) => {
|
|||||||
|
|
||||||
markCell(grid, apple, { type: "apple" });
|
markCell(grid, apple, { type: "apple" });
|
||||||
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,
|
||||||
|
length: snake.length
|
||||||
|
})
|
||||||
);
|
);
|
||||||
return { ...state, ...initialState, grid, zoom, snake, apple, ...options };
|
return { ...state, ...initialState, grid, zoom, snake, apple, ...options };
|
||||||
});
|
});
|
||||||
@@ -173,7 +176,7 @@ module.reducer(UPDATE_FRAME_SNAKE, state => {
|
|||||||
markCell(grid, snakePart, {
|
markCell(grid, snakePart, {
|
||||||
type: "snake",
|
type: "snake",
|
||||||
index: newSnake.length - 1 - i,
|
index: newSnake.length - 1 - i,
|
||||||
brightness: brightness(newSnake.length, i)
|
length: snake.length
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
score = newSnake.length;
|
score = newSnake.length;
|
||||||
@@ -201,6 +204,19 @@ module.after(UPDATE_FRAME_SNAKE, (dispatch, { died }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.middleware(INIT, (dispatch, { started, paused, died }) => {
|
||||||
|
if (!started && !died) {
|
||||||
|
dispatch(resetSnake({ started: false, died: false }));
|
||||||
|
}
|
||||||
|
// dispatch({ type: "[GAME] UPDATE_LOOP_ID", loopId: undefined });
|
||||||
|
if (!paused) {
|
||||||
|
dispatch(registerCaller(UPDATE_FRAME_SNAKE));
|
||||||
|
dispatch(subscribeKeyPressed(KEY_PRESSED_SNAKE));
|
||||||
|
}
|
||||||
|
// dispatch(startLoop());
|
||||||
|
dispatch(initGame());
|
||||||
|
});
|
||||||
|
|
||||||
module.middleware(START_SNAKE, dispatch => {
|
module.middleware(START_SNAKE, dispatch => {
|
||||||
dispatch(resetSnake({ started: true, died: false }));
|
dispatch(resetSnake({ started: true, died: false }));
|
||||||
dispatch(registerCaller(UPDATE_FRAME_SNAKE));
|
dispatch(registerCaller(UPDATE_FRAME_SNAKE));
|
||||||
@@ -224,7 +240,10 @@ module.middleware(PAUSE_SNAKE, (dispatch, { paused }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.middleware(KEY_PRESSED_SNAKE, (dispatch, _, { key }) => {
|
module.middleware(KEY_PRESSED_SNAKE, (dispatch, _, { key, altKey, ctrlKey, metaKey, shiftKey }) => {
|
||||||
|
if (altKey || ctrlKey || metaKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (key === keys.UP || key === keys.k) {
|
if (key === keys.UP || key === keys.k) {
|
||||||
dispatch(changeDirection(directions.UP));
|
dispatch(changeDirection(directions.UP));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,34 @@ import { createStore as createReduxStore, combineReducers, compose, applyMiddlew
|
|||||||
import { createBrowserHistory } from "history";
|
import { createBrowserHistory } from "history";
|
||||||
|
|
||||||
import { connectRouter, routerMiddleware } from "connected-react-router";
|
import { connectRouter, routerMiddleware } from "connected-react-router";
|
||||||
import { reducers as uiReducers } from "./ui.js";
|
import { loadState, saveState } from "./localStorage.js";
|
||||||
|
import { throttle } from "../utils/throttle.js";
|
||||||
|
|
||||||
import { module as snakeModule } from "./snake.js";
|
import { module as snakeModule } from "./snake.js";
|
||||||
import { module as gameModule } from "./game.js";
|
import { module as gameModule } from "./game.js";
|
||||||
|
import { module as uiModule } from "./ui.js";
|
||||||
|
|
||||||
export const createStore = () => {
|
export const createStore = () => {
|
||||||
const history = createBrowserHistory({
|
const history = createBrowserHistory({
|
||||||
basename: process.env.NODE_ENV === "production" ? "/redux/" : "/"
|
basename: process.env.NODE_ENV === "production" ? "/redux/" : "/"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("loadState", loadState(), {
|
||||||
|
router: connectRouter(history),
|
||||||
|
...uiModule.reducers,
|
||||||
|
...snakeModule.reducers,
|
||||||
|
...gameModule.reducers
|
||||||
|
});
|
||||||
|
|
||||||
const store = createReduxStore(
|
const store = createReduxStore(
|
||||||
combineReducers({
|
combineReducers({
|
||||||
router: connectRouter(history),
|
router: connectRouter(history),
|
||||||
ui: uiReducers,
|
|
||||||
...snakeModule.reducers,
|
...snakeModule.reducers,
|
||||||
|
...uiModule.reducers,
|
||||||
...gameModule.reducers
|
...gameModule.reducers
|
||||||
}),
|
}),
|
||||||
{},
|
loadState(),
|
||||||
|
// {},
|
||||||
compose.apply(
|
compose.apply(
|
||||||
this,
|
this,
|
||||||
[
|
[
|
||||||
@@ -31,11 +42,11 @@ export const createStore = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (module.hot) {
|
if (module.hot) {
|
||||||
module.hot.accept(["./ui.js", "./snake.js", "./game.js", "./module.js"], () => {
|
module.hot.accept(["./ui.js", "./snake.js", "./game.js", "./Module.js"], () => {
|
||||||
store.replaceReducer(
|
store.replaceReducer(
|
||||||
combineReducers({
|
combineReducers({
|
||||||
ui: uiReducers,
|
|
||||||
router: connectRouter(history),
|
router: connectRouter(history),
|
||||||
|
...uiModule.reducers,
|
||||||
...snakeModule.reducers,
|
...snakeModule.reducers,
|
||||||
...gameModule.reducers
|
...gameModule.reducers
|
||||||
})
|
})
|
||||||
@@ -45,6 +56,12 @@ export const createStore = () => {
|
|||||||
|
|
||||||
store.history = history;
|
store.history = history;
|
||||||
|
|
||||||
|
store.subscribe(
|
||||||
|
throttle(() => {
|
||||||
|
saveState(store.getState());
|
||||||
|
}, 500)
|
||||||
|
);
|
||||||
|
|
||||||
return store;
|
return store;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,41 @@
|
|||||||
const SHOW_SPINNER = "[UI] SHOW_SPINNER";
|
import Module from "./Module.js";
|
||||||
const HIDE_SPINNER = "[UI] HIDE_SPINNER";
|
|
||||||
const TOGGLE_SPINNER = "[UI] TOGGLE_SPINNER";
|
|
||||||
|
|
||||||
export const showSpinner = () => ({ type: SHOW_SPINNER });
|
export const MODULE_NAME = "UI";
|
||||||
export const hideSpinner = () => ({ type: HIDE_SPINNER });
|
export const initialState = {
|
||||||
export const toggleSpinner = () => ({ type: TOGGLE_SPINNER });
|
theme: "light"
|
||||||
|
|
||||||
export const reducers = (state = {}, action) => {
|
|
||||||
if (action.type === SHOW_SPINNER) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
spinner: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (action.type === HIDE_SPINNER) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
spinner: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (action.type === TOGGLE_SPINNER) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
spinner: !state.spinner
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const module = new Module(MODULE_NAME, initialState);
|
||||||
|
|
||||||
|
export const [SHOW_SPINNER, showSpinner] = module.action("SHOW_SPINNER");
|
||||||
|
export const [HIDE_SPINNER, hideSpinner] = module.action("HIDE_SPINNER");
|
||||||
|
export const [TOGGLE_SPINNER, toggleSpinner] = module.action("TOGGLE_SPINNER");
|
||||||
|
export const [CHANGE_THEME, changeTheme] = module.action("CHANGE_THEME", theme => ({ theme }));
|
||||||
|
|
||||||
|
module.reducer(SHOW_SPINNER, state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
spinner: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
module.reducer(HIDE_SPINNER, state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
spinner: false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
module.reducer(TOGGLE_SPINNER, state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
spinner: !state.spinner
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
module.reducer(CHANGE_THEME, (state, { theme }) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
theme
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default module;
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ const GlobalStyle = createGlobalStyle`
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
background: ${props => props.theme.body.background};
|
background: ${props => props.theme.body.background};
|
||||||
color: ${props => props.theme.body.color};
|
color: ${props => props.theme.body.color};
|
||||||
font-family: Raleway;
|
font-family: ${({ theme }) => theme.body.fontFamily};
|
||||||
font-size: 1rem;
|
font-size: ${({ theme }) => theme.body.fontSize};
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
h1, h2 {
|
h1, h2 {
|
||||||
|
|||||||
@@ -1,39 +1,13 @@
|
|||||||
const themes = {
|
import defaultTheme from "./themes/default.js";
|
||||||
main: {
|
import lightTheme from "./themes/light.js";
|
||||||
body: {
|
import darkTheme from "./themes/dark.js";
|
||||||
background: "papayawhip",
|
import darkOceanTheme from "./themes/darkOcean.js";
|
||||||
color: "palevioletred"
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
background: "#ffe6bd",
|
|
||||||
color: "palevioletred",
|
|
||||||
|
|
||||||
menuButton: {
|
const themes = {
|
||||||
background: "#ffe6bd",
|
default: defaultTheme,
|
||||||
color: "palevioletred",
|
light: lightTheme,
|
||||||
active: {
|
dark: darkTheme,
|
||||||
background: "palevioletred",
|
darkOcean: darkOceanTheme
|
||||||
color: "#ffe6bd"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
spinner: {
|
|
||||||
shadow: "#eeeeee",
|
|
||||||
highlight: "#db7093"
|
|
||||||
},
|
|
||||||
snake: {
|
|
||||||
cell: {
|
|
||||||
border: "0.0625rem solid papayawhip",
|
|
||||||
size: 1.5
|
|
||||||
},
|
|
||||||
cellColors: {
|
|
||||||
" ": "",
|
|
||||||
a: "",
|
|
||||||
ab: "#90dc90",
|
|
||||||
s: "#f12f00"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { themes as default, themes };
|
export { themes as default, themes };
|
||||||
|
|||||||
47
src/theming/themes/dark.js
Normal file
47
src/theming/themes/dark.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import merge from "deepmerge";
|
||||||
|
import { renderTheme } from "./default.js";
|
||||||
|
|
||||||
|
export const colors = {
|
||||||
|
background: "#333",
|
||||||
|
backgroundActive: "#828282",
|
||||||
|
backgroundInactive: "silver",
|
||||||
|
backgroundAlternate: "#555",
|
||||||
|
color: "palevioletred",
|
||||||
|
colorActive: "#ecb1c5",
|
||||||
|
colorInactive: "#e6b4c4",
|
||||||
|
colorAlternate: "#ecb1c5",
|
||||||
|
shadowColor: "#222",
|
||||||
|
borderColor: "#222",
|
||||||
|
borderColorActive: "silver",
|
||||||
|
spinnerShadow: "#444",
|
||||||
|
spinnerHighlight: "#db7093",
|
||||||
|
cardFoldHighlight: "#ad5a75",
|
||||||
|
cardFoldShadow: "#bdb19a",
|
||||||
|
selectColor: "#db7093"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapRange = (l, i, min, max) => Math.round((max - min) * ((l - i) / l) + min);
|
||||||
|
|
||||||
|
export const darkTheme = merge(renderTheme(colors), {
|
||||||
|
snakePart: {
|
||||||
|
boxShadow: "1px 1px 2px #888 inset, -1px -1px 2px #222 inset",
|
||||||
|
getColor(length, index, died) {
|
||||||
|
const hue = 340;
|
||||||
|
if (!died) {
|
||||||
|
const saturation = mapRange(length, index, 20, 65);
|
||||||
|
const brightness = 65;
|
||||||
|
return `hsl(${hue}, ${saturation}%, ${brightness}%)`;
|
||||||
|
}
|
||||||
|
const saturation = 0;
|
||||||
|
const brightness = mapRange(length, index, 70, 0);
|
||||||
|
return `hsl(${hue}, ${saturation}%, ${brightness}%)`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stage: {
|
||||||
|
cell: {
|
||||||
|
boxShadow: "1px 1px 3px #191919 inset, -1px -1px 1px #777777 inset"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default darkTheme;
|
||||||
47
src/theming/themes/darkOcean.js
Normal file
47
src/theming/themes/darkOcean.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import merge from "deepmerge";
|
||||||
|
import { renderTheme } from "./default.js";
|
||||||
|
|
||||||
|
export const colors = {
|
||||||
|
background: "#333",
|
||||||
|
backgroundActive: "#828282",
|
||||||
|
backgroundInactive: "silver",
|
||||||
|
backgroundAlternate: "#555",
|
||||||
|
color: "#7094db",
|
||||||
|
colorActive: "#ecb1c5",
|
||||||
|
colorInactive: "#b3c3e6",
|
||||||
|
colorAlternate: "#ecb1c5",
|
||||||
|
shadowColor: "#222",
|
||||||
|
borderColor: "#222",
|
||||||
|
borderColorActive: "silver",
|
||||||
|
spinnerShadow: "#444",
|
||||||
|
spinnerHighlight: "#db7093",
|
||||||
|
cardFoldHighlight: "#091225",
|
||||||
|
cardFoldShadow: "#bdb19a",
|
||||||
|
selectColor: "#7094db"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapRange = (l, i, min, max) => Math.round((max - min) * ((l - i) / l) + min);
|
||||||
|
|
||||||
|
export const darkTheme = merge(renderTheme(colors), {
|
||||||
|
snakePart: {
|
||||||
|
boxShadow: "1px 1px 2px #888 inset, -1px -1px 2px #222 inset",
|
||||||
|
getColor(length, index, died) {
|
||||||
|
const hue = 220;
|
||||||
|
if (!died) {
|
||||||
|
const saturation = mapRange(length, index, 20, 65);
|
||||||
|
const brightness = 65;
|
||||||
|
return `hsl(${hue}, ${saturation}%, ${brightness}%)`;
|
||||||
|
}
|
||||||
|
const saturation = 0;
|
||||||
|
const brightness = mapRange(length, index, 70, 0);
|
||||||
|
return `hsl(${hue}, ${saturation}%, ${brightness}%)`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stage: {
|
||||||
|
cell: {
|
||||||
|
boxShadow: "1px 1px 3px #191919 inset, -1px -1px 1px #777777 inset"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default darkTheme;
|
||||||
135
src/theming/themes/default.js
Normal file
135
src/theming/themes/default.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import merge from "deepmerge";
|
||||||
|
|
||||||
|
export const defaultColors = {
|
||||||
|
background: "white",
|
||||||
|
backgroundAlternate: "#ddd",
|
||||||
|
backgroundInactive: "silver",
|
||||||
|
backgroundActive: "#ccc",
|
||||||
|
color: "black",
|
||||||
|
colorActive: "#444",
|
||||||
|
colorAlternate: "#222",
|
||||||
|
colorInactive: "silver",
|
||||||
|
shadowColor: "gray",
|
||||||
|
borderColor: "black",
|
||||||
|
borderColorActive: "silver",
|
||||||
|
spinnerShadow: "#eee",
|
||||||
|
spinnerHighlight: "black",
|
||||||
|
cardFoldHighlight: "#888",
|
||||||
|
cardFoldShadow: "#bbb",
|
||||||
|
snakePartHueAlive: "340",
|
||||||
|
snakePartHueDied: "0",
|
||||||
|
snakePartLightness: "0%",
|
||||||
|
selectColor: "black"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapRange = (l, i, min, max) => Math.round((max - min) * ((l - i) / l) + min);
|
||||||
|
|
||||||
|
export const renderTheme = themeColors => {
|
||||||
|
const colors = merge(defaultColors, themeColors);
|
||||||
|
return {
|
||||||
|
body: {
|
||||||
|
background: colors.background,
|
||||||
|
color: colors.color,
|
||||||
|
fontSize: "1rem",
|
||||||
|
fontFamily: "Raleway"
|
||||||
|
},
|
||||||
|
banner: {
|
||||||
|
background: colors.color,
|
||||||
|
color: colors.background,
|
||||||
|
borderColor: colors.borderColor,
|
||||||
|
shadowColor: colors.shadowColor,
|
||||||
|
padding: `2rem`,
|
||||||
|
borderWidth: `${1 / 16}rem`
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
color: colors.selectColor
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
background: colors.backgroundAlternate,
|
||||||
|
color: colors.color,
|
||||||
|
|
||||||
|
menuButton: {
|
||||||
|
background: colors.backgroundAlternate,
|
||||||
|
color: colors.color,
|
||||||
|
active: {
|
||||||
|
background: colors.color,
|
||||||
|
color: colors.backgroundAlternate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
borderColor: colors.borderColor,
|
||||||
|
background: colors.color,
|
||||||
|
color: colors.background,
|
||||||
|
borderWidth: `${1 / 16}rem`,
|
||||||
|
borderRadius: `${3 / 16}rem`,
|
||||||
|
toggled: {
|
||||||
|
background: colors.backgroundInactive,
|
||||||
|
color: colors.colorInactive
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
background: colors.colorAlternate
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
background: colors.backgroundActive,
|
||||||
|
color: colors.colorActive,
|
||||||
|
borderColor: colors.borderColorActive
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
borderColor: colors.borderColor,
|
||||||
|
borderWidth: `${1 / 16}rem`,
|
||||||
|
borderRadius: `${3 / 16}rem`,
|
||||||
|
padding: `${5 / 16}rem`,
|
||||||
|
background: colors.color,
|
||||||
|
color: colors.background,
|
||||||
|
shadowColor: colors.shadowColor
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
shadow: colors.shadowColor,
|
||||||
|
fold: {
|
||||||
|
highlight: colors.cardFoldHighlight,
|
||||||
|
shadow: colors.cardFoldShadow
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apple: {
|
||||||
|
effect: "none"
|
||||||
|
},
|
||||||
|
snakePart: {
|
||||||
|
boxShadow: "1px 1px 2px #ddd inset, -1px -1px 2px gray inset",
|
||||||
|
getColor(length, index, died) {
|
||||||
|
const hue = 0;
|
||||||
|
if (!died) {
|
||||||
|
const saturation = 0;
|
||||||
|
const brightness = mapRange(length, index, 70, 0);
|
||||||
|
return `hsl(${hue}, ${saturation}%, ${brightness}%)`;
|
||||||
|
}
|
||||||
|
const saturation = mapRange(length, index, 70, 100);
|
||||||
|
const brightness = 65;
|
||||||
|
return `hsl(${hue}, ${saturation}%, ${brightness}%)`;
|
||||||
|
},
|
||||||
|
hueAlive: colors.snakePartHueAlive,
|
||||||
|
hueDied: colors.snakePartHueDied,
|
||||||
|
lightness: colors.snakePartLightness
|
||||||
|
},
|
||||||
|
stage: {
|
||||||
|
cell: {
|
||||||
|
boxShadow: "1px 1px 4px grey inset, -1px -1px 4px #fff inset"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spinner: {
|
||||||
|
shadow: colors.spinnerShadow,
|
||||||
|
highlight: colors.spinnerHighlight
|
||||||
|
},
|
||||||
|
snake: {
|
||||||
|
cell: {
|
||||||
|
border: `0.0625rem solid ${colors.background}`,
|
||||||
|
size: 1.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultTheme = renderTheme(defaultColors);
|
||||||
|
|
||||||
|
export default defaultTheme;
|
||||||
42
src/theming/themes/light.js
Normal file
42
src/theming/themes/light.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import merge from "deepmerge";
|
||||||
|
import { renderTheme } from "./default.js";
|
||||||
|
|
||||||
|
export const colors = {
|
||||||
|
background: "papayawhip",
|
||||||
|
backgroundActive: "#828282",
|
||||||
|
backgroundInactive: "silver",
|
||||||
|
backgroundAlternate: "#ffe6bd",
|
||||||
|
color: "palevioletred",
|
||||||
|
colorActive: "#ecb1c5",
|
||||||
|
colorInactive: "#e6b4c4",
|
||||||
|
colorAlternate: "#ecb1c5",
|
||||||
|
shadowColor: "#d0bfa3",
|
||||||
|
shadowColorx: "gray",
|
||||||
|
borderColor: "gray",
|
||||||
|
borderColorActive: "silver",
|
||||||
|
spinnerShadow: "#eee",
|
||||||
|
spinnerHighlight: "#db7093",
|
||||||
|
cardFoldHighlight: "#ad5a75",
|
||||||
|
cardFoldShadow: "#bdb19a",
|
||||||
|
selectColor: "#db7093"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapRange = (l, i, min, max) => Math.round((max - min) * ((l - i) / l) + min);
|
||||||
|
|
||||||
|
export const lightTheme = merge(renderTheme(colors), {
|
||||||
|
snakePart: {
|
||||||
|
getColor(length, index, died) {
|
||||||
|
const hue = 340;
|
||||||
|
if (!died) {
|
||||||
|
const saturation = mapRange(length, index, 20, 65);
|
||||||
|
const brightness = 65;
|
||||||
|
return `hsl(${hue}, ${saturation}%, ${brightness}%)`;
|
||||||
|
}
|
||||||
|
const saturation = 0;
|
||||||
|
const brightness = mapRange(length, index, 70, 0);
|
||||||
|
return `hsl(${hue}, ${saturation}%, ${brightness}%)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default lightTheme;
|
||||||
34
src/utils/throttle.js
Normal file
34
src/utils/throttle.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export const throttle = (fn, wait = 0) => {
|
||||||
|
let timeoutId = null;
|
||||||
|
|
||||||
|
const throttleFn = () => {
|
||||||
|
if (timeoutId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
fn();
|
||||||
|
timeoutId = null;
|
||||||
|
}, wait);
|
||||||
|
};
|
||||||
|
|
||||||
|
throttleFn.cancel = () => {
|
||||||
|
if (!timeoutId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
throttleFn.flush = () => {
|
||||||
|
if (!timeoutId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
fn();
|
||||||
|
};
|
||||||
|
|
||||||
|
return throttleFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default throttle;
|
||||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user