200 lines
5.9 KiB
TypeScript
200 lines
5.9 KiB
TypeScript
import {
|
|
createRef,
|
|
FC,
|
|
Fragment,
|
|
MouseEventHandler,
|
|
ReactNode,
|
|
RefObject,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import { css } from 'twin.macro';
|
|
import { useLedCount } from '../contents/led-count';
|
|
import { LedStripConfig } from '../models/led-strip-config';
|
|
import { PixelRgb } from '../models/pixel-rgb';
|
|
import { StyledPixel } from './styled-pixel';
|
|
import { logger } from './completed-led-strip';
|
|
|
|
interface DraggableStripProp {
|
|
config: LedStripConfig;
|
|
pixels: PixelRgb[];
|
|
index: number;
|
|
onConfigChange?: (config: LedStripConfig) => void;
|
|
onConfigFinish?: (config: LedStripConfig) => void;
|
|
}
|
|
export const DraggableStrip: FC<DraggableStripProp> = ({
|
|
config,
|
|
pixels,
|
|
index,
|
|
onConfigChange,
|
|
onConfigFinish,
|
|
}) => {
|
|
const { ledCount } = useLedCount();
|
|
|
|
const startXRef = useRef(0);
|
|
const currentXRef = useRef(0);
|
|
const configRef = useRef<LedStripConfig>();
|
|
const [availableConfig, setAvailableConfig] = useState<LedStripConfig>(config);
|
|
// const currentDiffRef = useRef(0);
|
|
const isDragRef = useRef(false);
|
|
const handleMouseMoveRef = useRef<(ev: MouseEvent) => void>();
|
|
const [boxTranslateX, setBoxTranslateX] = useState(0);
|
|
|
|
const ledItems = useMemo(() => {
|
|
const step = config.global_start_position - config.global_end_position < 0 ? 1 : -1;
|
|
return pixels.map((rgb, i) => (
|
|
<StyledPixel
|
|
key={i}
|
|
rgb={rgb}
|
|
css={css`
|
|
grid-column: ${(availableConfig.global_start_position ?? 0) + i * step + 1} /
|
|
span 1;
|
|
grid-row-start: ${index + 1};
|
|
pointer-events: none;
|
|
`}
|
|
/>
|
|
));
|
|
}, [pixels, availableConfig]);
|
|
|
|
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=" h-full w-full"
|
|
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],
|
|
);
|
|
|
|
// start and moving
|
|
const handleMouseDown: MouseEventHandler<HTMLDivElement> = useCallback(
|
|
(ev) => {
|
|
startXRef.current = ev.pageX;
|
|
ev.currentTarget.requestPointerLock();
|
|
isDragRef.current = true;
|
|
logger('handleMouseDown, config: %o', config);
|
|
|
|
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,
|
|
);
|
|
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;
|
|
setAvailableConfig(newValue);
|
|
logger('change config. old: %o, new: %o', config, newValue);
|
|
onConfigChange?.(newValue);
|
|
};
|
|
document.body.addEventListener('mousemove', handleMouseMoveRef.current);
|
|
},
|
|
[placeholderRefs, availableConfig, setAvailableConfig, config],
|
|
);
|
|
|
|
// move event.
|
|
useEffect(() => {
|
|
const handleMouseUp = (ev: MouseEvent) => {
|
|
if (configRef.current && isDragRef.current) {
|
|
onConfigFinish?.(configRef.current);
|
|
}
|
|
startXRef.current = 0;
|
|
isDragRef.current = false;
|
|
document.exitPointerLock();
|
|
if (handleMouseMoveRef.current) {
|
|
document.body.removeEventListener('mousemove', handleMouseMoveRef.current);
|
|
}
|
|
};
|
|
document.body.addEventListener('mouseup', handleMouseUp);
|
|
return () => {
|
|
document.body.removeEventListener('mouseup', handleMouseUp);
|
|
};
|
|
}, [onConfigFinish]);
|
|
// reset translateX when config updated.
|
|
useEffect(() => {
|
|
startXRef.current = currentXRef.current;
|
|
setAvailableConfig(config);
|
|
setBoxTranslateX(0);
|
|
logger('useEffect, config: %o', config);
|
|
}, [config]);
|
|
|
|
return (
|
|
<Fragment>
|
|
{placeholders}
|
|
{ledItems}
|
|
<div
|
|
tw="border border-gray-700 h-3 w-full rounded-full"
|
|
css={css`
|
|
grid-column: ${Math.min(
|
|
config?.global_start_position ?? 0,
|
|
config?.global_end_position ?? 0,
|
|
) + 1} / span
|
|
${Math.abs(config?.global_start_position - config?.global_end_position) + 1};
|
|
grid-row-start: ${index + 1};
|
|
cursor: ew-resize;
|
|
transform: translateX(${boxTranslateX}px);
|
|
`}
|
|
onMouseDown={handleMouseDown}
|
|
></div>
|
|
</Fragment>
|
|
);
|
|
};
|