Skip to content

Commit

Permalink
Many fixes to drawing to better support responsive layout
Browse files Browse the repository at this point in the history
- refactor drawing to account for layouts and drawing different things
  ( new hooks that wrap drawing, so drawing can be done from components )

- new elements for labels (new game, switch, score & version)

- a couple redux bugs / fixes... direction is now static for each type

- responsive viewport / no more css!

- update dependencies
  • Loading branch information
tswaters committed Jul 17, 2020
1 parent 3b701e2 commit 3670226
Show file tree
Hide file tree
Showing 25 changed files with 1,761 additions and 4,629 deletions.
5,203 changes: 1,102 additions & 4,101 deletions package-lock.json

Large diffs are not rendered by default.

35 changes: 15 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,33 @@
"_build": "webpack",
"build": "npm run _build -- --mode=production",
"format": "prettier --write **/*.*",
"lint": "npm run lint:ts && npm run lint:sass",
"lint:ts": "eslint .",
"lint:sass": "stylelint \"src/styles/*.scss\"",
"lint": "eslint .",
"start": "webpack-dev-server --mode=development --hot"
},
"author": "Tyler Waters <[email protected]>",
"license": "MIT",
"devDependencies": {
"@teamsupercell/typings-for-css-modules-loader": "^2.1.0",
"@typescript-eslint/eslint-plugin": "^3.0.2",
"@typescript-eslint/parser": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^3.6.1",
"@typescript-eslint/parser": "^3.6.1",
"awesome-typescript-loader": "^5.2.1",
"circular-dependency-plugin": "^5.2.0",
"css-loader": "^3.4.0",
"eslint": "^7.1.0",
"eslint": "^7.4.0",
"eslint-config-prettier": "^6.7.0",
"eslint-plugin-react": "^7.17.0",
"eslint-plugin-react-hooks": "^4.0.4",
"eslint-plugin-react": "^7.20.3",
"eslint-plugin-react-hooks": "^4.0.8",
"html-webpack-plugin": "^4.3.0",
"mini-css-extract-plugin": "^0.9.0",
"offline-plugin": "^5.0.7",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"prettier": "^2.0.5",
"style-loader": "^1.1.1",
"stylelint": "^13.5.0",
"stylelint-config-sass-guidelines": "^7.0.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.13.0",
"terser-webpack-plugin": "^3.0.2",
"typescript": "^3.7.4",
"terser-webpack-plugin": "^3.0.7",
"typescript": "^3.9.7",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.10"
},
"dependencies": {
"@types/classnames": "^2.2.9",
"@types/node": "^14.0.6",
"@types/react": "^16.9.17",
"@types/node": "^14.0.23",
"@types/react": "^16.9.43",
"@types/react-dom": "^16.9.4",
"@types/react-redux": "^7.1.5",
"@types/redux-logger": "^3.0.7",
Expand All @@ -70,6 +60,11 @@
"extends": [
"eslint:recommended"
],
"settings": {
"react": {
"version": "detect"
}
},
"env": {
"es6": true,
"browser": true
Expand Down
1 change: 1 addition & 0 deletions src/html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<title>Klondike</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="root"></div>
Expand Down
47 changes: 0 additions & 47 deletions src/styles/cards.css

This file was deleted.

19 changes: 0 additions & 19 deletions src/styles/cards.css.d.ts

This file was deleted.

36 changes: 2 additions & 34 deletions src/ts/components/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,19 @@ import * as React from 'react'
import { hot } from 'react-hot-loader/root'
import { useDispatch, useSelector } from 'react-redux'
import FireworksComponent from './Fireworks'
import { newGame, score, version, switchScoreType } from '../../styles/cards.css'
import { initialize } from '../redux/thunks'
import { getScore, getScoringType } from '../redux/selectors'
import { undo, redo } from '../redux/undoable'
import { ScoringType } from '../redux/game-state'
import GameCanvas from './GameCanvas'
import StackElement from './StackElement'

import { getDraws, getShowing, getAllStacks } from '../redux/selectors'
import TopBar from './TopBar'

const Container: React.FC = () => {
const dispatch = useDispatch()
const stacks = useSelector(getAllStacks)
const currentScore = useSelector(getScore)
const scoringType = useSelector(getScoringType)
const draws = useSelector(getDraws)
const showing = useSelector(getShowing)

const { otherGameType, switchLabel } = React.useMemo(() => {
const otherGameType = scoringType === ScoringType.vegas ? ScoringType.regular : ScoringType.vegas
const switchLabel = `Switch to ${ScoringType[otherGameType]}`
return { otherGameType, switchLabel }
}, [scoringType])

const handleNewGameClick = React.useCallback(() => {
dispatch(initialize())
}, [dispatch])

const handleSwitchGameTypeClick = React.useCallback(() => {
dispatch(initialize(otherGameType))
}, [dispatch, otherGameType])

const handleKeyDown = React.useCallback(
(e: KeyboardEvent) => {
if (e.keyCode !== 90) return
Expand All @@ -54,25 +35,12 @@ const Container: React.FC = () => {
return (
<div>
<FireworksComponent />
<div>
<button id="new-game" className={newGame} onClick={handleNewGameClick}>
{'New Game'}
</button>
<button id="change-type" className={switchScoreType} onClick={handleSwitchGameTypeClick}>
{' Switch to '}
{ScoringType[otherGameType]}
</button>
<label id="score" className={score}>
{'Score: '}
{currentScore}
</label>
</div>
<GameCanvas>
<TopBar />
{stacks.map((stack) => (
<StackElement key={`${stack.type}-${stack.index}`} stack={stack} showing={showing} draws={draws} />
))}
</GameCanvas>
<div className={version}>{process.env.version}</div>
</div>
)
}
Expand Down
154 changes: 44 additions & 110 deletions src/ts/components/GameCanvas.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,16 @@
import * as React from 'react'
import { Point, Drawable, DrawingContext } from '../drawing/Common'
import { Drawable, DrawingContext, Clickable, Handler } from '../drawing/Common'
import { ColorSchemeType, ColorScheme, colorSchemes } from '../drawing/ColorScheme'
import { initialize } from '../drawing/Common'
import { game } from '../../styles/cards.css'
import { useCanvasSize } from '../hooks/useCanvasSize'

interface Handler<T> {
(arg0: T, arg1: Point): void
}

interface Add<T> {
(
path: Path2D,
thing: T,
events: {
click?: Handler<T>
doubleClick?: Handler<T>
},
): void
}

type SizeDetails = {
canvasWidth: number
canvasHeight: number
cardWidth: number
cardHeight: number
gutterWidth: number
gutterHeight: number
}

export type GameContext<T extends Drawable> = {
export type GameContext = {
context: DrawingContext
add: Add<T>
add: (thing: Drawable, events: Clickable) => void
remove: (path: Path2D) => void
}

export const GameCtx = React.createContext<GameContext<Drawable> | null>(null)

const useCanvasSize = (canvas: HTMLCanvasElement | null) => {
const getSize = () => {
const gutterWidth = 20
const gutterHeight = 30
return {
canvasWidth: canvas?.width ?? 0,
canvasHeight: canvas?.height ?? 0,
cardWidth: Math.max(100, Math.floor((window.innerWidth - gutterWidth * 8) / 7)),
cardHeight: Math.max(200, Math.floor((window.innerHeight - gutterHeight * (19 + 3)) / 2)),
gutterWidth,
gutterHeight,
}
}
const [size, setSize] = React.useState<SizeDetails>(getSize())
React.useEffect(() => {
if (!canvas) return
const ctx = canvas.getContext('2d')
if (ctx == null) return
let tid: number
const handleSize = () => {
if (tid) clearTimeout(tid)
tid = window.setTimeout(() => {
setSize(getSize())
}, 300)
}
window.addEventListener('resize', handleSize)
return () => {
window.removeEventListener('resize', handleSize)
}
})
return size
}
export const GameCtx = React.createContext<GameContext | null>(null)

const intersect = (evt: React.MouseEvent<HTMLCanvasElement>, pointsRef: Map<Path2D, Drawable>) => {
const { nativeEvent: e } = evt
Expand All @@ -84,51 +27,36 @@ const intersect = (evt: React.MouseEvent<HTMLCanvasElement>, pointsRef: Map<Path

const GameCanvas: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const pointsRef = React.useRef<Map<Path2D, Drawable>>(new Map())
const clickHandlers = React.useRef<Map<Path2D, Handler<Drawable>>>(new Map())
const doubleClickHandlers = React.useRef<Map<Path2D, Handler<Drawable>>>(new Map())

const [colorScheme] = React.useState<ColorScheme>(colorSchemes[ColorSchemeType.light])
const [canvas, setContext] = React.useState<HTMLCanvasElement | null>(null)
const canvasSize = useCanvasSize(canvas)

const gameContext = React.useMemo(() => {
if (!canvas) return null
const ctx = canvas.getContext('2d')
if (!ctx) return null
return {
ctx,
canvasWidth: canvasSize.canvasWidth,
canvasHeight: canvasSize.canvasHeight,
cardWidth: canvasSize.cardWidth,
cardHeight: canvasSize.cardHeight,
gutterWidth: canvasSize.gutterWidth,
gutterHeight: canvasSize.gutterHeight,
colorScheme,
}
}, [canvas, canvasSize, colorScheme])

React.useLayoutEffect(() => {
if (gameContext) initialize(gameContext)
}, [gameContext])

const handleCanvasRef = React.useCallback((canvas: HTMLCanvasElement) => {
if (canvas) {
setContext(canvas)
}
}, [])

const add = React.useCallback<Add<Drawable>>((path, thing, events) => {
pointsRef.current.set(path, thing)
const { click, doubleClick } = events
if (click) clickHandlers.current.set(path, click)
if (doubleClick) doubleClickHandlers.current.set(path, doubleClick)
}, [])

const remove = React.useCallback((path: Path2D) => {
pointsRef.current.delete(path)
clickHandlers.current.delete(path)
doubleClickHandlers.current.delete(path)
}, [])
const clickHandlers = React.useRef<Map<Path2D, Handler>>(new Map())
const doubleClickHandlers = React.useRef<Map<Path2D, Handler>>(new Map())

const [colorScheme] = React.useState<ColorScheme>(colorSchemes[ColorSchemeType.dark])
const { ctx, width, height, handleCanvasRef } = useCanvasSize()

const context = React.useMemo<DrawingContext | null>(() => {
if (!width || !height || !ctx) return null
return { ctx, width, height, colorScheme }
}, [ctx, width, height, colorScheme])

React.useLayoutEffect(() => (context && initialize(context)) || void 0, [context])

const value = React.useMemo<GameContext | null>(
() =>
context && {
context,
add(thing, events) {
pointsRef.current.set(thing.path, thing)
if (events.onClick) clickHandlers.current.set(thing.path, events.onClick)
if (events.onDoubleClick) doubleClickHandlers.current.set(thing.path, events.onDoubleClick)
},
remove(path: Path2D) {
pointsRef.current.delete(path)
clickHandlers.current.delete(path)
doubleClickHandlers.current.delete(path)
},
},
[context],
)

const handleCanvasDoubleClick = React.useCallback((evt: React.MouseEvent<HTMLCanvasElement>) => {
const selection = intersect(evt, pointsRef.current)
Expand All @@ -148,12 +76,18 @@ const GameCanvas: React.FC<{ children: React.ReactNode }> = ({ children }) => {
}
}, [])

const value = React.useMemo(() => gameContext && { context: gameContext, add, remove }, [gameContext, add, remove])

return (
<>
<canvas
className={game}
id="canvas"
style={{
backgroundColor: colorScheme.background,
top: '0',
left: '0',
width: '100vw',
height: '100vh',
position: 'absolute',
}}
ref={handleCanvasRef}
width={window.innerWidth}
height={window.innerHeight}
Expand Down
Loading

0 comments on commit 3670226

Please sign in to comment.