Implemented theming and game resuming via local storage
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"connected-react-router": "^6.5.2",
|
||||
"deepmerge": "^4.0.0",
|
||||
"history": "^4.9.0",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0",
|
||||
|
||||
@@ -19,7 +19,6 @@ const Apple = styled.div.attrs(({ theme, zoom }) => ({
|
||||
|
||||
&:after {
|
||||
content: "${props => (!props.died ? "🍎" : "🐛")}";
|
||||
// content: "🍎";
|
||||
display: inline-block;
|
||||
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 { NavLink } from "react-router-dom";
|
||||
import styled, { css } from "styled-components";
|
||||
import styled from "styled-components";
|
||||
import Tooltip from "./Tooltip.js";
|
||||
|
||||
const SharedStyle = () => `
|
||||
border: 1px solid gray;
|
||||
border-radius: .1875rem;
|
||||
background-color: palevioletred;
|
||||
color: papayawhip;
|
||||
border-width: ${({ theme }) => theme.button.borderWidth};
|
||||
border-style: solid;
|
||||
border-color: ${({ theme }) => theme.button.borderColor};
|
||||
border-radius: ${({ theme }) => theme.button.borderRadius};
|
||||
background-color: ${({ theme }) => theme.button.background};
|
||||
color: ${({ theme }) => theme.button.color};
|
||||
text-align: center;
|
||||
padding: .3125rem;
|
||||
font-size: 1rem;
|
||||
@@ -17,7 +20,14 @@ const SharedStyle = () => `
|
||||
cursor: pointer;
|
||||
|
||||
&: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)`
|
||||
${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) => {
|
||||
@@ -102,9 +87,10 @@ export const Button = (props, refs) => {
|
||||
};
|
||||
|
||||
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)`
|
||||
margin: 0 0.2rem;
|
||||
display: inline-block;
|
||||
@@ -116,7 +102,7 @@ export const IconButton = styled(Button)`
|
||||
`;
|
||||
|
||||
export const ToggleIconButton = styled(IconButton)`
|
||||
color: ${props => (props.toggle ? "#e6b4c4" : null)};
|
||||
color: ${({ theme, toggle }) => (toggle ? theme.button.toggled.color : null)};
|
||||
&:before {
|
||||
text-shadow: 0 0 1px gray, 0 0 1px #d0bfa3;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import Button, { IconButton, ToggleIconButton } from "../components/Button.js";
|
||||
import { IconButton, ToggleIconButton } from "../components/Button.js";
|
||||
|
||||
const buttonSize = 2.5;
|
||||
|
||||
@@ -23,20 +23,40 @@ const HorizontalStack = styled.div`
|
||||
place-content: space-between;
|
||||
`;
|
||||
|
||||
const VerticalStack = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
place-content: center;
|
||||
align-content: space-around;
|
||||
const ThemeSelector = styled.select`
|
||||
appearance: none;
|
||||
border: 1px solid transparent;
|
||||
background: none;
|
||||
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 = ({
|
||||
updateFrameSnake,
|
||||
paused,
|
||||
started,
|
||||
pauseSnake,
|
||||
stopSnake,
|
||||
startSnake,
|
||||
theme,
|
||||
changeTheme,
|
||||
fps,
|
||||
zoom,
|
||||
setFps,
|
||||
@@ -44,13 +64,14 @@ export const ControlPanel = ({
|
||||
zoomOut
|
||||
}) => (
|
||||
<Layout>
|
||||
{
|
||||
// <h1>Control Panel</h1>
|
||||
// <ToggleButton toggle={paused} onClick={pauseSnake}>
|
||||
// {paused ? "Resume" : "Pause"}
|
||||
// </ToggleButton>
|
||||
}
|
||||
|
||||
<HorizontalStack style={{ minHeight: "2rem" }}>
|
||||
<ThemeSelector onChange={e => changeTheme(e.target.value)} value={theme}>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Miami Night</option>
|
||||
<option value="darkOcean">Dark Ocean</option>
|
||||
<option value="default">Black / White</option>
|
||||
</ThemeSelector>
|
||||
</HorizontalStack>
|
||||
<HorizontalStack style={{ minHeight: "2rem" }}>
|
||||
<span>Zoom: {zoom}</span>
|
||||
<div style={{ display: "inline-block" }}>
|
||||
|
||||
@@ -30,12 +30,12 @@ const MenuButton = styled(NavLink)`
|
||||
const HeaderMenu = () => (
|
||||
<MenuPanel>
|
||||
<MenuItem>
|
||||
<MenuButton to="/" exact>
|
||||
Home
|
||||
</MenuButton>
|
||||
<MenuButton to="/snake">Snake</MenuButton>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<MenuButton to="/snake">Snake</MenuButton>
|
||||
<MenuButton to="/" exact>
|
||||
About
|
||||
</MenuButton>
|
||||
</MenuItem>
|
||||
</MenuPanel>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ const Card = styled.div`
|
||||
font-size: ${props => (props.zoom || 1) * 1.5}rem;
|
||||
font-family: Rubik,monospace;
|
||||
font-weight: 500;
|
||||
box-shadow: 1px 1px 3px #d0bfa3;
|
||||
box-shadow: 1px 1px 3px ${({ theme }) => theme.card.shadow};
|
||||
text-shadow: 0 0 1px black;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -34,7 +34,7 @@ const Card = styled.div`
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
border-bottom: solid 2px #bdb19a;
|
||||
border-bottom: solid 2px ${({ theme }) => theme.card.fold.highlight};
|
||||
}
|
||||
|
||||
&::after {
|
||||
@@ -44,7 +44,7 @@ const Card = styled.div`
|
||||
top: 0;
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
border-bottom: dotted 2px #ad5a75;
|
||||
border-bottom: dotted 2px ${({ theme }) => theme.card.fold.shadow};
|
||||
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: {
|
||||
height: theme.snake.cell.size * (zoom || 1) + "rem",
|
||||
width: theme.snake.cell.size * (zoom || 1) + "rem",
|
||||
fontSize: (zoom || 1) * 1.3 + "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;
|
||||
vertical-align: top;
|
||||
box-shadow: ${({ theme }) => theme.snakePart.boxShadow};
|
||||
|
||||
${props =>
|
||||
!props.died
|
||||
? css`
|
||||
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;
|
||||
}
|
||||
`}
|
||||
&:after {
|
||||
content: "${({ value: { index }, died }) => index === 0 && died && "💀"}";
|
||||
display: inline-block;
|
||||
}
|
||||
`;
|
||||
|
||||
export default SnakePart;
|
||||
|
||||
@@ -19,7 +19,7 @@ const Cell = styled.div.attrs(({ theme, zoom }) => ({
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
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 }) => (
|
||||
|
||||
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 { Provider } from "react-redux";
|
||||
import { connect } from "react-redux";
|
||||
import { hot } from "react-hot-loader/root";
|
||||
import { ConnectedRouter } from "connected-react-router";
|
||||
|
||||
import { createStore } from "../redux/store";
|
||||
import { MODULE_NAME as UI_MODULE_NAME } from "../redux/ui.js";
|
||||
|
||||
import { ThemeProvider } from "styled-components";
|
||||
import { themes } from "../theming/theme.js";
|
||||
import GlobalStyle from "../theming/GlobalStyle.js";
|
||||
|
||||
const store = createStore();
|
||||
|
||||
const Document = ({ children }) => (
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={themes.main}>
|
||||
<ConnectedRouter history={store.history}>
|
||||
<GlobalStyle />
|
||||
{children}
|
||||
</ConnectedRouter>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
const Document = ({ theme, children }) => (
|
||||
<ThemeProvider theme={themes[theme]}>
|
||||
<React.Fragment>
|
||||
<GlobalStyle />
|
||||
{children}
|
||||
</React.Fragment>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
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";
|
||||
|
||||
/* Redux Modules */
|
||||
import { module as uiModule, changeTheme } from "../redux/ui.js";
|
||||
import { module as gameModule, keyPress, setFps } from "../redux/game.js";
|
||||
import {
|
||||
module as snakeModule,
|
||||
@@ -21,6 +22,7 @@ import Scoreboard from "../components/Scoreboard.js";
|
||||
import Stage from "../components/Stage.js";
|
||||
import SnakePart from "../components/SnakePart.js";
|
||||
import Apple from "../components/Apple.js";
|
||||
import Banner from "../components/Banner.js";
|
||||
|
||||
const Layout = styled.div`
|
||||
display: flex;
|
||||
@@ -29,6 +31,10 @@ const Layout = styled.div`
|
||||
margin-top: 2rem;
|
||||
`;
|
||||
|
||||
const StageContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const SidePanel = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -36,76 +42,61 @@ const SidePanel = styled.div`
|
||||
place-content: space-between;
|
||||
`;
|
||||
|
||||
class Snake extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
window.me = this;
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
this.props.keyPress(e.key);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("keydown", this.handleKeyDown, false);
|
||||
!this.props.started && this.props.startSnake();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
startSnake,
|
||||
stopSnake,
|
||||
pauseSnake,
|
||||
updateFrameSnake,
|
||||
grid,
|
||||
started,
|
||||
paused,
|
||||
died,
|
||||
fps,
|
||||
setFps,
|
||||
score,
|
||||
zoom,
|
||||
zoomIn,
|
||||
zoomOut
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Layout>
|
||||
const Snake = ({
|
||||
startSnake,
|
||||
stopSnake,
|
||||
pauseSnake,
|
||||
updateFrameSnake,
|
||||
changeTheme,
|
||||
theme,
|
||||
grid,
|
||||
started,
|
||||
paused,
|
||||
died,
|
||||
fps,
|
||||
setFps,
|
||||
score,
|
||||
zoom,
|
||||
zoomIn,
|
||||
zoomOut
|
||||
}) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Layout>
|
||||
<StageContainer>
|
||||
<Stage data={grid} zoom={zoom}>
|
||||
{cell =>
|
||||
(cell.type === "apple" && <Apple zoom={zoom} died={died} />) ||
|
||||
(cell.type === "snake" && <SnakePart zoom={zoom} value={cell} died={died} />)
|
||||
}
|
||||
</Stage>
|
||||
<SidePanel>
|
||||
<Scoreboard score={score} zoom={2} />
|
||||
<ControlPanel
|
||||
updateFrameSnake={updateFrameSnake}
|
||||
started={started}
|
||||
paused={paused}
|
||||
pauseSnake={pauseSnake}
|
||||
stopSnake={stopSnake}
|
||||
startSnake={startSnake}
|
||||
fps={fps}
|
||||
setFps={setFps}
|
||||
zoom={zoom}
|
||||
zoomIn={zoomIn}
|
||||
zoomOut={zoomOut}
|
||||
/>
|
||||
</SidePanel>
|
||||
</Layout>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
}
|
||||
{!started && <Banner>Press 'r' to start the game</Banner>}
|
||||
</StageContainer>
|
||||
<SidePanel>
|
||||
<Scoreboard score={score} zoom={2} />
|
||||
<ControlPanel
|
||||
updateFrameSnake={updateFrameSnake}
|
||||
started={started}
|
||||
paused={paused}
|
||||
pauseSnake={pauseSnake}
|
||||
stopSnake={stopSnake}
|
||||
startSnake={startSnake}
|
||||
theme={theme}
|
||||
changeTheme={changeTheme}
|
||||
fps={fps}
|
||||
setFps={setFps}
|
||||
zoom={zoom}
|
||||
zoomIn={zoomIn}
|
||||
zoomOut={zoomOut}
|
||||
/>
|
||||
</SidePanel>
|
||||
</Layout>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
...state[uiModule.name],
|
||||
...state[gameModule.name],
|
||||
...state[snakeModule.name]
|
||||
});
|
||||
@@ -119,7 +110,8 @@ const mapActionsToProps = {
|
||||
keyPress,
|
||||
setFps,
|
||||
zoomIn,
|
||||
zoomOut
|
||||
zoomOut,
|
||||
changeTheme
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
||||
28
src/index.js
28
src/index.js
@@ -1,12 +1,34 @@
|
||||
import React from "react";
|
||||
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 App from "./containers/App.js";
|
||||
|
||||
const store = createStore();
|
||||
window.store = store;
|
||||
|
||||
ReactDOM.render(
|
||||
<Document>
|
||||
<App />
|
||||
</Document>,
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={store.history}>
|
||||
<Document>
|
||||
<App />
|
||||
</Document>
|
||||
</ConnectedRouter>
|
||||
</Provider>,
|
||||
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";
|
||||
|
||||
export const MODULE_NAME = "GAME";
|
||||
@@ -7,18 +7,25 @@ export const module = new Module(MODULE_NAME, {
|
||||
loopId: null,
|
||||
callers: [],
|
||||
keyPressSubscribers: [],
|
||||
fps: 8
|
||||
fps: 6
|
||||
});
|
||||
|
||||
export const [START_LOOP, startLoop] = module.action("START_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 [UPDATE_LOOP_ID, updateLoopId] = module.action("UPDATE_LOOP_ID", loopId => ({ loopId }));
|
||||
export const [SET_FPS, setFps] = module.action("SET_FPS", fps => ({ fps }));
|
||||
export const [INVOKE_CALLERS, invokeCallers] = module.action("INVOKE_CALLERS");
|
||||
export const [REGISTER_CALLER, registerCaller] = module.action("REGISTER_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 [UNSUBSCRIBE_KEY_PRESSED, unsubscribeKeyPressed] = module.action("UNSUBSCRIBE_KEY_PRESSED", name => ({
|
||||
name
|
||||
@@ -84,16 +91,28 @@ module.reducer(UNSUBSCRIBE_KEY_PRESSED, (state, { name }) => {
|
||||
return { ...state, keyPressSubscribers: state.keyPressSubscribers.filter(subscriber => subscriber !== name) };
|
||||
});
|
||||
|
||||
module.middleware(KEY_PRESS, (dispatch, { keyPressSubscribers = [], fps }, { key }) => {
|
||||
keyPressSubscribers.forEach(subscriber => dispatch({ type: subscriber, key }));
|
||||
if (key === keys.PLUS || key === keys.EQUAL) {
|
||||
dispatch(setFps(fps + 1));
|
||||
}
|
||||
if (key === keys.MINUS) {
|
||||
dispatch(setFps(fps - 1));
|
||||
module.middleware(INIT, (dispatch, { loopId }) => {
|
||||
dispatch(updateLoopId(null));
|
||||
if (loopId) {
|
||||
dispatch(startLoop());
|
||||
}
|
||||
});
|
||||
|
||||
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 = [] }) => {
|
||||
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 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 DEFAULT_GRID_SIZE = 16;
|
||||
@@ -28,8 +28,6 @@ function randomPosition(size) {
|
||||
}
|
||||
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) {
|
||||
const rows = [];
|
||||
for (let y = 0; y < size; y += 1) {
|
||||
@@ -76,6 +74,7 @@ export const module = new Module(MODULE_NAME, initialState);
|
||||
|
||||
/* === Actions ================================================================================== */
|
||||
|
||||
export const [INIT, initSnake] = module.action("INIT");
|
||||
export const [START_SNAKE, startSnake] = module.action("START_SNAKE");
|
||||
export const [RESET_SNAKE, resetSnake] = module.action("RESET_SNAKE", options => ({ ...options }));
|
||||
export const [STOP_SNAKE, stopSnake] = module.action("STOP_SNAKE");
|
||||
@@ -99,7 +98,7 @@ 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(SET_ZOOM_LEVEL, (state, { level }) => ({ ...state, zoom: Math.round(level * 10) / 10 }));
|
||||
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 }
|
||||
);
|
||||
@@ -111,7 +110,11 @@ module.reducer(RESET_SNAKE, (state, options) => {
|
||||
|
||||
markCell(grid, apple, { type: "apple" });
|
||||
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 };
|
||||
});
|
||||
@@ -173,7 +176,7 @@ module.reducer(UPDATE_FRAME_SNAKE, state => {
|
||||
markCell(grid, snakePart, {
|
||||
type: "snake",
|
||||
index: newSnake.length - 1 - i,
|
||||
brightness: brightness(newSnake.length, i)
|
||||
length: snake.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 => {
|
||||
dispatch(resetSnake({ started: true, died: false }));
|
||||
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) {
|
||||
dispatch(changeDirection(directions.UP));
|
||||
}
|
||||
|
||||
@@ -2,23 +2,34 @@ import { createStore as createReduxStore, combineReducers, compose, applyMiddlew
|
||||
import { createBrowserHistory } from "history";
|
||||
|
||||
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 gameModule } from "./game.js";
|
||||
import { module as uiModule } from "./ui.js";
|
||||
|
||||
export const createStore = () => {
|
||||
const history = createBrowserHistory({
|
||||
basename: process.env.NODE_ENV === "production" ? "/redux/" : "/"
|
||||
});
|
||||
|
||||
console.log("loadState", loadState(), {
|
||||
router: connectRouter(history),
|
||||
...uiModule.reducers,
|
||||
...snakeModule.reducers,
|
||||
...gameModule.reducers
|
||||
});
|
||||
|
||||
const store = createReduxStore(
|
||||
combineReducers({
|
||||
router: connectRouter(history),
|
||||
ui: uiReducers,
|
||||
...snakeModule.reducers,
|
||||
...uiModule.reducers,
|
||||
...gameModule.reducers
|
||||
}),
|
||||
{},
|
||||
loadState(),
|
||||
// {},
|
||||
compose.apply(
|
||||
this,
|
||||
[
|
||||
@@ -31,11 +42,11 @@ export const createStore = () => {
|
||||
);
|
||||
|
||||
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(
|
||||
combineReducers({
|
||||
ui: uiReducers,
|
||||
router: connectRouter(history),
|
||||
...uiModule.reducers,
|
||||
...snakeModule.reducers,
|
||||
...gameModule.reducers
|
||||
})
|
||||
@@ -45,6 +56,12 @@ export const createStore = () => {
|
||||
|
||||
store.history = history;
|
||||
|
||||
store.subscribe(
|
||||
throttle(() => {
|
||||
saveState(store.getState());
|
||||
}, 500)
|
||||
);
|
||||
|
||||
return store;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
const SHOW_SPINNER = "[UI] SHOW_SPINNER";
|
||||
const HIDE_SPINNER = "[UI] HIDE_SPINNER";
|
||||
const TOGGLE_SPINNER = "[UI] TOGGLE_SPINNER";
|
||||
import Module from "./Module.js";
|
||||
|
||||
export const showSpinner = () => ({ type: SHOW_SPINNER });
|
||||
export const hideSpinner = () => ({ type: HIDE_SPINNER });
|
||||
export const toggleSpinner = () => ({ type: TOGGLE_SPINNER });
|
||||
|
||||
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_NAME = "UI";
|
||||
export const initialState = {
|
||||
theme: "light"
|
||||
};
|
||||
|
||||
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;
|
||||
background: ${props => props.theme.body.background};
|
||||
color: ${props => props.theme.body.color};
|
||||
font-family: Raleway;
|
||||
font-size: 1rem;
|
||||
font-family: ${({ theme }) => theme.body.fontFamily};
|
||||
font-size: ${({ theme }) => theme.body.fontSize};
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1, h2 {
|
||||
|
||||
@@ -1,39 +1,13 @@
|
||||
const themes = {
|
||||
main: {
|
||||
body: {
|
||||
background: "papayawhip",
|
||||
color: "palevioletred"
|
||||
},
|
||||
header: {
|
||||
background: "#ffe6bd",
|
||||
color: "palevioletred",
|
||||
import defaultTheme from "./themes/default.js";
|
||||
import lightTheme from "./themes/light.js";
|
||||
import darkTheme from "./themes/dark.js";
|
||||
import darkOceanTheme from "./themes/darkOcean.js";
|
||||
|
||||
menuButton: {
|
||||
background: "#ffe6bd",
|
||||
color: "palevioletred",
|
||||
active: {
|
||||
background: "palevioletred",
|
||||
color: "#ffe6bd"
|
||||
}
|
||||
}
|
||||
},
|
||||
spinner: {
|
||||
shadow: "#eeeeee",
|
||||
highlight: "#db7093"
|
||||
},
|
||||
snake: {
|
||||
cell: {
|
||||
border: "0.0625rem solid papayawhip",
|
||||
size: 1.5
|
||||
},
|
||||
cellColors: {
|
||||
" ": "",
|
||||
a: "",
|
||||
ab: "#90dc90",
|
||||
s: "#f12f00"
|
||||
}
|
||||
}
|
||||
}
|
||||
const themes = {
|
||||
default: defaultTheme,
|
||||
light: lightTheme,
|
||||
dark: darkTheme,
|
||||
darkOcean: darkOceanTheme
|
||||
};
|
||||
|
||||
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