From 5a7787c8d1fc83fee1fa469dddd3f8b89ef55718 Mon Sep 17 00:00:00 2001 From: WhiteMind Date: Sun, 18 Feb 2024 17:15:21 +0800 Subject: [PATCH 1/6] feat: the home page supports page navigation via the keyboard --- src/pages/home/index.page.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/pages/home/index.page.tsx b/src/pages/home/index.page.tsx index 6475327d..961077c6 100644 --- a/src/pages/home/index.page.tsx +++ b/src/pages/home/index.page.tsx @@ -2,7 +2,7 @@ import { FC, Fragment, PropsWithChildren, RefObject, useEffect, useMemo, useRef, import { GetServerSideProps, type NextPage } from 'next' import clsx from 'clsx' import { Swiper, SwiperSlide, SwiperSlideProps } from 'swiper/react' -import { Mousewheel } from 'swiper' +import { Keyboard, Mousewheel } from 'swiper' import { useTranslation } from 'next-i18next' import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { Portal, Transition } from '@headlessui/react' @@ -82,6 +82,7 @@ const Home: NextPage = () => { direction="vertical" slidesPerView="auto" autoHeight + modules={[Mousewheel, Keyboard]} mousewheel={{ // Supports operations from some Portal to elements outside the swiper container, such as SlideFooter. eventsTarget: 'body', @@ -90,7 +91,21 @@ const Home: NextPage = () => { thresholdDelta: 5, sensitivity: 0.5, }} - modules={[Mousewheel]} + keyboard={{ + enabled: true, + }} + onKeyPress={(swiper, keyCodeString) => { + // Here is a type error with swiper, which should actually be a number. So, let's handle this simply. + // This issue remains unresolved in swiper@11.0.6. + const keyCode = Number(keyCodeString) + const isHomeKeyPress = keyCode === 36 + const isEndKeyPress = keyCode === 35 + if (isHomeKeyPress) { + swiper.slideTo(0) + } else if (isEndKeyPress) { + swiper.slideTo(swiper.slides.length - 1) + } + }} // https://stackoverflow.com/questions/53367064/how-to-enable-select-text-in-swiper-js simulateTouch={false} onActiveIndexChange={swiper => setActiveIdx(swiper.activeIndex)} From 7433869084257419de857a90fb08a205a61d4827 Mon Sep 17 00:00:00 2001 From: WhiteMind Date: Sun, 18 Feb 2024 17:28:43 +0800 Subject: [PATCH 2/6] feat: change right-click to left-click for drawing --- public/locales/en/home.json | 2 +- public/locales/es/home.json | 2 +- public/locales/fr/home.json | 2 +- public/locales/pt/home.json | 2 +- public/locales/zh/home.json | 2 +- src/components/ConwayGameOfLife/useGameKeybindings.ts | 4 ++-- src/pages/home/SlideFooter/index.tsx | 2 +- src/pages/home/index.module.scss | 2 +- src/pages/home/index.page.tsx | 4 ++-- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/public/locales/en/home.json b/public/locales/en/home.json index 50b29074..d819f22a 100644 --- a/public/locales/en/home.json +++ b/public/locales/en/home.json @@ -46,5 +46,5 @@ "ckb_in_nervos_dao": "CKB in Nervos DAO", "loading": "loading" }, - "right_click_to_draw": "RIGHT CLICK TO DRAW" + "click_to_draw": "LEFT CLICK TO DRAW" } diff --git a/public/locales/es/home.json b/public/locales/es/home.json index 92b08818..42c51de3 100644 --- a/public/locales/es/home.json +++ b/public/locales/es/home.json @@ -46,5 +46,5 @@ "ckb_in_nervos_dao": "CKB en Nervos DAO", "loading": "cargando" }, - "right_click_to_draw": "CLIC DERECHO PARA DIBUJAR" + "click_to_draw": "CLIC IZQUIERDO PARA DIBUJAR" } diff --git a/public/locales/fr/home.json b/public/locales/fr/home.json index 034398c6..e139317e 100644 --- a/public/locales/fr/home.json +++ b/public/locales/fr/home.json @@ -46,5 +46,5 @@ "ckb_in_nervos_dao": "CKB dans le Nervos DAO", "loading": "en cours de chargement" }, - "right_click_to_draw": "CLIC DROIT POUR DESSINER" + "click_to_draw": "CLIC GAUCHE POUR DESSINER" } diff --git a/public/locales/pt/home.json b/public/locales/pt/home.json index 8c5ce99c..a2a97dfa 100644 --- a/public/locales/pt/home.json +++ b/public/locales/pt/home.json @@ -46,5 +46,5 @@ "ckb_in_nervos_dao": "CKB em Nervos DAO", "loading": "carregando" }, - "right_click_to_draw": "CLIQUE DIREITO PARA DESENHAR" + "click_to_draw": "CLIQUE ESQUERDO PARA DESENHAR" } diff --git a/public/locales/zh/home.json b/public/locales/zh/home.json index 3a8c6d28..d1a243a8 100644 --- a/public/locales/zh/home.json +++ b/public/locales/zh/home.json @@ -46,5 +46,5 @@ "ckb_in_nervos_dao": "Nervos DAO 中的 CKB", "loading": "正在加载" }, - "right_click_to_draw": "右击绘制" + "click_to_draw": "左击绘制" } diff --git a/src/components/ConwayGameOfLife/useGameKeybindings.ts b/src/components/ConwayGameOfLife/useGameKeybindings.ts index 81e0a23f..c1adca48 100644 --- a/src/components/ConwayGameOfLife/useGameKeybindings.ts +++ b/src/components/ConwayGameOfLife/useGameKeybindings.ts @@ -142,8 +142,8 @@ export function useGameMouseHandler( let prevRightClickEvent: MouseEvent | null = null const onMouseDown = (e: HTMLElementEventMap['mousedown']) => { - const isRightClick = e.button === 2 - if (!isAllowedGameControlEvent(e) || !isRightClick) return + const isLeftClick = e.button === 0 + if (!isAllowedGameControlEvent(e) || !isLeftClick) return prevRightClickEvent = e mouseControllerData.current = { affectedCellIndexes: [] } diff --git a/src/pages/home/SlideFooter/index.tsx b/src/pages/home/SlideFooter/index.tsx index 6e02a88d..85bec355 100644 --- a/src/pages/home/SlideFooter/index.tsx +++ b/src/pages/home/SlideFooter/index.tsx @@ -133,7 +133,7 @@ const InfoDialog: FC = () => { const mouseBindings = [ { key: 'Left button', bind: 'Move around' }, - { key: 'Right button', bind: 'Create / Delete cells' }, + { key: 'Left button', bind: 'Create / Delete cells' }, ] const keyboardBindings = [ diff --git a/src/pages/home/index.module.scss b/src/pages/home/index.module.scss index 96c8684d..7e51e9d5 100644 --- a/src/pages/home/index.module.scss +++ b/src/pages/home/index.module.scss @@ -95,7 +95,7 @@ $headerZ: 2; } } -.rightClickTip { +.clickDrawTip { $fontSize: 9px; $scale: $fontSize / 12px; diff --git a/src/pages/home/index.page.tsx b/src/pages/home/index.page.tsx index 961077c6..8ad54482 100644 --- a/src/pages/home/index.page.tsx +++ b/src/pages/home/index.page.tsx @@ -151,8 +151,8 @@ const Home: NextPage = () => { {!isMobile && isOnOperableArea && !hasDrawn && ( -
- + {t('right_click_to_draw')} +
+ + {t('click_to_draw')}
)} From a5c850979e59a4619ad936d70133218a44de82ab Mon Sep 17 00:00:00 2001 From: WhiteMind Date: Sun, 18 Feb 2024 19:02:12 +0800 Subject: [PATCH 3/6] feat: disable the ability to move the game by dragging --- src/components/ConwayGameOfLife/useGameKeybindings.ts | 10 +++++++--- src/pages/home/SlideFooter/index.tsx | 5 +---- src/pages/home/index.page.tsx | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/ConwayGameOfLife/useGameKeybindings.ts b/src/components/ConwayGameOfLife/useGameKeybindings.ts index c1adca48..7e26f22b 100644 --- a/src/components/ConwayGameOfLife/useGameKeybindings.ts +++ b/src/components/ConwayGameOfLife/useGameKeybindings.ts @@ -78,6 +78,7 @@ export function useGameMouseHandler( gameControllerRef?: RefObject, opts: { rootElement?: HTMLElement + drag?: boolean } = {}, ) { const mouseControllerData = useRef<{ @@ -97,6 +98,9 @@ export function useGameMouseHandler( // Avoid not being able to select the text of sub-level elements. preventDefault: 'never', }).draggable({ + enabled: opts.drag ?? true, + // Right mouse button. + mouseButtons: 2, // Default cursor is `move`, here empty string means no setting. cursorChecker: () => '', listeners: { @@ -142,9 +146,9 @@ export function useGameMouseHandler( let prevRightClickEvent: MouseEvent | null = null const onMouseDown = (e: HTMLElementEventMap['mousedown']) => { + prevRightClickEvent = e const isLeftClick = e.button === 0 if (!isAllowedGameControlEvent(e) || !isLeftClick) return - prevRightClickEvent = e mouseControllerData.current = { affectedCellIndexes: [] } setIsDrawing(true) @@ -193,7 +197,7 @@ export function useGameMouseHandler( prevRightClickEvent.target === e.target && prevRightClickEvent.clientX === e.clientX && prevRightClickEvent.clientY === e.clientY - if (!isTriggeredByPrevRightClick) return + if (isTriggeredByPrevRightClick) return e.preventDefault() } @@ -210,7 +214,7 @@ export function useGameMouseHandler( root.removeEventListener('mouseup', onMouseUp) root.removeEventListener('contextmenu', onContextMenu) } - }, [gameControllerRef, opts.rootElement]) + }, [gameControllerRef, opts.drag, opts.rootElement]) return { isOnOperableArea, isDrawing } } diff --git a/src/pages/home/SlideFooter/index.tsx b/src/pages/home/SlideFooter/index.tsx index 85bec355..62248979 100644 --- a/src/pages/home/SlideFooter/index.tsx +++ b/src/pages/home/SlideFooter/index.tsx @@ -131,10 +131,7 @@ const LiveMetrics: FC = () => { const InfoDialog: FC = () => { const [isOpen, setIsOpen] = useState(false) - const mouseBindings = [ - { key: 'Left button', bind: 'Move around' }, - { key: 'Left button', bind: 'Create / Delete cells' }, - ] + const mouseBindings = [{ key: 'Left button', bind: 'Create / Delete cells' }] const keyboardBindings = [ { key: 'Arrow keys', bind: 'Move around' }, diff --git a/src/pages/home/index.page.tsx b/src/pages/home/index.page.tsx index 8ad54482..0c71088f 100644 --- a/src/pages/home/index.page.tsx +++ b/src/pages/home/index.page.tsx @@ -45,7 +45,7 @@ const Home: NextPage = () => { const mousePos = useMouse() - const { isOnOperableArea, isDrawing } = useGameMouseHandler(controllerRef) + const { isOnOperableArea, isDrawing } = useGameMouseHandler(controllerRef, { drag: false }) const onKeyDown = useGameKeyboardHandler(controllerRef, e => e.target === ref.current) // Default focus on body, auto-focus this to respond to keyboard events. From 163fc7034971a5b4e7207033a37f615d3885279a Mon Sep 17 00:00:00 2001 From: WhiteMind Date: Sun, 18 Feb 2024 19:08:42 +0800 Subject: [PATCH 4/6] feat: disable the automatic timed on/off for game --- src/components/ConwayGameOfLife/index.tsx | 483 +++++++++++----------- src/pages/home/SlideFooter/index.tsx | 34 +- src/pages/home/index.page.tsx | 6 +- 3 files changed, 256 insertions(+), 267 deletions(-) diff --git a/src/components/ConwayGameOfLife/index.tsx b/src/components/ConwayGameOfLife/index.tsx index b50f121b..75b02846 100644 --- a/src/components/ConwayGameOfLife/index.tsx +++ b/src/components/ConwayGameOfLife/index.tsx @@ -22,272 +22,273 @@ import styles from './index.module.scss' export type { GameController, GameState } from './utils' export * from './useGameKeybindings' -export const ConwayGameOfLife = forwardRef }>( - function ConwayGameOfLife(props, ref) { - const isMobile = useIsMobile(true) - const canvasRef = useRef(null) +export const ConwayGameOfLife = forwardRef< + GameController, + { initializationIndicatorRef: RefObject; defaultRunning?: boolean } +>(function ConwayGameOfLife(props, ref) { + const isMobile = useIsMobile(true) + const canvasRef = useRef(null) - const [gameStateHistory, setGameStateHistory] = useState([]) - const [gameState, _setGameState] = useState({ cellLifeStates: {} }) - const setGameState: Dispatch> = useCallback(value => { - _setGameState(oldValue => { - const newValue = typeof value === 'function' ? value(oldValue) : value - setGameStateHistory(history => [...history, oldValue]) - return newValue - }) - }, []) - const rewind = useCallback(() => { - const lastIdx = gameStateHistory.length - 1 - const last = gameStateHistory[lastIdx] - if (last == null) return - _setGameState(last) - setGameStateHistory(gameStateHistory.slice(0, lastIdx)) - }, [gameStateHistory]) + const [gameStateHistory, setGameStateHistory] = useState([]) + const [gameState, _setGameState] = useState({ cellLifeStates: {} }) + const setGameState: Dispatch> = useCallback(value => { + _setGameState(oldValue => { + const newValue = typeof value === 'function' ? value(oldValue) : value + setGameStateHistory(history => [...history, oldValue]) + return newValue + }) + }, []) + const rewind = useCallback(() => { + const lastIdx = gameStateHistory.length - 1 + const last = gameStateHistory[lastIdx] + if (last == null) return + _setGameState(last) + setGameStateHistory(gameStateHistory.slice(0, lastIdx)) + }, [gameStateHistory]) - const cellSize = 6 - const cellGap = 2 - const pixelRatio = useDevicePixelRatio() + const cellSize = 6 + const cellGap = 2 + const pixelRatio = useDevicePixelRatio() - const [zoomLevel, setZoomLevel] = useState(isMobile ? -2 : 0) - const zoomFromLevel = useMemo( - () => (zoomLevel === 0 ? 1 : zoomLevel > 0 ? 1 + zoomLevel : 1 - Math.abs(zoomLevel) * 0.25), - [zoomLevel], - ) + const [zoomLevel, setZoomLevel] = useState(isMobile ? -2 : 0) + const zoomFromLevel = useMemo( + () => (zoomLevel === 0 ? 1 : zoomLevel > 0 ? 1 + zoomLevel : 1 - Math.abs(zoomLevel) * 0.25), + [zoomLevel], + ) - const numberOfCellsAllowedWithinRadius = useNumberOfCellsAllowedWithinRadius(cellSize, cellGap) - const sceneRect = useMemo(() => { - const radiusSize = numberOfCellsAllowedWithinRadius * (cellSize + cellGap) - return { - top: -radiusSize, - right: radiusSize, - bottom: radiusSize, - left: -radiusSize, - } - }, [numberOfCellsAllowedWithinRadius]) - const [viewportRect, setViewportRect] = useState({ top: 0, right: 0, bottom: 0, left: 0 }) - const { offset, setOffsetWithClamp, zoom, setZoom } = useViewport(sceneRect, viewportRect, zoomFromLevel) - useEffect(() => setZoom(zoomFromLevel), [setZoom, zoomFromLevel]) + const numberOfCellsAllowedWithinRadius = useNumberOfCellsAllowedWithinRadius(cellSize, cellGap) + const sceneRect = useMemo(() => { + const radiusSize = numberOfCellsAllowedWithinRadius * (cellSize + cellGap) + return { + top: -radiusSize, + right: radiusSize, + bottom: radiusSize, + left: -radiusSize, + } + }, [numberOfCellsAllowedWithinRadius]) + const [viewportRect, setViewportRect] = useState({ top: 0, right: 0, bottom: 0, left: 0 }) + const { offset, setOffsetWithClamp, zoom, setZoom } = useViewport(sceneRect, viewportRect, zoomFromLevel) + useEffect(() => setZoom(zoomFromLevel), [setZoom, zoomFromLevel]) - const drawCells = useCallback(() => { - const canvas = canvasRef.current - if (canvas == null) return - const ctx = canvas.getContext('2d') - if (ctx == null) return + const drawCells = useCallback(() => { + const canvas = canvasRef.current + if (canvas == null) return + const ctx = canvas.getContext('2d') + if (ctx == null) return - ctx.clearRect(0, 0, canvas.width, canvas.height) + ctx.clearRect(0, 0, canvas.width, canvas.height) - Object.entries(gameState.cellLifeStates).forEach(([row, colStates]) => { - Object.entries(colStates).forEach(([col, state]) => { - if (state === 0) return - ctx.fillRect( - (-offset.x + cellGap / 2 + parseInt(col) * (cellSize + cellGap)) * pixelRatio * zoom, - (-offset.y + cellGap / 2 + parseInt(row) * (cellSize + cellGap)) * pixelRatio * zoom, - cellSize * pixelRatio * zoom, - cellSize * pixelRatio * zoom, - ) - }) + Object.entries(gameState.cellLifeStates).forEach(([row, colStates]) => { + Object.entries(colStates).forEach(([col, state]) => { + if (state === 0) return + ctx.fillRect( + (-offset.x + cellGap / 2 + parseInt(col) * (cellSize + cellGap)) * pixelRatio * zoom, + (-offset.y + cellGap / 2 + parseInt(row) * (cellSize + cellGap)) * pixelRatio * zoom, + cellSize * pixelRatio * zoom, + cellSize * pixelRatio * zoom, + ) }) - }, [gameState.cellLifeStates, offset.x, offset.y, pixelRatio, zoom]) + }) + }, [gameState.cellLifeStates, offset.x, offset.y, pixelRatio, zoom]) + + useEffect(drawCells, [drawCells]) - useEffect(drawCells, [drawCells]) + useElementSize(canvasRef, () => { + const canvas = canvasRef.current + if (canvas == null) return + const rect = canvas.getBoundingClientRect() + setViewportRect(rect) + // https://stackoverflow.com/a/35244519 + canvas.width = Math.round(pixelRatio * rect.right) - Math.round(pixelRatio * rect.left) + canvas.height = Math.round(pixelRatio * rect.bottom) - Math.round(pixelRatio * rect.top) - useElementSize(canvasRef, () => { + drawCells() + }) + + const setOffsetToIndicator = useCallback( + (pattern: GOLPattern) => { + const indicator = props.initializationIndicatorRef.current const canvas = canvasRef.current - if (canvas == null) return - const rect = canvas.getBoundingClientRect() - setViewportRect(rect) - // https://stackoverflow.com/a/35244519 - canvas.width = Math.round(pixelRatio * rect.right) - Math.round(pixelRatio * rect.left) - canvas.height = Math.round(pixelRatio * rect.bottom) - Math.round(pixelRatio * rect.top) + if (indicator == null || canvas == null) return - drawCells() - }) + const patternRect = pattern.reduce( + (rect, point) => { + if (point.x < rect.left) rect.left = point.x + if (point.x > rect.right) rect.right = point.x + if (point.y < rect.top) rect.top = point.y + if (point.y > rect.bottom) rect.bottom = point.y + return rect + }, + { top: 0, right: 0, bottom: 0, left: 0 } as Rect, + ) + const patternWidth = patternRect.right * (cellSize + cellGap) + const patternHeight = patternRect.bottom * (cellSize + cellGap) - const setOffsetToIndicator = useCallback( - (pattern: GOLPattern) => { - const indicator = props.initializationIndicatorRef.current - const canvas = canvasRef.current - if (indicator == null || canvas == null) return + const canvasRect = canvas.getBoundingClientRect() + const indicatorRect = indicator.getBoundingClientRect() + const overlappingRect = { + left: clampNumber(indicatorRect.left, canvasRect.left, indicatorRect.right), + right: clampNumber(indicatorRect.right, canvasRect.left, indicatorRect.right), + top: clampNumber(indicatorRect.top, canvasRect.top, indicatorRect.bottom), + bottom: clampNumber(indicatorRect.bottom, canvasRect.top, indicatorRect.bottom), + } + const offsetOfOverlappingRect = { + left: overlappingRect.left - canvasRect.left, + right: overlappingRect.right - canvasRect.right, + top: overlappingRect.top - canvasRect.top, + bottom: overlappingRect.bottom - canvasRect.bottom, + } - const patternRect = pattern.reduce( - (rect, point) => { - if (point.x < rect.left) rect.left = point.x - if (point.x > rect.right) rect.right = point.x - if (point.y < rect.top) rect.top = point.y - if (point.y > rect.bottom) rect.bottom = point.y - return rect - }, - { top: 0, right: 0, bottom: 0, left: 0 } as Rect, - ) - const patternWidth = patternRect.right * (cellSize + cellGap) - const patternHeight = patternRect.bottom * (cellSize + cellGap) + const offset: Point = { x: 0, y: 0 } + if (offsetOfOverlappingRect.left != 0) offset.x = offsetOfOverlappingRect.left + else if (offsetOfOverlappingRect.right != 0) + offset.x = canvasRect.right - canvasRect.left - patternWidth - offsetOfOverlappingRect.right + if (offsetOfOverlappingRect.top != 0) offset.y = offsetOfOverlappingRect.top + else if (offsetOfOverlappingRect.bottom != 0) + offset.y = canvasRect.bottom - canvasRect.top - patternHeight - offsetOfOverlappingRect.bottom + setOffsetWithClamp({ x: -(offset.x / zoom), y: -(offset.y / zoom) }) + }, + [props.initializationIndicatorRef, setOffsetWithClamp, zoom], + ) - const canvasRect = canvas.getBoundingClientRect() - const indicatorRect = indicator.getBoundingClientRect() - const overlappingRect = { - left: clampNumber(indicatorRect.left, canvasRect.left, indicatorRect.right), - right: clampNumber(indicatorRect.right, canvasRect.left, indicatorRect.right), - top: clampNumber(indicatorRect.top, canvasRect.top, indicatorRect.bottom), - bottom: clampNumber(indicatorRect.bottom, canvasRect.top, indicatorRect.bottom), - } - const offsetOfOverlappingRect = { - left: overlappingRect.left - canvasRect.left, - right: overlappingRect.right - canvasRect.right, - top: overlappingRect.top - canvasRect.top, - bottom: overlappingRect.bottom - canvasRect.bottom, - } + const [speedLevel, setSpeedLevel] = useState(0) + const stepIntervalTime = useMemo( + () => (speedLevel === 0 ? 1 : speedLevel > 0 ? Math.pow(2, speedLevel) : 1 - Math.abs(speedLevel) * 0.25) * 100, + [speedLevel], + ) - const offset: Point = { x: 0, y: 0 } - if (offsetOfOverlappingRect.left != 0) offset.x = offsetOfOverlappingRect.left - else if (offsetOfOverlappingRect.right != 0) - offset.x = canvasRect.right - canvasRect.left - patternWidth - offsetOfOverlappingRect.right - if (offsetOfOverlappingRect.top != 0) offset.y = offsetOfOverlappingRect.top - else if (offsetOfOverlappingRect.bottom != 0) - offset.y = canvasRect.bottom - canvasRect.top - patternHeight - offsetOfOverlappingRect.bottom - setOffsetWithClamp({ x: -(offset.x / zoom), y: -(offset.y / zoom) }) - }, - [props.initializationIndicatorRef, setOffsetWithClamp, zoom], - ) + const [running, setRunning] = useState(props.defaultRunning ?? false) + useInterval( + () => { + if (!running) return + setGameState(state => stepGame(state, numberOfCellsAllowedWithinRadius)) + }, + stepIntervalTime, + [numberOfCellsAllowedWithinRadius, running], + ) - const [speedLevel, setSpeedLevel] = useState(0) - const stepIntervalTime = useMemo( - () => (speedLevel === 0 ? 1 : speedLevel > 0 ? Math.pow(2, speedLevel) : 1 - Math.abs(speedLevel) * 0.25) * 100, - [speedLevel], - ) + // eslint-disable-next-line react-hooks/exhaustive-deps + const paused$ = useMemo(() => new BehaviorSubject(!running), []) + useEffect(() => paused$.next(!running), [paused$, running]) - const [running, setRunning] = useState(false) - useInterval( - () => { - if (!running) return + useImperativeHandle( + ref, + () => ({ + step() { setGameState(state => stepGame(state, numberOfCellsAllowedWithinRadius)) }, - stepIntervalTime, - [numberOfCellsAllowedWithinRadius, running], - ) - - // eslint-disable-next-line react-hooks/exhaustive-deps - const paused$ = useMemo(() => new BehaviorSubject(!running), []) - useEffect(() => paused$.next(!running), [paused$, running]) - - useImperativeHandle( - ref, - () => ({ - step() { - setGameState(state => stepGame(state, numberOfCellsAllowedWithinRadius)) - }, - rewind, - play() { - setRunning(true) - }, - pause() { - setRunning(false) - }, - paused: !running, - paused$: paused$, - speedDown() { - setSpeedLevel(level => Math.min(level + 1, 3)) - }, - speedUp() { - setSpeedLevel(level => Math.max(level - 1, -3)) - }, - randomPattern() { - // TODO: Exclude the current pattern. - const pattern = sample(patterns) - if (pattern == null) return - setGameState(gameState => { - return { - ...gameState, - cellLifeStates: patternToCellLifeStates(pattern), - } - }) - }, - clear() { - setGameState(gameState => { - return { - ...gameState, - cellLifeStates: {}, - } - }) - }, - getCellLiving(row, col) { - return (gameState.cellLifeStates[row]?.[col] ?? 0) !== 0 - }, - setCellLiving(row, col, value) { - setGameState(state => ({ - ...state, - cellLifeStates: { - ...state.cellLifeStates, - [row]: { - ...state.cellLifeStates[row], - [col]: value ? 1 : 0, - }, - }, - })) - }, - - zoomIn() { - setZoomLevel(level => Math.min(level + 1, 3)) - }, - zoomOut() { - setZoomLevel(level => Math.max(level - 1, -3)) - }, - - addCameraOffset(x, y) { - setOffsetWithClamp(offset => ({ - x: offset.x - x / zoom, - y: offset.y - y / zoom, - })) - }, - getCellIndexFromExternalMouseControlEvent(e) { - if (e == null) return null - if (canvasRef.current == null) return null - const coordsInCanvas = mouseEventOffset(e, canvasRef.current) - const coordsInViewport = { - x: (coordsInCanvas.x * pixelRatio) / zoom, - y: (coordsInCanvas.y * pixelRatio) / zoom, - } - const coordsInScene = { - x: coordsInViewport.x + offset.x * pixelRatio, - y: coordsInViewport.y + offset.y * pixelRatio, + rewind, + play() { + setRunning(true) + }, + pause() { + setRunning(false) + }, + paused: !running, + paused$: paused$, + speedDown() { + setSpeedLevel(level => Math.min(level + 1, 3)) + }, + speedUp() { + setSpeedLevel(level => Math.max(level - 1, -3)) + }, + randomPattern() { + // TODO: Exclude the current pattern. + const pattern = sample(patterns) + if (pattern == null) return + setGameState(gameState => { + return { + ...gameState, + cellLifeStates: patternToCellLifeStates(pattern), } - const cellIndex = { - row: Math.floor(coordsInScene.y / ((cellSize + cellGap) * pixelRatio)), - col: Math.floor(coordsInScene.x / ((cellSize + cellGap) * pixelRatio)), + }) + }, + clear() { + setGameState(gameState => { + return { + ...gameState, + cellLifeStates: {}, } - return cellIndex - }, - }), - [ - gameState.cellLifeStates, - numberOfCellsAllowedWithinRadius, - offset.x, - offset.y, - paused$, - pixelRatio, - rewind, - running, - setGameState, - setOffsetWithClamp, - zoom, - ], - ) + }) + }, + getCellLiving(row, col) { + return (gameState.cellLifeStates[row]?.[col] ?? 0) !== 0 + }, + setCellLiving(row, col, value) { + setGameState(state => ({ + ...state, + cellLifeStates: { + ...state.cellLifeStates, + [row]: { + ...state.cellLifeStates[row], + [col]: value ? 1 : 0, + }, + }, + })) + }, - useEffect(() => { - const pattern = sample(patterns) - if (pattern == null) return - setGameState(gameState => { - return { - ...gameState, - cellLifeStates: patternToCellLifeStates(pattern), + zoomIn() { + setZoomLevel(level => Math.min(level + 1, 3)) + }, + zoomOut() { + setZoomLevel(level => Math.max(level - 1, -3)) + }, + + addCameraOffset(x, y) { + setOffsetWithClamp(offset => ({ + x: offset.x - x / zoom, + y: offset.y - y / zoom, + })) + }, + getCellIndexFromExternalMouseControlEvent(e) { + if (e == null) return null + if (canvasRef.current == null) return null + const coordsInCanvas = mouseEventOffset(e, canvasRef.current) + const coordsInViewport = { + x: (coordsInCanvas.x * pixelRatio) / zoom, + y: (coordsInCanvas.y * pixelRatio) / zoom, } - }) - setOffsetToIndicator(pattern) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + const coordsInScene = { + x: coordsInViewport.x + offset.x * pixelRatio, + y: coordsInViewport.y + offset.y * pixelRatio, + } + const cellIndex = { + row: Math.floor(coordsInScene.y / ((cellSize + cellGap) * pixelRatio)), + col: Math.floor(coordsInScene.x / ((cellSize + cellGap) * pixelRatio)), + } + return cellIndex + }, + }), + [ + gameState.cellLifeStates, + numberOfCellsAllowedWithinRadius, + offset.x, + offset.y, + paused$, + pixelRatio, + rewind, + running, + setGameState, + setOffsetWithClamp, + zoom, + ], + ) + + useEffect(() => { + const pattern = sample(patterns) + if (pattern == null) return + setGameState(gameState => { + return { + ...gameState, + cellLifeStates: patternToCellLifeStates(pattern), + } + }) + setOffsetToIndicator(pattern) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) - return - }, -) + return +}) function useNumberOfCellsAllowedWithinRadius(cellSize: number, cellGap: number) { return useMemo(() => { diff --git a/src/pages/home/SlideFooter/index.tsx b/src/pages/home/SlideFooter/index.tsx index 62248979..f9cf8b71 100644 --- a/src/pages/home/SlideFooter/index.tsx +++ b/src/pages/home/SlideFooter/index.tsx @@ -19,40 +19,24 @@ import PlusIcon from './plus.svg' import MinusIcon from './minus.svg' import RandomizeIcon from './randomize.svg' import InfoIcon from './info.svg' -import { useInterval } from '../../../hooks' export const SlideFooter: FC & { gameControllerRef: RefObject }> = props => { const { children, gameControllerRef, className, ...divProps } = props const ref = useRef(null) - const [autoMode, setAutoMode] = useState(true) const [paused] = useObservableState(() => gameControllerRef.current?.paused$ ?? of(true)) - const toggleRunning = useCallback( - (isByAuto?: boolean) => { - if (!isByAuto) setAutoMode(false) + const toggleRunning = useCallback(() => { + const ctl = gameControllerRef?.current + if (ctl == null) return - const ctl = gameControllerRef?.current - if (ctl == null) return - - if (ctl.paused) { - ctl.play() - } else { - ctl.pause() - } - }, - [gameControllerRef], - ) - - useInterval( - () => { - if (!autoMode) return - toggleRunning(true) - }, - 5e3, - [autoMode, toggleRunning], - ) + if (ctl.paused) { + ctl.play() + } else { + ctl.pause() + } + }, [gameControllerRef]) const onKeyDown = useGameKeyboardHandler(gameControllerRef, e => e.target === ref.current) diff --git a/src/pages/home/index.page.tsx b/src/pages/home/index.page.tsx index 0c71088f..80cc0ef6 100644 --- a/src/pages/home/index.page.tsx +++ b/src/pages/home/index.page.tsx @@ -146,7 +146,11 @@ const Home: NextPage = () => {
- +
From 9abe913156611957d9c4b6699826c3161d950b8d Mon Sep 17 00:00:00 2001 From: WhiteMind Date: Sun, 18 Feb 2024 19:13:20 +0800 Subject: [PATCH 5/6] feat: remove zomm buttons --- src/pages/home/SlideFooter/index.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/pages/home/SlideFooter/index.tsx b/src/pages/home/SlideFooter/index.tsx index f9cf8b71..1024f4f9 100644 --- a/src/pages/home/SlideFooter/index.tsx +++ b/src/pages/home/SlideFooter/index.tsx @@ -65,12 +65,6 @@ export const SlideFooter: FC & { gameControllerRef: RefObj gameControllerRef?.current?.clear()} /> - - gameControllerRef?.current?.zoomIn()} /> - - - gameControllerRef?.current?.zoomOut()} /> - gameControllerRef?.current?.randomPattern()} /> From 8589bcb39dd898d3df5efaea6b6a46c76e1ee384 Mon Sep 17 00:00:00 2001 From: WhiteMind Date: Sun, 18 Feb 2024 19:15:21 +0800 Subject: [PATCH 6/6] feat: disable the ability to move the game using the keyboard --- .../ConwayGameOfLife/useGameKeybindings.ts | 14 -------------- src/pages/home/SlideFooter/index.tsx | 1 - 2 files changed, 15 deletions(-) diff --git a/src/components/ConwayGameOfLife/useGameKeybindings.ts b/src/components/ConwayGameOfLife/useGameKeybindings.ts index 7e26f22b..58a0d40e 100644 --- a/src/components/ConwayGameOfLife/useGameKeybindings.ts +++ b/src/components/ConwayGameOfLife/useGameKeybindings.ts @@ -17,21 +17,7 @@ export function useGameKeyboardHandler( const ctl = controllerRef.current if (ctl == null) return - const stepDistance = 24 - switch (e.code) { - case 'ArrowUp': - ctl.addCameraOffset(0, -stepDistance) - break - case 'ArrowRight': - ctl.addCameraOffset(stepDistance, 0) - break - case 'ArrowDown': - ctl.addCameraOffset(0, stepDistance) - break - case 'ArrowLeft': - ctl.addCameraOffset(-stepDistance, 0) - break case 'Equal': ctl.zoomIn() break diff --git a/src/pages/home/SlideFooter/index.tsx b/src/pages/home/SlideFooter/index.tsx index 1024f4f9..4826fa02 100644 --- a/src/pages/home/SlideFooter/index.tsx +++ b/src/pages/home/SlideFooter/index.tsx @@ -112,7 +112,6 @@ const InfoDialog: FC = () => { const mouseBindings = [{ key: 'Left button', bind: 'Create / Delete cells' }] const keyboardBindings = [ - { key: 'Arrow keys', bind: 'Move around' }, { key: '+, -', bind: 'Zoom In and out' }, { key: 'Space', bind: 'One generation forward' }, { key: 'Tab', bind: 'Many generation forward' },