feat: 基本支持拖拽配置灯条。
This commit is contained in:
parent
a3e2b5a234
commit
6802dbb7c0
@ -2,12 +2,13 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import clsx from 'clsx';
|
|
||||||
import { Configurator } from './configurator/configurator';
|
import { Configurator } from './configurator/configurator';
|
||||||
import { ButtonSwitch } from './commons/components/button';
|
import { ButtonSwitch } from './commons/components/button';
|
||||||
|
|
||||||
type Mode = 'Flowing' | 'Follow' | null;
|
type Mode = 'Flowing' | 'Follow' | null;
|
||||||
|
|
||||||
|
localStorage.setItem('debug', '*');
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [screenshots, setScreenshots] = useState<string[]>([]);
|
const [screenshots, setScreenshots] = useState<string[]>([]);
|
||||||
const [ledStripColors, setLedStripColors] = useState<string[]>([]);
|
const [ledStripColors, setLedStripColors] = useState<string[]>([]);
|
||||||
|
@ -1,26 +1,43 @@
|
|||||||
import { isNil, splitEvery } from 'ramda';
|
import debug from 'debug';
|
||||||
import { FC, useMemo } from 'react';
|
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 tw, { css, styled } from 'twin.macro';
|
||||||
import { borders } from '../../constants/border';
|
import { Borders, borders } from '../../constants/border';
|
||||||
import { DisplayConfig } from '../models/display-config';
|
import { useLedCount } from '../contents/led-count';
|
||||||
|
import { DisplayConfig, LedStripConfigOfBorders } from '../models/display-config';
|
||||||
import { LedStripConfig } from '../models/led-strip-config';
|
import { LedStripConfig } from '../models/led-strip-config';
|
||||||
|
import { PixelRgb } from '../models/pixel-rgb';
|
||||||
import { ScreenshotDto } from '../models/screenshot.dto';
|
import { ScreenshotDto } from '../models/screenshot.dto';
|
||||||
import { LedStrip } from './led-strip';
|
|
||||||
import { StyledPixel } from './styled-pixel';
|
import { StyledPixel } from './styled-pixel';
|
||||||
|
|
||||||
|
const logger = debug('app:completed-led-strip');
|
||||||
|
|
||||||
interface CompletedLedStripProps {
|
interface CompletedLedStripProps {
|
||||||
screenshots: ScreenshotDto[];
|
screenshots: ScreenshotDto[];
|
||||||
onDisplayConfigChange?: (value: DisplayConfig) => void;
|
onDisplayConfigChange?: (value: DisplayConfig) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type BorderLedStrip = {
|
type BorderLedStrip = {
|
||||||
pixels: [number, number, number][];
|
pixels: PixelRgb[];
|
||||||
config: LedStripConfig | null;
|
config: LedStripConfig | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.section(
|
const StyledContainer = styled.section(
|
||||||
({ rows, columns }: { rows: number; columns: number }) => [
|
({ 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`
|
css`
|
||||||
grid-template-columns: repeat(${columns}, 1fr);
|
grid-template-columns: repeat(${columns}, 1fr);
|
||||||
grid-template-rows: auto repeat(${rows}, 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`,
|
tw`dark:bg-transparent shadow-xl border-gray-500 border rounded-full flex flex-wrap justify-around items-center mb-2`,
|
||||||
css`
|
css`
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
|
justify-self: stretch;
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -41,7 +59,7 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
|
|||||||
const borderLedStrips: BorderLedStrip[] = useMemo(() => {
|
const borderLedStrips: BorderLedStrip[] = useMemo(() => {
|
||||||
return screenshots.flatMap((ss) =>
|
return screenshots.flatMap((ss) =>
|
||||||
borders.map((b) => ({
|
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],
|
config: ss.config.led_strip_of_borders[b],
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
@ -51,9 +69,18 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
|
|||||||
[borderLedStrips],
|
[borderLedStrips],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { setLedCount } = useLedCount();
|
||||||
|
// setLedCount for context
|
||||||
|
useEffect(() => {
|
||||||
|
setLedCount(ledCount);
|
||||||
|
}, [ledCount, setLedCount]);
|
||||||
|
|
||||||
|
const [overrideBorderLedStrips, setOverrideBorderLedStrips] =
|
||||||
|
useState<BorderLedStrip[]>();
|
||||||
|
|
||||||
const completedPixels = useMemo(() => {
|
const completedPixels = useMemo(() => {
|
||||||
const completed: [number, number, number][] = new Array(ledCount).fill([0, 0, 0]);
|
const completed: PixelRgb[] = new Array(ledCount).fill([0, 0, 0]);
|
||||||
borderLedStrips.forEach(({ pixels, config }) => {
|
(overrideBorderLedStrips ?? borderLedStrips).forEach(({ pixels, config }) => {
|
||||||
if (isNil(config)) {
|
if (isNil(config)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -68,21 +95,45 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return completed.map((color) => <StyledPixel rgb={color} />);
|
return completed.map((color, i) => <StyledPixel rgb={color} key={i} />);
|
||||||
}, [ledCount, borderLedStrips]);
|
}, [ledCount, borderLedStrips, overrideBorderLedStrips]);
|
||||||
|
|
||||||
const strips = useMemo(() => {
|
const strips = useMemo(() => {
|
||||||
return borderLedStrips.map(({ config, pixels }, index) => (
|
return (overrideBorderLedStrips ?? borderLedStrips).map(({ config, pixels }, index) =>
|
||||||
<LedStrip
|
config ? (
|
||||||
|
<DraggableStrip
|
||||||
key={index}
|
key={index}
|
||||||
colors={Uint8Array.from(pixels.flat())}
|
{...{ config, pixels, index: index + 1 }}
|
||||||
config={config}
|
onConfigChange={(c) => {
|
||||||
css={css`
|
setOverrideBorderLedStrips(
|
||||||
grid-column-start: ${(config?.global_start_position ?? 0) + 1};
|
update(index, { config: c, pixels }, borderLedStrips),
|
||||||
grid-column-end: ${(config?.global_end_position ?? 0) + 1};
|
);
|
||||||
`}
|
}}
|
||||||
|
onConfigFinish={(c) => {
|
||||||
|
const indexOfDisplay = Math.round(index / borders.length);
|
||||||
|
const xLens = lensPath<LedStripConfigOfBorders, Borders>([
|
||||||
|
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);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
));
|
) : (
|
||||||
|
<div key={index} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [borderLedStrips, overrideBorderLedStrips]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOverrideBorderLedStrips(undefined);
|
||||||
}, [borderLedStrips]);
|
}, [borderLedStrips]);
|
||||||
return (
|
return (
|
||||||
<StyledContainer rows={screenshots.length * borders.length} columns={ledCount}>
|
<StyledContainer rows={screenshots.length * borders.length} columns={ledCount}>
|
||||||
@ -91,3 +142,173 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
|
|||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface DraggableStripProp {
|
||||||
|
config: LedStripConfig;
|
||||||
|
pixels: PixelRgb[];
|
||||||
|
index: number;
|
||||||
|
onConfigChange?: (config: LedStripConfig) => void;
|
||||||
|
onConfigFinish?: (config: LedStripConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DraggableStrip: FC<DraggableStripProp> = ({
|
||||||
|
config,
|
||||||
|
pixels,
|
||||||
|
index,
|
||||||
|
onConfigChange,
|
||||||
|
onConfigFinish,
|
||||||
|
}) => {
|
||||||
|
const ledItems = pixels.map((rgb, i) => (
|
||||||
|
<StyledPixel
|
||||||
|
key={i}
|
||||||
|
rgb={rgb}
|
||||||
|
css={css`
|
||||||
|
grid-column: ${(config?.global_start_position ?? 0) + i + 1} / span 1;
|
||||||
|
grid-row-start: ${index + 1};
|
||||||
|
pointer-events: none;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
const { ledCount } = useLedCount();
|
||||||
|
|
||||||
|
const startXRef = useRef(0);
|
||||||
|
const currentXRef = useRef(0);
|
||||||
|
const configRef = useRef<LedStripConfig>();
|
||||||
|
// 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<HTMLSpanElement>[]] =
|
||||||
|
useMemo(
|
||||||
|
() =>
|
||||||
|
new Array(ledCount)
|
||||||
|
.fill(undefined)
|
||||||
|
.map((_, i) => {
|
||||||
|
const ref = createRef<HTMLSpanElement>();
|
||||||
|
const n = (
|
||||||
|
<span
|
||||||
|
ref={ref}
|
||||||
|
key={i}
|
||||||
|
tw="opacity-30 bg-red-500 h-full w-full border border-yellow-400"
|
||||||
|
css={css`
|
||||||
|
grid-column-start: ${i + 1};
|
||||||
|
grid-row-start: ${index + 1};
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return [n, ref] as [ReactNode, RefObject<HTMLSpanElement>];
|
||||||
|
})
|
||||||
|
.reduce(
|
||||||
|
([nList, refList], [n, ref]) => [
|
||||||
|
[...nList, n],
|
||||||
|
[...refList, ref],
|
||||||
|
],
|
||||||
|
[[], []] as [ReactNode[], RefObject<HTMLSpanElement>[]],
|
||||||
|
),
|
||||||
|
[ledCount],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseDown: MouseEventHandler<HTMLDivElement> = 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 (
|
||||||
|
<Fragment>
|
||||||
|
{placeholders}
|
||||||
|
{ledItems}
|
||||||
|
<div
|
||||||
|
tw="border border-gray-700 h-3 w-full rounded-full"
|
||||||
|
css={css`
|
||||||
|
grid-column-start: ${(config?.global_start_position ?? 0) + 1 - currentDiff};
|
||||||
|
grid-column-end: ${(config?.global_end_position ?? 0) + 1 - currentDiff};
|
||||||
|
grid-row-start: ${index + 1};
|
||||||
|
cursor: ew-resize;
|
||||||
|
transform: translateX(${boxTranslateX}px);
|
||||||
|
`}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
></div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -11,6 +11,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { update } from 'ramda';
|
import { update } from 'ramda';
|
||||||
import { CompletedLedStrip } from './components/completed-led-strip';
|
import { CompletedLedStrip } from './components/completed-led-strip';
|
||||||
|
import { LedCountProvider } from './contents/led-count';
|
||||||
|
|
||||||
const getPickerConfig = () => invoke<PickerConfiguration>('get_picker_config');
|
const getPickerConfig = () => invoke<PickerConfiguration>('get_picker_config');
|
||||||
const getScreenshotOfDisplays = () =>
|
const getScreenshotOfDisplays = () =>
|
||||||
@ -31,7 +32,7 @@ const StyledConfiguratorContainer = styled.section(tw`flex flex-col items-stretc
|
|||||||
|
|
||||||
const StyledDisplayContainer = styled.section(tw`overflow-auto`);
|
const StyledDisplayContainer = styled.section(tw`overflow-auto`);
|
||||||
|
|
||||||
export const Configurator: FC = () => {
|
const ConfiguratorInner: FC = () => {
|
||||||
const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync(
|
const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync(
|
||||||
getPickerConfig,
|
getPickerConfig,
|
||||||
[],
|
[],
|
||||||
@ -96,7 +97,10 @@ export const Configurator: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledConfiguratorContainer>
|
<StyledConfiguratorContainer>
|
||||||
<CompletedLedStrip screenshots={screenshotOfDisplays} />
|
<CompletedLedStrip
|
||||||
|
screenshots={screenshotOfDisplays}
|
||||||
|
onDisplayConfigChange={onDisplayConfigChange}
|
||||||
|
/>
|
||||||
<StyledDisplayContainer>{displays}</StyledDisplayContainer>;
|
<StyledDisplayContainer>{displays}</StyledDisplayContainer>;
|
||||||
<Snackbar open={pendingGetLedColorsByConfig} autoHideDuration={3000}>
|
<Snackbar open={pendingGetLedColorsByConfig} autoHideDuration={3000}>
|
||||||
<Alert icon={<FontAwesomeIcon icon={faSpinner} />} sx={{ width: '100%' }}>
|
<Alert icon={<FontAwesomeIcon icon={faSpinner} />} sx={{ width: '100%' }}>
|
||||||
@ -106,3 +110,11 @@ export const Configurator: FC = () => {
|
|||||||
</StyledConfiguratorContainer>
|
</StyledConfiguratorContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Configurator = () => {
|
||||||
|
return (
|
||||||
|
<LedCountProvider>
|
||||||
|
<ConfiguratorInner />
|
||||||
|
</LedCountProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
25
src/configurator/contents/led-count.tsx
Normal file
25
src/configurator/contents/led-count.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
Dispatch,
|
||||||
|
FC,
|
||||||
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
interface LedCountContext {
|
||||||
|
ledCount: number;
|
||||||
|
setLedCount: Dispatch<SetStateAction<number>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Context = createContext<LedCountContext>(undefined as any);
|
||||||
|
|
||||||
|
export const LedCountProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const [ledCount, setLedCount] = useState(0);
|
||||||
|
return (
|
||||||
|
<Context.Provider value={{ ledCount, setLedCount }}>{children}</Context.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLedCount = () => useContext(Context);
|
1
src/configurator/models/pixel-rgb.ts
Normal file
1
src/configurator/models/pixel-rgb.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type PixelRgb = [number, number, number];
|
Loading…
Reference in New Issue
Block a user