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:
alex 2023-06-24 14:46:04 +01:00 committed by GitHub
parent 15ce98b277
commit e8bc114bf3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1126 additions and 873 deletions

View file

@ -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']) {

View file

@ -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()

View file

@ -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:*",

View file

@ -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',
}}
>

View file

@ -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()} 🍒🍊🍋🍏🫐🍇

View file

@ -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()}

View file

@ -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";
}

View file

@ -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));

View file

@ -6,8 +6,10 @@ global.FontFace = class FontFace {
return Promise.resolve()
}
}
document.fonts = {
add: () => {},
delete: () => {},
forEach: () => {},
[Symbol.iterator]: () => [][Symbol.iterator](),
}

View file

@ -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,

View file

@ -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 />

View file

@ -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(() => {

View file

@ -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 */

View file

@ -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
}

View file

@ -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
/**

View file

@ -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) {

View file

@ -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

View file

@ -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)

View file

@ -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 }

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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}`)

View file

@ -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])
}
}

View file

@ -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)

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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">

View file

@ -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
}

View file

@ -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'
}

View file

@ -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,
}
)

View file

@ -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 > *')!
}

View file

@ -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,13 +150,25 @@ exports[`Matches a snapshot: Basic SVG 1`] = `
opacity="1"
transform="matrix(1, 0, 0, 1, 400, 400)"
>
<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=""
stroke="#1d1d1d"
stroke-width="3.5"
/>
</g>
</g>
</svg>
</wrapper>
`;

View file

@ -43,6 +43,7 @@ beforeEach(() => {
props: {
w: 100,
h: 100,
fill: 'pattern',
},
},
])

View file

@ -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>;

View file

@ -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,

View file

@ -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',

View file

@ -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",
}

View file

@ -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]
)

View file

@ -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}

View file

@ -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