Merge pull request #64 from toeverything/feat/frame-background-color

feat: support set frame background color
This commit is contained in:
wang xinglong 2022-08-04 12:08:28 +08:00 committed by GitHub
commit fbe9add924
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 182 additions and 86 deletions

View File

@ -11,6 +11,7 @@ import { StrokeLineStyleConfig } from './stroke-line-style-config';
import { Group, UnGroup } from './GroupOperation'; import { Group, UnGroup } from './GroupOperation';
import { DeleteShapes } from './DeleteOperation'; import { DeleteShapes } from './DeleteOperation';
import { Lock, Unlock } from './LockOperation'; import { Lock, Unlock } from './LockOperation';
import { FrameFillColorConfig } from './FrameFillColorConfig';
export const CommandPanel: FC<{ app: TldrawApp }> = ({ app }) => { export const CommandPanel: FC<{ app: TldrawApp }> = ({ app }) => {
const state = app.useStore(); const state = app.useStore();
@ -51,6 +52,13 @@ export const CommandPanel: FC<{ app: TldrawApp }> = ({ app }) => {
shapes={config.fill.selectedShapes} shapes={config.fill.selectedShapes}
/> />
) : null, ) : null,
frameFill: config.frameFill.selectedShapes.length ? (
<FrameFillColorConfig
key="fill"
app={app}
shapes={config.frameFill.selectedShapes}
/>
) : null,
font: config.font.selectedShapes.length ? ( font: config.font.selectedShapes.length ? (
<FontSizeConfig <FontSizeConfig
key="font" key="font"

View File

@ -0,0 +1,89 @@
import type { FC } from 'react';
import type { TldrawApp } from '@toeverything/components/board-state';
import type { TDShape } from '@toeverything/components/board-types';
import {
Popover,
Tooltip,
IconButton,
useTheme,
} from '@toeverything/components/ui';
import {
ShapeColorNoneIcon,
ShapeColorDuotoneIcon,
} from '@toeverything/components/icons';
import { countBy, maxBy } from '@toeverything/utils';
import { getShapeIds } from './utils';
import { Palette } from '../palette';
interface BorderColorConfigProps {
app: TldrawApp;
shapes: TDShape[];
}
type ColorType = 'none' | string;
const _colors: ColorType[] = [
'rgba(255, 133, 137, 0.5)',
'rgba(255, 159, 101, 0.5)',
'rgba(255, 251, 69, 0.5)',
'rgba(64, 255, 138, 0.5)',
'rgba(26, 252, 255, 0.5)',
'rgba(198, 156, 255, 0.5)',
'rgba(255, 143, 224, 0.5)',
'rgba(152, 172, 189, 0.5)',
'rgba(216, 226, 248, 0.5)',
];
const _getIconRenderColor = (shapes: TDShape[]) => {
const counted = countBy(shapes, shape => shape.style.fill);
const max = maxBy(Object.entries(counted), ([c, n]) => n);
return max[0];
};
export const FrameFillColorConfig: FC<BorderColorConfigProps> = ({
app,
shapes,
}) => {
const theme = useTheme();
const setFillColor = (color: ColorType) => {
app.style(
{ fill: color, isFilled: color !== 'none' },
getShapeIds(shapes)
);
};
const iconColor = _getIconRenderColor(shapes);
return (
<Popover
trigger="hover"
placement="bottom-start"
content={
<Palette
colors={_colors}
selected={iconColor}
onSelect={setFillColor}
/>
}
>
<Tooltip content="Frame Background Color" placement="top-start">
<IconButton>
{iconColor === 'none' ? (
<ShapeColorNoneIcon />
) : (
<ShapeColorDuotoneIcon
style={{
color: iconColor,
border:
iconColor === '#FFFFFF'
? `1px solid ${theme.affine.palette.tagHover}`
: 0,
borderRadius: '5px',
}}
/>
)}
</IconButton>
</Tooltip>
</Popover>
);
};

View File

@ -7,6 +7,7 @@ interface Config {
type: type:
| 'stroke' | 'stroke'
| 'fill' | 'fill'
| 'frameFill'
| 'font' | 'font'
| 'group' | 'group'
| 'ungroup' | 'ungroup'
@ -22,6 +23,10 @@ const _createInitConfig = (): Record<Config['type'], Config> => {
type: 'fill', type: 'fill',
selectedShapes: [], selectedShapes: [],
}, },
frameFill: {
type: 'frameFill',
selectedShapes: [],
},
stroke: { stroke: {
type: 'stroke', type: 'stroke',
selectedShapes: [], selectedShapes: [],
@ -64,6 +69,7 @@ const _isSupportStroke = (shape: TDShape): boolean => {
TDShapeType.Pencil, TDShapeType.Pencil,
TDShapeType.Laser, TDShapeType.Laser,
TDShapeType.Highlight, TDShapeType.Highlight,
TDShapeType.Draw,
TDShapeType.Arrow, TDShapeType.Arrow,
TDShapeType.Line, TDShapeType.Line,
].some(type => type === shape.type); ].some(type => type === shape.type);
@ -91,6 +97,10 @@ const _isSupportFont = (shape: TDShape): boolean => {
].some(type => type === shape.type); ].some(type => type === shape.type);
}; };
const _isSupportFrameFill = (shape: TDShape): boolean => {
return shape.type === TDShapeType.Frame;
};
export const useConfig = (app: TldrawApp): Record<Config['type'], Config> => { export const useConfig = (app: TldrawApp): Record<Config['type'], Config> => {
const state = app.useStore(); const state = app.useStore();
const selectedShapes = TLDR.get_selected_shapes(state, app.currentPageId); const selectedShapes = TLDR.get_selected_shapes(state, app.currentPageId);
@ -105,6 +115,9 @@ export const useConfig = (app: TldrawApp): Record<Config['type'], Config> => {
if (_isSupportFont(cur)) { if (_isSupportFont(cur)) {
acc.font.selectedShapes.push(cur); acc.font.selectedShapes.push(cur);
} }
if (_isSupportFrameFill(cur)) {
acc.frameFill.selectedShapes.push(cur);
}
return acc; return acc;
}, },
_createInitConfig() _createInitConfig()

View File

@ -1,12 +1,9 @@
import * as React from 'react'; /* eslint-disable no-restricted-syntax */
import { Utils, SVGContainer } from '@tldraw/core'; import { Utils, SVGContainer } from '@tldraw/core';
import { import {
FrameShape, FrameShape,
DashStyle,
TDShapeType, TDShapeType,
TDMeta, TDMeta,
GHOSTED_OPACITY,
LABEL_POINT,
} from '@toeverything/components/board-types'; } from '@toeverything/components/board-types';
import { TDShapeUtil } from '../TDShapeUtil'; import { TDShapeUtil } from '../TDShapeUtil';
import { import {
@ -14,14 +11,13 @@ import {
getShapeStyle, getShapeStyle,
getBoundsRectangle, getBoundsRectangle,
transformRectangle, transformRectangle,
getFontStyle,
transformSingleRectangle, transformSingleRectangle,
} from '../shared'; } from '../shared';
import { DrawFrame } from './components/draw-frame'; import { Frame } from './components/Frame';
import { styled } from '@toeverything/components/ui'; import { styled } from '@toeverything/components/ui';
type T = FrameShape; type T = FrameShape;
type E = HTMLDivElement; type E = SVGSVGElement;
export class FrameUtil extends TDShapeUtil<T, E> { export class FrameUtil extends TDShapeUtil<T, E> {
type = TDShapeType.Frame as const; type = TDShapeType.Frame as const;
@ -56,10 +52,7 @@ export class FrameUtil extends TDShapeUtil<T, E> {
( (
{ {
shape, shape,
isEditing,
isBinding,
isSelected, isSelected,
isGhost,
meta, meta,
bounds, bounds,
events, events,
@ -70,21 +63,20 @@ export class FrameUtil extends TDShapeUtil<T, E> {
) => { ) => {
const { id, size, style } = shape; const { id, size, style } = shape;
return ( return (
<FullWrapper ref={ref} {...events}> <SVGContainer
<SVGContainer ref={ref}
id={shape.id + '_svg'} {...events}
opacity={1} id={shape.id + '_svg'}
fill={'#fff'} opacity={1}
> >
<DrawFrame <Frame
id={id} id={id}
style={style} style={style}
size={size} size={size}
isSelected={isSelected} isSelected={isSelected}
isDarkMode={meta.isDarkMode} isDarkMode={meta.isDarkMode}
/> />
</SVGContainer> </SVGContainer>
</FullWrapper>
); );
} }
); );
@ -121,27 +113,9 @@ export class FrameUtil extends TDShapeUtil<T, E> {
override transform = transformRectangle; override transform = transformRectangle;
override transformSingle = transformSingleRectangle; override transformSingle = transformSingleRectangle;
override hitTestPoint = (shape: T, point: number[]): boolean => {
return false;
};
override hitTestLineSegment = (
shape: T,
A: number[],
B: number[]
): boolean => {
return false;
};
} }
const FullWrapper = styled('div')({ const FullWrapper = styled('div')({
width: '100%', width: '100%',
height: '100%', height: '100%',
'.tl-fill-hitarea': {
fill: '#F7F9FF',
},
'.tl-stroke-hitarea': {
fill: '#F7F9FF',
},
}); });

View File

@ -0,0 +1,54 @@
import * as React from 'react';
import { BINDING_DISTANCE } from '@toeverything/components/board-types';
import type { ShapeStyles } from '@toeverything/components/board-types';
import { getShapeStyle } from '../../shared';
interface RectangleSvgProps {
id: string;
style: ShapeStyles;
isSelected: boolean;
size: number[];
isDarkMode: boolean;
}
export const Frame = React.memo(function DashedRectangle({
id,
style,
size,
isSelected,
isDarkMode,
}: RectangleSvgProps) {
const { strokeWidth, fill } = getShapeStyle(style, isDarkMode);
const _fill = fill && fill !== 'none' ? fill : '#F7F9FF';
const sw = 1 + strokeWidth * 1.618;
const w = Math.max(0, size[0] - sw / 2);
const h = Math.max(0, size[1] - sw / 2);
return (
<>
<rect
className={
isSelected || style.isFilled
? 'tl-fill-hitarea'
: 'tl-stroke-hitarea'
}
x={sw / 2}
y={sw / 2}
width={w}
height={h}
strokeWidth={BINDING_DISTANCE}
/>
<rect
x={sw / 2}
y={sw / 2}
width={w}
height={h}
fill={_fill}
pointerEvents="none"
/>
</>
);
});

View File

@ -1,42 +0,0 @@
import * as React from 'react';
import { BINDING_DISTANCE } from '@toeverything/components/board-types';
import type { ShapeStyles } from '@toeverything/components/board-types';
import { getShapeStyle } from '../../shared';
interface RectangleSvgProps {
id: string;
style: ShapeStyles;
isSelected: boolean;
size: number[];
isDarkMode: boolean;
}
export const DrawFrame = React.memo(function DashedRectangle({
id,
style,
size,
isSelected,
isDarkMode,
}: RectangleSvgProps) {
const { stroke, strokeWidth, fill } = getShapeStyle(style, isDarkMode);
const sw = 1 + strokeWidth * 1.618;
const w = Math.max(0, size[0] - sw / 2);
const h = Math.max(0, size[1] - sw / 2);
return (
<rect
className={
isSelected || style.isFilled
? 'tl-fill-hitarea'
: 'tl-stroke-hitarea'
}
x={sw / 2}
y={sw / 2}
width={w}
height={h}
strokeWidth={BINDING_DISTANCE}
/>
);
});

View File

@ -1 +1 @@
export * from './frame-util'; export * from './FrameUtil';