Styles API follow-ups (#1636)
tldraw-zero themed follow-ups to the styles API added in #1580. - Removed style related helpers from `ShapeUtil` - `editor.css` no longer includes the tldraw default color palette. Instead, a global `DefaultColorPalette` is defined as part of the color style. If developers wish to cusomise the colors, they can mutate that global. - `ShapeUtil.toSvg` no longer takes font/color. Instead, it takes an "svg export context" that can be used to add `<defs>` to the exported SVG element. Converting e.g. fonts to inlined data urls is now the responsibility of the shapes that use them rather than the Editor. - `usePattern` is not longer a core part of the editor. Instead, `ShapeUtil` has a `getCanvasSvgDefs` method for returning react components representing anything a shape needs included in `<defs>` for the canvas. - The shape-specific cleanup logic in `setStyle` has been deleted. It turned out that none of that logic has been running anyway, and instead the relevant logic lives in shape `onBeforeChange` callbacks already. ### Change Type - [x] `minor` — New feature ### Test Plan - [x] Unit Tests - [x] End to end tests ### Release Notes -- --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
15ce98b277
commit
e8bc114bf3
47 changed files with 1126 additions and 873 deletions
|
@ -35,7 +35,9 @@ export async function cleanup({ page }: PlaywrightTestArgs) {
|
|||
export async function setupPage(page: PlaywrightTestArgs['page']) {
|
||||
await page.goto('http://localhost:5420/end-to-end')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
await page.evaluate(() => editor.setAnimationSpeed(0))
|
||||
await page.evaluate(() => {
|
||||
editor.setAnimationSpeed(0)
|
||||
})
|
||||
}
|
||||
|
||||
export async function setupPageWithShapes(page: PlaywrightTestArgs['page']) {
|
||||
|
@ -45,7 +47,9 @@ export async function setupPageWithShapes(page: PlaywrightTestArgs['page']) {
|
|||
await page.mouse.click(200, 250)
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.click(250, 300)
|
||||
await page.evaluate(() => editor.selectNone())
|
||||
await page.evaluate(() => {
|
||||
editor.selectNone()
|
||||
})
|
||||
}
|
||||
|
||||
export async function cleanupPage(page: PlaywrightTestArgs['page']) {
|
||||
|
|
|
@ -7,7 +7,6 @@ import { setupPage } from '../shared-e2e'
|
|||
let page: Page
|
||||
declare const editor: Editor
|
||||
|
||||
// this is currently skipped as we can't enforce it on CI. i'm going to enable it in a follow-up though!
|
||||
test.describe('Export snapshots', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@playwright/test": "^1.34.3",
|
||||
"@playwright/test": "^1.35.1",
|
||||
"@tldraw/assets": "workspace:*",
|
||||
"@tldraw/state": "workspace:*",
|
||||
"@tldraw/tldraw": "workspace:*",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
export default function Example() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
|
@ -12,7 +13,7 @@ function CustomShareZone() {
|
|||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--palette-light-blue)',
|
||||
backgroundColor: 'thistle',
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
minWidth: '80px',
|
||||
|
@ -28,7 +29,7 @@ function CustomTopZone() {
|
|||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
backgroundColor: 'var(--palette-light-green)',
|
||||
backgroundColor: 'dodgerblue',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
TLBaseShape,
|
||||
TLDefaultColorStyle,
|
||||
defineShape,
|
||||
getDefaultColorTheme,
|
||||
} from '@tldraw/tldraw'
|
||||
import { T } from '@tldraw/validate'
|
||||
|
||||
|
@ -47,19 +48,20 @@ export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
|
|||
|
||||
component(shape: CardShape) {
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
const theme = getDefaultColorTheme(this.editor)
|
||||
|
||||
return (
|
||||
<HTMLContainer
|
||||
id={shape.id}
|
||||
style={{
|
||||
border: `4px solid var(--palette-${shape.props.color})`,
|
||||
border: `4px solid ${theme[shape.props.color].solid}`,
|
||||
borderRadius: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'all',
|
||||
filter: this.filterStyleToCss(shape.props.filter),
|
||||
backgroundColor: `var(--palette-${shape.props.color}-semi)`,
|
||||
backgroundColor: theme[shape.props.color].semi,
|
||||
}}
|
||||
>
|
||||
🍇🫐🍏🍋🍊🍒 {bounds.w.toFixed()}x{bounds.h.toFixed()} 🍒🍊🍋🍏🫐🍇
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { resizeBox } from '@tldraw/editor/src/lib/editor/shapes/shared/resizeBox'
|
||||
import { Box2d, HTMLContainer, ShapeUtil, TLOnResizeHandler } from '@tldraw/tldraw'
|
||||
import {
|
||||
Box2d,
|
||||
HTMLContainer,
|
||||
ShapeUtil,
|
||||
TLOnResizeHandler,
|
||||
getDefaultColorTheme,
|
||||
} from '@tldraw/tldraw'
|
||||
import { ICardShape } from './card-shape-types'
|
||||
|
||||
// A utility class for the card shape. This is where you define
|
||||
|
@ -30,6 +36,7 @@ export class CardShapeUtil extends ShapeUtil<ICardShape> {
|
|||
// Render method — the React component that will be rendered for the shape
|
||||
component(shape: ICardShape) {
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
const theme = getDefaultColorTheme(this.editor)
|
||||
|
||||
return (
|
||||
<HTMLContainer
|
||||
|
@ -41,7 +48,7 @@ export class CardShapeUtil extends ShapeUtil<ICardShape> {
|
|||
justifyContent: 'center',
|
||||
pointerEvents: 'all',
|
||||
fontWeight: shape.props.weight,
|
||||
color: `var(--palette-${shape.props.color})`,
|
||||
color: theme[shape.props.color].solid,
|
||||
}}
|
||||
>
|
||||
{bounds.w.toFixed()}x{bounds.h.toFixed()}
|
||||
|
|
|
@ -50,7 +50,6 @@ import { TLBookmarkAsset } from '@tldraw/tlschema';
|
|||
import { TLBookmarkShape } from '@tldraw/tlschema';
|
||||
import { TLCamera } from '@tldraw/tlschema';
|
||||
import { TLCursor } from '@tldraw/tlschema';
|
||||
import { TLDefaultColorStyle } from '@tldraw/tlschema';
|
||||
import { TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema';
|
||||
import { TLDocument } from '@tldraw/tlschema';
|
||||
import { TLDrawShape } from '@tldraw/tlschema';
|
||||
|
@ -125,6 +124,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
// (undocumented)
|
||||
getBounds(shape: TLArrowShape): Box2d;
|
||||
// (undocumented)
|
||||
getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[];
|
||||
// (undocumented)
|
||||
getCenter(shape: TLArrowShape): Vec2d;
|
||||
// (undocumented)
|
||||
getDefaultProps(): TLArrowShape['props'];
|
||||
|
@ -167,7 +168,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
// (undocumented)
|
||||
snapPoints(_shape: TLArrowShape): Vec2d[];
|
||||
// (undocumented)
|
||||
toSvg(shape: TLArrowShape, font: string, colors: TLExportColors): SVGGElement;
|
||||
toSvg(shape: TLArrowShape, ctx: SvgExportContext): SVGGElement;
|
||||
// (undocumented)
|
||||
static type: "arrow";
|
||||
}
|
||||
|
@ -331,6 +332,8 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
|||
// (undocumented)
|
||||
getBounds(shape: TLDrawShape): Box2d;
|
||||
// (undocumented)
|
||||
getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[];
|
||||
// (undocumented)
|
||||
getCenter(shape: TLDrawShape): Vec2d;
|
||||
// (undocumented)
|
||||
getDefaultProps(): TLDrawShape['props'];
|
||||
|
@ -355,7 +358,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
|||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<TLDrawShape>;
|
||||
// (undocumented)
|
||||
toSvg(shape: TLDrawShape, _font: string | undefined, colors: TLExportColors): SVGGElement;
|
||||
toSvg(shape: TLDrawShape, ctx: SvgExportContext): SVGGElement;
|
||||
// (undocumented)
|
||||
static type: "draw";
|
||||
}
|
||||
|
@ -506,6 +509,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
getShapeById<T extends TLShape = TLShape>(id: TLParentId): T | undefined;
|
||||
getShapeIdsInPage(pageId: TLPageId): Set<TLShapeId>;
|
||||
getShapesAtPoint(point: VecLike): TLShape[];
|
||||
// (undocumented)
|
||||
getShapeStyleIfExists<T>(shape: TLShape, style: StyleProp<T>): T | undefined;
|
||||
getShapeUtil<C extends {
|
||||
new (...args: any[]): ShapeUtil<any>;
|
||||
type: string;
|
||||
|
@ -825,7 +830,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
// (undocumented)
|
||||
providesBackgroundForChildren(): boolean;
|
||||
// (undocumented)
|
||||
toSvg(shape: TLFrameShape, font: string, colors: TLExportColors): Promise<SVGElement> | SVGElement;
|
||||
toSvg(shape: TLFrameShape): Promise<SVGElement> | SVGElement;
|
||||
// (undocumented)
|
||||
static type: "frame";
|
||||
}
|
||||
|
@ -842,6 +847,8 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
// (undocumented)
|
||||
getBounds(shape: TLGeoShape): Box2d;
|
||||
// (undocumented)
|
||||
getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[];
|
||||
// (undocumented)
|
||||
getCenter(shape: TLGeoShape): Vec2d;
|
||||
// (undocumented)
|
||||
getDefaultProps(): TLGeoShape['props'];
|
||||
|
@ -946,7 +953,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<TLGeoShape>;
|
||||
// (undocumented)
|
||||
toSvg(shape: TLGeoShape, font: string, colors: TLExportColors): SVGElement;
|
||||
toSvg(shape: TLGeoShape, ctx: SvgExportContext): SVGElement;
|
||||
// (undocumented)
|
||||
static type: "geo";
|
||||
}
|
||||
|
@ -1093,7 +1100,7 @@ export function hardReset({ shouldReload }?: {
|
|||
export function hardResetEditor(): void;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const HASH_PATERN_ZOOM_NAMES: Record<string, string>;
|
||||
export const HASH_PATTERN_ZOOM_NAMES: Record<string, string>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const HighlightShape: TLShapeInfo<TLHighlightShape>;
|
||||
|
@ -1131,9 +1138,9 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
|||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<TLHighlightShape>;
|
||||
// (undocumented)
|
||||
toBackgroundSvg(shape: TLHighlightShape, font: string | undefined, colors: TLExportColors): SVGPathElement;
|
||||
toBackgroundSvg(shape: TLHighlightShape): SVGPathElement;
|
||||
// (undocumented)
|
||||
toSvg(shape: TLHighlightShape, _font: string | undefined, colors: TLExportColors): SVGPathElement;
|
||||
toSvg(shape: TLHighlightShape): SVGPathElement;
|
||||
// (undocumented)
|
||||
static type: "highlight";
|
||||
}
|
||||
|
@ -1231,7 +1238,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<TLLineShape>;
|
||||
// (undocumented)
|
||||
toSvg(shape: TLLineShape, _font: string, colors: TLExportColors): SVGGElement;
|
||||
toSvg(shape: TLLineShape): SVGGElement;
|
||||
// (undocumented)
|
||||
static type: "line";
|
||||
}
|
||||
|
@ -1733,7 +1740,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
// (undocumented)
|
||||
onEditEnd: TLOnEditEndHandler<TLNoteShape>;
|
||||
// (undocumented)
|
||||
toSvg(shape: TLNoteShape, font: string, colors: TLExportColors): SVGGElement;
|
||||
toSvg(shape: TLNoteShape, ctx: SvgExportContext): SVGGElement;
|
||||
// (undocumented)
|
||||
static type: "note";
|
||||
}
|
||||
|
@ -1857,15 +1864,12 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
// @internal (undocumented)
|
||||
expandSelectionOutlinePx(shape: Shape): number;
|
||||
abstract getBounds(shape: Shape): Box2d;
|
||||
getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[];
|
||||
getCenter(shape: Shape): Vec2d;
|
||||
abstract getDefaultProps(): Shape['props'];
|
||||
getHandles?(shape: Shape): TLHandle[];
|
||||
getOutline(shape: Shape): Vec2d[];
|
||||
getOutlineSegments(shape: Shape): Vec2d[][];
|
||||
// (undocumented)
|
||||
getStyleIfExists<T>(style: StyleProp<T>, shape: Shape | TLShapePartial<Shape>): T | undefined;
|
||||
// (undocumented)
|
||||
hasStyle(style: StyleProp<unknown>): boolean;
|
||||
hideResizeHandles: TLShapeUtilFlag<Shape>;
|
||||
hideRotateHandle: TLShapeUtilFlag<Shape>;
|
||||
hideSelectionBoundsBg: TLShapeUtilFlag<Shape>;
|
||||
|
@ -1875,8 +1879,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
abstract indicator(shape: Shape): any;
|
||||
isAspectRatioLocked: TLShapeUtilFlag<Shape>;
|
||||
isClosed: TLShapeUtilFlag<Shape>;
|
||||
// (undocumented)
|
||||
iterateStyles(shape: Shape | TLShapePartial<Shape>): Generator<[StyleProp<unknown>, unknown], void, unknown>;
|
||||
onBeforeCreate?: TLOnBeforeCreateHandler<Shape>;
|
||||
onBeforeUpdate?: TLOnBeforeUpdateHandler<Shape>;
|
||||
// @internal
|
||||
|
@ -1909,8 +1911,8 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
snapPoints(shape: Shape): Vec2d[];
|
||||
// (undocumented)
|
||||
readonly styleProps: ReadonlyMap<StyleProp<unknown>, string>;
|
||||
toBackgroundSvg?(shape: Shape, font: string | undefined, colors: TLExportColors): null | Promise<SVGElement> | SVGElement;
|
||||
toSvg?(shape: Shape, font: string | undefined, colors: TLExportColors): Promise<SVGElement> | SVGElement;
|
||||
toBackgroundSvg?(shape: Shape, ctx: SvgExportContext): null | Promise<SVGElement> | SVGElement;
|
||||
toSvg?(shape: Shape, ctx: SvgExportContext): Promise<SVGElement> | SVGElement;
|
||||
// (undocumented)
|
||||
readonly type: Shape['type'];
|
||||
static type: string;
|
||||
|
@ -2115,7 +2117,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<TLTextShape>;
|
||||
// (undocumented)
|
||||
toSvg(shape: TLTextShape, font: string | undefined, colors: TLExportColors): SVGGElement;
|
||||
toSvg(shape: TLTextShape, ctx: SvgExportContext): SVGGElement;
|
||||
// (undocumented)
|
||||
static type: "text";
|
||||
}
|
||||
|
|
|
@ -120,63 +120,6 @@
|
|||
--color-warn: #d10b0b;
|
||||
--color-text: #000000;
|
||||
--color-laser: #ff0000;
|
||||
--palette-black: #1d1d1d;
|
||||
--palette-blue: #4263eb;
|
||||
--palette-green: #099268;
|
||||
--palette-grey: #adb5bd;
|
||||
--palette-light-blue: #4dabf7;
|
||||
--palette-light-green: #40c057;
|
||||
--palette-light-red: #ff8787;
|
||||
--palette-light-violet: #e599f7;
|
||||
--palette-orange: #f76707;
|
||||
--palette-red: #e03131;
|
||||
--palette-violet: #ae3ec9;
|
||||
--palette-white: #ffffff;
|
||||
--palette-yellow: #ffc078;
|
||||
/* TODO: fill style colors should be generated at runtime (later task) */
|
||||
/* for fill style 'semi' */
|
||||
--palette-solid: #fcfffe;
|
||||
--palette-black-semi: #e8e8e8;
|
||||
--palette-blue-semi: #dce1f8;
|
||||
--palette-green-semi: #d3e9e3;
|
||||
--palette-grey-semi: #eceef0;
|
||||
--palette-light-blue-semi: #ddedfa;
|
||||
--palette-light-green-semi: #dbf0e0;
|
||||
--palette-light-red-semi: #f4dadb;
|
||||
--palette-light-violet-semi: #f5eafa;
|
||||
--palette-orange-semi: #f8e2d4;
|
||||
--palette-red-semi: #f4dadb;
|
||||
--palette-violet-semi: #ecdcf2;
|
||||
--palette-white-semi: #ffffff;
|
||||
--palette-yellow-semi: #f9f0e6;
|
||||
/* for fill style 'pattern' */
|
||||
--palette-black-pattern: #494949;
|
||||
--palette-blue-pattern: #6681ee;
|
||||
--palette-green-pattern: #39a785;
|
||||
--palette-grey-pattern: #bcc3c9;
|
||||
--palette-light-blue-pattern: #6fbbf8;
|
||||
--palette-light-green-pattern: #65cb78;
|
||||
--palette-light-red-pattern: #fe9e9e;
|
||||
--palette-light-violet-pattern: #e9acf8;
|
||||
--palette-orange-pattern: #f78438;
|
||||
--palette-red-pattern: #e55959;
|
||||
--palette-violet-pattern: #bd63d3;
|
||||
--palette-white-pattern: #ffffff;
|
||||
--palette-yellow-pattern: #fecb92;
|
||||
|
||||
/* for highlighter pen */
|
||||
--palette-black-highlight: #fddd00;
|
||||
--palette-grey-highlight: #cbe7f1;
|
||||
--palette-green-highlight: #00ffc8;
|
||||
--palette-light-green-highlight: #65f641;
|
||||
--palette-blue-highlight: #10acff;
|
||||
--palette-light-blue-highlight: #00f4ff;
|
||||
--palette-violet-highlight: #c77cff;
|
||||
--palette-light-violet-highlight: #ff88ff;
|
||||
--palette-red-highlight: #ff636e;
|
||||
--palette-light-red-highlight: #ff7fa3;
|
||||
--palette-orange-highlight: #ffa500;
|
||||
--palette-yellow-highlight: #fddd00;
|
||||
|
||||
--shadow-1: 0px 1px 2px rgba(0, 0, 0, 0.22), 0px 1px 3px rgba(0, 0, 0, 0.09);
|
||||
--shadow-2: 0px 0px 2px rgba(0, 0, 0, 0.12), 0px 2px 3px rgba(0, 0, 0, 0.24),
|
||||
|
@ -217,64 +160,6 @@
|
|||
--color-warn: #d10b0b;
|
||||
--color-text: #f8f9fa;
|
||||
--color-laser: #ff0000;
|
||||
--palette-black: #e1e1e1;
|
||||
--palette-blue: #4156be;
|
||||
--palette-green: #3b7b5e;
|
||||
--palette-grey: #93989f;
|
||||
--palette-light-blue: #588fc9;
|
||||
--palette-light-green: #599f57;
|
||||
--palette-light-red: #c67877;
|
||||
--palette-light-violet: #b583c9;
|
||||
--palette-orange: #bf612e;
|
||||
--palette-red: #aa3c37;
|
||||
--palette-violet: #873fa3;
|
||||
--palette-white: #1d1d1d;
|
||||
--palette-yellow: #cba371;
|
||||
/* TODO: fill style colors should be generated at runtime (later task) */
|
||||
/* for fill style 'semi' */
|
||||
--palette-solid: #28292e;
|
||||
--palette-black-semi: #2c3036;
|
||||
--palette-blue-semi: #262d40;
|
||||
--palette-green-semi: #253231;
|
||||
--palette-grey-semi: #33373c;
|
||||
--palette-light-blue-semi: #2a3642;
|
||||
--palette-light-green-semi: #2a3830;
|
||||
--palette-light-red-semi: #3b3235;
|
||||
--palette-light-violet-semi: #383442;
|
||||
--palette-orange-semi: #3a2e2a;
|
||||
--palette-red-semi: #36292b;
|
||||
--palette-violet-semi: #31293c;
|
||||
--palette-white-semi: #ffffff;
|
||||
--palette-yellow-semi: #3c3934;
|
||||
|
||||
/* for fill style 'pattern' */
|
||||
--palette-black-pattern: #989898;
|
||||
--palette-blue-pattern: #3a4b9e;
|
||||
--palette-green-pattern: #366a53;
|
||||
--palette-grey-pattern: #7c8187;
|
||||
--palette-light-blue-pattern: #4d7aa9;
|
||||
--palette-light-green-pattern: #4e874e;
|
||||
--palette-light-red-pattern: #a56767;
|
||||
--palette-light-violet-pattern: #9770a9;
|
||||
--palette-orange-pattern: #9f552d;
|
||||
--palette-red-pattern: #8f3734;
|
||||
--palette-violet-pattern: #763a8b;
|
||||
--palette-white-pattern: #ffffff;
|
||||
--palette-yellow-pattern: #fecb92;
|
||||
|
||||
/* for highlighter pen */
|
||||
--palette-black-highlight: #d2b700;
|
||||
--palette-grey-highlight: #9cb4cb;
|
||||
--palette-green-highlight: #009774;
|
||||
--palette-light-green-highlight: #00a000;
|
||||
--palette-blue-highlight: #0079d2;
|
||||
--palette-light-blue-highlight: #00bdc8;
|
||||
--palette-violet-highlight: #9e00ee;
|
||||
--palette-light-violet-highlight: #c400c7;
|
||||
--palette-red-highlight: #de002c;
|
||||
--palette-light-red-highlight: #db005b;
|
||||
--palette-orange-highlight: #d07a00;
|
||||
--palette-yellow-highlight: #d2b700;
|
||||
|
||||
--shadow-1: 0px 1px 2px #00000029, 0px 1px 3px #00000038,
|
||||
inset 0px 0px 0px 1px var(--color-panel-contrast);
|
||||
|
@ -284,41 +169,6 @@
|
|||
inset 0px 0px 0px 1px var(--color-panel-contrast);
|
||||
}
|
||||
|
||||
/** p3 colors */
|
||||
@media (color-gamut: p3) {
|
||||
.tl-theme__light:not(.tl-theme__force-sRGB) {
|
||||
/* for highlighter pen */
|
||||
--palette-black-highlight: color(display-p3 0.972 0.8705 0.05);
|
||||
--palette-grey-highlight: color(display-p3 0.8163 0.9023 0.9416);
|
||||
--palette-green-highlight: color(display-p3 0.2536 0.984 0.7981);
|
||||
--palette-light-green-highlight: color(display-p3 0.563 0.9495 0.3857);
|
||||
--palette-blue-highlight: color(display-p3 0.308 0.6632 0.9996);
|
||||
--palette-light-blue-highlight: color(display-p3 0.1512 0.9414 0.9996);
|
||||
--palette-violet-highlight: color(display-p3 0.7469 0.5089 0.9995);
|
||||
--palette-light-violet-highlight: color(display-p3 0.9676 0.5652 0.9999);
|
||||
--palette-red-highlight: color(display-p3 0.9992 0.4376 0.45);
|
||||
--palette-light-red-highlight: color(display-p3 0.9988 0.5301 0.6397);
|
||||
--palette-orange-highlight: color(display-p3 0.9988 0.6905 0.266);
|
||||
--palette-yellow-highlight: color(display-p3 0.972 0.8705 0.05);
|
||||
}
|
||||
|
||||
.tl-theme__dark:not(.tl-theme__force-sRGB) {
|
||||
/* for highlighter pen */
|
||||
--palette-black-highlight: color(display-p3 0.8078 0.7225 0.0312);
|
||||
--palette-grey-highlight: color(display-p3 0.6299 0.7012 0.7856);
|
||||
--palette-green-highlight: color(display-p3 0.0085 0.582 0.4604);
|
||||
--palette-light-green-highlight: color(display-p3 0.2711 0.6172 0.0195);
|
||||
--palette-blue-highlight: color(display-p3 0.0032 0.4655 0.7991);
|
||||
--palette-light-blue-highlight: color(display-p3 0.0023 0.7259 0.7735);
|
||||
--palette-violet-highlight: color(display-p3 0.5651 0.0079 0.8986);
|
||||
--palette-light-violet-highlight: color(display-p3 0.7024 0.0403 0.753);
|
||||
--palette-red-highlight: color(display-p3 0.7978 0.0509 0.2035);
|
||||
--palette-light-red-highlight: color(display-p3 0.7849 0.0585 0.3589);
|
||||
--palette-orange-highlight: color(display-p3 0.7699 0.4937 0.0085);
|
||||
--palette-yellow-highlight: color(display-p3 0.8078 0.7225 0.0312);
|
||||
}
|
||||
}
|
||||
|
||||
.tl-counter-scaled {
|
||||
transform: scale(var(--tl-scale));
|
||||
transform-origin: top left;
|
||||
|
@ -1454,24 +1304,9 @@ input,
|
|||
/* -------------------- FrameShape ------------------- */
|
||||
|
||||
.tl-frame__body {
|
||||
fill: var(--palette-solid);
|
||||
stroke: var(--color-text);
|
||||
stroke-width: calc(1px * var(--tl-scale));
|
||||
}
|
||||
|
||||
.tl-frame__background {
|
||||
border-style: solid;
|
||||
border-width: calc(1px * var(--tl-scale));
|
||||
border-color: currentColor;
|
||||
background-color: var(--palette-solid);
|
||||
border-radius: calc(var(--radius-1) * var(--tl-scale));
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tl-frame__hitarea {
|
||||
border-style: solid;
|
||||
border-width: calc(8px * var(--tl-scale));
|
||||
|
|
|
@ -6,8 +6,10 @@ global.FontFace = class FontFace {
|
|||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
document.fonts = {
|
||||
add: () => {},
|
||||
delete: () => {},
|
||||
forEach: () => {},
|
||||
[Symbol.iterator]: () => [][Symbol.iterator](),
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ export {
|
|||
GRID_INCREMENT,
|
||||
GRID_STEPS,
|
||||
HAND_TOOL_FRICTION,
|
||||
HASH_PATERN_ZOOM_NAMES,
|
||||
HASH_PATTERN_ZOOM_NAMES,
|
||||
MAJOR_NUDGE_FACTOR,
|
||||
MAX_ASSET_HEIGHT,
|
||||
MAX_ASSET_WIDTH,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Matrix2d, toDomPrecision } from '@tldraw/primitives'
|
||||
import { react, track, useQuickReactor, useValue } from '@tldraw/state'
|
||||
import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
||||
import { dedupe, modulate } from '@tldraw/utils'
|
||||
import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
|
||||
import React from 'react'
|
||||
import { useCanvasEvents } from '../hooks/useCanvasEvents'
|
||||
import { useCoarsePointer } from '../hooks/useCoarsePointer'
|
||||
|
@ -11,7 +11,6 @@ import { useEditorComponents } from '../hooks/useEditorComponents'
|
|||
import { useFixSafariDoubleTapZoomPencilEvents } from '../hooks/useFixSafariDoubleTapZoomPencilEvents'
|
||||
import { useGestureEvents } from '../hooks/useGestureEvents'
|
||||
import { useHandleEvents } from '../hooks/useHandleEvents'
|
||||
import { usePattern } from '../hooks/usePattern'
|
||||
import { useScreenBounds } from '../hooks/useScreenBounds'
|
||||
import { debugFlags } from '../utils/debug-flags'
|
||||
import { LiveCollaborators } from './LiveCollaborators'
|
||||
|
@ -60,32 +59,28 @@ export const Canvas = track(function Canvas() {
|
|||
[editor]
|
||||
)
|
||||
|
||||
const { context: patternContext, isReady: patternIsReady } = usePattern()
|
||||
|
||||
const events = useCanvasEvents()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (patternIsReady && editor.isSafari) {
|
||||
const htmlElm = rHtmlLayer.current
|
||||
|
||||
if (htmlElm) {
|
||||
// Wait for `patternContext` to be picked up
|
||||
requestAnimationFrame(() => {
|
||||
htmlElm.style.display = 'none'
|
||||
|
||||
// Wait for 'display = "none"' to take effect
|
||||
requestAnimationFrame(() => {
|
||||
htmlElm.style.display = ''
|
||||
})
|
||||
})
|
||||
const shapeSvgDefs = useValue(
|
||||
'shapeSvgDefs',
|
||||
() => {
|
||||
const shapeSvgDefsByKey = new Map<string, JSX.Element>()
|
||||
for (const util of objectMapValues(editor.shapeUtils)) {
|
||||
if (!util) return
|
||||
const defs = util.getCanvasSvgDefs()
|
||||
for (const { key, component: Component } of defs) {
|
||||
if (shapeSvgDefsByKey.has(key)) continue
|
||||
shapeSvgDefsByKey.set(key, <Component key={key} />)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [editor, patternIsReady])
|
||||
return [...shapeSvgDefsByKey.values()]
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
rCanvas.current?.focus()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div ref={rCanvas} draggable={false} className="tl-canvas" data-testid="canvas" {...events}>
|
||||
{Background && <Background />}
|
||||
|
@ -94,7 +89,7 @@ export const Canvas = track(function Canvas() {
|
|||
<div ref={rHtmlLayer} className="tl-html-layer" draggable={false}>
|
||||
<svg className="tl-svg-context">
|
||||
<defs>
|
||||
{patternContext}
|
||||
{shapeSvgDefs}
|
||||
{Cursor && <Cursor />}
|
||||
<CollaboratorHint />
|
||||
<ArrowheadDot />
|
||||
|
|
|
@ -69,7 +69,7 @@ export const DefaultErrorFallback: TLErrorFallbackComponent = ({ error, editor }
|
|||
|
||||
// if we can't find a theme class from the app or from a parent, we have
|
||||
// to fall back on using a media query:
|
||||
setIsDarkMode(window.matchMedia('(prefetl-color-scheme: dark)').matches)
|
||||
setIsDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
}, [isDarkModeFromApp])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -70,11 +70,11 @@ export const DRAG_DISTANCE = 4
|
|||
export const SVG_PADDING = 32
|
||||
|
||||
/** @internal */
|
||||
export const HASH_PATERN_ZOOM_NAMES: Record<string, string> = {}
|
||||
export const HASH_PATTERN_ZOOM_NAMES: Record<string, string> = {}
|
||||
|
||||
for (let zoom = 1; zoom <= Math.ceil(MAX_ZOOM); zoom++) {
|
||||
HASH_PATERN_ZOOM_NAMES[zoom + '_dark'] = `hash_pattern_zoom_${zoom}_dark`
|
||||
HASH_PATERN_ZOOM_NAMES[zoom + '_light'] = `hash_pattern_zoom_${zoom}_light`
|
||||
HASH_PATTERN_ZOOM_NAMES[zoom + '_dark'] = `hash_pattern_zoom_${zoom}_dark`
|
||||
HASH_PATTERN_ZOOM_NAMES[zoom + '_light'] = `hash_pattern_zoom_${zoom}_light`
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
|
|
@ -26,8 +26,6 @@ import { ComputedCache, RecordType } from '@tldraw/store'
|
|||
import {
|
||||
Box2dModel,
|
||||
CameraRecordType,
|
||||
DefaultColorStyle,
|
||||
DefaultFontStyle,
|
||||
InstancePageStateRecordType,
|
||||
PageRecordType,
|
||||
StyleProp,
|
||||
|
@ -60,6 +58,7 @@ import {
|
|||
TLVideoAsset,
|
||||
Vec2dModel,
|
||||
createShapeId,
|
||||
getDefaultColorTheme,
|
||||
getShapePropKeysByStyle,
|
||||
isPageId,
|
||||
isShape,
|
||||
|
@ -73,7 +72,6 @@ import {
|
|||
deepCopy,
|
||||
getOwnProperty,
|
||||
hasOwnProperty,
|
||||
objectMapFromEntries,
|
||||
partition,
|
||||
sortById,
|
||||
structuredClone,
|
||||
|
@ -108,7 +106,6 @@ import {
|
|||
SVG_PADDING,
|
||||
ZOOMS,
|
||||
} from '../constants'
|
||||
import { exportPatternSvgDefs } from '../hooks/usePattern'
|
||||
import { ReadonlySharedStyleMap, SharedStyle, SharedStyleMap } from '../utils/SharedStylesMap'
|
||||
import { WeakMapCache } from '../utils/WeakMapCache'
|
||||
import { dataUrlToFile } from '../utils/assets'
|
||||
|
@ -131,8 +128,7 @@ import { getArrowTerminalsInArrowSpace, getIsArrowStraight } from './shapes/arro
|
|||
import { getStraightArrowInfo } from './shapes/arrow/arrow/straight-arrow'
|
||||
import { FrameShapeUtil } from './shapes/frame/FrameShapeUtil'
|
||||
import { GroupShapeUtil } from './shapes/group/GroupShapeUtil'
|
||||
import { TLExportColors } from './shapes/shared/TLExportColors'
|
||||
import { TextShapeUtil } from './shapes/text/TextShapeUtil'
|
||||
import { SvgExportContext, SvgExportDef } from './shapes/shared/SvgExportContext'
|
||||
import { RootState } from './tools/RootState'
|
||||
import { StateNode, TLStateNodeConstructor } from './tools/StateNode'
|
||||
import { TLContent } from './types/clipboard-types'
|
||||
|
@ -7730,8 +7726,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
} else {
|
||||
const util = this.getShapeUtil(shape)
|
||||
for (const [style, value] of util.iterateStyles(shape)) {
|
||||
sharedStyleMap.applyValue(style, value)
|
||||
for (const [style, propKey] of util.styleProps) {
|
||||
sharedStyleMap.applyValue(style, getOwnProperty(shape.props, propKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7765,6 +7761,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
return value === undefined ? style.defaultValue : (value as T)
|
||||
}
|
||||
|
||||
getShapeStyleIfExists<T>(shape: TLShape, style: StyleProp<T>): T | undefined {
|
||||
const util = this.getShapeUtil(shape)
|
||||
const styleKey = util.styleProps.get(style)
|
||||
if (styleKey === undefined) return undefined
|
||||
return getOwnProperty(shape.props, styleKey) as T | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* A derived object containing either all current styles among the user's selected shapes, or
|
||||
* else the user's most recent style choices that correspond to the current active state (i.e.
|
||||
|
@ -7921,7 +7924,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
} = this
|
||||
|
||||
if (selectedIds.length > 0) {
|
||||
const updates: { originalShape: TLShape; updatePartial: TLShapePartial }[] = []
|
||||
const updates: {
|
||||
util: ShapeUtil
|
||||
originalShape: TLShape
|
||||
updatePartial: TLShapePartial
|
||||
}[] = []
|
||||
|
||||
// We can have many deep levels of grouped shape
|
||||
// Making a recursive function to look through all the levels
|
||||
|
@ -7935,15 +7942,17 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
} else {
|
||||
const util = this.getShapeUtil(shape)
|
||||
if (util.hasStyle(style)) {
|
||||
const stylePropKey = util.styleProps.get(style)
|
||||
if (stylePropKey) {
|
||||
const shapePartial: TLShapePartial = {
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
props: {},
|
||||
props: { [stylePropKey]: value },
|
||||
}
|
||||
updates.push({
|
||||
util,
|
||||
originalShape: shape,
|
||||
updatePartial: util.setStyleInPartial(style, shapePartial, value),
|
||||
updatePartial: shapePartial,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -7957,51 +7966,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
updates.map(({ updatePartial }) => updatePartial),
|
||||
ephemeral
|
||||
)
|
||||
|
||||
// TODO: find a way to sink this stuff into shape utils directly?
|
||||
const changes: TLShapePartial[] = []
|
||||
for (const { originalShape: originalShape } of updates) {
|
||||
const currentShape = this.getShapeById(originalShape.id)
|
||||
if (!currentShape) continue
|
||||
const boundsA = this.getBounds(originalShape)
|
||||
const boundsB = this.getBounds(currentShape)
|
||||
|
||||
const change: TLShapePartial = { id: originalShape.id, type: originalShape.type }
|
||||
|
||||
let didChange = false
|
||||
|
||||
if (boundsA.width !== boundsB.width) {
|
||||
didChange = true
|
||||
|
||||
if (this.isShapeOfType(originalShape, TextShapeUtil)) {
|
||||
switch (originalShape.props.align) {
|
||||
case 'middle': {
|
||||
change.x = currentShape.x + (boundsA.width - boundsB.width) / 2
|
||||
break
|
||||
}
|
||||
case 'end': {
|
||||
change.x = currentShape.x + boundsA.width - boundsB.width
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
change.x = currentShape.x + (boundsA.width - boundsB.width) / 2
|
||||
}
|
||||
}
|
||||
|
||||
if (boundsA.height !== boundsB.height) {
|
||||
didChange = true
|
||||
change.y = currentShape.y + (boundsA.height - boundsB.height) / 2
|
||||
}
|
||||
|
||||
if (didChange) {
|
||||
changes.push(change)
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.length) {
|
||||
this.updateShapes(changes, ephemeral)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8543,56 +8507,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
scale = 1,
|
||||
background = false,
|
||||
padding = SVG_PADDING,
|
||||
darkMode = this.isDarkMode,
|
||||
preserveAspectRatio = false,
|
||||
} = opts
|
||||
|
||||
const realContainerEl = this.getContainer()
|
||||
const realContainerStyle = getComputedStyle(realContainerEl)
|
||||
|
||||
// Get the styles from the container. We'll use these to pull out colors etc.
|
||||
// NOTE: We can force force a light theme here because we don't want export
|
||||
const fakeContainerEl = document.createElement('div')
|
||||
fakeContainerEl.className = `tl-container tl-theme__${
|
||||
darkMode ? 'dark' : 'light'
|
||||
} tl-theme__force-sRGB`
|
||||
document.body.appendChild(fakeContainerEl)
|
||||
|
||||
const containerStyle = getComputedStyle(fakeContainerEl)
|
||||
const fontsUsedInExport = new Map<string, string>()
|
||||
|
||||
const colors: TLExportColors = {
|
||||
fill: objectMapFromEntries(
|
||||
DefaultColorStyle.values.map((color) => [
|
||||
color,
|
||||
containerStyle.getPropertyValue(`--palette-${color}`),
|
||||
])
|
||||
),
|
||||
pattern: objectMapFromEntries(
|
||||
DefaultColorStyle.values.map((color) => [
|
||||
color,
|
||||
containerStyle.getPropertyValue(`--palette-${color}-pattern`),
|
||||
])
|
||||
),
|
||||
semi: objectMapFromEntries(
|
||||
DefaultColorStyle.values.map((color) => [
|
||||
color,
|
||||
containerStyle.getPropertyValue(`--palette-${color}-semi`),
|
||||
])
|
||||
),
|
||||
highlight: objectMapFromEntries(
|
||||
DefaultColorStyle.values.map((color) => [
|
||||
color,
|
||||
containerStyle.getPropertyValue(`--palette-${color}-highlight`),
|
||||
])
|
||||
),
|
||||
text: containerStyle.getPropertyValue(`--color-text`),
|
||||
background: containerStyle.getPropertyValue(`--color-background`),
|
||||
solid: containerStyle.getPropertyValue(`--palette-solid`),
|
||||
}
|
||||
|
||||
// Remove containerEl from DOM (temp DOM node)
|
||||
document.body.removeChild(fakeContainerEl)
|
||||
// todo: we shouldn't depend on the public theme here
|
||||
const theme = getDefaultColorTheme(this)
|
||||
|
||||
// ---Figure out which shapes we need to include
|
||||
const shapeIdsToInclude = this.getShapeAndDescendantIds(ids)
|
||||
|
@ -8646,29 +8565,43 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
if (background) {
|
||||
if (singleFrameShapeId) {
|
||||
svg.style.setProperty('background', colors.solid)
|
||||
svg.style.setProperty('background', theme.solid)
|
||||
} else {
|
||||
svg.style.setProperty('background-color', colors.background)
|
||||
svg.style.setProperty('background-color', theme.background)
|
||||
}
|
||||
} else {
|
||||
svg.style.setProperty('background-color', 'transparent')
|
||||
}
|
||||
|
||||
// Add the defs to the svg
|
||||
const defs = window.document.createElementNS('http://www.w3.org/2000/svg', 'defs')
|
||||
|
||||
for (const element of Array.from(exportPatternSvgDefs(colors.solid))) {
|
||||
defs.appendChild(element)
|
||||
}
|
||||
|
||||
try {
|
||||
document.body.focus?.() // weird but necessary
|
||||
} catch (e) {
|
||||
// not implemented
|
||||
}
|
||||
|
||||
// Add the defs to the svg
|
||||
const defs = window.document.createElementNS('http://www.w3.org/2000/svg', 'defs')
|
||||
svg.append(defs)
|
||||
|
||||
const exportDefPromisesById = new Map<string, Promise<void>>()
|
||||
const exportContext: SvgExportContext = {
|
||||
addExportDef: (def: SvgExportDef) => {
|
||||
if (exportDefPromisesById.has(def.key)) return
|
||||
const promise = (async () => {
|
||||
const elements = await def.getElement()
|
||||
if (!elements) return
|
||||
|
||||
const comment = document.createComment(`def: ${def.key}`)
|
||||
defs.appendChild(comment)
|
||||
|
||||
for (const element of Array.isArray(elements) ? elements : [elements]) {
|
||||
defs.appendChild(element)
|
||||
}
|
||||
})()
|
||||
exportDefPromisesById.set(def.key, promise)
|
||||
},
|
||||
}
|
||||
|
||||
const unorderedShapeElements = (
|
||||
await Promise.all(
|
||||
renderingShapes.map(async ({ id, opacity, index, backgroundIndex }) => {
|
||||
|
@ -8681,23 +8614,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
const util = this.getShapeUtil(shape)
|
||||
|
||||
let font: string | undefined
|
||||
// TODO: `Editor` shouldn't know about `DefaultFontStyle`. We need another way
|
||||
// for shapes to register fonts for export.
|
||||
const fontFromShape = util.getStyleIfExists(DefaultFontStyle, shape)
|
||||
if (fontFromShape) {
|
||||
if (fontsUsedInExport.has(fontFromShape)) {
|
||||
font = fontsUsedInExport.get(fontFromShape)!
|
||||
} else {
|
||||
// For some reason these styles aren't present in the fake element
|
||||
// so we need to get them from the real element
|
||||
font = realContainerStyle.getPropertyValue(`--tl-font-${fontFromShape}`)
|
||||
fontsUsedInExport.set(fontFromShape, font)
|
||||
}
|
||||
}
|
||||
|
||||
let shapeSvgElement = await util.toSvg?.(shape, font, colors)
|
||||
let backgroundSvgElement = await util.toBackgroundSvg?.(shape, font, colors)
|
||||
let shapeSvgElement = await util.toSvg?.(shape, exportContext)
|
||||
let backgroundSvgElement = await util.toBackgroundSvg?.(shape, exportContext)
|
||||
|
||||
// wrap the shapes in groups so we can apply properties without overwriting ones from the shape util
|
||||
if (shapeSvgElement) {
|
||||
|
@ -8717,8 +8635,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
const elm = window.document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
elm.setAttribute('width', bounds.width + '')
|
||||
elm.setAttribute('height', bounds.height + '')
|
||||
elm.setAttribute('fill', colors.solid)
|
||||
elm.setAttribute('stroke', colors.pattern.grey)
|
||||
elm.setAttribute('fill', theme.solid)
|
||||
elm.setAttribute('stroke', theme.grey.pattern)
|
||||
elm.setAttribute('stroke-width', '1')
|
||||
shapeSvgElement = elm
|
||||
}
|
||||
|
@ -8778,58 +8696,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
)
|
||||
).flat()
|
||||
|
||||
await Promise.all(exportDefPromisesById.values())
|
||||
|
||||
for (const { element } of unorderedShapeElements.sort((a, b) => a.zIndex - b.zIndex)) {
|
||||
svg.appendChild(element)
|
||||
}
|
||||
|
||||
// Add styles to the defs
|
||||
let styles = ``
|
||||
const style = window.document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
||||
|
||||
// Insert fonts into app
|
||||
const fontInstances: FontFace[] = []
|
||||
|
||||
if ('fonts' in document) {
|
||||
document.fonts.forEach((font) => fontInstances.push(font))
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
fontInstances.map(async (font) => {
|
||||
const fileReader = new FileReader()
|
||||
|
||||
let isUsed = false
|
||||
|
||||
fontsUsedInExport.forEach((fontName) => {
|
||||
if (fontName.includes(font.family)) {
|
||||
isUsed = true
|
||||
}
|
||||
})
|
||||
|
||||
if (!isUsed) return
|
||||
|
||||
const url = (font as any).$$_url
|
||||
|
||||
const fontFaceRule = (font as any).$$_fontface
|
||||
|
||||
if (url) {
|
||||
const fontFile = await (await fetch(url)).blob()
|
||||
|
||||
const base64Font = await new Promise<string>((resolve, reject) => {
|
||||
fileReader.onload = () => resolve(fileReader.result as string)
|
||||
fileReader.onerror = () => reject(fileReader.error)
|
||||
fileReader.readAsDataURL(fontFile)
|
||||
})
|
||||
|
||||
const newFontFaceRule = '\n' + fontFaceRule.replaceAll(url, base64Font)
|
||||
styles += newFontFaceRule
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
style.textContent = styles
|
||||
|
||||
defs.append(style)
|
||||
|
||||
return svg
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Box2d, linesIntersect, Vec2d, VecLike } from '@tldraw/primitives'
|
|||
import { StyleProp, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema'
|
||||
import type { Editor } from '../Editor'
|
||||
import { TLResizeHandle } from '../types/selection-types'
|
||||
import { TLExportColors } from './shared/TLExportColors'
|
||||
import { SvgExportContext } from './shared/SvgExportContext'
|
||||
|
||||
/** @public */
|
||||
export interface TLShapeUtilConstructor<
|
||||
|
@ -17,6 +17,12 @@ export interface TLShapeUtilConstructor<
|
|||
/** @public */
|
||||
export type TLShapeUtilFlag<T> = (shape: T) => boolean
|
||||
|
||||
/** @public */
|
||||
export interface TLShapeUtilCanvasSvgDef {
|
||||
key: string
|
||||
component: React.ComponentType
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||
constructor(
|
||||
|
@ -25,23 +31,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
public readonly styleProps: ReadonlyMap<StyleProp<unknown>, string>
|
||||
) {}
|
||||
|
||||
hasStyle(style: StyleProp<unknown>) {
|
||||
return this.styleProps.has(style)
|
||||
}
|
||||
|
||||
getStyleIfExists<T>(style: StyleProp<T>, shape: Shape | TLShapePartial<Shape>): T | undefined {
|
||||
const styleKey = this.styleProps.get(style)
|
||||
if (!styleKey) return undefined
|
||||
return (shape.props as any)[styleKey]
|
||||
}
|
||||
|
||||
*iterateStyles(shape: Shape | TLShapePartial<Shape>) {
|
||||
for (const [style, styleKey] of this.styleProps) {
|
||||
const value = (shape.props as any)[styleKey]
|
||||
yield [style, value] as [StyleProp<unknown>, unknown]
|
||||
}
|
||||
}
|
||||
|
||||
setStyleInPartial<T>(
|
||||
style: StyleProp<T>,
|
||||
shape: TLShapePartial<Shape>,
|
||||
|
@ -307,31 +296,21 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
* Get the shape as an SVG object.
|
||||
*
|
||||
* @param shape - The shape.
|
||||
* @param color - The shape's CSS color (actual).
|
||||
* @param font - The shape's CSS font (actual).
|
||||
* @param ctx - The export context for the SVG - used for adding e.g. \<def\>s
|
||||
* @returns An SVG element.
|
||||
* @public
|
||||
*/
|
||||
toSvg?(
|
||||
shape: Shape,
|
||||
font: string | undefined,
|
||||
colors: TLExportColors
|
||||
): SVGElement | Promise<SVGElement>
|
||||
toSvg?(shape: Shape, ctx: SvgExportContext): SVGElement | Promise<SVGElement>
|
||||
|
||||
/**
|
||||
* Get the shape's background layer as an SVG object.
|
||||
*
|
||||
* @param shape - The shape.
|
||||
* @param color - The shape's CSS color (actual).
|
||||
* @param font - The shape's CSS font (actual).
|
||||
* @param ctx - ctx - The export context for the SVG - used for adding e.g. \<def\>s
|
||||
* @returns An SVG element.
|
||||
* @public
|
||||
*/
|
||||
toBackgroundSvg?(
|
||||
shape: Shape,
|
||||
font: string | undefined,
|
||||
colors: TLExportColors
|
||||
): SVGElement | Promise<SVGElement> | null
|
||||
toBackgroundSvg?(shape: Shape, ctx: SvgExportContext): SVGElement | Promise<SVGElement> | null
|
||||
|
||||
/** @internal */
|
||||
expandSelectionOutlinePx(shape: Shape): number {
|
||||
|
@ -371,6 +350,18 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return elements to be added to the \<defs\> section of the canvases SVG context. This can be
|
||||
* used to define SVG content (e.g. patterns & masks) that can be referred to by ID from svg
|
||||
* elements returned by `component`.
|
||||
*
|
||||
* Each def should have a unique `key`. If multiple defs from different shapes all have the same
|
||||
* key, only one will be used.
|
||||
*/
|
||||
getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] {
|
||||
return []
|
||||
}
|
||||
|
||||
// Events
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,9 +13,12 @@ import {
|
|||
import { computed, EMPTY_ARRAY } from '@tldraw/state'
|
||||
import { ComputedCache } from '@tldraw/store'
|
||||
import {
|
||||
DefaultFontFamilies,
|
||||
getDefaultColorTheme,
|
||||
TLArrowShape,
|
||||
TLArrowShapeArrowheadStyle,
|
||||
TLDefaultColorStyle,
|
||||
TLDefaultColorTheme,
|
||||
TLDefaultFillStyle,
|
||||
TLHandle,
|
||||
TLShapeId,
|
||||
|
@ -31,6 +34,7 @@ import {
|
|||
TLOnHandleChangeHandler,
|
||||
TLOnResizeHandler,
|
||||
TLOnTranslateStartHandler,
|
||||
TLShapeUtilCanvasSvgDef,
|
||||
TLShapeUtilFlag,
|
||||
} from '../ShapeUtil'
|
||||
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
||||
|
@ -40,9 +44,14 @@ import {
|
|||
STROKE_SIZES,
|
||||
TEXT_PROPS,
|
||||
} from '../shared/default-shape-constants'
|
||||
import {
|
||||
getFillDefForCanvas,
|
||||
getFillDefForExport,
|
||||
getFontDefForExport,
|
||||
} from '../shared/defaultStyleDefs'
|
||||
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
|
||||
import { getShapeFillSvg, ShapeFill } from '../shared/ShapeFill'
|
||||
import { TLExportColors } from '../shared/TLExportColors'
|
||||
import { getShapeFillSvg, ShapeFill, useDefaultColorTheme } from '../shared/ShapeFill'
|
||||
import { SvgExportContext } from '../shared/SvgExportContext'
|
||||
import { ArrowInfo } from './arrow/arrow-types'
|
||||
import { getArrowheadPathForType } from './arrow/arrowheads'
|
||||
import {
|
||||
|
@ -561,6 +570,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
|
||||
component(shape: TLArrowShape) {
|
||||
// Not a class component, but eslint can't tell that :(
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const theme = useDefaultColorTheme()
|
||||
const onlySelectedShape = this.editor.onlySelectedShape
|
||||
const shouldDisplayHandles =
|
||||
this.editor.isInAny(
|
||||
|
@ -697,7 +708,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
)}
|
||||
<g
|
||||
fill="none"
|
||||
stroke={`var(--palette-${shape.props.color})`}
|
||||
stroke={theme[shape.props.color].solid}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
|
@ -921,8 +932,11 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
}
|
||||
}
|
||||
|
||||
toSvg(shape: TLArrowShape, font: string, colors: TLExportColors) {
|
||||
const color = colors.fill[shape.props.color]
|
||||
toSvg(shape: TLArrowShape, ctx: SvgExportContext) {
|
||||
const theme = getDefaultColorTheme(this.editor)
|
||||
ctx.addExportDef(getFillDefForExport(shape.props.fill, theme))
|
||||
|
||||
const color = theme[shape.props.color].solid
|
||||
|
||||
const info = this.getArrowInfo(shape)
|
||||
|
||||
|
@ -1026,7 +1040,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
shape.props.color,
|
||||
strokeWidth,
|
||||
shape.props.arrowheadStart === 'arrow' ? 'none' : shape.props.fill,
|
||||
colors
|
||||
theme
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -1038,17 +1052,19 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
shape.props.color,
|
||||
strokeWidth,
|
||||
shape.props.arrowheadEnd === 'arrow' ? 'none' : shape.props.fill,
|
||||
colors
|
||||
theme
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Text Label
|
||||
if (labelSize) {
|
||||
ctx.addExportDef(getFontDefForExport(shape.props.font))
|
||||
|
||||
const opts = {
|
||||
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
|
||||
lineHeight: TEXT_PROPS.lineHeight,
|
||||
fontFamily: font,
|
||||
fontFamily: DefaultFontFamilies[shape.props.font],
|
||||
padding: 0,
|
||||
textAlign: 'middle' as const,
|
||||
width: labelSize.w - 8,
|
||||
|
@ -1064,7 +1080,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
this.editor.textMeasure.measureTextSpans(shape.props.text, opts),
|
||||
opts
|
||||
)
|
||||
textElm.setAttribute('fill', colors.fill[shape.props.labelColor])
|
||||
textElm.setAttribute('fill', theme[shape.props.labelColor].solid)
|
||||
|
||||
const children = Array.from(textElm.children) as unknown as SVGTSpanElement[]
|
||||
|
||||
|
@ -1078,8 +1094,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
|
||||
const textBgEl = textElm.cloneNode(true) as SVGTextElement
|
||||
textBgEl.setAttribute('stroke-width', '2')
|
||||
textBgEl.setAttribute('fill', colors.background)
|
||||
textBgEl.setAttribute('stroke', colors.background)
|
||||
textBgEl.setAttribute('fill', theme.background)
|
||||
textBgEl.setAttribute('stroke', theme.background)
|
||||
|
||||
g.appendChild(textBgEl)
|
||||
g.appendChild(textElm)
|
||||
|
@ -1087,6 +1103,10 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
|
||||
return g
|
||||
}
|
||||
|
||||
getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] {
|
||||
return [getFillDefForCanvas()]
|
||||
}
|
||||
}
|
||||
|
||||
function getArrowheadSvgMask(d: string, arrowhead: TLArrowShapeArrowheadStyle) {
|
||||
|
@ -1111,12 +1131,12 @@ function getArrowheadSvgPath(
|
|||
color: TLDefaultColorStyle,
|
||||
strokeWidth: number,
|
||||
fill: TLDefaultFillStyle,
|
||||
colors: TLExportColors
|
||||
theme: TLDefaultColorTheme
|
||||
) {
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
path.setAttribute('d', d)
|
||||
path.setAttribute('fill', 'none')
|
||||
path.setAttribute('stroke', colors.fill[color])
|
||||
path.setAttribute('stroke', theme[color].solid)
|
||||
path.setAttribute('stroke-width', strokeWidth + '')
|
||||
|
||||
// Get the fill element, if any
|
||||
|
@ -1124,7 +1144,7 @@ function getArrowheadSvgPath(
|
|||
d,
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
|
||||
if (shapeFill) {
|
||||
|
|
|
@ -10,14 +10,15 @@ import {
|
|||
Vec2d,
|
||||
VecLike,
|
||||
} from '@tldraw/primitives'
|
||||
import { TLDrawShape, TLDrawShapeSegment } from '@tldraw/tlschema'
|
||||
import { getDefaultColorTheme, TLDrawShape, TLDrawShapeSegment } from '@tldraw/tlschema'
|
||||
import { last, rng } from '@tldraw/utils'
|
||||
import { SVGContainer } from '../../../components/SVGContainer'
|
||||
import { getSvgPathFromStroke, getSvgPathFromStrokePoints } from '../../../utils/svg'
|
||||
import { ShapeUtil, TLOnResizeHandler } from '../ShapeUtil'
|
||||
import { ShapeUtil, TLOnResizeHandler, TLShapeUtilCanvasSvgDef } from '../ShapeUtil'
|
||||
import { STROKE_SIZES } from '../shared/default-shape-constants'
|
||||
import { getShapeFillSvg, ShapeFill } from '../shared/ShapeFill'
|
||||
import { TLExportColors } from '../shared/TLExportColors'
|
||||
import { getFillDefForCanvas, getFillDefForExport } from '../shared/defaultStyleDefs'
|
||||
import { getShapeFillSvg, ShapeFill, useDefaultColorTheme } from '../shared/ShapeFill'
|
||||
import { SvgExportContext } from '../shared/SvgExportContext'
|
||||
import { useForceSolid } from '../shared/useForceSolid'
|
||||
import { getDrawShapeStrokeDashArray, getFreehandOptions, getPointsFromSegments } from './getPath'
|
||||
|
||||
|
@ -118,6 +119,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
|||
}
|
||||
|
||||
component(shape: TLDrawShape) {
|
||||
const theme = useDefaultColorTheme()
|
||||
const forceSolid = useForceSolid()
|
||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
||||
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
||||
|
@ -156,7 +158,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
|||
<path
|
||||
d={getSvgPathFromStroke(strokeOutlinePoints, true)}
|
||||
strokeLinecap="round"
|
||||
fill={`var(--palette-${shape.props.color})`}
|
||||
fill={theme[shape.props.color].solid}
|
||||
/>
|
||||
</SVGContainer>
|
||||
)
|
||||
|
@ -173,7 +175,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
|||
d={solidStrokePath}
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
stroke={`var(--palette-${shape.props.color})`}
|
||||
stroke={theme[shape.props.color].solid}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={getDrawShapeStrokeDashArray(shape, strokeWidth)}
|
||||
strokeDashoffset="0"
|
||||
|
@ -208,7 +210,10 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
|||
return <path d={solidStrokePath} />
|
||||
}
|
||||
|
||||
toSvg(shape: TLDrawShape, _font: string | undefined, colors: TLExportColors) {
|
||||
toSvg(shape: TLDrawShape, ctx: SvgExportContext) {
|
||||
const theme = getDefaultColorTheme(this.editor)
|
||||
ctx.addExportDef(getFillDefForExport(shape.props.fill, theme))
|
||||
|
||||
const { color } = shape.props
|
||||
|
||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
||||
|
@ -236,14 +241,14 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
|||
|
||||
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
p.setAttribute('d', getSvgPathFromStroke(strokeOutlinePoints, true))
|
||||
p.setAttribute('fill', colors.fill[color])
|
||||
p.setAttribute('fill', theme[color].solid)
|
||||
p.setAttribute('stroke-linecap', 'round')
|
||||
|
||||
foregroundPath = p
|
||||
} else {
|
||||
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
p.setAttribute('d', solidStrokePath)
|
||||
p.setAttribute('stroke', colors.fill[color])
|
||||
p.setAttribute('stroke', theme[color].solid)
|
||||
p.setAttribute('fill', 'none')
|
||||
p.setAttribute('stroke-linecap', 'round')
|
||||
p.setAttribute('stroke-width', strokeWidth.toString())
|
||||
|
@ -257,7 +262,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
|||
fill: shape.props.isClosed ? shape.props.fill : 'none',
|
||||
d: solidStrokePath,
|
||||
color: shape.props.color,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
|
||||
if (fillPath) {
|
||||
|
@ -270,6 +275,10 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
|||
return foregroundPath
|
||||
}
|
||||
|
||||
getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] {
|
||||
return [getFillDefForCanvas()]
|
||||
}
|
||||
|
||||
override onResize: TLOnResizeHandler<TLDrawShape> = (shape, info) => {
|
||||
const { scaleX, scaleY } = info
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { canolicalizeRotation, SelectionEdge, toDomPrecision } from '@tldraw/primitives'
|
||||
import { TLFrameShape, TLShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import { getDefaultColorTheme, TLFrameShape, TLShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import { last } from '@tldraw/utils'
|
||||
import { SVGContainer } from '../../../components/SVGContainer'
|
||||
import { defaultEmptyAs } from '../../../utils/string'
|
||||
|
@ -7,7 +7,7 @@ import { BaseBoxShapeUtil } from '../BaseBoxShapeUtil'
|
|||
import { GroupShapeUtil } from '../group/GroupShapeUtil'
|
||||
import { TLOnResizeEndHandler } from '../ShapeUtil'
|
||||
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
||||
import { TLExportColors } from '../shared/TLExportColors'
|
||||
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
||||
import { FrameHeading } from './components/FrameHeading'
|
||||
|
||||
/** @public */
|
||||
|
@ -24,6 +24,8 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
|
||||
override component(shape: TLFrameShape) {
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const theme = useDefaultColorTheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -33,7 +35,8 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
className="tl-frame__body"
|
||||
width={bounds.width}
|
||||
height={bounds.height}
|
||||
fill="none"
|
||||
fill={theme.solid}
|
||||
stroke={theme.text}
|
||||
/>
|
||||
</SVGContainer>
|
||||
<FrameHeading
|
||||
|
@ -46,18 +49,15 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
)
|
||||
}
|
||||
|
||||
override toSvg(
|
||||
shape: TLFrameShape,
|
||||
font: string,
|
||||
colors: TLExportColors
|
||||
): SVGElement | Promise<SVGElement> {
|
||||
override toSvg(shape: TLFrameShape): SVGElement | Promise<SVGElement> {
|
||||
const theme = getDefaultColorTheme(this.editor)
|
||||
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
rect.setAttribute('width', shape.props.w.toString())
|
||||
rect.setAttribute('height', shape.props.h.toString())
|
||||
rect.setAttribute('fill', colors.solid)
|
||||
rect.setAttribute('stroke', colors.fill.black)
|
||||
rect.setAttribute('fill', theme.solid)
|
||||
rect.setAttribute('stroke', theme.black.solid)
|
||||
rect.setAttribute('stroke-width', '1')
|
||||
rect.setAttribute('rx', '1')
|
||||
rect.setAttribute('ry', '1')
|
||||
|
@ -128,7 +128,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
textBg.setAttribute('height', `${opts.height}px`)
|
||||
textBg.setAttribute('rx', 4 + 'px')
|
||||
textBg.setAttribute('ry', 4 + 'px')
|
||||
textBg.setAttribute('fill', colors.background)
|
||||
textBg.setAttribute('fill', theme.background)
|
||||
|
||||
g.appendChild(textBg)
|
||||
g.appendChild(text)
|
||||
|
|
|
@ -12,21 +12,31 @@ import {
|
|||
Vec2d,
|
||||
VecLike,
|
||||
} from '@tldraw/primitives'
|
||||
import { TLDefaultDashStyle, TLGeoShape } from '@tldraw/tlschema'
|
||||
import {
|
||||
DefaultFontFamilies,
|
||||
getDefaultColorTheme,
|
||||
TLDefaultDashStyle,
|
||||
TLGeoShape,
|
||||
} from '@tldraw/tlschema'
|
||||
import { SVGContainer } from '../../../components/SVGContainer'
|
||||
import { Editor } from '../../Editor'
|
||||
import { BaseBoxShapeUtil } from '../BaseBoxShapeUtil'
|
||||
import { TLOnEditEndHandler, TLOnResizeHandler } from '../ShapeUtil'
|
||||
import { TLOnEditEndHandler, TLOnResizeHandler, TLShapeUtilCanvasSvgDef } from '../ShapeUtil'
|
||||
import {
|
||||
FONT_FAMILIES,
|
||||
LABEL_FONT_SIZES,
|
||||
STROKE_SIZES,
|
||||
TEXT_PROPS,
|
||||
} from '../shared/default-shape-constants'
|
||||
import {
|
||||
getFillDefForCanvas,
|
||||
getFillDefForExport,
|
||||
getFontDefForExport,
|
||||
} from '../shared/defaultStyleDefs'
|
||||
import { getTextLabelSvgElement } from '../shared/getTextLabelSvgElement'
|
||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||
import { SvgExportContext } from '../shared/SvgExportContext'
|
||||
import { TextLabel } from '../shared/TextLabel'
|
||||
import { TLExportColors } from '../shared/TLExportColors'
|
||||
import { useForceSolid } from '../shared/useForceSolid'
|
||||
import { DashStyleEllipse, DashStyleEllipseSvg } from './components/DashStyleEllipse'
|
||||
import { DashStyleOval, DashStyleOvalSvg } from './components/DashStyleOval'
|
||||
|
@ -502,9 +512,11 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
}
|
||||
}
|
||||
|
||||
toSvg(shape: TLGeoShape, font: string, colors: TLExportColors) {
|
||||
toSvg(shape: TLGeoShape, ctx: SvgExportContext) {
|
||||
const { id, props } = shape
|
||||
const strokeWidth = STROKE_SIZES[props.size]
|
||||
const theme = getDefaultColorTheme(this.editor)
|
||||
ctx.addExportDef(getFillDefForExport(shape.props.fill, theme))
|
||||
|
||||
let svgElm: SVGElement
|
||||
|
||||
|
@ -519,7 +531,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
color: props.color,
|
||||
fill: props.fill,
|
||||
strokeWidth,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
break
|
||||
|
||||
|
@ -530,7 +542,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
h: props.h,
|
||||
color: props.color,
|
||||
fill: props.fill,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
break
|
||||
|
||||
|
@ -543,7 +555,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
dash: props.dash,
|
||||
color: props.color,
|
||||
fill: props.fill,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
@ -561,7 +573,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
dash: props.dash,
|
||||
color: props.color,
|
||||
fill: props.fill,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
break
|
||||
|
||||
|
@ -572,7 +584,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
h: props.h,
|
||||
color: props.color,
|
||||
fill: props.fill,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
break
|
||||
|
||||
|
@ -585,7 +597,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
dash: props.dash,
|
||||
color: props.color,
|
||||
fill: props.fill,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
}
|
||||
break
|
||||
|
@ -603,7 +615,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
strokeWidth,
|
||||
outline,
|
||||
lines,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
break
|
||||
|
||||
|
@ -614,7 +626,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
strokeWidth,
|
||||
outline,
|
||||
lines,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
break
|
||||
|
||||
|
@ -626,7 +638,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
strokeWidth,
|
||||
outline,
|
||||
lines,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
@ -637,21 +649,23 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
if (props.text) {
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
|
||||
ctx.addExportDef(getFontDefForExport(shape.props.font))
|
||||
|
||||
const rootTextElm = getTextLabelSvgElement({
|
||||
editor: this.editor,
|
||||
shape,
|
||||
font,
|
||||
font: DefaultFontFamilies[shape.props.font],
|
||||
bounds,
|
||||
})
|
||||
|
||||
const textElm = rootTextElm.cloneNode(true) as SVGTextElement
|
||||
textElm.setAttribute('fill', colors.fill[shape.props.labelColor])
|
||||
textElm.setAttribute('fill', theme[shape.props.labelColor].solid)
|
||||
textElm.setAttribute('stroke', 'none')
|
||||
|
||||
const textBgEl = rootTextElm.cloneNode(true) as SVGTextElement
|
||||
textBgEl.setAttribute('stroke-width', '2')
|
||||
textBgEl.setAttribute('fill', colors.background)
|
||||
textBgEl.setAttribute('stroke', colors.background)
|
||||
textBgEl.setAttribute('fill', theme.background)
|
||||
textBgEl.setAttribute('stroke', theme.background)
|
||||
|
||||
const groupEl = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||
groupEl.append(textBgEl)
|
||||
|
@ -671,6 +685,10 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
return svgElm
|
||||
}
|
||||
|
||||
getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] {
|
||||
return [getFillDefForCanvas()]
|
||||
}
|
||||
|
||||
onResize: TLOnResizeHandler<TLGeoShape> = (
|
||||
shape,
|
||||
{ initialBounds, handle, newPoint, scaleX, scaleY }
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import { perimeterOfEllipse, toDomPrecision } from '@tldraw/primitives'
|
||||
import { TLGeoShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import { TLDefaultColorTheme, TLGeoShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import * as React from 'react'
|
||||
import { ShapeFill, getShapeFillSvg, getSvgWithShapeFill } from '../../shared/ShapeFill'
|
||||
import { TLExportColors } from '../../shared/TLExportColors'
|
||||
import {
|
||||
ShapeFill,
|
||||
getShapeFillSvg,
|
||||
getSvgWithShapeFill,
|
||||
useDefaultColorTheme,
|
||||
} from '../../shared/ShapeFill'
|
||||
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
|
||||
|
||||
export const DashStyleEllipse = React.memo(function DashStyleEllipse({
|
||||
|
@ -16,6 +20,7 @@ export const DashStyleEllipse = React.memo(function DashStyleEllipse({
|
|||
strokeWidth: number
|
||||
id: TLShapeId
|
||||
}) {
|
||||
const theme = useDefaultColorTheme()
|
||||
const cx = w / 2
|
||||
const cy = h / 2
|
||||
const rx = Math.max(0, cx - sw / 2)
|
||||
|
@ -44,7 +49,7 @@ export const DashStyleEllipse = React.memo(function DashStyleEllipse({
|
|||
width={toDomPrecision(w)}
|
||||
height={toDomPrecision(h)}
|
||||
fill="none"
|
||||
stroke={`var(--palette-${color})`}
|
||||
stroke={theme[color].solid}
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
pointerEvents="all"
|
||||
|
@ -59,12 +64,12 @@ export function DashStyleEllipseSvg({
|
|||
strokeWidth: sw,
|
||||
dash,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
fill,
|
||||
}: Pick<TLGeoShape['props'], 'w' | 'h' | 'dash' | 'color' | 'fill'> & {
|
||||
strokeWidth: number
|
||||
id: TLShapeId
|
||||
colors: TLExportColors
|
||||
theme: TLDefaultColorTheme
|
||||
}) {
|
||||
const cx = w / 2
|
||||
const cy = h / 2
|
||||
|
@ -91,7 +96,7 @@ export function DashStyleEllipseSvg({
|
|||
strokeElement.setAttribute('width', w.toString())
|
||||
strokeElement.setAttribute('height', h.toString())
|
||||
strokeElement.setAttribute('fill', 'none')
|
||||
strokeElement.setAttribute('stroke', colors.fill[color])
|
||||
strokeElement.setAttribute('stroke', theme[color].solid)
|
||||
strokeElement.setAttribute('stroke-dasharray', strokeDasharray)
|
||||
strokeElement.setAttribute('stroke-dashoffset', strokeDashoffset)
|
||||
|
||||
|
@ -100,7 +105,7 @@ export function DashStyleEllipseSvg({
|
|||
d,
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
|
||||
return getSvgWithShapeFill(strokeElement, fillElement)
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import { toDomPrecision } from '@tldraw/primitives'
|
||||
import { TLGeoShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import { TLDefaultColorTheme, TLGeoShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import * as React from 'react'
|
||||
import { ShapeFill, getShapeFillSvg, getSvgWithShapeFill } from '../../shared/ShapeFill'
|
||||
import { TLExportColors } from '../../shared/TLExportColors'
|
||||
import {
|
||||
ShapeFill,
|
||||
getShapeFillSvg,
|
||||
getSvgWithShapeFill,
|
||||
useDefaultColorTheme,
|
||||
} from '../../shared/ShapeFill'
|
||||
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
|
||||
import { getOvalPerimeter, getOvalSolidPath } from '../helpers'
|
||||
|
||||
|
@ -17,6 +21,7 @@ export const DashStyleOval = React.memo(function DashStyleOval({
|
|||
strokeWidth: number
|
||||
id: TLShapeId
|
||||
}) {
|
||||
const theme = useDefaultColorTheme()
|
||||
const d = getOvalSolidPath(w, h)
|
||||
const perimeter = getOvalPerimeter(w, h)
|
||||
|
||||
|
@ -41,7 +46,7 @@ export const DashStyleOval = React.memo(function DashStyleOval({
|
|||
width={toDomPrecision(w)}
|
||||
height={toDomPrecision(h)}
|
||||
fill="none"
|
||||
stroke={`var(--palette-${color})`}
|
||||
stroke={theme[color].solid}
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
pointerEvents="all"
|
||||
|
@ -56,12 +61,12 @@ export function DashStyleOvalSvg({
|
|||
strokeWidth: sw,
|
||||
dash,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
fill,
|
||||
}: Pick<TLGeoShape['props'], 'w' | 'h' | 'dash' | 'color' | 'fill'> & {
|
||||
strokeWidth: number
|
||||
id: TLShapeId
|
||||
colors: TLExportColors
|
||||
theme: TLDefaultColorTheme
|
||||
}) {
|
||||
const d = getOvalSolidPath(w, h)
|
||||
const perimeter = getOvalPerimeter(w, h)
|
||||
|
@ -82,7 +87,7 @@ export function DashStyleOvalSvg({
|
|||
strokeElement.setAttribute('width', w.toString())
|
||||
strokeElement.setAttribute('height', h.toString())
|
||||
strokeElement.setAttribute('fill', 'none')
|
||||
strokeElement.setAttribute('stroke', colors.fill[color])
|
||||
strokeElement.setAttribute('stroke', theme[color].solid)
|
||||
strokeElement.setAttribute('stroke-dasharray', strokeDasharray)
|
||||
strokeElement.setAttribute('stroke-dashoffset', strokeDashoffset)
|
||||
|
||||
|
@ -91,7 +96,7 @@ export function DashStyleOvalSvg({
|
|||
d,
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
|
||||
return getSvgWithShapeFill(strokeElement, fillElement)
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import { Vec2d, VecLike } from '@tldraw/primitives'
|
||||
import { TLGeoShape } from '@tldraw/tlschema'
|
||||
import { TLDefaultColorTheme, TLGeoShape } from '@tldraw/tlschema'
|
||||
import * as React from 'react'
|
||||
import { ShapeFill, getShapeFillSvg, getSvgWithShapeFill } from '../../shared/ShapeFill'
|
||||
import { TLExportColors } from '../../shared/TLExportColors'
|
||||
import {
|
||||
ShapeFill,
|
||||
getShapeFillSvg,
|
||||
getSvgWithShapeFill,
|
||||
useDefaultColorTheme,
|
||||
} from '../../shared/ShapeFill'
|
||||
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
|
||||
|
||||
export const DashStylePolygon = React.memo(function DashStylePolygon({
|
||||
|
@ -17,6 +21,7 @@ export const DashStylePolygon = React.memo(function DashStylePolygon({
|
|||
outline: VecLike[]
|
||||
lines?: VecLike[][]
|
||||
}) {
|
||||
const theme = useDefaultColorTheme()
|
||||
const innerPath = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
|
||||
|
||||
return (
|
||||
|
@ -31,12 +36,7 @@ export const DashStylePolygon = React.memo(function DashStylePolygon({
|
|||
d={`M${l[0].x},${l[0].y}L${l[1].x},${l[1].y}`}
|
||||
/>
|
||||
))}
|
||||
<g
|
||||
strokeWidth={strokeWidth}
|
||||
stroke={`var(--palette-${color})`}
|
||||
fill="none"
|
||||
pointerEvents="all"
|
||||
>
|
||||
<g strokeWidth={strokeWidth} stroke={theme[color].solid} fill="none" pointerEvents="all">
|
||||
{Array.from(Array(outline.length)).map((_, i) => {
|
||||
const A = outline[i]
|
||||
const B = outline[(i + 1) % outline.length]
|
||||
|
@ -76,7 +76,7 @@ export const DashStylePolygon = React.memo(function DashStylePolygon({
|
|||
<path
|
||||
key={`line_fg_${i}`}
|
||||
d={`M${A.x},${A.y}L${B.x},${B.y}`}
|
||||
stroke={`var(--palette-${color})`}
|
||||
stroke={theme[color].solid}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
strokeDasharray={strokeDasharray}
|
||||
|
@ -93,19 +93,19 @@ export function DashStylePolygonSvg({
|
|||
dash,
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
strokeWidth,
|
||||
outline,
|
||||
lines,
|
||||
}: Pick<TLGeoShape['props'], 'dash' | 'fill' | 'color'> & {
|
||||
outline: VecLike[]
|
||||
strokeWidth: number
|
||||
colors: TLExportColors
|
||||
theme: TLDefaultColorTheme
|
||||
lines?: VecLike[][]
|
||||
}) {
|
||||
const strokeElement = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||
strokeElement.setAttribute('stroke-width', strokeWidth.toString())
|
||||
strokeElement.setAttribute('stroke', colors.fill[color])
|
||||
strokeElement.setAttribute('stroke', theme[color].solid)
|
||||
strokeElement.setAttribute('fill', 'none')
|
||||
|
||||
Array.from(Array(outline.length)).forEach((_, i) => {
|
||||
|
@ -155,7 +155,7 @@ export function DashStylePolygonSvg({
|
|||
d: 'M' + outline[0] + 'L' + outline.slice(1) + 'Z',
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
|
||||
return getSvgWithShapeFill(strokeElement, fillElement)
|
||||
|
|
|
@ -8,12 +8,16 @@ import {
|
|||
TAU,
|
||||
Vec2d,
|
||||
} from '@tldraw/primitives'
|
||||
import { TLGeoShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import { TLDefaultColorTheme, TLGeoShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import { rng } from '@tldraw/utils'
|
||||
import * as React from 'react'
|
||||
import { getSvgPathFromStroke, getSvgPathFromStrokePoints } from '../../../../utils/svg'
|
||||
import { getShapeFillSvg, getSvgWithShapeFill, ShapeFill } from '../../shared/ShapeFill'
|
||||
import { TLExportColors } from '../../shared/TLExportColors'
|
||||
import {
|
||||
getShapeFillSvg,
|
||||
getSvgWithShapeFill,
|
||||
ShapeFill,
|
||||
useDefaultColorTheme,
|
||||
} from '../../shared/ShapeFill'
|
||||
|
||||
export const DrawStyleEllipse = React.memo(function DrawStyleEllipse({
|
||||
id,
|
||||
|
@ -26,13 +30,14 @@ export const DrawStyleEllipse = React.memo(function DrawStyleEllipse({
|
|||
strokeWidth: number
|
||||
id: TLShapeId
|
||||
}) {
|
||||
const theme = useDefaultColorTheme()
|
||||
const innerPath = getEllipseIndicatorPath(id, w, h, sw)
|
||||
const outerPath = getEllipsePath(id, w, h, sw)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShapeFill d={innerPath} color={color} fill={fill} />
|
||||
<path d={outerPath} fill={`var(--palette-${color})`} strokeWidth={0} pointerEvents="all" />
|
||||
<path d={outerPath} fill={theme[color].solid} strokeWidth={0} pointerEvents="all" />
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
@ -44,22 +49,22 @@ export function DrawStyleEllipseSvg({
|
|||
strokeWidth: sw,
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
}: Pick<TLGeoShape['props'], 'w' | 'h' | 'fill' | 'color'> & {
|
||||
strokeWidth: number
|
||||
id: TLShapeId
|
||||
colors: TLExportColors
|
||||
theme: TLDefaultColorTheme
|
||||
}) {
|
||||
const strokeElement = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
strokeElement.setAttribute('d', getEllipsePath(id, w, h, sw))
|
||||
strokeElement.setAttribute('fill', colors.fill[color])
|
||||
strokeElement.setAttribute('fill', theme[color].solid)
|
||||
|
||||
// Get the fill element, if any
|
||||
const fillElement = getShapeFillSvg({
|
||||
d: getEllipseIndicatorPath(id, w, h, sw),
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
|
||||
return getSvgWithShapeFill(strokeElement, fillElement)
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import { getRoundedInkyPolygonPath, getRoundedPolygonPoints, VecLike } from '@tldraw/primitives'
|
||||
import { TLGeoShape } from '@tldraw/tlschema'
|
||||
import { TLDefaultColorTheme, TLGeoShape } from '@tldraw/tlschema'
|
||||
import * as React from 'react'
|
||||
import { getShapeFillSvg, getSvgWithShapeFill, ShapeFill } from '../../shared/ShapeFill'
|
||||
import { TLExportColors } from '../../shared/TLExportColors'
|
||||
import {
|
||||
getShapeFillSvg,
|
||||
getSvgWithShapeFill,
|
||||
ShapeFill,
|
||||
useDefaultColorTheme,
|
||||
} from '../../shared/ShapeFill'
|
||||
|
||||
export const DrawStylePolygon = React.memo(function DrawStylePolygon({
|
||||
id,
|
||||
|
@ -17,6 +21,7 @@ export const DrawStylePolygon = React.memo(function DrawStylePolygon({
|
|||
strokeWidth: number
|
||||
lines?: VecLike[][]
|
||||
}) {
|
||||
const theme = useDefaultColorTheme()
|
||||
const polygonPoints = getRoundedPolygonPoints(id, outline, strokeWidth / 3, strokeWidth * 2, 2)
|
||||
let strokePathData = getRoundedInkyPolygonPath(polygonPoints)
|
||||
|
||||
|
@ -32,12 +37,7 @@ export const DrawStylePolygon = React.memo(function DrawStylePolygon({
|
|||
return (
|
||||
<>
|
||||
<ShapeFill d={innerPathData} fill={fill} color={color} />
|
||||
<path
|
||||
d={strokePathData}
|
||||
stroke={`var(--palette-${color})`}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
/>
|
||||
<path d={strokePathData} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
@ -48,14 +48,14 @@ export function DrawStylePolygonSvg({
|
|||
lines,
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
strokeWidth,
|
||||
}: Pick<TLGeoShape['props'], 'fill' | 'color'> & {
|
||||
id: TLGeoShape['id']
|
||||
outline: VecLike[]
|
||||
lines?: VecLike[][]
|
||||
strokeWidth: number
|
||||
colors: TLExportColors
|
||||
theme: TLDefaultColorTheme
|
||||
}) {
|
||||
const polygonPoints = getRoundedPolygonPoints(id, outline, strokeWidth / 3, strokeWidth * 2, 2)
|
||||
|
||||
|
@ -73,7 +73,7 @@ export function DrawStylePolygonSvg({
|
|||
const strokeElement = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
strokeElement.setAttribute('d', strokePathData)
|
||||
strokeElement.setAttribute('fill', 'none')
|
||||
strokeElement.setAttribute('stroke', colors.fill[color])
|
||||
strokeElement.setAttribute('stroke', theme[color].solid)
|
||||
strokeElement.setAttribute('stroke-width', strokeWidth.toString())
|
||||
|
||||
// Get the fill element, if any
|
||||
|
@ -81,7 +81,7 @@ export function DrawStylePolygonSvg({
|
|||
d: innerPathData,
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
|
||||
return getSvgWithShapeFill(strokeElement, fillElement)
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { TLGeoShape } from '@tldraw/tlschema'
|
||||
import { TLDefaultColorTheme, TLGeoShape } from '@tldraw/tlschema'
|
||||
import * as React from 'react'
|
||||
import { getShapeFillSvg, getSvgWithShapeFill, ShapeFill } from '../../shared/ShapeFill'
|
||||
import { TLExportColors } from '../../shared/TLExportColors'
|
||||
import {
|
||||
ShapeFill,
|
||||
getShapeFillSvg,
|
||||
getSvgWithShapeFill,
|
||||
useDefaultColorTheme,
|
||||
} from '../../shared/ShapeFill'
|
||||
|
||||
export const SolidStyleEllipse = React.memo(function SolidStyleEllipse({
|
||||
w,
|
||||
|
@ -10,6 +14,7 @@ export const SolidStyleEllipse = React.memo(function SolidStyleEllipse({
|
|||
fill,
|
||||
color,
|
||||
}: Pick<TLGeoShape['props'], 'w' | 'h' | 'fill' | 'color'> & { strokeWidth: number }) {
|
||||
const theme = useDefaultColorTheme()
|
||||
const cx = w / 2
|
||||
const cy = h / 2
|
||||
const rx = Math.max(0, cx)
|
||||
|
@ -20,7 +25,7 @@ export const SolidStyleEllipse = React.memo(function SolidStyleEllipse({
|
|||
return (
|
||||
<>
|
||||
<ShapeFill d={d} color={color} fill={fill} />
|
||||
<path d={d} stroke={`var(--palette-${color})`} strokeWidth={sw} fill="none" />
|
||||
<path d={d} stroke={theme[color].solid} strokeWidth={sw} fill="none" />
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
@ -31,10 +36,10 @@ export function SolidStyleEllipseSvg({
|
|||
strokeWidth: sw,
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
}: Pick<TLGeoShape['props'], 'w' | 'h' | 'fill' | 'color'> & {
|
||||
strokeWidth: number
|
||||
colors: TLExportColors
|
||||
theme: TLDefaultColorTheme
|
||||
}) {
|
||||
const cx = w / 2
|
||||
const cy = h / 2
|
||||
|
@ -49,14 +54,14 @@ export function SolidStyleEllipseSvg({
|
|||
strokeElement.setAttribute('width', w.toString())
|
||||
strokeElement.setAttribute('height', h.toString())
|
||||
strokeElement.setAttribute('fill', 'none')
|
||||
strokeElement.setAttribute('stroke', colors.fill[color])
|
||||
strokeElement.setAttribute('stroke', theme[color].solid)
|
||||
|
||||
// Get the fill element, if any
|
||||
const fillElement = getShapeFillSvg({
|
||||
d,
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
|
||||
return getSvgWithShapeFill(strokeElement, fillElement)
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { TLGeoShape } from '@tldraw/tlschema'
|
||||
import { TLDefaultColorTheme, TLGeoShape } from '@tldraw/tlschema'
|
||||
import * as React from 'react'
|
||||
import { getShapeFillSvg, getSvgWithShapeFill, ShapeFill } from '../../shared/ShapeFill'
|
||||
import { TLExportColors } from '../../shared/TLExportColors'
|
||||
import {
|
||||
ShapeFill,
|
||||
getShapeFillSvg,
|
||||
getSvgWithShapeFill,
|
||||
useDefaultColorTheme,
|
||||
} from '../../shared/ShapeFill'
|
||||
|
||||
export const SolidStyleOval = React.memo(function SolidStyleOval({
|
||||
w,
|
||||
|
@ -12,11 +16,12 @@ export const SolidStyleOval = React.memo(function SolidStyleOval({
|
|||
}: Pick<TLGeoShape['props'], 'w' | 'h' | 'fill' | 'color'> & {
|
||||
strokeWidth: number
|
||||
}) {
|
||||
const theme = useDefaultColorTheme()
|
||||
const d = getOvalIndicatorPath(w, h)
|
||||
return (
|
||||
<>
|
||||
<ShapeFill d={d} color={color} fill={fill} />
|
||||
<path d={d} stroke={`var(--palette-${color})`} strokeWidth={sw} fill="none" />
|
||||
<path d={d} stroke={theme[color].solid} strokeWidth={sw} fill="none" />
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
@ -27,10 +32,10 @@ export function SolidStyleOvalSvg({
|
|||
strokeWidth: sw,
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
}: Pick<TLGeoShape['props'], 'w' | 'h' | 'fill' | 'color'> & {
|
||||
strokeWidth: number
|
||||
colors: TLExportColors
|
||||
theme: TLDefaultColorTheme
|
||||
}) {
|
||||
const d = getOvalIndicatorPath(w, h)
|
||||
const strokeElement = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
|
@ -39,14 +44,14 @@ export function SolidStyleOvalSvg({
|
|||
strokeElement.setAttribute('width', w.toString())
|
||||
strokeElement.setAttribute('height', h.toString())
|
||||
strokeElement.setAttribute('fill', 'none')
|
||||
strokeElement.setAttribute('stroke', colors.fill[color])
|
||||
strokeElement.setAttribute('stroke', theme[color].solid)
|
||||
|
||||
// Get the fill element, if any
|
||||
const fillElement = getShapeFillSvg({
|
||||
d,
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
|
||||
return getSvgWithShapeFill(strokeElement, fillElement)
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import { VecLike } from '@tldraw/primitives'
|
||||
import { TLGeoShape } from '@tldraw/tlschema'
|
||||
import { TLDefaultColorTheme, TLGeoShape } from '@tldraw/tlschema'
|
||||
import * as React from 'react'
|
||||
import { ShapeFill, getShapeFillSvg, getSvgWithShapeFill } from '../../shared/ShapeFill'
|
||||
import { TLExportColors } from '../../shared/TLExportColors'
|
||||
import {
|
||||
ShapeFill,
|
||||
getShapeFillSvg,
|
||||
getSvgWithShapeFill,
|
||||
useDefaultColorTheme,
|
||||
} from '../../shared/ShapeFill'
|
||||
|
||||
export const SolidStylePolygon = React.memo(function SolidStylePolygon({
|
||||
outline,
|
||||
|
@ -15,6 +19,7 @@ export const SolidStylePolygon = React.memo(function SolidStylePolygon({
|
|||
lines?: VecLike[][]
|
||||
strokeWidth: number
|
||||
}) {
|
||||
const theme = useDefaultColorTheme()
|
||||
let path = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
|
||||
|
||||
if (lines) {
|
||||
|
@ -26,7 +31,7 @@ export const SolidStylePolygon = React.memo(function SolidStylePolygon({
|
|||
return (
|
||||
<>
|
||||
<ShapeFill d={path} fill={fill} color={color} />
|
||||
<path d={path} stroke={`var(--palette-${color}`} strokeWidth={strokeWidth} fill="none" />
|
||||
<path d={path} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
@ -37,11 +42,11 @@ export function SolidStylePolygonSvg({
|
|||
fill,
|
||||
color,
|
||||
strokeWidth,
|
||||
colors,
|
||||
theme,
|
||||
}: Pick<TLGeoShape['props'], 'fill' | 'color'> & {
|
||||
outline: VecLike[]
|
||||
strokeWidth: number
|
||||
colors: TLExportColors
|
||||
theme: TLDefaultColorTheme
|
||||
lines?: VecLike[][]
|
||||
}) {
|
||||
const pathData = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
|
||||
|
@ -58,7 +63,7 @@ export function SolidStylePolygonSvg({
|
|||
const strokeElement = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
strokeElement.setAttribute('d', strokePathData)
|
||||
strokeElement.setAttribute('stroke-width', strokeWidth.toString())
|
||||
strokeElement.setAttribute('stroke', colors.fill[color])
|
||||
strokeElement.setAttribute('stroke', theme[color].solid)
|
||||
strokeElement.setAttribute('fill', 'none')
|
||||
|
||||
// Get the fill element, if any
|
||||
|
@ -66,7 +71,7 @@ export function SolidStylePolygonSvg({
|
|||
d: fillPathData,
|
||||
fill,
|
||||
color,
|
||||
colors,
|
||||
theme,
|
||||
})
|
||||
|
||||
return getSvgWithShapeFill(strokeElement, fillElement)
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { Box2d, getStrokePoints, linesIntersect, Vec2d, VecLike } from '@tldraw/primitives'
|
||||
import { TLDrawShapeSegment, TLHighlightShape } from '@tldraw/tlschema'
|
||||
import {
|
||||
getDefaultColorTheme,
|
||||
TLDefaultColorTheme,
|
||||
TLDrawShapeSegment,
|
||||
TLHighlightShape,
|
||||
} from '@tldraw/tlschema'
|
||||
import { last, rng } from '@tldraw/utils'
|
||||
import { SVGContainer } from '../../../components/SVGContainer'
|
||||
import { getSvgPathFromStrokePoints } from '../../../utils/svg'
|
||||
import { getHighlightFreehandSettings, getPointsFromSegments } from '../draw/getPath'
|
||||
import { ShapeUtil, TLOnResizeHandler } from '../ShapeUtil'
|
||||
import { FONT_SIZES } from '../shared/default-shape-constants'
|
||||
import { TLExportColors } from '../shared/TLExportColors'
|
||||
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
||||
import { useColorSpace } from '../shared/useColorSpace'
|
||||
import { useForceSolid } from '../shared/useForceSolid'
|
||||
|
||||
const OVERLAY_OPACITY = 0.35
|
||||
|
@ -144,16 +150,14 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
|||
return getStrokeWidth(shape) / 2
|
||||
}
|
||||
|
||||
override toSvg(shape: TLHighlightShape, _font: string | undefined, colors: TLExportColors) {
|
||||
return highlighterToSvg(getStrokeWidth(shape), shape, OVERLAY_OPACITY, colors)
|
||||
override toSvg(shape: TLHighlightShape) {
|
||||
const theme = getDefaultColorTheme(this.editor)
|
||||
return highlighterToSvg(getStrokeWidth(shape), shape, OVERLAY_OPACITY, theme)
|
||||
}
|
||||
|
||||
override toBackgroundSvg(
|
||||
shape: TLHighlightShape,
|
||||
font: string | undefined,
|
||||
colors: TLExportColors
|
||||
) {
|
||||
return highlighterToSvg(getStrokeWidth(shape), shape, UNDERLAY_OPACITY, colors)
|
||||
override toBackgroundSvg(shape: TLHighlightShape) {
|
||||
const theme = getDefaultColorTheme(this.editor)
|
||||
return highlighterToSvg(getStrokeWidth(shape), shape, UNDERLAY_OPACITY, theme)
|
||||
}
|
||||
|
||||
override onResize: TLOnResizeHandler<TLHighlightShape> = (shape, info) => {
|
||||
|
@ -228,8 +232,11 @@ function HighlightRenderer({
|
|||
shape: TLHighlightShape
|
||||
opacity?: number
|
||||
}) {
|
||||
const theme = useDefaultColorTheme()
|
||||
const forceSolid = useForceSolid()
|
||||
const { solidStrokePath, sw } = getHighlightSvgPath(shape, strokeWidth, forceSolid)
|
||||
const colorSpace = useColorSpace()
|
||||
const color = theme[shape.props.color].highlight[colorSpace]
|
||||
|
||||
return (
|
||||
<SVGContainer id={shape.id} style={{ opacity }}>
|
||||
|
@ -238,7 +245,7 @@ function HighlightRenderer({
|
|||
strokeLinecap="round"
|
||||
fill="none"
|
||||
pointerEvents="all"
|
||||
stroke={`var(--palette-${shape.props.color}-highlight)`}
|
||||
stroke={color}
|
||||
strokeWidth={sw}
|
||||
/>
|
||||
</SVGContainer>
|
||||
|
@ -249,14 +256,14 @@ function highlighterToSvg(
|
|||
strokeWidth: number,
|
||||
shape: TLHighlightShape,
|
||||
opacity: number,
|
||||
colors: TLExportColors
|
||||
theme: TLDefaultColorTheme
|
||||
) {
|
||||
const { solidStrokePath, sw } = getHighlightSvgPath(shape, strokeWidth, false)
|
||||
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
path.setAttribute('d', solidStrokePath)
|
||||
path.setAttribute('fill', 'none')
|
||||
path.setAttribute('stroke', colors.highlight[shape.props.color])
|
||||
path.setAttribute('stroke', theme[shape.props.color].highlight.srgb)
|
||||
path.setAttribute('stroke-width', `${sw}`)
|
||||
path.setAttribute('opacity', `${opacity}`)
|
||||
|
||||
|
|
|
@ -9,13 +9,12 @@ import {
|
|||
intersectLineSegmentPolyline,
|
||||
pointNearToPolyline,
|
||||
} from '@tldraw/primitives'
|
||||
import { TLHandle, TLLineShape } from '@tldraw/tlschema'
|
||||
import { TLHandle, TLLineShape, getDefaultColorTheme } from '@tldraw/tlschema'
|
||||
import { deepCopy } from '@tldraw/utils'
|
||||
import { SVGContainer } from '../../../components/SVGContainer'
|
||||
import { WeakMapCache } from '../../../utils/WeakMapCache'
|
||||
import { ShapeUtil, TLOnHandleChangeHandler, TLOnResizeHandler } from '../ShapeUtil'
|
||||
import { ShapeFill } from '../shared/ShapeFill'
|
||||
import { TLExportColors } from '../shared/TLExportColors'
|
||||
import { ShapeFill, useDefaultColorTheme } from '../shared/ShapeFill'
|
||||
import { STROKE_SIZES } from '../shared/default-shape-constants'
|
||||
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
|
||||
import { useForceSolid } from '../shared/useForceSolid'
|
||||
|
@ -178,6 +177,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
}
|
||||
|
||||
component(shape: TLLineShape) {
|
||||
const theme = useDefaultColorTheme()
|
||||
const forceSolid = useForceSolid()
|
||||
const spline = getSplineForLineShape(shape)
|
||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
||||
|
@ -193,12 +193,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
return (
|
||||
<SVGContainer id={shape.id}>
|
||||
<ShapeFill d={pathData} fill={'none'} color={color} />
|
||||
<path
|
||||
d={pathData}
|
||||
stroke={`var(--palette-${color})`}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
/>
|
||||
<path d={pathData} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||
</SVGContainer>
|
||||
)
|
||||
}
|
||||
|
@ -210,7 +205,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
return (
|
||||
<SVGContainer id={shape.id}>
|
||||
<ShapeFill d={pathData} fill={'none'} color={color} />
|
||||
<g stroke={`var(--palette-${color})`} strokeWidth={strokeWidth}>
|
||||
<g stroke={theme[color].solid} strokeWidth={strokeWidth}>
|
||||
{spline.segments.map((segment, i) => {
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
segment.length,
|
||||
|
@ -246,7 +241,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
<ShapeFill d={innerPathData} fill={'none'} color={color} />
|
||||
<path
|
||||
d={outerPathData}
|
||||
stroke={`var(--palette-${color})`}
|
||||
stroke={theme[color].solid}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
/>
|
||||
|
@ -265,7 +260,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
<ShapeFill d={splinePath} fill={'none'} color={color} />
|
||||
<path
|
||||
strokeWidth={strokeWidth}
|
||||
stroke={`var(--palette-${color})`}
|
||||
stroke={theme[color].solid}
|
||||
fill="none"
|
||||
d={splinePath}
|
||||
/>
|
||||
|
@ -277,7 +272,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
return (
|
||||
<SVGContainer id={shape.id}>
|
||||
<ShapeFill d={splinePath} fill={'none'} color={color} />
|
||||
<g stroke={`var(--palette-${color})`} strokeWidth={strokeWidth}>
|
||||
<g stroke={theme[color].solid} strokeWidth={strokeWidth}>
|
||||
{spline.segments.map((segment, i) => {
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
segment.length,
|
||||
|
@ -311,8 +306,8 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
<path
|
||||
d={getLineDrawPath(shape, spline, strokeWidth)}
|
||||
strokeWidth={1}
|
||||
stroke={`var(--palette-${color})`}
|
||||
fill={`var(--palette-${color})`}
|
||||
stroke={theme[color].solid}
|
||||
fill={theme[color].solid}
|
||||
/>
|
||||
</SVGContainer>
|
||||
)
|
||||
|
@ -342,11 +337,11 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
return <path d={path} />
|
||||
}
|
||||
|
||||
toSvg(shape: TLLineShape, _font: string, colors: TLExportColors) {
|
||||
const { color: _color, size } = shape.props
|
||||
const color = colors.fill[_color]
|
||||
toSvg(shape: TLLineShape) {
|
||||
const theme = getDefaultColorTheme(this.editor)
|
||||
const color = theme[shape.props.color].solid
|
||||
const spline = getSplineForLineShape(shape)
|
||||
return getLineSvg(shape, spline, color, STROKE_SIZES[size])
|
||||
return getLineSvg(shape, spline, color, STROKE_SIZES[shape.props.size])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives'
|
||||
import { TLNoteShape } from '@tldraw/tlschema'
|
||||
import { DefaultFontFamilies, getDefaultColorTheme, TLNoteShape } from '@tldraw/tlschema'
|
||||
import { Editor } from '../../Editor'
|
||||
import { ShapeUtil, TLOnEditEndHandler } from '../ShapeUtil'
|
||||
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
||||
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
||||
import { getTextLabelSvgElement } from '../shared/getTextLabelSvgElement'
|
||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
||||
import { SvgExportContext } from '../shared/SvgExportContext'
|
||||
import { TextLabel } from '../shared/TextLabel'
|
||||
import { TLExportColors } from '../shared/TLExportColors'
|
||||
|
||||
const NOTE_SIZE = 200
|
||||
|
||||
|
@ -56,6 +58,8 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
props: { color, font, size, align, text, verticalAlign },
|
||||
} = shape
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const theme = useDefaultColorTheme()
|
||||
const adjustedColor = color === 'black' ? 'yellow' : color
|
||||
|
||||
return (
|
||||
|
@ -70,8 +74,8 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
<div
|
||||
className="tl-note__container tl-hitarea-fill"
|
||||
style={{
|
||||
color: `var(--palette-${adjustedColor})`,
|
||||
backgroundColor: `var(--palette-${adjustedColor})`,
|
||||
color: theme[adjustedColor].solid,
|
||||
backgroundColor: theme[adjustedColor].solid,
|
||||
}}
|
||||
>
|
||||
<div className="tl-note__scrim" />
|
||||
|
@ -83,7 +87,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
align={align}
|
||||
verticalAlign={verticalAlign}
|
||||
text={text}
|
||||
labelColor={adjustedColor}
|
||||
labelColor="black"
|
||||
wrap
|
||||
/>
|
||||
</div>
|
||||
|
@ -105,7 +109,9 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
)
|
||||
}
|
||||
|
||||
toSvg(shape: TLNoteShape, font: string, colors: TLExportColors) {
|
||||
toSvg(shape: TLNoteShape, ctx: SvgExportContext) {
|
||||
ctx.addExportDef(getFontDefForExport(shape.props.font))
|
||||
const theme = getDefaultColorTheme(this.editor)
|
||||
const bounds = this.getBounds(shape)
|
||||
|
||||
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||
|
@ -116,8 +122,8 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
rect1.setAttribute('rx', '10')
|
||||
rect1.setAttribute('width', NOTE_SIZE.toString())
|
||||
rect1.setAttribute('height', bounds.height.toString())
|
||||
rect1.setAttribute('fill', colors.fill[adjustedColor])
|
||||
rect1.setAttribute('stroke', colors.fill[adjustedColor])
|
||||
rect1.setAttribute('fill', theme[adjustedColor].solid)
|
||||
rect1.setAttribute('stroke', theme[adjustedColor].solid)
|
||||
rect1.setAttribute('stroke-width', '1')
|
||||
g.appendChild(rect1)
|
||||
|
||||
|
@ -125,18 +131,18 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
rect2.setAttribute('rx', '10')
|
||||
rect2.setAttribute('width', NOTE_SIZE.toString())
|
||||
rect2.setAttribute('height', bounds.height.toString())
|
||||
rect2.setAttribute('fill', colors.background)
|
||||
rect2.setAttribute('fill', theme.background)
|
||||
rect2.setAttribute('opacity', '.28')
|
||||
g.appendChild(rect2)
|
||||
|
||||
const textElm = getTextLabelSvgElement({
|
||||
editor: this.editor,
|
||||
shape,
|
||||
font,
|
||||
font: DefaultFontFamilies[shape.props.font],
|
||||
bounds,
|
||||
})
|
||||
|
||||
textElm.setAttribute('fill', colors.text)
|
||||
textElm.setAttribute('fill', theme.text)
|
||||
textElm.setAttribute('stroke', 'none')
|
||||
g.appendChild(textElm)
|
||||
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import { useValue } from '@tldraw/state'
|
||||
import { TLDefaultColorStyle, TLDefaultFillStyle } from '@tldraw/tlschema'
|
||||
import {
|
||||
TLDefaultColorStyle,
|
||||
TLDefaultColorTheme,
|
||||
TLDefaultFillStyle,
|
||||
getDefaultColorTheme,
|
||||
} from '@tldraw/tlschema'
|
||||
import * as React from 'react'
|
||||
import { HASH_PATERN_ZOOM_NAMES } from '../../../constants'
|
||||
import { HASH_PATTERN_ZOOM_NAMES } from '../../../constants'
|
||||
import { useEditor } from '../../../hooks/useEditor'
|
||||
import { TLExportColors } from './TLExportColors'
|
||||
|
||||
export interface ShapeFillProps {
|
||||
d: string
|
||||
|
@ -11,18 +15,22 @@ export interface ShapeFillProps {
|
|||
color: TLDefaultColorStyle
|
||||
}
|
||||
|
||||
export function useDefaultColorTheme() {
|
||||
const editor = useEditor()
|
||||
return getDefaultColorTheme(editor)
|
||||
}
|
||||
|
||||
export const ShapeFill = React.memo(function ShapeFill({ d, color, fill }: ShapeFillProps) {
|
||||
const theme = useDefaultColorTheme()
|
||||
switch (fill) {
|
||||
case 'none': {
|
||||
return <path className={'tl-hitarea-stroke'} fill="none" d={d} />
|
||||
}
|
||||
case 'solid': {
|
||||
return (
|
||||
<path className={'tl-hitarea-fill-solid'} fill={`var(--palette-${color}-semi)`} d={d} />
|
||||
)
|
||||
return <path className={'tl-hitarea-fill-solid'} fill={theme[color].semi} d={d} />
|
||||
}
|
||||
case 'semi': {
|
||||
return <path className={'tl-hitarea-fill-solid'} fill={`var(--palette-solid)`} d={d} />
|
||||
return <path className={'tl-hitarea-fill-solid'} fill={theme.solid} d={d} />
|
||||
}
|
||||
case 'pattern': {
|
||||
return <PatternFill color={color} fill={fill} d={d} />
|
||||
|
@ -32,6 +40,7 @@ export const ShapeFill = React.memo(function ShapeFill({ d, color, fill }: Shape
|
|||
|
||||
const PatternFill = function PatternFill({ d, color }: ShapeFillProps) {
|
||||
const editor = useEditor()
|
||||
const theme = useDefaultColorTheme()
|
||||
const zoomLevel = useValue('zoomLevel', () => editor.zoomLevel, [editor])
|
||||
const isDarkMode = useValue('isDarkMode', () => editor.isDarkMode, [editor])
|
||||
|
||||
|
@ -40,12 +49,12 @@ const PatternFill = function PatternFill({ d, color }: ShapeFillProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<path className={'tl-hitarea-fill-solid'} fill={`var(--palette-${color}-pattern)`} d={d} />
|
||||
<path className={'tl-hitarea-fill-solid'} fill={theme[color].pattern} d={d} />
|
||||
<path
|
||||
fill={
|
||||
teenyTiny
|
||||
? `var(--palette-${color}-semi)`
|
||||
: `url(#${HASH_PATERN_ZOOM_NAMES[intZoom + (isDarkMode ? '_dark' : '_light')]})`
|
||||
? theme[color].semi
|
||||
: `url(#${HASH_PATTERN_ZOOM_NAMES[intZoom + (isDarkMode ? '_dark' : '_light')]})`
|
||||
}
|
||||
d={d}
|
||||
/>
|
||||
|
@ -57,8 +66,8 @@ export function getShapeFillSvg({
|
|||
d,
|
||||
color,
|
||||
fill,
|
||||
colors,
|
||||
}: ShapeFillProps & { colors: TLExportColors }) {
|
||||
theme,
|
||||
}: ShapeFillProps & { theme: TLDefaultColorTheme }) {
|
||||
if (fill === 'none') {
|
||||
return
|
||||
}
|
||||
|
@ -67,7 +76,7 @@ export function getShapeFillSvg({
|
|||
const gEl = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||
const path1El = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
path1El.setAttribute('d', d)
|
||||
path1El.setAttribute('fill', colors.pattern[color])
|
||||
path1El.setAttribute('fill', theme[color].pattern)
|
||||
|
||||
const path2El = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
path2El.setAttribute('d', d)
|
||||
|
@ -83,12 +92,12 @@ export function getShapeFillSvg({
|
|||
|
||||
switch (fill) {
|
||||
case 'semi': {
|
||||
path.setAttribute('fill', colors.solid)
|
||||
path.setAttribute('fill', theme.solid)
|
||||
break
|
||||
}
|
||||
case 'solid': {
|
||||
{
|
||||
path.setAttribute('fill', colors.semi[color])
|
||||
path.setAttribute('fill', theme[color].semi)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
export interface SvgExportDef {
|
||||
key: string
|
||||
getElement: () => Promise<SVGElement | SVGElement[] | null> | SVGElement | SVGElement[] | null
|
||||
}
|
||||
|
||||
export interface SvgExportContext {
|
||||
/**
|
||||
* Add contents to the <defs> section of the export SVG. Each export def should have a unique
|
||||
* key. If multiple defs come with the same key, only one will be added.
|
||||
*/
|
||||
addExportDef(def: SvgExportDef): void
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { TLDefaultColorStyle } from '@tldraw/tlschema'
|
||||
|
||||
export type TLExportColors = {
|
||||
fill: Record<TLDefaultColorStyle, string>
|
||||
pattern: Record<TLDefaultColorStyle, string>
|
||||
semi: Record<TLDefaultColorStyle, string>
|
||||
highlight: Record<TLDefaultColorStyle, string>
|
||||
solid: string
|
||||
text: string
|
||||
background: string
|
||||
}
|
|
@ -11,6 +11,7 @@ import React from 'react'
|
|||
import { stopEventPropagation } from '../../../utils/dom'
|
||||
import { isLegacyAlign } from '../../../utils/legacy'
|
||||
import { TextHelpers } from '../text/TextHelpers'
|
||||
import { useDefaultColorTheme } from './ShapeFill'
|
||||
import { LABEL_FONT_SIZES, TEXT_PROPS } from './default-shape-constants'
|
||||
import { useEditableText } from './useEditableText'
|
||||
|
||||
|
@ -53,6 +54,7 @@ export const TextLabel = React.memo(function TextLabel<
|
|||
const finalText = TextHelpers.normalizeTextForDom(text)
|
||||
const hasText = finalText.trim().length > 0
|
||||
const legacyAlign = isLegacyAlign(align)
|
||||
const theme = useDefaultColorTheme()
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -78,7 +80,7 @@ export const TextLabel = React.memo(function TextLabel<
|
|||
lineHeight: LABEL_FONT_SIZES[size] * TEXT_PROPS.lineHeight + 'px',
|
||||
minHeight: isEmpty ? LABEL_FONT_SIZES[size] * TEXT_PROPS.lineHeight + 32 : 0,
|
||||
minWidth: isEmpty ? 33 : 0,
|
||||
color: `var(--palette-${labelColor})`,
|
||||
color: theme[labelColor].solid,
|
||||
}}
|
||||
>
|
||||
<div className="tl-text tl-text-content" dir="ltr">
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
import {
|
||||
DefaultColorThemePalette,
|
||||
DefaultFontFamilies,
|
||||
DefaultFontStyle,
|
||||
TLDefaultColorTheme,
|
||||
TLDefaultFillStyle,
|
||||
TLDefaultFontStyle,
|
||||
} from '@tldraw/tlschema'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { HASH_PATTERN_ZOOM_NAMES, MAX_ZOOM } from '../../../constants'
|
||||
import { useEditor } from '../../../hooks/useEditor'
|
||||
import { debugFlags } from '../../../utils/debug-flags'
|
||||
import { TLShapeUtilCanvasSvgDef } from '../ShapeUtil'
|
||||
import { SvgExportDef } from './SvgExportContext'
|
||||
|
||||
/** @public */
|
||||
export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef {
|
||||
return {
|
||||
key: `${DefaultFontStyle.id}:${fontStyle}`,
|
||||
getElement: async () => {
|
||||
const font = findFont(fontStyle)
|
||||
if (!font) return null
|
||||
|
||||
const url = (font as any).$$_url
|
||||
const fontFaceRule = (font as any).$$_fontface
|
||||
if (!url || !fontFaceRule) return null
|
||||
|
||||
const fontFile = await (await fetch(url)).blob()
|
||||
const base64FontFile = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(reader.result as string)
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(fontFile)
|
||||
})
|
||||
|
||||
const newFontFaceRule = fontFaceRule.replace(url, base64FontFile)
|
||||
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
||||
style.textContent = newFontFaceRule
|
||||
return style
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function findFont(name: TLDefaultFontStyle): FontFace | null {
|
||||
const fontFamily = DefaultFontFamilies[name]
|
||||
for (const font of document.fonts) {
|
||||
if (fontFamily.includes(font.family)) {
|
||||
return font
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function getFillDefForExport(
|
||||
fill: TLDefaultFillStyle,
|
||||
theme: TLDefaultColorTheme
|
||||
): SvgExportDef {
|
||||
return {
|
||||
key: `${DefaultFontStyle.id}:${fill}`,
|
||||
getElement: async () => {
|
||||
if (fill !== 'pattern') return null
|
||||
|
||||
const t = 8 / 12
|
||||
const divEl = document.createElement('div')
|
||||
divEl.innerHTML = `
|
||||
<svg>
|
||||
<defs>
|
||||
<mask id="hash_pattern_mask">
|
||||
<rect x="0" y="0" width="8" height="8" fill="white" />
|
||||
<g
|
||||
strokeLinecap="round"
|
||||
stroke="black"
|
||||
>
|
||||
<line x1="${t * 1}" y1="${t * 3}" x2="${t * 3}" y2="${t * 1}" />
|
||||
<line x1="${t * 5}" y1="${t * 7}" x2="${t * 7}" y2="${t * 5}" />
|
||||
<line x1="${t * 9}" y1="${t * 11}" x2="${t * 11}" y2="${t * 9}" />
|
||||
</g>
|
||||
</mask>
|
||||
<pattern
|
||||
id="hash_pattern"
|
||||
width="8"
|
||||
height="8"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<rect x="0" y="0" width="8" height="8" fill="${theme.solid}" mask="url(#hash_pattern_mask)" />
|
||||
</pattern>
|
||||
</defs>
|
||||
</svg>
|
||||
`
|
||||
return Array.from(divEl.querySelectorAll('defs > *'))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function getFillDefForCanvas(): TLShapeUtilCanvasSvgDef {
|
||||
return {
|
||||
key: `${DefaultFontStyle.id}:pattern`,
|
||||
component: PatternFillDefForCanvas,
|
||||
}
|
||||
}
|
||||
const TILE_PATTERN_SIZE = 8
|
||||
|
||||
const generateImage = (dpr: number, currentZoom: number, darkMode: boolean) => {
|
||||
return new Promise<Blob>((resolve, reject) => {
|
||||
const size = TILE_PATTERN_SIZE * currentZoom * dpr
|
||||
|
||||
const canvasEl = document.createElement('canvas')
|
||||
canvasEl.width = size
|
||||
canvasEl.height = size
|
||||
|
||||
const ctx = canvasEl.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
ctx.fillStyle = darkMode ? '#212529' : '#f8f9fa'
|
||||
ctx.fillRect(0, 0, size, size)
|
||||
|
||||
// This essentially generates an inverse of the pattern we're drawing.
|
||||
ctx.globalCompositeOperation = 'destination-out'
|
||||
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineWidth = 1.25 * currentZoom * dpr
|
||||
|
||||
const t = 8 / 12
|
||||
const s = (v: number) => v * currentZoom * dpr
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(s(t * 1), s(t * 3))
|
||||
ctx.lineTo(s(t * 3), s(t * 1))
|
||||
|
||||
ctx.moveTo(s(t * 5), s(t * 7))
|
||||
ctx.lineTo(s(t * 7), s(t * 5))
|
||||
|
||||
ctx.moveTo(s(t * 9), s(t * 11))
|
||||
ctx.lineTo(s(t * 11), s(t * 9))
|
||||
ctx.stroke()
|
||||
|
||||
canvasEl.toBlob((blob) => {
|
||||
if (!blob || debugFlags.throwToBlob.value) {
|
||||
reject()
|
||||
} else {
|
||||
resolve(blob)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const canvasBlob = (size: [number, number], fn: (ctx: CanvasRenderingContext2D) => void) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = size[0]
|
||||
canvas.height = size[1]
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return ''
|
||||
fn(ctx)
|
||||
return canvas.toDataURL()
|
||||
}
|
||||
type PatternDef = { zoom: number; url: string; darkMode: boolean }
|
||||
|
||||
const getDefaultPatterns = () => {
|
||||
const defaultPatterns: PatternDef[] = []
|
||||
for (let i = 1; i <= Math.ceil(MAX_ZOOM); i++) {
|
||||
const whitePixelBlob = canvasBlob([1, 1], (ctx) => {
|
||||
ctx.fillStyle = DefaultColorThemePalette.lightMode.black.semi
|
||||
ctx.fillRect(0, 0, 1, 1)
|
||||
})
|
||||
const blackPixelBlob = canvasBlob([1, 1], (ctx) => {
|
||||
ctx.fillStyle = DefaultColorThemePalette.darkMode.black.semi
|
||||
ctx.fillRect(0, 0, 1, 1)
|
||||
})
|
||||
defaultPatterns.push({
|
||||
zoom: i,
|
||||
url: whitePixelBlob,
|
||||
darkMode: false,
|
||||
})
|
||||
defaultPatterns.push({
|
||||
zoom: i,
|
||||
url: blackPixelBlob,
|
||||
darkMode: true,
|
||||
})
|
||||
}
|
||||
return defaultPatterns
|
||||
}
|
||||
|
||||
function usePattern() {
|
||||
const editor = useEditor()
|
||||
const dpr = editor.devicePixelRatio
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const defaultPatterns = useMemo(() => getDefaultPatterns(), [])
|
||||
const [backgroundUrls, setBackgroundUrls] = useState<PatternDef[]>(defaultPatterns)
|
||||
|
||||
useEffect(() => {
|
||||
const promises: Promise<{ zoom: number; url: string; darkMode: boolean }>[] = []
|
||||
|
||||
for (let i = 1; i <= Math.ceil(MAX_ZOOM); i++) {
|
||||
promises.push(
|
||||
generateImage(dpr, i, false).then((blob) => ({
|
||||
zoom: i,
|
||||
url: URL.createObjectURL(blob),
|
||||
darkMode: false,
|
||||
}))
|
||||
)
|
||||
promises.push(
|
||||
generateImage(dpr, i, true).then((blob) => ({
|
||||
zoom: i,
|
||||
url: URL.createObjectURL(blob),
|
||||
darkMode: true,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
let isCancelled = false
|
||||
Promise.all(promises).then((urls) => {
|
||||
if (isCancelled) return
|
||||
setBackgroundUrls(urls)
|
||||
setIsReady(true)
|
||||
})
|
||||
|
||||
return () => {
|
||||
isCancelled = true
|
||||
setIsReady(false)
|
||||
}
|
||||
}, [dpr])
|
||||
|
||||
const defs = (
|
||||
<>
|
||||
{backgroundUrls.map((item) => {
|
||||
const key = item.zoom + (item.darkMode ? '_dark' : '_light')
|
||||
return (
|
||||
<pattern
|
||||
key={key}
|
||||
id={HASH_PATTERN_ZOOM_NAMES[key]}
|
||||
width={TILE_PATTERN_SIZE}
|
||||
height={TILE_PATTERN_SIZE}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<image href={item.url} width={TILE_PATTERN_SIZE} height={TILE_PATTERN_SIZE} />
|
||||
</pattern>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
|
||||
return { defs, isReady }
|
||||
}
|
||||
|
||||
function PatternFillDefForCanvas() {
|
||||
const editor = useEditor()
|
||||
const containerRef = useRef<SVGGElement>(null)
|
||||
const { defs, isReady } = usePattern()
|
||||
|
||||
useEffect(() => {
|
||||
if (isReady && editor.isSafari) {
|
||||
const htmlLayer = findHtmlLayerParent(containerRef.current!)
|
||||
if (htmlLayer) {
|
||||
// Wait for `patternContext` to be picked up
|
||||
requestAnimationFrame(() => {
|
||||
htmlLayer.style.display = 'none'
|
||||
|
||||
// Wait for 'display = "none"' to take effect
|
||||
requestAnimationFrame(() => {
|
||||
htmlLayer.style.display = ''
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [editor, isReady])
|
||||
|
||||
return <g ref={containerRef}>{defs}</g>
|
||||
}
|
||||
|
||||
function findHtmlLayerParent(element: Element): HTMLElement | null {
|
||||
if (element.classList.contains('tl-html-layer')) return element as HTMLElement
|
||||
if (element.parentElement) return findHtmlLayerParent(element.parentElement)
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { useValue } from '@tldraw/state'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { debugFlags } from '../../../utils/debug-flags'
|
||||
|
||||
export function useColorSpace(): 'srgb' | 'p3' {
|
||||
const [supportsP3, setSupportsP3] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const supportsSyntax = CSS.supports('color', 'color(display-p3 1 1 1)')
|
||||
const query = matchMedia('(color-gamut: p3)')
|
||||
setSupportsP3(supportsSyntax && query.matches)
|
||||
|
||||
const onChange = () => setSupportsP3(supportsSyntax && query.matches)
|
||||
|
||||
query.addEventListener('change', onChange)
|
||||
return () => query.removeEventListener('change', onChange)
|
||||
}, [])
|
||||
|
||||
const forceSrgb = useValue(debugFlags.forceSrgb)
|
||||
|
||||
return forceSrgb || !supportsP3 ? 'srgb' : 'p3'
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives'
|
||||
import { TLTextShape } from '@tldraw/tlschema'
|
||||
import { DefaultFontFamilies, getDefaultColorTheme, TLTextShape } from '@tldraw/tlschema'
|
||||
import { HTMLContainer } from '../../../components/HTMLContainer'
|
||||
import { stopEventPropagation } from '../../../utils/dom'
|
||||
import { WeakMapCache } from '../../../utils/WeakMapCache'
|
||||
|
@ -8,8 +8,9 @@ import { Editor } from '../../Editor'
|
|||
import { ShapeUtil, TLOnEditEndHandler, TLOnResizeHandler, TLShapeUtilFlag } from '../ShapeUtil'
|
||||
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
||||
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
||||
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
||||
import { resizeScaled } from '../shared/resizeScaled'
|
||||
import { TLExportColors } from '../shared/TLExportColors'
|
||||
import { SvgExportContext } from '../shared/SvgExportContext'
|
||||
import { useEditableText } from '../shared/useEditableText'
|
||||
|
||||
export { INDENT } from './TextHelpers'
|
||||
|
@ -37,21 +38,6 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
}
|
||||
}
|
||||
|
||||
// @computed
|
||||
// private get minDimensionsCache() {
|
||||
// return this.editor.store.createSelectedComputedCache<
|
||||
// TLTextShape['props'],
|
||||
// { width: number; height: number },
|
||||
// TLTextShape
|
||||
// >(
|
||||
// 'text measure cache',
|
||||
// (shape) => {
|
||||
// return shape.props
|
||||
// },
|
||||
// (props) => getTextSize(this.editor, props)
|
||||
// )
|
||||
// }
|
||||
|
||||
getMinDimensions(shape: TLTextShape) {
|
||||
return sizeCache.get(shape.props, (props) => getTextSize(this.editor, props))
|
||||
}
|
||||
|
@ -149,7 +135,10 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
return <rect width={toDomPrecision(bounds.width)} height={toDomPrecision(bounds.height)} />
|
||||
}
|
||||
|
||||
toSvg(shape: TLTextShape, font: string | undefined, colors: TLExportColors) {
|
||||
toSvg(shape: TLTextShape, ctx: SvgExportContext) {
|
||||
ctx.addExportDef(getFontDefForExport(shape.props.font))
|
||||
|
||||
const theme = getDefaultColorTheme(this.editor)
|
||||
const bounds = this.getBounds(shape)
|
||||
const text = shape.props.text
|
||||
|
||||
|
@ -158,7 +147,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
|
||||
const opts = {
|
||||
fontSize: FONT_SIZES[shape.props.size],
|
||||
fontFamily: font!,
|
||||
fontFamily: DefaultFontFamilies[shape.props.font],
|
||||
textAlign: shape.props.align,
|
||||
verticalTextAlign: 'middle' as const,
|
||||
width,
|
||||
|
@ -170,7 +159,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
overflow: 'wrap' as const,
|
||||
}
|
||||
|
||||
const color = colors.fill[shape.props.color]
|
||||
const color = theme[shape.props.color].solid
|
||||
const groupEl = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||
|
||||
const textBgEl = createTextSvgElementFromSpans(
|
||||
|
@ -178,9 +167,9 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
this.editor.textMeasure.measureTextSpans(text, opts),
|
||||
{
|
||||
...opts,
|
||||
stroke: colors.background,
|
||||
stroke: theme.background,
|
||||
strokeWidth: 2,
|
||||
fill: colors.background,
|
||||
fill: theme.background,
|
||||
padding: 0,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1,181 +0,0 @@
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { HASH_PATERN_ZOOM_NAMES, MAX_ZOOM } from '../constants'
|
||||
import { debugFlags } from '../utils/debug-flags'
|
||||
import { useEditor } from './useEditor'
|
||||
|
||||
const TILE_PATTERN_SIZE = 8
|
||||
|
||||
const generateImage = (dpr: number, currentZoom: number, darkMode: boolean) => {
|
||||
return new Promise<Blob>((resolve, reject) => {
|
||||
const size = TILE_PATTERN_SIZE * currentZoom * dpr
|
||||
|
||||
const canvasEl = document.createElement('canvas')
|
||||
canvasEl.width = size
|
||||
canvasEl.height = size
|
||||
|
||||
const ctx = canvasEl.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
ctx.fillStyle = darkMode ? '#212529' : '#f8f9fa'
|
||||
ctx.fillRect(0, 0, size, size)
|
||||
|
||||
// This essentially generates an inverse of the pattern we're drawing.
|
||||
ctx.globalCompositeOperation = 'destination-out'
|
||||
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineWidth = 1.25 * currentZoom * dpr
|
||||
|
||||
const t = 8 / 12
|
||||
const s = (v: number) => v * currentZoom * dpr
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(s(t * 1), s(t * 3))
|
||||
ctx.lineTo(s(t * 3), s(t * 1))
|
||||
|
||||
ctx.moveTo(s(t * 5), s(t * 7))
|
||||
ctx.lineTo(s(t * 7), s(t * 5))
|
||||
|
||||
ctx.moveTo(s(t * 9), s(t * 11))
|
||||
ctx.lineTo(s(t * 11), s(t * 9))
|
||||
ctx.stroke()
|
||||
|
||||
canvasEl.toBlob((blob) => {
|
||||
if (!blob || debugFlags.throwToBlob.value) {
|
||||
reject()
|
||||
} else {
|
||||
resolve(blob)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const canvasBlob = (size: [number, number], fn: (ctx: CanvasRenderingContext2D) => void) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = size[0]
|
||||
canvas.height = size[1]
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return ''
|
||||
fn(ctx)
|
||||
return canvas.toDataURL()
|
||||
}
|
||||
type PatternDef = { zoom: number; url: string; darkMode: boolean }
|
||||
|
||||
const getDefaultPatterns = () => {
|
||||
const defaultPatterns: PatternDef[] = []
|
||||
for (let i = 1; i <= Math.ceil(MAX_ZOOM); i++) {
|
||||
const whitePixelBlob = canvasBlob([1, 1], (ctx) => {
|
||||
// Hard coded '--palette-black-semi'
|
||||
ctx.fillStyle = '#e8e8e8'
|
||||
ctx.fillRect(0, 0, 1, 1)
|
||||
})
|
||||
const blackPixelBlob = canvasBlob([1, 1], (ctx) => {
|
||||
// Hard coded '--palette-black-semi'
|
||||
ctx.fillStyle = '#2c3036'
|
||||
ctx.fillRect(0, 0, 1, 1)
|
||||
})
|
||||
defaultPatterns.push({
|
||||
zoom: i,
|
||||
url: whitePixelBlob,
|
||||
darkMode: false,
|
||||
})
|
||||
defaultPatterns.push({
|
||||
zoom: i,
|
||||
url: blackPixelBlob,
|
||||
darkMode: true,
|
||||
})
|
||||
}
|
||||
return defaultPatterns
|
||||
}
|
||||
|
||||
export const usePattern = () => {
|
||||
const editor = useEditor()
|
||||
const dpr = editor.devicePixelRatio
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const defaultPatterns = useMemo(() => getDefaultPatterns(), [])
|
||||
const [backgroundUrls, setBackgroundUrls] = useState<PatternDef[]>(defaultPatterns)
|
||||
|
||||
useEffect(() => {
|
||||
const promises: Promise<{ zoom: number; url: string; darkMode: boolean }>[] = []
|
||||
|
||||
for (let i = 1; i <= Math.ceil(MAX_ZOOM); i++) {
|
||||
promises.push(
|
||||
generateImage(dpr, i, false).then((blob) => ({
|
||||
zoom: i,
|
||||
url: URL.createObjectURL(blob),
|
||||
darkMode: false,
|
||||
}))
|
||||
)
|
||||
promises.push(
|
||||
generateImage(dpr, i, true).then((blob) => ({
|
||||
zoom: i,
|
||||
url: URL.createObjectURL(blob),
|
||||
darkMode: true,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
let isCancelled = false
|
||||
Promise.all(promises).then((urls) => {
|
||||
if (isCancelled) return
|
||||
setBackgroundUrls(urls)
|
||||
setIsReady(true)
|
||||
})
|
||||
|
||||
return () => {
|
||||
isCancelled = true
|
||||
setIsReady(false)
|
||||
}
|
||||
}, [dpr])
|
||||
|
||||
const context = (
|
||||
<>
|
||||
{backgroundUrls.map((item) => {
|
||||
const key = item.zoom + (item.darkMode ? '_dark' : '_light')
|
||||
return (
|
||||
<pattern
|
||||
key={key}
|
||||
id={HASH_PATERN_ZOOM_NAMES[key]}
|
||||
width={TILE_PATTERN_SIZE}
|
||||
height={TILE_PATTERN_SIZE}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<image href={item.url} width={TILE_PATTERN_SIZE} height={TILE_PATTERN_SIZE} />
|
||||
</pattern>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
|
||||
return { context, isReady }
|
||||
}
|
||||
|
||||
const t = 8 / 12
|
||||
export function exportPatternSvgDefs(backgroundColor: string) {
|
||||
const divEl = document.createElement('div')
|
||||
divEl.innerHTML = `
|
||||
<svg>
|
||||
<defs>
|
||||
<mask id="hash_pattern_mask">
|
||||
<rect x="0" y="0" width="8" height="8" fill="white" />
|
||||
<g
|
||||
strokeLinecap="round"
|
||||
stroke="black"
|
||||
>
|
||||
<line x1="${t * 1}" y1="${t * 3}" x2="${t * 3}" y2="${t * 1}" />
|
||||
<line x1="${t * 5}" y1="${t * 7}" x2="${t * 7}" y2="${t * 5}" />
|
||||
<line x1="${t * 9}" y1="${t * 11}" x2="${t * 11}" y2="${t * 9}" />
|
||||
</g>
|
||||
</mask>
|
||||
<pattern
|
||||
id="hash_pattern"
|
||||
width="8"
|
||||
height="8"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<rect x="0" y="0" width="8" height="8" fill="${backgroundColor}" mask="url(#hash_pattern_mask)" />
|
||||
</pattern>
|
||||
</defs>
|
||||
</svg>
|
||||
`
|
||||
return divEl.querySelectorAll('defs > *')!
|
||||
}
|
|
@ -12,6 +12,7 @@ exports[`Matches a snapshot: Basic SVG 1`] = `
|
|||
width="564"
|
||||
>
|
||||
<defs>
|
||||
<!--def: tldraw:font:pattern-->
|
||||
<mask
|
||||
id="hash_pattern_mask"
|
||||
>
|
||||
|
@ -69,7 +70,7 @@ exports[`Matches a snapshot: Basic SVG 1`] = `
|
|||
|
||||
|
||||
<rect
|
||||
fill=""
|
||||
fill="#fcfffe"
|
||||
height="8"
|
||||
mask="url(#hash_pattern_mask)"
|
||||
width="8"
|
||||
|
@ -79,7 +80,6 @@ exports[`Matches a snapshot: Basic SVG 1`] = `
|
|||
|
||||
|
||||
</pattern>
|
||||
<style />
|
||||
</defs>
|
||||
<g
|
||||
opacity="1"
|
||||
|
@ -89,20 +89,20 @@ exports[`Matches a snapshot: Basic SVG 1`] = `
|
|||
<path
|
||||
d="M0, 0L100, 0,100, 100,0, 100Z"
|
||||
fill="none"
|
||||
stroke=""
|
||||
stroke="#1d1d1d"
|
||||
stroke-width="3.5"
|
||||
/>
|
||||
<g>
|
||||
<text
|
||||
alignment-baseline="mathematical"
|
||||
dominant-baseline="mathematical"
|
||||
fill=""
|
||||
font-family=""
|
||||
fill="rgb(249, 250, 251)"
|
||||
font-family="'tldraw_draw', sans-serif"
|
||||
font-size="22px"
|
||||
font-style="normal"
|
||||
font-weight="normal"
|
||||
line-height="29.700000000000003px"
|
||||
stroke=""
|
||||
stroke="rgb(249, 250, 251)"
|
||||
stroke-width="2"
|
||||
>
|
||||
<tspan
|
||||
|
@ -116,8 +116,8 @@ exports[`Matches a snapshot: Basic SVG 1`] = `
|
|||
<text
|
||||
alignment-baseline="mathematical"
|
||||
dominant-baseline="mathematical"
|
||||
fill=""
|
||||
font-family=""
|
||||
fill="#1d1d1d"
|
||||
font-family="'tldraw_draw', sans-serif"
|
||||
font-size="22px"
|
||||
font-style="normal"
|
||||
font-weight="normal"
|
||||
|
@ -142,7 +142,7 @@ exports[`Matches a snapshot: Basic SVG 1`] = `
|
|||
<path
|
||||
d="M0, 0L50, 0,50, 50,0, 50Z"
|
||||
fill="none"
|
||||
stroke=""
|
||||
stroke="#1d1d1d"
|
||||
stroke-width="3.5"
|
||||
/>
|
||||
</g>
|
||||
|
@ -150,12 +150,24 @@ exports[`Matches a snapshot: Basic SVG 1`] = `
|
|||
opacity="1"
|
||||
transform="matrix(1, 0, 0, 1, 400, 400)"
|
||||
>
|
||||
<path
|
||||
d="M0, 0L100, 0,100, 100,0, 100Z"
|
||||
fill="none"
|
||||
stroke=""
|
||||
stroke-width="3.5"
|
||||
/>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
d="M0, 0L100, 0,100, 100,0, 100Z"
|
||||
fill="#494949"
|
||||
/>
|
||||
<path
|
||||
d="M0, 0L100, 0,100, 100,0, 100Z"
|
||||
fill="url(#hash_pattern)"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M0, 0L100, 0,100, 100,0, 100Z"
|
||||
fill="none"
|
||||
stroke="#1d1d1d"
|
||||
stroke-width="3.5"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</wrapper>
|
||||
|
|
|
@ -43,6 +43,7 @@ beforeEach(() => {
|
|||
props: {
|
||||
w: 100,
|
||||
h: 100,
|
||||
fill: 'pattern',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
|
@ -155,12 +155,26 @@ export function createTLSchema({ shapes }: {
|
|||
// @public (undocumented)
|
||||
export const DefaultColorStyle: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
|
||||
|
||||
// @public (undocumented)
|
||||
export const DefaultColorThemePalette: {
|
||||
lightMode: TLDefaultColorTheme;
|
||||
darkMode: TLDefaultColorTheme;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const DefaultDashStyle: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
||||
|
||||
// @public (undocumented)
|
||||
export const DefaultFillStyle: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
||||
|
||||
// @public (undocumented)
|
||||
export const DefaultFontFamilies: {
|
||||
draw: string;
|
||||
sans: string;
|
||||
serif: string;
|
||||
mono: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const DefaultFontStyle: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
||||
|
||||
|
@ -474,6 +488,11 @@ export const geoShapeProps: {
|
|||
text: T.Validator<string>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export function getDefaultColorTheme(opts: {
|
||||
isDarkMode: boolean;
|
||||
}): TLDefaultColorTheme;
|
||||
|
||||
// @public (undocumented)
|
||||
export function getDefaultTranslationLocale(): TLLanguage['locale'];
|
||||
|
||||
|
@ -850,6 +869,24 @@ export type TLCursorType = SetValue<typeof TL_CURSOR_TYPES>;
|
|||
// @public (undocumented)
|
||||
export type TLDefaultColorStyle = T.TypeOf<typeof DefaultColorStyle>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLDefaultColorTheme = Expand<{
|
||||
text: string;
|
||||
background: string;
|
||||
solid: string;
|
||||
} & Record<(typeof colors)[number], TLDefaultColorThemeColor>>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLDefaultColorThemeColor = {
|
||||
solid: string;
|
||||
semi: string;
|
||||
pattern: string;
|
||||
highlight: {
|
||||
srgb: string;
|
||||
p3: string;
|
||||
};
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLDefaultDashStyle = T.TypeOf<typeof DefaultDashStyle>;
|
||||
|
||||
|
|
|
@ -137,10 +137,21 @@ export {
|
|||
} from './shapes/TLTextShape'
|
||||
export { videoShapeMigrations, videoShapeProps, type TLVideoShape } from './shapes/TLVideoShape'
|
||||
export { EnumStyleProp, StyleProp } from './styles/StyleProp'
|
||||
export { DefaultColorStyle, type TLDefaultColorStyle } from './styles/TLColorStyle'
|
||||
export {
|
||||
DefaultColorStyle,
|
||||
DefaultColorThemePalette,
|
||||
getDefaultColorTheme,
|
||||
type TLDefaultColorStyle,
|
||||
type TLDefaultColorTheme,
|
||||
type TLDefaultColorThemeColor,
|
||||
} from './styles/TLColorStyle'
|
||||
export { DefaultDashStyle, type TLDefaultDashStyle } from './styles/TLDashStyle'
|
||||
export { DefaultFillStyle, type TLDefaultFillStyle } from './styles/TLFillStyle'
|
||||
export { DefaultFontStyle, type TLDefaultFontStyle } from './styles/TLFontStyle'
|
||||
export {
|
||||
DefaultFontFamilies,
|
||||
DefaultFontStyle,
|
||||
type TLDefaultFontStyle,
|
||||
} from './styles/TLFontStyle'
|
||||
export {
|
||||
DefaultHorizontalAlignStyle,
|
||||
type TLDefaultHorizontalAlignStyle,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Expand } from '@tldraw/utils'
|
||||
import { T } from '@tldraw/validate'
|
||||
import { StyleProp } from './StyleProp'
|
||||
|
||||
|
@ -16,6 +17,266 @@ const colors = [
|
|||
'red',
|
||||
] as const
|
||||
|
||||
/** @public */
|
||||
export type TLDefaultColorThemeColor = {
|
||||
solid: string
|
||||
semi: string
|
||||
pattern: string
|
||||
highlight: {
|
||||
srgb: string
|
||||
p3: string
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type TLDefaultColorTheme = Expand<
|
||||
{
|
||||
text: string
|
||||
background: string
|
||||
solid: string
|
||||
} & Record<(typeof colors)[number], TLDefaultColorThemeColor>
|
||||
>
|
||||
|
||||
/** @public */
|
||||
export const DefaultColorThemePalette: {
|
||||
lightMode: TLDefaultColorTheme
|
||||
darkMode: TLDefaultColorTheme
|
||||
} = {
|
||||
lightMode: {
|
||||
text: '#000000',
|
||||
background: 'rgb(249, 250, 251)',
|
||||
solid: '#fcfffe',
|
||||
|
||||
black: {
|
||||
solid: '#1d1d1d',
|
||||
semi: '#e8e8e8',
|
||||
pattern: '#494949',
|
||||
highlight: {
|
||||
srgb: '#fddd00',
|
||||
p3: 'color(display-p3 0.972 0.8705 0.05)',
|
||||
},
|
||||
},
|
||||
blue: {
|
||||
solid: '#4263eb',
|
||||
semi: '#dce1f8',
|
||||
pattern: '#6681ee',
|
||||
highlight: {
|
||||
srgb: '#10acff',
|
||||
p3: 'color(display-p3 0.308 0.6632 0.9996)',
|
||||
},
|
||||
},
|
||||
green: {
|
||||
solid: '#099268',
|
||||
semi: '#d3e9e3',
|
||||
pattern: '#39a785',
|
||||
highlight: {
|
||||
srgb: '#00ffc8',
|
||||
p3: 'color(display-p3 0.2536 0.984 0.7981)',
|
||||
},
|
||||
},
|
||||
grey: {
|
||||
solid: '#adb5bd',
|
||||
semi: '#eceef0',
|
||||
pattern: '#bcc3c9',
|
||||
highlight: {
|
||||
srgb: '#cbe7f1',
|
||||
p3: 'color(display-p3 0.8163 0.9023 0.9416)',
|
||||
},
|
||||
},
|
||||
'light-blue': {
|
||||
solid: '#4dabf7',
|
||||
semi: '#ddedfa',
|
||||
pattern: '#6fbbf8',
|
||||
highlight: {
|
||||
srgb: '#00f4ff',
|
||||
p3: 'color(display-p3 0.1512 0.9414 0.9996)',
|
||||
},
|
||||
},
|
||||
'light-green': {
|
||||
solid: '#40c057',
|
||||
semi: '#dbf0e0',
|
||||
pattern: '#65cb78',
|
||||
highlight: {
|
||||
srgb: '#65f641',
|
||||
p3: 'color(display-p3 0.563 0.9495 0.3857)',
|
||||
},
|
||||
},
|
||||
'light-red': {
|
||||
solid: '#ff8787',
|
||||
semi: '#f4dadb',
|
||||
pattern: '#fe9e9e',
|
||||
highlight: {
|
||||
srgb: '#ff7fa3',
|
||||
p3: 'color(display-p3 0.9988 0.5301 0.6397)',
|
||||
},
|
||||
},
|
||||
'light-violet': {
|
||||
solid: '#e599f7',
|
||||
semi: '#f5eafa',
|
||||
pattern: '#e9acf8',
|
||||
highlight: {
|
||||
srgb: '#ff88ff',
|
||||
p3: 'color(display-p3 0.9676 0.5652 0.9999)',
|
||||
},
|
||||
},
|
||||
orange: {
|
||||
solid: '#f76707',
|
||||
semi: '#f8e2d4',
|
||||
pattern: '#f78438',
|
||||
highlight: {
|
||||
srgb: '#ffa500',
|
||||
p3: 'color(display-p3 0.9988 0.6905 0.266)',
|
||||
},
|
||||
},
|
||||
red: {
|
||||
solid: '#e03131',
|
||||
semi: '#f4dadb',
|
||||
pattern: '#e55959',
|
||||
highlight: {
|
||||
srgb: '#ff636e',
|
||||
p3: 'color(display-p3 0.9992 0.4376 0.45)',
|
||||
},
|
||||
},
|
||||
violet: {
|
||||
solid: '#ae3ec9',
|
||||
semi: '#ecdcf2',
|
||||
pattern: '#bd63d3',
|
||||
highlight: {
|
||||
srgb: '#c77cff',
|
||||
p3: 'color(display-p3 0.7469 0.5089 0.9995)',
|
||||
},
|
||||
},
|
||||
yellow: {
|
||||
solid: '#ffc078',
|
||||
semi: '#f9f0e6',
|
||||
pattern: '#fecb92',
|
||||
highlight: {
|
||||
srgb: '#fddd00',
|
||||
p3: 'color(display-p3 0.972 0.8705 0.05)',
|
||||
},
|
||||
},
|
||||
},
|
||||
darkMode: {
|
||||
text: '#f8f9fa',
|
||||
background: '#212529',
|
||||
solid: '#28292e',
|
||||
|
||||
black: {
|
||||
solid: '#e1e1e1',
|
||||
semi: '#2c3036',
|
||||
pattern: '#989898',
|
||||
highlight: {
|
||||
srgb: '#d2b700',
|
||||
p3: 'color(display-p3 0.8078 0.7225 0.0312)',
|
||||
},
|
||||
},
|
||||
blue: {
|
||||
solid: '#4156be',
|
||||
semi: '#262d40',
|
||||
pattern: '#3a4b9e',
|
||||
highlight: {
|
||||
srgb: '#0079d2',
|
||||
p3: 'color(display-p3 0.0032 0.4655 0.7991)',
|
||||
},
|
||||
},
|
||||
green: {
|
||||
solid: '#3b7b5e',
|
||||
semi: '#253231',
|
||||
pattern: '#366a53',
|
||||
highlight: {
|
||||
srgb: '#009774',
|
||||
p3: 'color(display-p3 0.0085 0.582 0.4604)',
|
||||
},
|
||||
},
|
||||
grey: {
|
||||
solid: '#93989f',
|
||||
semi: '#33373c',
|
||||
pattern: '#7c8187',
|
||||
highlight: {
|
||||
srgb: '#9cb4cb',
|
||||
p3: 'color(display-p3 0.6299 0.7012 0.7856)',
|
||||
},
|
||||
},
|
||||
'light-blue': {
|
||||
solid: '#588fc9',
|
||||
semi: '#2a3642',
|
||||
pattern: '#4d7aa9',
|
||||
highlight: {
|
||||
srgb: '#00bdc8',
|
||||
p3: 'color(display-p3 0.0023 0.7259 0.7735)',
|
||||
},
|
||||
},
|
||||
'light-green': {
|
||||
solid: '#599f57',
|
||||
semi: '#2a3830',
|
||||
pattern: '#4e874e',
|
||||
highlight: {
|
||||
srgb: '#00a000',
|
||||
p3: 'color(display-p3 0.2711 0.6172 0.0195)',
|
||||
},
|
||||
},
|
||||
'light-red': {
|
||||
solid: '#c67877',
|
||||
semi: '#3b3235',
|
||||
pattern: '#a56767',
|
||||
highlight: {
|
||||
srgb: '#db005b',
|
||||
p3: 'color(display-p3 0.7849 0.0585 0.3589)',
|
||||
},
|
||||
},
|
||||
'light-violet': {
|
||||
solid: '#b583c9',
|
||||
semi: '#383442',
|
||||
pattern: '#9770a9',
|
||||
highlight: {
|
||||
srgb: '#c400c7',
|
||||
p3: 'color(display-p3 0.7024 0.0403 0.753)',
|
||||
},
|
||||
},
|
||||
orange: {
|
||||
solid: '#bf612e',
|
||||
semi: '#3a2e2a',
|
||||
pattern: '#9f552d',
|
||||
highlight: {
|
||||
srgb: '#d07a00',
|
||||
p3: 'color(display-p3 0.7699 0.4937 0.0085)',
|
||||
},
|
||||
},
|
||||
red: {
|
||||
solid: '#aa3c37',
|
||||
semi: '#36292b',
|
||||
pattern: '#8f3734',
|
||||
highlight: {
|
||||
srgb: '#de002c',
|
||||
p3: 'color(display-p3 0.7978 0.0509 0.2035)',
|
||||
},
|
||||
},
|
||||
violet: {
|
||||
solid: '#873fa3',
|
||||
semi: '#31293c',
|
||||
pattern: '#763a8b',
|
||||
highlight: {
|
||||
srgb: '#9e00ee',
|
||||
p3: 'color(display-p3 0.5651 0.0079 0.8986)',
|
||||
},
|
||||
},
|
||||
yellow: {
|
||||
solid: '#cba371',
|
||||
semi: '#3c3934',
|
||||
pattern: '#fecb92',
|
||||
highlight: {
|
||||
srgb: '#d2b700',
|
||||
p3: 'color(display-p3 0.8078 0.7225 0.0312)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function getDefaultColorTheme(opts: { isDarkMode: boolean }): TLDefaultColorTheme {
|
||||
return opts.isDarkMode ? DefaultColorThemePalette.darkMode : DefaultColorThemePalette.lightMode
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const DefaultColorStyle = StyleProp.defineEnum('tldraw:color', {
|
||||
defaultValue: 'black',
|
||||
|
|
|
@ -9,3 +9,11 @@ export const DefaultFontStyle = StyleProp.defineEnum('tldraw:font', {
|
|||
|
||||
/** @public */
|
||||
export type TLDefaultFontStyle = T.TypeOf<typeof DefaultFontStyle>
|
||||
|
||||
/** @public */
|
||||
export const DefaultFontFamilies = {
|
||||
draw: "'tldraw_draw', sans-serif",
|
||||
sans: "'tldraw_sans', sans-serif",
|
||||
serif: "'tldraw_serif', serif",
|
||||
mono: "'tldraw_mono', monospace",
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { DefaultColorStyle, useEditor } from '@tldraw/editor'
|
||||
import { DefaultColorStyle, getDefaultColorTheme, useEditor } from '@tldraw/editor'
|
||||
import { useValue } from '@tldraw/state'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
|
@ -17,7 +17,8 @@ export function MobileStylePanel() {
|
|||
const color = editor.sharedStyles.get(DefaultColorStyle)
|
||||
if (!color) return 'var(--color-muted-1)'
|
||||
if (color.type === 'mixed') return null
|
||||
return `var(--palette-${color})`
|
||||
const theme = getDefaultColorTheme(editor)
|
||||
return theme[color.value].solid
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { DefaultColorStyle, SharedStyle, StyleProp, useEditor } from '@tldraw/editor'
|
||||
import {
|
||||
DefaultColorStyle,
|
||||
SharedStyle,
|
||||
StyleProp,
|
||||
TLDefaultColorStyle,
|
||||
getDefaultColorTheme,
|
||||
useEditor,
|
||||
useValue,
|
||||
} from '@tldraw/editor'
|
||||
import { clamp } from '@tldraw/primitives'
|
||||
import classNames from 'classnames'
|
||||
import * as React from 'react'
|
||||
|
@ -84,6 +92,8 @@ function _ButtonPicker<T extends string>(props: ButtonPickerProps<T>) {
|
|||
}
|
||||
}, [value, editor, onValueChange, style])
|
||||
|
||||
const theme = useValue('theme', () => getDefaultColorTheme(editor), [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('tlui-button-grid', {
|
||||
|
@ -103,7 +113,7 @@ function _ButtonPicker<T extends string>(props: ButtonPickerProps<T>) {
|
|||
className={classNames('tlui-button-grid__button')}
|
||||
style={
|
||||
style === (DefaultColorStyle as StyleProp<unknown>)
|
||||
? { color: `var(--palette-${item.value})` }
|
||||
? { color: theme[item.value as TLDefaultColorStyle].solid }
|
||||
: undefined
|
||||
}
|
||||
onPointerEnter={handleButtonPointerEnter}
|
||||
|
|
|
@ -3172,19 +3172,19 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@playwright/test@npm:^1.34.3":
|
||||
version: 1.34.3
|
||||
resolution: "@playwright/test@npm:1.34.3"
|
||||
"@playwright/test@npm:^1.35.1":
|
||||
version: 1.35.1
|
||||
resolution: "@playwright/test@npm:1.35.1"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
fsevents: 2.3.2
|
||||
playwright-core: 1.34.3
|
||||
playwright-core: 1.35.1
|
||||
dependenciesMeta:
|
||||
fsevents:
|
||||
optional: true
|
||||
bin:
|
||||
playwright: cli.js
|
||||
checksum: b387d85f09d275ed49aac4f600c0657205b4c20979108a52e7072ef0c43ea9b2d3d6d7206869230634680ab1f8e7b03373c63555899da4293cae700320bd31e1
|
||||
checksum: 3509d2f2c7397f9b0d4f49088cab8625f17d186f7e9b3389ddebf7c52ee8aae6407eab48f66b300b7bf6a33f6e3533fd5951e72bfdb001b68838af98596d5a53
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -9140,7 +9140,7 @@ __metadata:
|
|||
resolution: "examples.tldraw.com@workspace:apps/examples"
|
||||
dependencies:
|
||||
"@babel/plugin-proposal-decorators": ^7.21.0
|
||||
"@playwright/test": ^1.34.3
|
||||
"@playwright/test": ^1.35.1
|
||||
"@tldraw/assets": "workspace:*"
|
||||
"@tldraw/state": "workspace:*"
|
||||
"@tldraw/tldraw": "workspace:*"
|
||||
|
@ -14372,12 +14372,12 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"playwright-core@npm:1.34.3":
|
||||
version: 1.34.3
|
||||
resolution: "playwright-core@npm:1.34.3"
|
||||
"playwright-core@npm:1.35.1":
|
||||
version: 1.35.1
|
||||
resolution: "playwright-core@npm:1.35.1"
|
||||
bin:
|
||||
playwright-core: cli.js
|
||||
checksum: eaf9e9b2d77b9726867dcbc641a1c72b0e8f680cdd71ff904366deea1c96141ff7563f6c6fb29f9975309d1b87dead97ea93f6f44953b59946882fb785b34867
|
||||
checksum: 179abc0051f00474e528935b507fa8cedc986b2803b020d7679878ba28cdd7036ad5a779792aad2ad281f8dc625eb1d2fb77663cb8de0d20c7ffbda7c18febdd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
Loading…
Reference in a new issue