From 6802dbb7c05f0efd0351b43ecabc30dce0a88f11 Mon Sep 17 00:00:00 2001 From: Ivan Li Date: Mon, 16 Jan 2023 00:56:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9F=BA=E6=9C=AC=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8B=96=E6=8B=BD=E9=85=8D=E7=BD=AE=E7=81=AF=E6=9D=A1=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 3 +- .../components/completed-led-strip.tsx | 267 ++++++++++++++++-- src/configurator/configurator.tsx | 16 +- src/configurator/contents/led-count.tsx | 25 ++ src/configurator/models/pixel-rgb.ts | 1 + 5 files changed, 286 insertions(+), 26 deletions(-) create mode 100644 src/configurator/contents/led-count.tsx create mode 100644 src/configurator/models/pixel-rgb.ts diff --git a/src/App.tsx b/src/App.tsx index e916cf8..d688bd5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,12 +2,13 @@ import { useCallback, useEffect, useState } from 'react'; import tw from 'twin.macro'; import { invoke } from '@tauri-apps/api/tauri'; import './App.css'; -import clsx from 'clsx'; import { Configurator } from './configurator/configurator'; import { ButtonSwitch } from './commons/components/button'; type Mode = 'Flowing' | 'Follow' | null; +localStorage.setItem('debug', '*'); + function App() { const [screenshots, setScreenshots] = useState([]); const [ledStripColors, setLedStripColors] = useState([]); diff --git a/src/configurator/components/completed-led-strip.tsx b/src/configurator/components/completed-led-strip.tsx index 28f52a0..cc0f9a6 100644 --- a/src/configurator/components/completed-led-strip.tsx +++ b/src/configurator/components/completed-led-strip.tsx @@ -1,26 +1,43 @@ -import { isNil, splitEvery } from 'ramda'; -import { FC, useMemo } from 'react'; +import debug from 'debug'; +import { isNil, lensPath, set, splitEvery, update } from 'ramda'; +import { + createRef, + FC, + Fragment, + MouseEventHandler, + ReactEventHandler, + ReactNode, + RefObject, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import tw, { css, styled } from 'twin.macro'; -import { borders } from '../../constants/border'; -import { DisplayConfig } from '../models/display-config'; +import { Borders, borders } from '../../constants/border'; +import { useLedCount } from '../contents/led-count'; +import { DisplayConfig, LedStripConfigOfBorders } from '../models/display-config'; import { LedStripConfig } from '../models/led-strip-config'; +import { PixelRgb } from '../models/pixel-rgb'; import { ScreenshotDto } from '../models/screenshot.dto'; -import { LedStrip } from './led-strip'; import { StyledPixel } from './styled-pixel'; +const logger = debug('app:completed-led-strip'); + interface CompletedLedStripProps { screenshots: ScreenshotDto[]; onDisplayConfigChange?: (value: DisplayConfig) => void; } type BorderLedStrip = { - pixels: [number, number, number][]; + pixels: PixelRgb[]; config: LedStripConfig | null; }; const StyledContainer = styled.section( ({ rows, columns }: { rows: number; columns: number }) => [ - tw`grid m-4 pb-2`, + tw`grid m-4 pb-2 items-center justify-items-center select-none`, css` grid-template-columns: repeat(${columns}, 1fr); grid-template-rows: auto repeat(${rows}, 1fr); @@ -31,6 +48,7 @@ const StyledCompletedContainer = styled.section( tw`dark:bg-transparent shadow-xl border-gray-500 border rounded-full flex flex-wrap justify-around items-center mb-2`, css` grid-column: 1 / -1; + justify-self: stretch; `, ); @@ -41,7 +59,7 @@ export const CompletedLedStrip: FC = ({ const borderLedStrips: BorderLedStrip[] = useMemo(() => { return screenshots.flatMap((ss) => borders.map((b) => ({ - pixels: splitEvery(3, Array.from(ss.colors[b])) as [number, number, number][], + pixels: splitEvery(3, Array.from(ss.colors[b])) as PixelRgb[], config: ss.config.led_strip_of_borders[b], })), ); @@ -51,9 +69,18 @@ export const CompletedLedStrip: FC = ({ [borderLedStrips], ); + const { setLedCount } = useLedCount(); + // setLedCount for context + useEffect(() => { + setLedCount(ledCount); + }, [ledCount, setLedCount]); + + const [overrideBorderLedStrips, setOverrideBorderLedStrips] = + useState(); + const completedPixels = useMemo(() => { - const completed: [number, number, number][] = new Array(ledCount).fill([0, 0, 0]); - borderLedStrips.forEach(({ pixels, config }) => { + const completed: PixelRgb[] = new Array(ledCount).fill([0, 0, 0]); + (overrideBorderLedStrips ?? borderLedStrips).forEach(({ pixels, config }) => { if (isNil(config)) { return; } @@ -68,21 +95,45 @@ export const CompletedLedStrip: FC = ({ } }); - return completed.map((color) => ); - }, [ledCount, borderLedStrips]); + return completed.map((color, i) => ); + }, [ledCount, borderLedStrips, overrideBorderLedStrips]); const strips = useMemo(() => { - return borderLedStrips.map(({ config, pixels }, index) => ( - - )); + return (overrideBorderLedStrips ?? borderLedStrips).map(({ config, pixels }, index) => + config ? ( + { + setOverrideBorderLedStrips( + update(index, { config: c, pixels }, borderLedStrips), + ); + }} + onConfigFinish={(c) => { + const indexOfDisplay = Math.round(index / borders.length); + const xLens = lensPath([ + borders[index % borders.length], + ]); + const displayConfig: DisplayConfig = { + ...screenshots[indexOfDisplay].config, + led_strip_of_borders: set( + xLens, + c, + screenshots[indexOfDisplay].config.led_strip_of_borders, + ), + }; + logger('Change DisplayConfig. %o', displayConfig); + onDisplayConfigChange?.(displayConfig); + }} + /> + ) : ( +
+ ), + ); + }, [borderLedStrips, overrideBorderLedStrips]); + + useEffect(() => { + setOverrideBorderLedStrips(undefined); }, [borderLedStrips]); return ( @@ -91,3 +142,173 @@ export const CompletedLedStrip: FC = ({ ); }; + +interface DraggableStripProp { + config: LedStripConfig; + pixels: PixelRgb[]; + index: number; + onConfigChange?: (config: LedStripConfig) => void; + onConfigFinish?: (config: LedStripConfig) => void; +} + +const DraggableStrip: FC = ({ + config, + pixels, + index, + onConfigChange, + onConfigFinish, +}) => { + const ledItems = pixels.map((rgb, i) => ( + + )); + + const { ledCount } = useLedCount(); + + const startXRef = useRef(0); + const currentXRef = useRef(0); + const configRef = useRef(); + // const currentDiffRef = useRef(0); + const [currentDiff, setCurrentDiff] = useState(0); + const isDragRef = useRef(false); + const handleMouseMoveRef = useRef<(ev: MouseEvent) => void>(); + const [boxTranslateX, setBoxTranslateX] = useState(0); + + const [placeholders, placeholderRefs]: [ReactNode[], RefObject[]] = + useMemo( + () => + new Array(ledCount) + .fill(undefined) + .map((_, i) => { + const ref = createRef(); + const n = ( + + ); + return [n, ref] as [ReactNode, RefObject]; + }) + .reduce( + ([nList, refList], [n, ref]) => [ + [...nList, n], + [...refList, ref], + ], + [[], []] as [ReactNode[], RefObject[]], + ), + [ledCount], + ); + + const handleMouseDown: MouseEventHandler = useCallback( + (ev) => { + startXRef.current = ev.pageX; + ev.currentTarget.requestPointerLock(); + isDragRef.current = true; + + const placeholderPositions = placeholderRefs.map((it) => { + if (!it.current) { + return [0, 0]; + } + const viewportOffset = it.current.getBoundingClientRect(); + return [viewportOffset.left, viewportOffset.right] as [number, number]; + }); + + logger('placeholderPositions: %o', placeholderPositions); + + // set init position + const initPos = placeholderPositions.findIndex( + ([l, r]) => l <= ev.pageX && r >= ev.pageX, + ); + setCurrentDiff(initPos); + let prevMatch = 0; + + if (handleMouseMoveRef.current) { + document.body.removeEventListener('mousemove', handleMouseMoveRef.current); + } + handleMouseMoveRef.current = (ev) => { + if (!isDragRef.current) { + return; + } + currentXRef.current = ev.pageX; + setBoxTranslateX(currentXRef.current - startXRef.current); + const match = placeholderPositions.findIndex( + ([l, r]) => l <= currentXRef.current && r >= currentXRef.current, + ); + if (match === -1) { + return; + } + + if (match === prevMatch) { + return; + } + prevMatch = match; + + const diff = match - initPos; + const newValue: LedStripConfig = { + ...config, + global_start_position: config.global_start_position + diff, + global_end_position: config.global_end_position + diff, + }; + configRef.current = newValue; + logger('change config. new: $o', newValue); + onConfigChange?.(newValue); + }; + document.body.addEventListener('mousemove', handleMouseMoveRef.current); + }, + [placeholderRefs], + ); + + // move event. + useEffect(() => { + const handleMouseUp = (ev: MouseEvent) => { + startXRef.current = 0; + isDragRef.current = false; + if (configRef.current) { + onConfigFinish?.(configRef.current); + } + document.exitPointerLock(); + if (handleMouseMoveRef.current) { + document.body.removeEventListener('mousemove', handleMouseMoveRef.current); + } + }; + document.body.addEventListener('mouseup', handleMouseUp); + return () => { + document.body.removeEventListener('mouseup', handleMouseUp); + }; + }, []); + // reset translateX when config updated. + useEffect(() => { + startXRef.current = currentXRef.current; + setCurrentDiff(0); + }, [config]); + + return ( + + {placeholders} + {ledItems} +
+
+ ); +}; diff --git a/src/configurator/configurator.tsx b/src/configurator/configurator.tsx index ec5ebc2..882d274 100644 --- a/src/configurator/configurator.tsx +++ b/src/configurator/configurator.tsx @@ -11,6 +11,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { update } from 'ramda'; import { CompletedLedStrip } from './components/completed-led-strip'; +import { LedCountProvider } from './contents/led-count'; const getPickerConfig = () => invoke('get_picker_config'); const getScreenshotOfDisplays = () => @@ -31,7 +32,7 @@ const StyledConfiguratorContainer = styled.section(tw`flex flex-col items-stretc const StyledDisplayContainer = styled.section(tw`overflow-auto`); -export const Configurator: FC = () => { +const ConfiguratorInner: FC = () => { const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync( getPickerConfig, [], @@ -96,7 +97,10 @@ export const Configurator: FC = () => { return ( - + {displays}; } sx={{ width: '100%' }}> @@ -106,3 +110,11 @@ export const Configurator: FC = () => { ); }; + +export const Configurator = () => { + return ( + + + + ); +}; diff --git a/src/configurator/contents/led-count.tsx b/src/configurator/contents/led-count.tsx new file mode 100644 index 0000000..ec3ba4a --- /dev/null +++ b/src/configurator/contents/led-count.tsx @@ -0,0 +1,25 @@ +import { + createContext, + Dispatch, + FC, + ReactNode, + SetStateAction, + useContext, + useState, +} from 'react'; + +interface LedCountContext { + ledCount: number; + setLedCount: Dispatch>; +} + +const Context = createContext(undefined as any); + +export const LedCountProvider: FC<{ children: ReactNode }> = ({ children }) => { + const [ledCount, setLedCount] = useState(0); + return ( + {children} + ); +}; + +export const useLedCount = () => useContext(Context); diff --git a/src/configurator/models/pixel-rgb.ts b/src/configurator/models/pixel-rgb.ts new file mode 100644 index 0000000..2229ef0 --- /dev/null +++ b/src/configurator/models/pixel-rgb.ts @@ -0,0 +1 @@ +export type PixelRgb = [number, number, number]; \ No newline at end of file