feat: allows color selection for nodes

This commit is contained in:
Mark Mankarious
2023-07-21 22:26:08 +01:00
parent f93f901521
commit 39afd84553
13 changed files with 177 additions and 43 deletions

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Box } from '@mui/material';
import { ColorSwatch } from './ColorSwatch';
interface Props {
colors: string[];
onChange: (color: string) => void;
activeColor: string;
}
export const ColorSelector = ({ colors, onChange, activeColor }: Props) => (
<Box>
{colors.map((color) => (
<ColorSwatch
key={color}
hex={color}
onClick={() => onChange(color)}
isActive={activeColor === color}
/>
))}
</Box>
);

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { Box, Button } from '@mui/material';
export type Props = {
hex: string;
isActive?: boolean;
onClick: () => void;
};
export const ColorSwatch = ({ hex, onClick, isActive }: Props) => (
<Button
onClick={onClick}
variant="text"
size="small"
sx={{ width: 40, height: 40, minWidth: 'auto' }}
>
<Box>
<Box
sx={{
border: '1px solid',
borderColor: isActive ? 'grey.400' : 'grey.800',
bgcolor: hex,
width: 28,
height: 28,
trasformOrigin: 'center',
transform: `scale(${isActive ? 1.25 : 1})`,
borderRadius: '100%'
}}
/>
</Box>
</Button>
);

View File

@@ -44,6 +44,7 @@ export const NodeControls = ({ nodeId }: Props) => {
>
{tab === 0 && (
<NodeSettings
color={node.color}
label={node.label}
labelHeight={node.labelHeight}
onUpdate={onNodeUpdated}

View File

@@ -1,34 +1,53 @@
import React from 'react';
import { Slider } from '@mui/material';
import { Slider, useTheme } from '@mui/material';
import { Node } from 'src/stores/useSceneStore';
import { ColorSelector } from 'src/components/ColorSelector/ColorSelector';
import { MarkdownEditor } from '../../../MarkdownEditor/MarkdownEditor';
import { Section } from '../../components/Section';
interface Props {
label: string;
labelHeight: number;
onUpdate: (updates: Partial<Node>) => void;
color: string;
}
export const NodeSettings = ({ label, labelHeight, onUpdate }: Props) => (
<>
<Section title="Label">
<MarkdownEditor
value={label}
onChange={(text) => onUpdate({ label: text })}
/>
</Section>
<Section title="Label height">
<Slider
marks
step={20}
min={0}
max={200}
value={labelHeight}
onChange={(e, newHeight) =>
onUpdate({ labelHeight: newHeight as number })
}
/>
</Section>
</>
);
export const NodeSettings = ({
color,
label,
labelHeight,
onUpdate
}: Props) => {
const theme = useTheme();
return (
<>
<Section title="Label">
<MarkdownEditor
value={label}
onChange={(text) => onUpdate({ label: text })}
/>
</Section>
<Section title="Label height">
<Slider
marks
step={20}
min={0}
max={200}
value={labelHeight}
onChange={(e, newHeight) =>
onUpdate({ labelHeight: newHeight as number })
}
/>
</Section>
<Section title="Color">
<ColorSelector
activeColor={color}
colors={Object.values(theme.customVars.diagramPalette)}
onChange={(col) => onUpdate({ color: col })}
/>
</Section>
</>
);
};

View File

@@ -6,11 +6,17 @@ interface Props {
value: string;
onChange?: (value: string) => void;
readOnly?: boolean;
height?: number;
}
const tools = ['bold', 'italic', 'underline', 'strike', 'bullet', 'link'];
export const MarkdownEditor = ({ value, onChange, readOnly }: Props) => {
export const MarkdownEditor = ({
value,
onChange,
readOnly,
height = 120
}: Props) => {
const modules = useMemo(() => {
if (!readOnly)
return {
@@ -33,7 +39,7 @@ export const MarkdownEditor = ({ value, onChange, readOnly }: Props) => {
borderColor: 'grey.800',
borderTop: 'auto',
borderRadius: 1.5,
height: 200
height
},
'.ql-container.ql-snow': {
...(readOnly ? { border: 'none' } : {})

View File

@@ -43,15 +43,15 @@ export const Node = ({ node, parentContainer }: NodeProps) => {
updateHeight: updateLabelHeight,
setVisible: setLabelConnectorVisible
} = labelConnector;
const { init: initNodeTile } = nodeTile;
const { init: initNodeTile, updateColor } = nodeTile;
useEffect(() => {
const nodeIconContainer = initNodeIcon();
const labelConnectorContainer = initLabelConnector();
const nodeColorContainer = initNodeTile();
const nodeTileContainer = initNodeTile();
groupRef.current.removeChildren();
groupRef.current.addChild(nodeColorContainer);
groupRef.current.addChild(nodeTileContainer);
groupRef.current.addChild(labelConnectorContainer);
groupRef.current.addChild(nodeIconContainer);
groupRef.current.pivot = nodeIconContainer.bounds.bottomCenter;
@@ -112,6 +112,10 @@ export const Node = ({ node, parentContainer }: NodeProps) => {
updateLabelHeight(node.labelHeight);
}, [node.labelHeight, updateLabelHeight]);
useEffect(() => {
updateColor(node.color);
}, [node.color, updateColor]);
if (!node.label) return null;
return (

View File

@@ -16,7 +16,8 @@ export const NodeLabel = ({ label }: Props) => (
maxHeight: 150,
borderRadius: 2,
overflow: 'hidden',
p: 1.5
py: 1,
px: 1.5
}}
>
<MarkdownEditor readOnly value={label} />

View File

@@ -2,37 +2,47 @@ import { useCallback, useRef } from 'react';
import { Group, Shape } from 'paper';
import { TILE_SIZE, PIXEL_UNIT } from 'src/renderer/utils/constants';
import { applyProjectionMatrix } from 'src/renderer/utils/projection';
import { getColorVariant } from 'src/utils';
export const useNodeTile = () => {
const containerRef = useRef(new Group());
const shapeRef = useRef<paper.Shape.Rectangle>();
const updateColor = useCallback((color: string) => {
if (!shapeRef.current) return;
shapeRef.current.set({
fillColor: color,
strokeColor: getColorVariant(color, 'dark', 2)
});
}, []);
const init = useCallback(() => {
containerRef.current.removeChildren();
containerRef.current.set({ pivot: [0, 0] });
const rectangle = new Shape.Rectangle({
shapeRef.current = new Shape.Rectangle({
strokeCap: 'round',
fillColor: 'blue',
size: [TILE_SIZE, TILE_SIZE],
radius: PIXEL_UNIT * 8,
strokeWidth: 0,
strokeColor: 'transparent',
strokeWidth: 1,
pivot: [0, 0],
position: [0, 0],
dashArray: null
});
containerRef.current.addChild(rectangle);
containerRef.current.addChild(shapeRef.current);
applyProjectionMatrix(containerRef.current);
rectangle.position.set(0, 0);
rectangle.scaling.set(1.2);
rectangle.applyMatrix = true;
shapeRef.current.position.set(0, 0);
shapeRef.current.scaling.set(1.2);
shapeRef.current.applyMatrix = true;
return containerRef.current;
}, []);
return {
init
init,
updateColor
};
};

View File

@@ -19,6 +19,7 @@ export interface Node {
type: SceneItemTypeEnum.NODE;
id: string;
iconId: string;
color: string;
label: string;
labelHeight: number;
position: Coords;

View File

@@ -9,7 +9,7 @@ interface CustomThemeVars {
height: number;
};
diagramPalette: {
purple: string;
[key in string]: string;
};
}
@@ -23,7 +23,7 @@ declare module '@mui/material/styles' {
}
}
const customVars: CustomThemeVars = {
export const customVars: CustomThemeVars = {
appPadding: {
x: 40,
y: 40
@@ -32,7 +32,18 @@ const customVars: CustomThemeVars = {
height: 55
},
diagramPalette: {
purple: '#cabffa'
green: '#53b435',
blue: '#4a82f7',
teal: '#5a9f9f',
salmon: '#e08079',
orange: '#e58b48',
white: '#ffffff',
pink: '#e1034c',
purple: '#5155ad',
lightPurple: '#8441fc',
lightBlue: '#f4f6fe',
yellow: '#ffdc73',
red: '#d62727'
}
};

View File

@@ -1,4 +1,7 @@
import { customVars } from '../styles/theme';
export const NODE_DEFAULTS = {
label: '',
labelHeight: 100
labelHeight: 100,
color: customVars.diagramPalette.blue
};

View File

@@ -1,11 +1,12 @@
import gsap from 'gsap';
import { Coords } from 'src/utils/Coords';
import chroma from 'chroma-js';
import type { NodeInput } from 'src/validation/SceneSchema';
import { Node, SceneItemTypeEnum } from 'src/stores/useSceneStore';
import { NODE_DEFAULTS } from 'src/utils/defaults';
export const clamp = (num: number, min: number, max: number) =>
num <= min ? min : num >= max ? max : num;
num <= min ? min : num >= max ? max : num; // eslint-disable-line no-nested-ternary
export const nonZeroCoords = (coords: Coords) => {
// For some reason, gsap doesn't like to tween x and y both to 0, so we force 0 to be just above 0.
@@ -50,6 +51,7 @@ export const nodeInputToNode = (nodeInput: NodeInput): Node => {
id: nodeInput.id,
label: nodeInput.label ?? NODE_DEFAULTS.label,
labelHeight: nodeInput.labelHeight ?? NODE_DEFAULTS.labelHeight,
color: nodeInput.color ?? NODE_DEFAULTS.color,
iconId: nodeInput.iconId,
position: Coords.fromObject(nodeInput.position),
isSelected: false,
@@ -58,3 +60,24 @@ export const nodeInputToNode = (nodeInput: NodeInput): Node => {
return node;
};
export const getColorVariant = (
color: string,
variant: 'light' | 'dark',
grade?: number
) => {
switch (variant) {
case 'light':
return chroma(color)
.brighten(grade ?? 1)
.saturate(2)
.hex();
case 'dark':
return chroma(color)
.darken(grade ?? 1)
.saturate(2)
.hex();
default:
return color;
}
};

View File

@@ -11,6 +11,7 @@ export const nodeInput = z.object({
id: z.string(),
label: z.string().optional(),
labelHeight: z.number().optional(),
color: z.string().optional(),
iconId: z.string(),
position: z.object({
x: z.number(),