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