From e8bc114bf3ccd666079a704880622e300234bf88 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 24 Jun 2023 14:46:04 +0100 Subject: [PATCH] Styles API follow-ups (#1636) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `` 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 `` 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 --- apps/examples/e2e/shared-e2e.ts | 8 +- .../e2e/tests/export-snapshots.spec.ts | 1 - apps/examples/package.json | 2 +- .../src/15-custom-zones/ZonesExample.tsx | 5 +- .../src/16-custom-styles/CardShape.tsx | 6 +- .../CardShape/CardShapeUtil.tsx | 11 +- packages/editor/api-report.md | 40 +-- packages/editor/editor.css | 165 ----------- packages/editor/setupTests.js | 2 + packages/editor/src/index.ts | 2 +- packages/editor/src/lib/components/Canvas.tsx | 39 ++- .../lib/components/DefaultErrorFallback.tsx | 2 +- packages/editor/src/lib/constants.ts | 6 +- packages/editor/src/lib/editor/Editor.ts | 232 ++++----------- .../editor/src/lib/editor/shapes/ShapeUtil.ts | 55 ++-- .../editor/shapes/arrow/ArrowShapeUtil.tsx | 48 ++- .../lib/editor/shapes/draw/DrawShapeUtil.tsx | 29 +- .../editor/shapes/frame/FrameShapeUtil.tsx | 22 +- .../lib/editor/shapes/geo/GeoShapeUtil.tsx | 52 ++-- .../geo/components/DashStyleEllipse.tsx | 21 +- .../shapes/geo/components/DashStyleOval.tsx | 21 +- .../geo/components/DashStylePolygon.tsx | 28 +- .../geo/components/DrawStyleEllipse.tsx | 21 +- .../geo/components/DrawStylePolygon.tsx | 26 +- .../geo/components/SolidStyleEllipse.tsx | 21 +- .../shapes/geo/components/SolidStyleOval.tsx | 21 +- .../geo/components/SolidStylePolygon.tsx | 21 +- .../shapes/highlight/HighlightShapeUtil.tsx | 33 ++- .../lib/editor/shapes/line/LineShapeUtil.tsx | 33 +-- .../lib/editor/shapes/note/NoteShapeUtil.tsx | 28 +- .../lib/editor/shapes/shared/ShapeFill.tsx | 39 ++- .../editor/shapes/shared/SvgExportContext.tsx | 12 + .../editor/shapes/shared/TLExportColors.ts | 11 - .../lib/editor/shapes/shared/TextLabel.tsx | 4 +- .../editor/shapes/shared/defaultStyleDefs.tsx | 275 ++++++++++++++++++ .../editor/shapes/shared/useColorSpace.tsx | 22 ++ .../lib/editor/shapes/text/TextShapeUtil.tsx | 33 +-- packages/editor/src/lib/hooks/usePattern.tsx | 181 ------------ .../__snapshots__/getSvg.test.ts.snap | 60 ++-- .../src/lib/test/commands/getSvg.test.ts | 1 + packages/tlschema/api-report.md | 37 +++ packages/tlschema/src/index.ts | 15 +- packages/tlschema/src/styles/TLColorStyle.ts | 261 +++++++++++++++++ packages/tlschema/src/styles/TLFontStyle.ts | 8 + .../src/lib/components/MobileStylePanel.tsx | 5 +- .../components/primitives/ButtonPicker.tsx | 14 +- public-yarn.lock | 20 +- 47 files changed, 1126 insertions(+), 873 deletions(-) create mode 100644 packages/editor/src/lib/editor/shapes/shared/SvgExportContext.tsx delete mode 100644 packages/editor/src/lib/editor/shapes/shared/TLExportColors.ts create mode 100644 packages/editor/src/lib/editor/shapes/shared/defaultStyleDefs.tsx create mode 100644 packages/editor/src/lib/editor/shapes/shared/useColorSpace.tsx delete mode 100644 packages/editor/src/lib/hooks/usePattern.tsx diff --git a/apps/examples/e2e/shared-e2e.ts b/apps/examples/e2e/shared-e2e.ts index 6acd0830d..515584285 100644 --- a/apps/examples/e2e/shared-e2e.ts +++ b/apps/examples/e2e/shared-e2e.ts @@ -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']) { diff --git a/apps/examples/e2e/tests/export-snapshots.spec.ts b/apps/examples/e2e/tests/export-snapshots.spec.ts index bd104fc83..95314b9a0 100644 --- a/apps/examples/e2e/tests/export-snapshots.spec.ts +++ b/apps/examples/e2e/tests/export-snapshots.spec.ts @@ -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() diff --git a/apps/examples/package.json b/apps/examples/package.json index 01eb796d7..968e46adb 100644 --- a/apps/examples/package.json +++ b/apps/examples/package.json @@ -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:*", diff --git a/apps/examples/src/15-custom-zones/ZonesExample.tsx b/apps/examples/src/15-custom-zones/ZonesExample.tsx index 22143613b..cd9f70f1e 100644 --- a/apps/examples/src/15-custom-zones/ZonesExample.tsx +++ b/apps/examples/src/15-custom-zones/ZonesExample.tsx @@ -1,5 +1,6 @@ import { Tldraw } from '@tldraw/tldraw' import '@tldraw/tldraw/tldraw.css' + export default function Example() { return (
@@ -12,7 +13,7 @@ function CustomShareZone() { return (
diff --git a/apps/examples/src/16-custom-styles/CardShape.tsx b/apps/examples/src/16-custom-styles/CardShape.tsx index a352422a6..c4fb4080c 100644 --- a/apps/examples/src/16-custom-styles/CardShape.tsx +++ b/apps/examples/src/16-custom-styles/CardShape.tsx @@ -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 { component(shape: CardShape) { const bounds = this.editor.getBounds(shape) + const theme = getDefaultColorTheme(this.editor) return ( 🍇🫐🍏🍋🍊🍒 {bounds.w.toFixed()}x{bounds.h.toFixed()} 🍒🍊🍋🍏🫐🍇 diff --git a/apps/examples/src/3-custom-config/CardShape/CardShapeUtil.tsx b/apps/examples/src/3-custom-config/CardShape/CardShapeUtil.tsx index adde01bee..d1d24130d 100644 --- a/apps/examples/src/3-custom-config/CardShape/CardShapeUtil.tsx +++ b/apps/examples/src/3-custom-config/CardShape/CardShapeUtil.tsx @@ -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 { // 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 ( { 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()} diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 81859e2d4..01f13fa67 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -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 { // (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 { // (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 { // (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 { // (undocumented) onResize: TLOnResizeHandler; // (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 { getShapeById(id: TLParentId): T | undefined; getShapeIdsInPage(pageId: TLPageId): Set; getShapesAtPoint(point: VecLike): TLShape[]; + // (undocumented) + getShapeStyleIfExists(shape: TLShape, style: StyleProp): T | undefined; getShapeUtil; type: string; @@ -825,7 +830,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil { // (undocumented) providesBackgroundForChildren(): boolean; // (undocumented) - toSvg(shape: TLFrameShape, font: string, colors: TLExportColors): Promise | SVGElement; + toSvg(shape: TLFrameShape): Promise | SVGElement; // (undocumented) static type: "frame"; } @@ -842,6 +847,8 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { // (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 { // (undocumented) onResize: TLOnResizeHandler; // (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; +export const HASH_PATTERN_ZOOM_NAMES: Record; // @public (undocumented) export const HighlightShape: TLShapeInfo; @@ -1131,9 +1138,9 @@ export class HighlightShapeUtil extends ShapeUtil { // (undocumented) onResize: TLOnResizeHandler; // (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 { // (undocumented) onResize: TLOnResizeHandler; // (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 { // (undocumented) onEditEnd: TLOnEditEndHandler; // (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 { // @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(style: StyleProp, shape: Shape | TLShapePartial): T | undefined; - // (undocumented) - hasStyle(style: StyleProp): boolean; hideResizeHandles: TLShapeUtilFlag; hideRotateHandle: TLShapeUtilFlag; hideSelectionBoundsBg: TLShapeUtilFlag; @@ -1875,8 +1879,6 @@ export abstract class ShapeUtil { abstract indicator(shape: Shape): any; isAspectRatioLocked: TLShapeUtilFlag; isClosed: TLShapeUtilFlag; - // (undocumented) - iterateStyles(shape: Shape | TLShapePartial): Generator<[StyleProp, unknown], void, unknown>; onBeforeCreate?: TLOnBeforeCreateHandler; onBeforeUpdate?: TLOnBeforeUpdateHandler; // @internal @@ -1909,8 +1911,8 @@ export abstract class ShapeUtil { snapPoints(shape: Shape): Vec2d[]; // (undocumented) readonly styleProps: ReadonlyMap, string>; - toBackgroundSvg?(shape: Shape, font: string | undefined, colors: TLExportColors): null | Promise | SVGElement; - toSvg?(shape: Shape, font: string | undefined, colors: TLExportColors): Promise | SVGElement; + toBackgroundSvg?(shape: Shape, ctx: SvgExportContext): null | Promise | SVGElement; + toSvg?(shape: Shape, ctx: SvgExportContext): Promise | SVGElement; // (undocumented) readonly type: Shape['type']; static type: string; @@ -2115,7 +2117,7 @@ export class TextShapeUtil extends ShapeUtil { // (undocumented) onResize: TLOnResizeHandler; // (undocumented) - toSvg(shape: TLTextShape, font: string | undefined, colors: TLExportColors): SVGGElement; + toSvg(shape: TLTextShape, ctx: SvgExportContext): SVGGElement; // (undocumented) static type: "text"; } diff --git a/packages/editor/editor.css b/packages/editor/editor.css index eafee5ef6..cd814498f 100644 --- a/packages/editor/editor.css +++ b/packages/editor/editor.css @@ -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)); diff --git a/packages/editor/setupTests.js b/packages/editor/setupTests.js index b61be23d3..50569abdc 100644 --- a/packages/editor/setupTests.js +++ b/packages/editor/setupTests.js @@ -6,8 +6,10 @@ global.FontFace = class FontFace { return Promise.resolve() } } + document.fonts = { add: () => {}, delete: () => {}, forEach: () => {}, + [Symbol.iterator]: () => [][Symbol.iterator](), } diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index d97253fcd..214865d1d 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -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, diff --git a/packages/editor/src/lib/components/Canvas.tsx b/packages/editor/src/lib/components/Canvas.tsx index a57fb9700..826fe8c89 100644 --- a/packages/editor/src/lib/components/Canvas.tsx +++ b/packages/editor/src/lib/components/Canvas.tsx @@ -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() + 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, ) + } } - } - }, [editor, patternIsReady]) + return [...shapeSvgDefsByKey.values()] + }, + [editor] + ) React.useEffect(() => { rCanvas.current?.focus() }, []) - return (
{Background && } @@ -94,7 +89,7 @@ export const Canvas = track(function Canvas() {
- {patternContext} + {shapeSvgDefs} {Cursor && } diff --git a/packages/editor/src/lib/components/DefaultErrorFallback.tsx b/packages/editor/src/lib/components/DefaultErrorFallback.tsx index 9c867f436..1781cad0e 100644 --- a/packages/editor/src/lib/components/DefaultErrorFallback.tsx +++ b/packages/editor/src/lib/components/DefaultErrorFallback.tsx @@ -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(() => { diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index 11444f205..89b8653b2 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -70,11 +70,11 @@ export const DRAG_DISTANCE = 4 export const SVG_PADDING = 32 /** @internal */ -export const HASH_PATERN_ZOOM_NAMES: Record = {} +export const HASH_PATTERN_ZOOM_NAMES: Record = {} 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 */ diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 5559c6089..91839c08f 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -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 { } } 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 { return value === undefined ? style.defaultValue : (value as T) } + getShapeStyleIfExists(shape: TLShape, style: StyleProp): 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 { } = 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 { } } 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 { 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 { 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() - - 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 { 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>() + 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 { 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 { 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 { ) ).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((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 } diff --git a/packages/editor/src/lib/editor/shapes/ShapeUtil.ts b/packages/editor/src/lib/editor/shapes/ShapeUtil.ts index 712cdd6a7..14c142876 100644 --- a/packages/editor/src/lib/editor/shapes/ShapeUtil.ts +++ b/packages/editor/src/lib/editor/shapes/ShapeUtil.ts @@ -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 = (shape: T) => boolean +/** @public */ +export interface TLShapeUtilCanvasSvgDef { + key: string + component: React.ComponentType +} + /** @public */ export abstract class ShapeUtil { constructor( @@ -25,23 +31,6 @@ export abstract class ShapeUtil { public readonly styleProps: ReadonlyMap, string> ) {} - hasStyle(style: StyleProp) { - return this.styleProps.has(style) - } - - getStyleIfExists(style: StyleProp, shape: Shape | TLShapePartial): T | undefined { - const styleKey = this.styleProps.get(style) - if (!styleKey) return undefined - return (shape.props as any)[styleKey] - } - - *iterateStyles(shape: Shape | TLShapePartial) { - for (const [style, styleKey] of this.styleProps) { - const value = (shape.props as any)[styleKey] - yield [style, value] as [StyleProp, unknown] - } - } - setStyleInPartial( style: StyleProp, shape: TLShapePartial, @@ -307,31 +296,21 @@ export abstract class ShapeUtil { * 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. \s * @returns An SVG element. * @public */ - toSvg?( - shape: Shape, - font: string | undefined, - colors: TLExportColors - ): SVGElement | Promise + toSvg?(shape: Shape, ctx: SvgExportContext): SVGElement | Promise /** * 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. \s * @returns An SVG element. * @public */ - toBackgroundSvg?( - shape: Shape, - font: string | undefined, - colors: TLExportColors - ): SVGElement | Promise | null + toBackgroundSvg?(shape: Shape, ctx: SvgExportContext): SVGElement | Promise | null /** @internal */ expandSelectionOutlinePx(shape: Shape): number { @@ -371,6 +350,18 @@ export abstract class ShapeUtil { return false } + /** + * Return elements to be added to the \ 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 /** diff --git a/packages/editor/src/lib/editor/shapes/arrow/ArrowShapeUtil.tsx b/packages/editor/src/lib/editor/shapes/arrow/ArrowShapeUtil.tsx index 43d9ce867..f48bb0f9b 100644 --- a/packages/editor/src/lib/editor/shapes/arrow/ArrowShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapes/arrow/ArrowShapeUtil.tsx @@ -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 { 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 { )} { } } - 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 { shape.props.color, strokeWidth, shape.props.arrowheadStart === 'arrow' ? 'none' : shape.props.fill, - colors + theme ) ) } @@ -1038,17 +1052,19 @@ export class ArrowShapeUtil extends ShapeUtil { 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 { 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 { 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 { 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) { diff --git a/packages/editor/src/lib/editor/shapes/draw/DrawShapeUtil.tsx b/packages/editor/src/lib/editor/shapes/draw/DrawShapeUtil.tsx index be0965324..c365e5fa0 100644 --- a/packages/editor/src/lib/editor/shapes/draw/DrawShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapes/draw/DrawShapeUtil.tsx @@ -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 { } 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 { ) @@ -173,7 +175,7 @@ export class DrawShapeUtil extends ShapeUtil { 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 { return } - 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 { 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 { 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 { return foregroundPath } + getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] { + return [getFillDefForCanvas()] + } + override onResize: TLOnResizeHandler = (shape, info) => { const { scaleX, scaleY } = info diff --git a/packages/editor/src/lib/editor/shapes/frame/FrameShapeUtil.tsx b/packages/editor/src/lib/editor/shapes/frame/FrameShapeUtil.tsx index 085ff65d2..46f0c2186 100644 --- a/packages/editor/src/lib/editor/shapes/frame/FrameShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapes/frame/FrameShapeUtil.tsx @@ -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 { 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 { className="tl-frame__body" width={bounds.width} height={bounds.height} - fill="none" + fill={theme.solid} + stroke={theme.text} /> { ) } - override toSvg( - shape: TLFrameShape, - font: string, - colors: TLExportColors - ): SVGElement | Promise { + override toSvg(shape: TLFrameShape): SVGElement | Promise { + 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 { 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) diff --git a/packages/editor/src/lib/editor/shapes/geo/GeoShapeUtil.tsx b/packages/editor/src/lib/editor/shapes/geo/GeoShapeUtil.tsx index ad0a1a88e..e086e4e98 100644 --- a/packages/editor/src/lib/editor/shapes/geo/GeoShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapes/geo/GeoShapeUtil.tsx @@ -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 { } } - 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 { color: props.color, fill: props.fill, strokeWidth, - colors, + theme, }) break @@ -530,7 +542,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { h: props.h, color: props.color, fill: props.fill, - colors, + theme, }) break @@ -543,7 +555,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { dash: props.dash, color: props.color, fill: props.fill, - colors, + theme, }) break } @@ -561,7 +573,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { dash: props.dash, color: props.color, fill: props.fill, - colors, + theme, }) break @@ -572,7 +584,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { h: props.h, color: props.color, fill: props.fill, - colors, + theme, }) break @@ -585,7 +597,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { dash: props.dash, color: props.color, fill: props.fill, - colors, + theme, }) } break @@ -603,7 +615,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { strokeWidth, outline, lines, - colors, + theme, }) break @@ -614,7 +626,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { strokeWidth, outline, lines, - colors, + theme, }) break @@ -626,7 +638,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { strokeWidth, outline, lines, - colors, + theme, }) break } @@ -637,21 +649,23 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { 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 { return svgElm } + getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] { + return [getFillDefForCanvas()] + } + onResize: TLOnResizeHandler = ( shape, { initialBounds, handle, newPoint, scaleX, scaleY } diff --git a/packages/editor/src/lib/editor/shapes/geo/components/DashStyleEllipse.tsx b/packages/editor/src/lib/editor/shapes/geo/components/DashStyleEllipse.tsx index e78a4c54c..aa0bcc112 100644 --- a/packages/editor/src/lib/editor/shapes/geo/components/DashStyleEllipse.tsx +++ b/packages/editor/src/lib/editor/shapes/geo/components/DashStyleEllipse.tsx @@ -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 & { 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) diff --git a/packages/editor/src/lib/editor/shapes/geo/components/DashStyleOval.tsx b/packages/editor/src/lib/editor/shapes/geo/components/DashStyleOval.tsx index a7e8e57ed..fdeb79bcc 100644 --- a/packages/editor/src/lib/editor/shapes/geo/components/DashStyleOval.tsx +++ b/packages/editor/src/lib/editor/shapes/geo/components/DashStyleOval.tsx @@ -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 & { 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) diff --git a/packages/editor/src/lib/editor/shapes/geo/components/DashStylePolygon.tsx b/packages/editor/src/lib/editor/shapes/geo/components/DashStylePolygon.tsx index 123494ad5..a6f7ffb1d 100644 --- a/packages/editor/src/lib/editor/shapes/geo/components/DashStylePolygon.tsx +++ b/packages/editor/src/lib/editor/shapes/geo/components/DashStylePolygon.tsx @@ -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}`} /> ))} - + {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({ & { 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) diff --git a/packages/editor/src/lib/editor/shapes/geo/components/DrawStyleEllipse.tsx b/packages/editor/src/lib/editor/shapes/geo/components/DrawStyleEllipse.tsx index 28f2b9f49..95df5b3c3 100644 --- a/packages/editor/src/lib/editor/shapes/geo/components/DrawStyleEllipse.tsx +++ b/packages/editor/src/lib/editor/shapes/geo/components/DrawStyleEllipse.tsx @@ -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 ( <> - + ) }) @@ -44,22 +49,22 @@ export function DrawStyleEllipseSvg({ strokeWidth: sw, fill, color, - colors, + theme, }: Pick & { 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) diff --git a/packages/editor/src/lib/editor/shapes/geo/components/DrawStylePolygon.tsx b/packages/editor/src/lib/editor/shapes/geo/components/DrawStylePolygon.tsx index 18506fb2b..18be5c133 100644 --- a/packages/editor/src/lib/editor/shapes/geo/components/DrawStylePolygon.tsx +++ b/packages/editor/src/lib/editor/shapes/geo/components/DrawStylePolygon.tsx @@ -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 ( <> - + ) }) @@ -48,14 +48,14 @@ export function DrawStylePolygonSvg({ lines, fill, color, - colors, + theme, strokeWidth, }: Pick & { 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) diff --git a/packages/editor/src/lib/editor/shapes/geo/components/SolidStyleEllipse.tsx b/packages/editor/src/lib/editor/shapes/geo/components/SolidStyleEllipse.tsx index 67488a505..29b62cf80 100644 --- a/packages/editor/src/lib/editor/shapes/geo/components/SolidStyleEllipse.tsx +++ b/packages/editor/src/lib/editor/shapes/geo/components/SolidStyleEllipse.tsx @@ -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 & { 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 ( <> - + ) }) @@ -31,10 +36,10 @@ export function SolidStyleEllipseSvg({ strokeWidth: sw, fill, color, - colors, + theme, }: Pick & { 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) diff --git a/packages/editor/src/lib/editor/shapes/geo/components/SolidStyleOval.tsx b/packages/editor/src/lib/editor/shapes/geo/components/SolidStyleOval.tsx index c0b6b82f2..066ed55cf 100644 --- a/packages/editor/src/lib/editor/shapes/geo/components/SolidStyleOval.tsx +++ b/packages/editor/src/lib/editor/shapes/geo/components/SolidStyleOval.tsx @@ -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 & { strokeWidth: number }) { + const theme = useDefaultColorTheme() const d = getOvalIndicatorPath(w, h) return ( <> - + ) }) @@ -27,10 +32,10 @@ export function SolidStyleOvalSvg({ strokeWidth: sw, fill, color, - colors, + theme, }: Pick & { 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) diff --git a/packages/editor/src/lib/editor/shapes/geo/components/SolidStylePolygon.tsx b/packages/editor/src/lib/editor/shapes/geo/components/SolidStylePolygon.tsx index da2d3cb0c..65689ac12 100644 --- a/packages/editor/src/lib/editor/shapes/geo/components/SolidStylePolygon.tsx +++ b/packages/editor/src/lib/editor/shapes/geo/components/SolidStylePolygon.tsx @@ -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 ( <> - + ) }) @@ -37,11 +42,11 @@ export function SolidStylePolygonSvg({ fill, color, strokeWidth, - colors, + theme, }: Pick & { 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) diff --git a/packages/editor/src/lib/editor/shapes/highlight/HighlightShapeUtil.tsx b/packages/editor/src/lib/editor/shapes/highlight/HighlightShapeUtil.tsx index bac63f0b2..284fa171b 100644 --- a/packages/editor/src/lib/editor/shapes/highlight/HighlightShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapes/highlight/HighlightShapeUtil.tsx @@ -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 { 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 = (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 ( @@ -238,7 +245,7 @@ function HighlightRenderer({ strokeLinecap="round" fill="none" pointerEvents="all" - stroke={`var(--palette-${shape.props.color}-highlight)`} + stroke={color} strokeWidth={sw} /> @@ -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}`) diff --git a/packages/editor/src/lib/editor/shapes/line/LineShapeUtil.tsx b/packages/editor/src/lib/editor/shapes/line/LineShapeUtil.tsx index 288b214d4..6c8693496 100644 --- a/packages/editor/src/lib/editor/shapes/line/LineShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapes/line/LineShapeUtil.tsx @@ -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 { } 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 { return ( - + ) } @@ -210,7 +205,7 @@ export class LineShapeUtil extends ShapeUtil { return ( - + {spline.segments.map((segment, i) => { const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( segment.length, @@ -246,7 +241,7 @@ export class LineShapeUtil extends ShapeUtil { @@ -265,7 +260,7 @@ export class LineShapeUtil extends ShapeUtil { @@ -277,7 +272,7 @@ export class LineShapeUtil extends ShapeUtil { return ( - + {spline.segments.map((segment, i) => { const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( segment.length, @@ -311,8 +306,8 @@ export class LineShapeUtil extends ShapeUtil { ) @@ -342,11 +337,11 @@ export class LineShapeUtil extends ShapeUtil { return } - 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]) } } diff --git a/packages/editor/src/lib/editor/shapes/note/NoteShapeUtil.tsx b/packages/editor/src/lib/editor/shapes/note/NoteShapeUtil.tsx index f250f844f..0aac5947f 100644 --- a/packages/editor/src/lib/editor/shapes/note/NoteShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapes/note/NoteShapeUtil.tsx @@ -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 { 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 {
@@ -83,7 +87,7 @@ export class NoteShapeUtil extends ShapeUtil { align={align} verticalAlign={verticalAlign} text={text} - labelColor={adjustedColor} + labelColor="black" wrap />
@@ -105,7 +109,9 @@ export class NoteShapeUtil extends ShapeUtil { ) } - 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 { 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 { 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) diff --git a/packages/editor/src/lib/editor/shapes/shared/ShapeFill.tsx b/packages/editor/src/lib/editor/shapes/shared/ShapeFill.tsx index 9175b6937..29bf440a1 100644 --- a/packages/editor/src/lib/editor/shapes/shared/ShapeFill.tsx +++ b/packages/editor/src/lib/editor/shapes/shared/ShapeFill.tsx @@ -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 } case 'solid': { - return ( - - ) + return } case 'semi': { - return + return } case 'pattern': { return @@ -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 ( <> - + @@ -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 } diff --git a/packages/editor/src/lib/editor/shapes/shared/SvgExportContext.tsx b/packages/editor/src/lib/editor/shapes/shared/SvgExportContext.tsx new file mode 100644 index 000000000..4143901a3 --- /dev/null +++ b/packages/editor/src/lib/editor/shapes/shared/SvgExportContext.tsx @@ -0,0 +1,12 @@ +export interface SvgExportDef { + key: string + getElement: () => Promise | SVGElement | SVGElement[] | null +} + +export interface SvgExportContext { + /** + * Add contents to the 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 +} diff --git a/packages/editor/src/lib/editor/shapes/shared/TLExportColors.ts b/packages/editor/src/lib/editor/shapes/shared/TLExportColors.ts deleted file mode 100644 index db6b9042b..000000000 --- a/packages/editor/src/lib/editor/shapes/shared/TLExportColors.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { TLDefaultColorStyle } from '@tldraw/tlschema' - -export type TLExportColors = { - fill: Record - pattern: Record - semi: Record - highlight: Record - solid: string - text: string - background: string -} diff --git a/packages/editor/src/lib/editor/shapes/shared/TextLabel.tsx b/packages/editor/src/lib/editor/shapes/shared/TextLabel.tsx index 5e28857a3..dfd4713ac 100644 --- a/packages/editor/src/lib/editor/shapes/shared/TextLabel.tsx +++ b/packages/editor/src/lib/editor/shapes/shared/TextLabel.tsx @@ -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 (
diff --git a/packages/editor/src/lib/editor/shapes/shared/defaultStyleDefs.tsx b/packages/editor/src/lib/editor/shapes/shared/defaultStyleDefs.tsx new file mode 100644 index 000000000..65cbfe14e --- /dev/null +++ b/packages/editor/src/lib/editor/shapes/shared/defaultStyleDefs.tsx @@ -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((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 = ` + + + + + + + + + + + + + + + + ` + 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((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(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 ( + + + + ) + })} + + ) + + return { defs, isReady } +} + +function PatternFillDefForCanvas() { + const editor = useEditor() + const containerRef = useRef(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 {defs} +} + +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 +} diff --git a/packages/editor/src/lib/editor/shapes/shared/useColorSpace.tsx b/packages/editor/src/lib/editor/shapes/shared/useColorSpace.tsx new file mode 100644 index 000000000..cdab00bf5 --- /dev/null +++ b/packages/editor/src/lib/editor/shapes/shared/useColorSpace.tsx @@ -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' +} diff --git a/packages/editor/src/lib/editor/shapes/text/TextShapeUtil.tsx b/packages/editor/src/lib/editor/shapes/text/TextShapeUtil.tsx index a9ee18705..5b270b1e5 100644 --- a/packages/editor/src/lib/editor/shapes/text/TextShapeUtil.tsx +++ b/packages/editor/src/lib/editor/shapes/text/TextShapeUtil.tsx @@ -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 { } } - // @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 { return } - 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 { 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 { 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 { this.editor.textMeasure.measureTextSpans(text, opts), { ...opts, - stroke: colors.background, + stroke: theme.background, strokeWidth: 2, - fill: colors.background, + fill: theme.background, padding: 0, } ) diff --git a/packages/editor/src/lib/hooks/usePattern.tsx b/packages/editor/src/lib/hooks/usePattern.tsx deleted file mode 100644 index eabf0718f..000000000 --- a/packages/editor/src/lib/hooks/usePattern.tsx +++ /dev/null @@ -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((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(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 ( - - - - ) - })} - - ) - - return { context, isReady } -} - -const t = 8 / 12 -export function exportPatternSvgDefs(backgroundColor: string) { - const divEl = document.createElement('div') - divEl.innerHTML = ` - - - - - - - - - - - - - - - - ` - return divEl.querySelectorAll('defs > *')! -} diff --git a/packages/editor/src/lib/test/commands/__snapshots__/getSvg.test.ts.snap b/packages/editor/src/lib/test/commands/__snapshots__/getSvg.test.ts.snap index 564c84aca..4e77da6c4 100644 --- a/packages/editor/src/lib/test/commands/__snapshots__/getSvg.test.ts.snap +++ b/packages/editor/src/lib/test/commands/__snapshots__/getSvg.test.ts.snap @@ -12,11 +12,12 @@ exports[`Matches a snapshot: Basic SVG 1`] = ` width="564" > + - + - + - + - + - + - + - + - + - + -