hoist opacity out of props (#1526)

This change hoists opacity out of props and changes it to a number
instead of an enum.

The change to a number is to make tldraw more flexible for library
consumers who might want more expressivity with opacity than our 5
possible values allow. the tldraw editor will now happily respect any
opacity between 0 and 1. The limit to our supported values is enforced
only in the UI. I think this is limited enough that it's a reasonable
tradeoff between in-app simplicity and giving external developers the
flexibility they need.

There's a new `opacityForNextShape` property on the instance. This works
exactly the same way as propsForNextShape does, except... it's just for
opacity. With this, there should be no user-facing changes to how
opacity works in tldraw. There are also new `opacity`/`setOpacity` APIs
in the editor that work with it/selections similar to how props do.

@ds300 do you mind reviewing the migrations here?

### Change Type

- [x] `major` — Breaking Change

### Test Plan

- [x] Unit Tests
- [ ] Webdriver tests

### Release Notes

[internal only for now]
This commit is contained in:
alex 2023-06-06 17:15:12 +01:00 committed by GitHub
parent 355ed1de72
commit f2d8fae6ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 478 additions and 289 deletions

View file

@ -1,9 +1,8 @@
import { TLBaseShape, TLOpacityType } from '@tldraw/tldraw'
import { TLBaseShape } from '@tldraw/tldraw'
export type CardShape = TLBaseShape<
'card',
{
opacity: TLOpacityType // necessary for all shapes at the moment, others can be whatever you want!
w: number
h: number
}

View file

@ -13,7 +13,6 @@ export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
// Default props — used for shapes created with the tool
override defaultProps(): CardShape['props'] {
return {
opacity: '1',
w: 300,
h: 300,
}

View file

@ -178,8 +178,6 @@ export abstract class BaseBoxShapeTool extends StateNode {
static initial: string;
// (undocumented)
abstract shapeType: string;
// (undocumented)
styles: ("align" | "arrowheadEnd" | "arrowheadStart" | "color" | "dash" | "fill" | "font" | "geo" | "icon" | "labelColor" | "opacity" | "size" | "spline" | "verticalAlign")[];
}
// @public (undocumented)
@ -623,6 +621,8 @@ export class Editor extends EventEmitter<TLEventMap> {
description: string;
}>;
get onlySelectedShape(): null | TLShape;
// (undocumented)
get opacity(): null | number;
get openMenus(): string[];
packShapes(ids?: TLShapeId[], padding?: number): this;
get pages(): TLPage[];
@ -717,6 +717,7 @@ export class Editor extends EventEmitter<TLEventMap> {
setHoveredId(id?: null | TLShapeId): this;
setInstancePageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void;
setLocale(locale: string): void;
setOpacity(opacity: number, ephemeral?: boolean, squashing?: boolean): this;
// (undocumented)
setPenMode(isPenMode: boolean): this;
// @internal (undocumented)
@ -915,7 +916,6 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
fill: "none" | "pattern" | "semi" | "solid";
dash: "dashed" | "dotted" | "draw" | "solid";
size: "l" | "m" | "s" | "xl";
opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1";
font: "draw" | "mono" | "sans" | "serif";
align: "end" | "middle" | "start";
verticalAlign: "end" | "middle" | "start";
@ -931,6 +931,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
index: string;
parentId: TLParentId;
isLocked: boolean;
opacity: number;
id: TLShapeId;
typeName: "shape";
} | undefined;
@ -944,7 +945,6 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
fill: "none" | "pattern" | "semi" | "solid";
dash: "dashed" | "dotted" | "draw" | "solid";
size: "l" | "m" | "s" | "xl";
opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1";
font: "draw" | "mono" | "sans" | "serif";
align: "end" | "middle" | "start";
verticalAlign: "end" | "middle" | "start";
@ -960,6 +960,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
index: string;
parentId: TLParentId;
isLocked: boolean;
opacity: number;
id: TLShapeId;
typeName: "shape";
} | undefined;
@ -975,6 +976,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
index: string;
parentId: TLParentId;
isLocked: boolean;
opacity: number;
id: TLShapeId;
typeName: "shape";
} | {
@ -988,6 +990,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
index: string;
parentId: TLParentId;
isLocked: boolean;
opacity: number;
id: TLShapeId;
typeName: "shape";
} | undefined;
@ -1702,7 +1705,6 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
font: "draw" | "mono" | "sans" | "serif";
align: "end" | "middle" | "start";
verticalAlign: "end" | "middle" | "start";
opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1";
url: string;
text: string;
};
@ -1713,6 +1715,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
index: string;
parentId: TLParentId;
isLocked: boolean;
opacity: number;
id: TLShapeId;
typeName: "shape";
} | undefined;
@ -1725,7 +1728,6 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
font: "draw" | "mono" | "sans" | "serif";
align: "end" | "middle" | "start";
verticalAlign: "end" | "middle" | "start";
opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1";
url: string;
text: string;
};
@ -1736,6 +1738,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
index: string;
parentId: TLParentId;
isLocked: boolean;
opacity: number;
id: TLShapeId;
typeName: "shape";
} | undefined;
@ -1962,6 +1965,8 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
// (undocumented)
path: Computed<string>;
// (undocumented)
shapeType?: string;
// (undocumented)
readonly styles: TLStyleType[];
// (undocumented)
transition(id: string, info: any): this;
@ -2024,6 +2029,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
index: string;
parentId: TLParentId;
isLocked: boolean;
opacity: number;
props: TLTextShapeProps;
id: TLShapeId;
typeName: "shape";
@ -2038,7 +2044,6 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
size: "l" | "m" | "s" | "xl";
font: "draw" | "mono" | "sans" | "serif";
align: "end" | "middle" | "start";
opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1";
text: string;
scale: number;
autoSize: boolean;
@ -2048,6 +2053,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
index: string;
parentId: TLParentId;
isLocked: boolean;
opacity: number;
id: TLShapeId;
typeName: "shape";
} | undefined;

View file

@ -217,13 +217,6 @@ export const STYLES: TLStyleCollections = {
{ id: 'l', type: 'size', icon: 'size-large' },
{ id: 'xl', type: 'size', icon: 'size-extra-large' },
],
opacity: [
{ id: '0.1', type: 'opacity', icon: 'color' },
{ id: '0.25', type: 'opacity', icon: 'color' },
{ id: '0.5', type: 'opacity', icon: 'color' },
{ id: '0.75', type: 'opacity', icon: 'color' },
{ id: '1', type: 'opacity', icon: 'color' },
],
font: [
{ id: 'draw', type: 'font', icon: 'font-draw' },
{ id: 'sans', type: 'font', icon: 'font-sans' },

View file

@ -1151,6 +1151,42 @@ export class Editor extends EventEmitter<TLEventMap> {
return next
}
@computed get opacity(): number | null {
if (this.isIn('select') && this.selectedIds.length > 0) {
const shapesToCheck: TLShape[] = []
const addShape = (shapeId: TLShapeId) => {
const shape = this.getShapeById(shapeId)
if (!shape) return
// For groups, ignore the opacity of the group shape and instead include
// the opacity of the group's children. These are the shapes that would have
// their opacity changed if the user called `setOpacity` on the current selection.
if (shape.type === 'group') {
for (const childId of this.getSortedChildIds(shape.id)) {
addShape(childId)
}
} else {
shapesToCheck.push(shape)
}
}
for (const shapeId of this.selectedIds) {
addShape(shapeId)
}
let opacity: number | null = null
for (const shape of shapesToCheck) {
if (opacity === null) {
opacity = shape.opacity
} else if (opacity !== shape.opacity) {
return null
}
}
return opacity
} else {
return this.instanceState.opacityForNextShape
}
}
/**
* An array of all of the shapes on the current page.
*
@ -2242,8 +2278,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const shape = this.getShapeById(id)
if (!shape) return
// todo: move opacity to a property of shape, rather than a property of props
let opacity = (+(shape.props as { opacity: string }).opacity ?? 1) * parentOpacity
let opacity = shape.opacity * parentOpacity
let isShapeErasing = false
if (!isAncestorErasing && erasingIdsSet?.has(id)) {
@ -4675,7 +4710,7 @@ export class Editor extends EventEmitter<TLEventMap> {
// We then look up each key in the tab state's props; and if it's there,
// we use the value from the tab state's props instead of the default.
// Note that props will never include opacity.
const { propsForNextShape } = this.instanceState
const { propsForNextShape, opacityForNextShape } = this.instanceState
for (const key in initialProps) {
if (key in propsForNextShape) {
if (key === 'url') continue
@ -4693,6 +4728,7 @@ export class Editor extends EventEmitter<TLEventMap> {
).create({
...partial,
index,
opacity: partial.opacity ?? opacityForNextShape,
parentId: partial.parentId ?? focusLayerId,
props: 'props' in partial ? { ...initialProps, ...partial.props } : initialProps,
})
@ -7757,11 +7793,75 @@ export class Editor extends EventEmitter<TLEventMap> {
return this
}
/**
* Set the current opacity. This will effect any selected shapes, or the
* next-created shape.
*
* @example
* ```ts
* editor.setOpacity(0.5)
* editor.setOpacity(0.5, true)
* ```
*
* @param opacity - The opacity to set. Must be a number between 0 and 1
* inclusive.
* @param ephemeral - Whether the opacity change is ephemeral. Ephemeral
* changes don't get added to the undo/redo stack. Defaults to false.
* @param squashing - Whether the opacity change will be squashed into the
* existing history entry rather than creating a new one. Defaults to false.
*/
setOpacity(opacity: number, ephemeral = false, squashing = false) {
this.history.batch(() => {
if (this.isIn('select')) {
const {
pageState: { selectedIds },
} = this
const shapesToUpdate: TLShape[] = []
// We can have many deep levels of grouped shape
// Making a recursive function to look through all the levels
const addShapeById = (id: TLShape['id']) => {
const shape = this.getShapeById(id)
if (!shape) return
if (this.isShapeOfType(shape, GroupShapeUtil)) {
const childIds = this.getSortedChildIds(id)
for (const childId of childIds) {
addShapeById(childId)
}
} else {
shapesToUpdate.push(shape)
}
}
if (selectedIds.length > 0) {
for (const id of selectedIds) {
addShapeById(id)
}
this.updateShapes(
shapesToUpdate.map((shape) => {
return {
id: shape.id,
type: shape.type,
opacity,
}
}),
ephemeral
)
}
}
this.updateInstanceState({ opacityForNextShape: opacity }, ephemeral, squashing)
})
return this
}
/**
* Set the current props (generally styles).
*
* @example
*
* ```ts
* editor.setProp('color', 'red')
* editor.setProp('color', 'red', true)
@ -7769,67 +7869,43 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @param key - The key to set.
* @param value - The value to set.
* @param ephemeral - Whether the style is ephemeral. Defaults to false.
* @param ephemeral - Whether the style change is ephemeral. Ephemeral
* changes don't get added to the undo/redo stack. Defaults to false.
* @param squashing - Whether the style change will be squashed into the
* existing history entry rather than creating a new one. Defaults to false.
* @public
*/
setProp(key: TLShapeProp, value: any, ephemeral = false, squashing = false) {
const children: (TLShape | undefined)[] = []
// We can have many deep levels of grouped shape
// Making a recursive function to look through all the levels
const getChildProp = (id: TLShape['id']) => {
const childIds = this.getSortedChildIds(id)
for (const childId of childIds) {
const childShape = this.getShapeById(childId)
if (childShape?.type === 'group') {
getChildProp(childShape.id)
}
children.push(childShape)
}
}
this.history.batch(() => {
this.updateInstanceState(
{
propsForNextShape: setPropsForNextShape(this.instanceState.propsForNextShape, {
[key]: value,
}),
},
ephemeral,
squashing
)
if (this.isIn('select')) {
const {
pageState: { selectedIds },
} = this
if (selectedIds.length > 0) {
const shapes = compact(
selectedIds.map((id) => {
const shape = this.getShapeById(id)
if (shape?.type === 'group') {
const childIds = this.getSortedChildIds(shape.id)
for (const childId of childIds) {
const childShape = this.getShapeById(childId)
if (childShape?.type === 'group') {
getChildProp(childShape.id)
}
children.push(childShape)
}
return children
} else {
return shape
const shapesToUpdate: TLShape[] = []
// We can have many deep levels of grouped shape
// Making a recursive function to look through all the levels
const addShapeById = (id: TLShape['id']) => {
const shape = this.getShapeById(id)
if (!shape) return
if (this.isShapeOfType(shape, GroupShapeUtil)) {
const childIds = this.getSortedChildIds(id)
for (const childId of childIds) {
addShapeById(childId)
}
})
)
.flat()
.filter(
(shape) =>
shape!.props[key as keyof TLShape['props']] !== undefined && shape?.type !== 'group'
) as TLShape[]
} else if (shape!.props[key as keyof TLShape['props']] !== undefined) {
shapesToUpdate.push(shape)
}
}
for (const id of selectedIds) {
addShapeById(id)
}
this.updateShapes(
shapes.map((shape) => {
shapesToUpdate.map((shape) => {
const props = { ...shape.props, [key]: value }
if (key === 'color' && 'labelColor' in props) {
props.labelColor = 'black'
@ -7844,10 +7920,10 @@ export class Editor extends EventEmitter<TLEventMap> {
ephemeral
)
if (key !== 'color' && key !== 'opacity') {
if (key !== 'color') {
const changes: TLShapePartial[] = []
for (const shape of shapes) {
for (const shape of shapesToUpdate) {
const currentShape = this.getShapeById(shape.id)
if (!currentShape) continue
const util = this.getShapeUtil(currentShape)
@ -8903,9 +8979,8 @@ export class Editor extends EventEmitter<TLEventMap> {
index: highestIndex,
x,
y,
props: {
opacity: '1',
},
opacity: 1,
props: {},
},
])
this.reparentShapesById(sortedShapeIds, groupId)

View file

@ -69,7 +69,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
override defaultProps(): TLArrowShape['props'] {
return {
opacity: '1',
dash: 'draw',
size: 'm',
fill: 'none',

View file

@ -27,7 +27,6 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
override defaultProps(): TLBookmarkShape['props'] {
return {
opacity: '1',
url: '',
w: DEFAULT_BOOKMARK_WIDTH,
h: DEFAULT_BOOKMARK_HEIGHT,

View file

@ -36,7 +36,6 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
fill: 'none',
dash: 'draw',
size: 'm',
opacity: '1',
isComplete: false,
isClosed: false,
isPen: false,

View file

@ -42,7 +42,6 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
override defaultProps(): TLEmbedShape['props'] {
return {
opacity: '1',
w: 300,
h: 300,
url: '',

View file

@ -18,7 +18,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
override canEdit = () => true
override defaultProps(): TLFrameShape['props'] {
return { opacity: '1', w: 160 * 2, h: 90 * 2, name: '' }
return { w: 160 * 2, h: 90 * 2, name: '' }
}
override render(shape: TLFrameShape) {
@ -56,7 +56,6 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
rect.setAttribute('width', shape.props.w.toString())
rect.setAttribute('height', shape.props.h.toString())
rect.setAttribute('fill', colors.solid)
rect.setAttribute('opacity', shape.props.opacity)
rect.setAttribute('stroke', colors.fill.black)
rect.setAttribute('stroke-width', '1')
rect.setAttribute('rx', '1')

View file

@ -55,7 +55,6 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
fill: 'none',
dash: 'draw',
size: 'm',
opacity: '1',
font: 'draw',
text: '',
align: 'middle',

View file

@ -16,7 +16,7 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
canBind = () => false
defaultProps(): TLGroupShape['props'] {
return { opacity: '1' }
return {}
}
getBounds(shape: TLGroupShape): Box2d {

View file

@ -27,7 +27,6 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
segments: [],
color: 'black',
size: 'm',
opacity: '1',
isComplete: false,
isPen: false,
}

View file

@ -56,7 +56,6 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
override defaultProps(): TLImageShape['props'] {
return {
opacity: '1',
w: 100,
h: 100,
assetId: null,

View file

@ -36,7 +36,6 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
override defaultProps(): TLLineShape['props'] {
return {
opacity: '1',
dash: 'draw',
size: 'm',
color: 'black',

View file

@ -5,6 +5,7 @@ Object {
"id": "shape:line1",
"index": "a1",
"isLocked": false,
"opacity": 1,
"parentId": "page:id50",
"props": Object {
"color": "black",
@ -27,7 +28,6 @@ Object {
"y": 0,
},
},
"opacity": "1",
"size": "m",
"spline": "line",
},

View file

@ -21,7 +21,6 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
defaultProps(): TLNoteShape['props'] {
return {
opacity: '1',
color: 'black',
size: 'm',
text: '',

View file

@ -26,7 +26,6 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
defaultProps(): TLTextShape['props'] {
return {
opacity: '1',
color: 'black',
size: 'm',
w: 8,

View file

@ -18,7 +18,6 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
override defaultProps(): TLVideoShape['props'] {
return {
opacity: '1',
w: 100,
h: 100,
assetId: null,

View file

@ -12,7 +12,6 @@ export class ArrowShapeTool extends StateNode {
styles = [
'color',
'opacity',
'dash',
'size',
'arrowheadStart',

View file

@ -1,4 +1,3 @@
import { TLStyleType } from '@tldraw/tlschema'
import { StateNode } from '../StateNode'
import { Idle } from './children/Idle'
import { Pointing } from './children/Pointing'
@ -10,6 +9,4 @@ export abstract class BaseBoxShapeTool extends StateNode {
static children = () => [Idle, Pointing]
abstract shapeType: string
styles = ['opacity'] as TLStyleType[]
}

View file

@ -9,7 +9,8 @@ export class DrawShapeTool extends StateNode {
static initial = 'idle'
static children = () => [Idle, Drawing]
styles = ['color', 'opacity', 'dash', 'fill', 'size'] as TLStyleType[]
styles = ['color', 'dash', 'fill', 'size'] as TLStyleType[]
shapeType = 'draw'
onExit = () => {
const drawingState = this.children!['drawing'] as Drawing

View file

@ -1,4 +1,3 @@
import { TLStyleType } from '@tldraw/tlschema'
import { BaseBoxShapeTool } from '../BaseBoxShapeTool/BaseBoxShapeTool'
export class FrameShapeTool extends BaseBoxShapeTool {
@ -6,6 +5,4 @@ export class FrameShapeTool extends BaseBoxShapeTool {
static initial = 'idle'
shapeType = 'frame'
styles = ['opacity'] as TLStyleType[]
}

View file

@ -11,7 +11,6 @@ export class GeoShapeTool extends StateNode {
styles = [
'color',
'opacity',
'dash',
'fill',
'size',
@ -20,4 +19,5 @@ export class GeoShapeTool extends StateNode {
'align',
'verticalAlign',
] as TLStyleType[]
shapeType = 'geo'
}

View file

@ -10,7 +10,8 @@ export class HighlightShapeTool extends StateNode {
static initial = 'idle'
static children = () => [Idle, Drawing]
styles = ['color', 'opacity', 'size'] as TLStyleType[]
styles = ['color', 'size'] as TLStyleType[]
shapeType = 'highlight'
onExit = () => {
const drawingState = this.children!['drawing'] as Drawing

View file

@ -11,5 +11,5 @@ export class LineShapeTool extends StateNode {
shapeType = 'line'
styles = ['color', 'opacity', 'dash', 'size', 'spline'] as TLStyleType[]
styles = ['color', 'dash', 'size', 'spline'] as TLStyleType[]
}

View file

@ -8,5 +8,6 @@ export class NoteShapeTool extends StateNode {
static initial = 'idle'
static children = () => [Idle, Pointing]
styles = ['color', 'opacity', 'size', 'align', 'verticalAlign', 'font'] as TLStyleType[]
styles = ['color', 'size', 'align', 'verticalAlign', 'font'] as TLStyleType[]
shapeType = 'note'
}

View file

@ -42,7 +42,7 @@ export class SelectTool extends StateNode {
DraggingHandle,
]
styles = ['color', 'opacity', 'dash', 'fill', 'size'] as TLStyleType[]
styles = ['color', 'dash', 'fill', 'size'] as TLStyleType[]
onExit = () => {
if (this.editor.pageState.editingId) {

View file

@ -70,6 +70,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
current: Atom<StateNode | undefined>
type: TLStateNodeType
readonly styles: TLStyleType[] = []
shapeType?: string
initial?: string
children?: Record<string, StateNode>
parent: StateNode

View file

@ -9,5 +9,6 @@ export class TextShapeTool extends StateNode {
static children = () => [Idle, Pointing]
styles = ['color', 'opacity', 'font', 'align', 'size'] as TLStyleType[]
styles = ['color', 'font', 'align', 'size'] as TLStyleType[]
shapeType = 'text'
}

View file

@ -1,6 +1,7 @@
import { PageRecordType, createShapeId } from '@tldraw/tlschema'
import { structuredClone } from '@tldraw/utils'
import { TestEditor } from './TestEditor'
import { TL } from './jsx'
let editor: TestEditor
@ -156,6 +157,98 @@ describe('Editor.setProp', () => {
})
})
describe('Editor.opacity', () => {
it('should return the current opacity', () => {
expect(editor.opacity).toBe(1)
editor.setOpacity(0.5)
expect(editor.opacity).toBe(0.5)
})
it('should return opacity for a single selected shape', () => {
const { A } = editor.createShapesFromJsx(<TL.geo ref="A" opacity={0.3} x={0} y={0} />)
editor.setSelectedIds([A])
expect(editor.opacity).toBe(0.3)
})
it('should return opacity for multiple selected shapes', () => {
const { A, B } = editor.createShapesFromJsx([
<TL.geo ref="A" opacity={0.3} x={0} y={0} />,
<TL.geo ref="B" opacity={0.3} x={0} y={0} />,
])
editor.setSelectedIds([A, B])
expect(editor.opacity).toBe(0.3)
})
it('should return null when multiple selected shapes have different opacity', () => {
const { A, B } = editor.createShapesFromJsx([
<TL.geo ref="A" opacity={0.3} x={0} y={0} />,
<TL.geo ref="B" opacity={0.5} x={0} y={0} />,
])
editor.setSelectedIds([A, B])
expect(editor.opacity).toBe(null)
})
it('ignores the opacity of groups and returns the opacity of their children', () => {
const ids = editor.createShapesFromJsx([
<TL.group ref="group" x={0} y={0}>
<TL.geo ref="A" opacity={0.3} x={0} y={0} />
</TL.group>,
])
editor.setSelectedIds([ids.group])
expect(editor.opacity).toBe(0.3)
})
})
describe('Editor.setOpacity', () => {
it('should set opacity for selected shapes', () => {
const ids = editor.createShapesFromJsx([
<TL.geo ref="A" opacity={0.3} x={0} y={0} />,
<TL.geo ref="B" opacity={0.4} x={0} y={0} />,
])
editor.setSelectedIds([ids.A, ids.B])
editor.setOpacity(0.5)
expect(editor.getShapeById(ids.A)!.opacity).toBe(0.5)
expect(editor.getShapeById(ids.B)!.opacity).toBe(0.5)
})
it('should traverse into groups and set opacity in their children', () => {
const ids = editor.createShapesFromJsx([
<TL.geo ref="boxA" x={0} y={0} />,
<TL.group ref="groupA" x={0} y={0}>
<TL.geo ref="boxB" x={0} y={0} />
<TL.group ref="groupB" x={0} y={0}>
<TL.geo ref="boxC" x={0} y={0} />
<TL.geo ref="boxD" x={0} y={0} />
</TL.group>
</TL.group>,
])
editor.setSelectedIds([ids.groupA])
editor.setOpacity(0.5)
// a wasn't selected...
expect(editor.getShapeById(ids.boxA)!.opacity).toBe(1)
// b, c, & d were within a selected group...
expect(editor.getShapeById(ids.boxB)!.opacity).toBe(0.5)
expect(editor.getShapeById(ids.boxC)!.opacity).toBe(0.5)
expect(editor.getShapeById(ids.boxD)!.opacity).toBe(0.5)
// groups get skipped
expect(editor.getShapeById(ids.groupA)!.opacity).toBe(1)
expect(editor.getShapeById(ids.groupB)!.opacity).toBe(1)
})
it('stores opacity on opacityForNextShape', () => {
editor.setOpacity(0.5)
expect(editor.instanceState.opacityForNextShape).toBe(0.5)
editor.setOpacity(0.6)
expect(editor.instanceState.opacityForNextShape).toBe(0.6)
})
})
describe('Editor.TickManager', () => {
it('Does not produce NaN values when elapsed is 0', () => {
// a helper that calls update pointer velocity with a given elapsed time.

View file

@ -1,5 +1,5 @@
import { act, render, screen } from '@testing-library/react'
import { TLBaseShape, TLOpacityType, createShapeId } from '@tldraw/tlschema'
import { TLBaseShape, createShapeId } from '@tldraw/tlschema'
import { TldrawEditor } from '../TldrawEditor'
import { Canvas } from '../components/Canvas'
import { HTMLContainer } from '../components/HTMLContainer'
@ -137,7 +137,8 @@ describe('<TldrawEditor />', () => {
type: 'geo',
x: 0,
y: 0,
props: { geo: 'rectangle', w: 100, h: 100, opacity: '1' },
opacity: 1,
props: { geo: 'rectangle', w: 100, h: 100 },
})
// Is the shape's component rendering?
@ -165,7 +166,6 @@ describe('Custom shapes', () => {
{
w: number
h: number
opacity: TLOpacityType
}
>
@ -178,7 +178,6 @@ describe('Custom shapes', () => {
override defaultProps(): CardShape['props'] {
return {
opacity: '1',
w: 300,
h: 300,
}
@ -262,7 +261,8 @@ describe('Custom shapes', () => {
type: 'card',
x: 0,
y: 0,
props: { w: 100, h: 100, opacity: '1' },
opacity: 1,
props: { w: 100, h: 100 },
})
// Is the shape's component rendering?

View file

@ -6,6 +6,7 @@ Array [
"id": "shape:boxA",
"index": "a1",
"isLocked": false,
"opacity": 1,
"parentId": "wahtever",
"props": Object {
"align": "middle",
@ -17,7 +18,6 @@ Array [
"growY": 0,
"h": 100,
"labelColor": "black",
"opacity": "1",
"size": "m",
"text": "",
"url": "",
@ -34,6 +34,7 @@ Array [
"id": "shape:boxB",
"index": "a2",
"isLocked": false,
"opacity": 1,
"parentId": "wahtever",
"props": Object {
"align": "middle",
@ -45,7 +46,6 @@ Array [
"growY": 0,
"h": 100,
"labelColor": "black",
"opacity": "1",
"size": "m",
"text": "",
"url": "",
@ -62,6 +62,7 @@ Array [
"id": "shape:boxC",
"index": "a3",
"isLocked": false,
"opacity": 1,
"parentId": "wahtever",
"props": Object {
"align": "middle",
@ -73,7 +74,6 @@ Array [
"growY": 0,
"h": 100,
"labelColor": "black",
"opacity": "1",
"size": "m",
"text": "",
"url": "",
@ -95,6 +95,7 @@ Array [
"id": "shape:boxA",
"index": "a1",
"isLocked": false,
"opacity": 1,
"parentId": "wahtever",
"props": Object {
"align": "middle",
@ -106,7 +107,6 @@ Array [
"growY": 0,
"h": 100,
"labelColor": "black",
"opacity": "1",
"size": "m",
"text": "",
"url": "",
@ -123,6 +123,7 @@ Array [
"id": "shape:boxB",
"index": "a2",
"isLocked": false,
"opacity": 1,
"parentId": "wahtever",
"props": Object {
"align": "middle",
@ -134,7 +135,6 @@ Array [
"growY": 0,
"h": 100,
"labelColor": "black",
"opacity": "1",
"size": "m",
"text": "",
"url": "",
@ -151,6 +151,7 @@ Array [
"id": "shape:boxC",
"index": "a3",
"isLocked": false,
"opacity": 1,
"parentId": "wahtever",
"props": Object {
"align": "middle",
@ -162,7 +163,6 @@ Array [
"growY": 0,
"h": 100,
"labelColor": "black",
"opacity": "1",
"size": "m",
"text": "",
"url": "",

View file

@ -33,9 +33,9 @@ it('Creates shapes with the current style', () => {
})
it('Creates shapes with the current opacity', () => {
editor.setProp('opacity', '0.5')
editor.setOpacity(0.5)
editor.createShapes([{ id: ids.box3, type: 'geo' }])
expect(editor.getShapeById<TLGeoShape>(ids.box3)!.props.opacity).toEqual('0.5')
expect(editor.getShapeById<TLGeoShape>(ids.box3)!.opacity).toEqual(0.5)
})
it('Creates shapes at the correct index', () => {

View file

@ -30,13 +30,13 @@ it('updates shapes', () => {
id: ids.box1,
rotation: 0,
type: 'geo',
opacity: 1,
props: {
h: 100,
w: 100,
color: 'black',
dash: 'draw',
fill: 'none',
opacity: '1',
size: 'm',
},
})
@ -49,13 +49,13 @@ it('updates shapes', () => {
id: ids.box1,
rotation: 0,
type: 'geo',
opacity: 1,
props: {
h: 100,
w: 100,
color: 'black',
dash: 'draw',
fill: 'none',
opacity: '1',
size: 'm',
},
})
@ -68,13 +68,13 @@ it('updates shapes', () => {
id: ids.box1,
rotation: 0,
type: 'geo',
opacity: 1,
props: {
h: 100,
w: 100,
color: 'black',
dash: 'draw',
fill: 'none',
opacity: '1',
size: 'm',
},
})

View file

@ -20,6 +20,7 @@ type CommonProps = {
isLocked?: number
ref?: string
children?: JSX.Element | JSX.Element[]
opacity?: number
}
type ShapeByType<Type extends TLDefaultShape['type']> = Extract<TLDefaultShape, { type: Type }>
@ -89,7 +90,7 @@ export function shapesFromJsx(shapes: JSX.Element | Array<JSX.Element>) {
if (key === 'x' || key === 'y' || key === 'ref' || key === 'id' || key === 'children') {
continue
}
if (key === 'rotation' || key === 'isLocked') {
if (key === 'rotation' || key === 'isLocked' || key === 'opacity') {
shapePartial[key] = value as any
continue
}

View file

@ -16,7 +16,6 @@ describe('Editor.props', () => {
dash: 'draw',
fill: 'none',
size: 'm',
opacity: '1',
})
})
@ -32,12 +31,6 @@ describe('Editor.props', () => {
font: 'draw',
geo: 'rectangle',
verticalAlign: 'middle',
// h: 100,
// w: 100,
// growY: 0,
opacity: '1',
// text: '',
// url: '',
})
})
@ -53,12 +46,6 @@ describe('Editor.props', () => {
font: 'draw',
geo: 'rectangle',
verticalAlign: 'middle',
// h: 100, // blacklisted
// w: 100, // blacklisted
// growY: 0, // blacklist
opacity: '1',
// text: '', // blacklisted
// url: '', // blacklisted
})
})
@ -82,13 +69,7 @@ describe('Editor.props', () => {
size: 'm',
font: 'draw',
geo: 'rectangle',
opacity: '1',
verticalAlign: 'middle',
// h: null, // mixed! but also blacklisted
// w: null, // mixed! but also blacklisted
// growY: 0, // blacklist
// text: '', // blacklisted
// url: '', // blacklist
})
})
@ -111,7 +92,6 @@ describe('Editor.props', () => {
align: 'start',
text: 'hello world this is a long sentence that should wrap',
w: 100,
opacity: '0.5',
url: 'https://aol.com',
verticalAlign: 'start',
},
@ -128,10 +108,7 @@ describe('Editor.props', () => {
geo: null,
size: null,
font: null,
opacity: null,
verticalAlign: null,
// growY: null, // blacklist
// url: null, // blacklist
})
})
})

View file

@ -405,7 +405,8 @@ describe('When in readonly mode', () => {
type: 'embed',
x: 100,
y: 100,
props: { opacity: '1', w: 100, h: 100, url: '', doesResize: false },
opacity: 1,
props: { w: 100, h: 100, url: '', doesResize: false },
},
])
editor.setReadOnly(true)

View file

@ -5,6 +5,7 @@ Object {
"id": "shape:lineA",
"index": "a3",
"isLocked": false,
"opacity": 1,
"parentId": "shape:boxA",
"props": Object {
"color": "black",
@ -13,7 +14,6 @@ Object {
"isClosed": false,
"isComplete": false,
"isPen": false,
"opacity": "1",
"segments": Array [
Object {
"points": Array [

View file

@ -20,7 +20,6 @@ const imageWidth = 1200
const imageHeight = 800
const imageProps = {
opacity: '1',
assetId: null,
playing: true,
url: '',

View file

@ -1896,10 +1896,10 @@ describe('Group opacity', () => {
it("should set the group's opacity to max even if the selected style panel opacity is lower", () => {
editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0)])
editor.select(ids.boxA, ids.boxB)
editor.setProp('opacity', '0.5')
editor.setOpacity(0.5)
editor.groupShapes()
const group = editor.getShapeById(onlySelectedId())!
assert(editor.isShapeOfType(group, GroupShapeUtil))
expect(group.props.opacity).toBe('1')
expect(group.opacity).toBe(1)
})
})

View file

@ -292,7 +292,6 @@ export async function createShapesFromFiles(
props: {
w: asset.props!.w,
h: asset.props!.h,
opacity: '1',
},
}
@ -403,7 +402,6 @@ export function createEmbedShapeAtPoint(
h: props.height,
doesResize: props.doesResize,
url,
opacity: '1',
},
},
],
@ -430,10 +428,10 @@ export async function createBookmarkShapeAtPoint(editor: Editor, url: string, po
type: 'bookmark',
x: point.x - 150,
y: point.y - 160,
opacity: 1,
props: {
assetId: existing.id,
url: existing.props.src!,
opacity: '1',
},
},
])
@ -450,9 +448,9 @@ export async function createBookmarkShapeAtPoint(editor: Editor, url: string, po
type: 'bookmark',
x: point.x,
y: point.y,
opacity: 1,
props: {
url: url,
opacity: '1',
},
},
],
@ -480,9 +478,9 @@ export async function createBookmarkShapeAtPoint(editor: Editor, url: string, po
{
id: shapeId,
type: 'bookmark',
opacity: 1,
props: {
assetId: assetId,
opacity: '1',
},
},
])
@ -531,11 +529,11 @@ export async function createAssetShapeAtPoint(
type: 'image',
x: point.x - width / 2,
y: point.y - height / 2,
opacity: 1,
props: {
assetId: asset.id,
w: width,
h: height,
opacity: '1',
},
},
],

View file

@ -83,6 +83,7 @@ export function createShapeValidator<Type extends string, Props extends object>(
parentId: TLParentId;
type: Type;
isLocked: boolean;
opacity: number;
props: Props;
}>;
@ -450,7 +451,7 @@ export const LANGUAGES: readonly [{
}];
// @internal (undocumented)
export const opacityValidator: T.Validator<"0.1" | "0.25" | "0.5" | "0.75" | "1">;
export const opacityValidator: T.Validator<number>;
// @internal (undocumented)
export const pageIdValidator: T.Validator<TLPageId>;
@ -500,9 +501,6 @@ export const TL_FONT_TYPES: Set<"draw" | "mono" | "sans" | "serif">;
// @public (undocumented)
export const TL_GEO_TYPES: Set<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
// @public (undocumented)
export const TL_OPACITY_TYPES: Set<"0.1" | "0.25" | "0.5" | "0.75" | "1">;
// @public (undocumented)
export const TL_SIZE_TYPES: Set<"l" | "m" | "s" | "xl">;
@ -510,7 +508,7 @@ export const TL_SIZE_TYPES: Set<"l" | "m" | "s" | "xl">;
export const TL_SPLINE_TYPES: Set<"cubic" | "line">;
// @public (undocumented)
export const TL_STYLE_TYPES: Set<"align" | "arrowheadEnd" | "arrowheadStart" | "color" | "dash" | "fill" | "font" | "geo" | "icon" | "labelColor" | "opacity" | "size" | "spline" | "verticalAlign">;
export const TL_STYLE_TYPES: Set<"align" | "arrowheadEnd" | "arrowheadStart" | "color" | "dash" | "fill" | "font" | "geo" | "icon" | "labelColor" | "size" | "spline" | "verticalAlign">;
// @public (undocumented)
export interface TLAlignStyle extends TLBaseStyle {
@ -552,7 +550,6 @@ export type TLArrowShapeProps = {
fill: TLFillType;
dash: TLDashType;
size: TLSizeType;
opacity: TLOpacityType;
arrowheadStart: TLArrowheadType;
arrowheadEnd: TLArrowheadType;
font: TLFontType;
@ -612,6 +609,8 @@ export interface TLBaseShape<Type extends string, Props extends object> extends
// (undocumented)
isLocked: boolean;
// (undocumented)
opacity: TLOpacityType;
// (undocumented)
parentId: TLParentId;
// (undocumented)
props: Props;
@ -816,7 +815,6 @@ export type TLImageShape = TLBaseShape<'image', TLImageShapeProps>;
// @public (undocumented)
export type TLImageShapeProps = {
opacity: TLOpacityType;
url: string;
playing: boolean;
w: number;
@ -848,6 +846,8 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
// (undocumented)
isToolLocked: boolean;
// (undocumented)
opacityForNextShape: TLOpacityType;
// (undocumented)
propsForNextShape: TLInstancePropsForNextShape;
// (undocumented)
screenBounds: Box2dModel;
@ -938,15 +938,7 @@ export type TLNullableShapeProps = {
};
// @public (undocumented)
export interface TLOpacityStyle extends TLBaseStyle {
// (undocumented)
id: TLOpacityType;
// (undocumented)
type: 'opacity';
}
// @public (undocumented)
export type TLOpacityType = SetValue<typeof TL_OPACITY_TYPES>;
export type TLOpacityType = number;
// @public
export interface TLPage extends BaseRecord<'page', TLPageId> {
@ -995,7 +987,7 @@ export type TLShapePartial<T extends TLShape = TLShape> = T extends T ? {
export type TLShapeProp = keyof TLShapeProps;
// @public (undocumented)
export type TLShapeProps = SmooshedUnionObject<TLShape['props']>;
export type TLShapeProps = Identity<UnionToIntersection<TLDefaultShape['props']>>;
// @public (undocumented)
export interface TLSizeStyle extends TLBaseStyle {
@ -1052,8 +1044,6 @@ export interface TLStyleCollections {
// (undocumented)
geo: TLGeoStyle[];
// (undocumented)
opacity: TLOpacityStyle[];
// (undocumented)
size: TLSizeStyle[];
// (undocumented)
spline: TLSplineStyle[];
@ -1062,7 +1052,7 @@ export interface TLStyleCollections {
}
// @public (undocumented)
export type TLStyleItem = TLAlignStyle | TLArrowheadEndStyle | TLArrowheadStartStyle | TLColorStyle | TLDashStyle | TLFillStyle | TLFontStyle | TLGeoStyle | TLOpacityStyle | TLSizeStyle | TLSplineStyle | TLVerticalAlignStyle;
export type TLStyleItem = TLAlignStyle | TLArrowheadEndStyle | TLArrowheadStartStyle | TLColorStyle | TLDashStyle | TLFillStyle | TLFontStyle | TLGeoStyle | TLSizeStyle | TLSplineStyle | TLVerticalAlignStyle;
// @public (undocumented)
export type TLStyleProps = Pick<TLShapeProps, TLStyleType>;
@ -1079,7 +1069,6 @@ export type TLTextShapeProps = {
size: TLSizeType;
font: TLFontType;
align: TLAlignType;
opacity: TLOpacityType;
w: number;
text: string;
scale: number;

View file

@ -132,7 +132,7 @@ export function createTLSchema(
),
})
),
}).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false }))
}).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false, opacity: 1 }))
return StoreSchema.create<TLRecord, TLStoreProps>(
{

View file

@ -127,12 +127,7 @@ export {
} from './styles/TLFontStyle'
export { TL_GEO_TYPES, geoValidator, type TLGeoStyle, type TLGeoType } from './styles/TLGeoStyle'
export { iconValidator, type TLIconStyle, type TLIconType } from './styles/TLIconStyle'
export {
TL_OPACITY_TYPES,
opacityValidator,
type TLOpacityStyle,
type TLOpacityType,
} from './styles/TLOpacityStyle'
export { opacityValidator, type TLOpacityType } from './styles/TLOpacityStyle'
export {
TL_SIZE_TYPES,
sizeValidator,

View file

@ -6,7 +6,7 @@ import { documentMigrations } from './records/TLDocument'
import { instanceMigrations, instanceTypeVersions } from './records/TLInstance'
import { instancePageStateMigrations, instancePageStateVersions } from './records/TLPageState'
import { instancePresenceMigrations, instancePresenceVersions } from './records/TLPresence'
import { TLShape, rootShapeMigrations } from './records/TLShape'
import { TLShape, rootShapeMigrations, Versions as rootShapeVersions } from './records/TLShape'
import { arrowShapeMigrations } from './shapes/TLArrowShape'
import { bookmarkShapeMigrations } from './shapes/TLBookmarkShape'
import { drawShapeMigrations } from './shapes/TLDrawShape'
@ -1044,6 +1044,72 @@ describe('Adds NoteShape vertical alignment', () => {
})
})
describe('hoist opacity', () => {
test('hoists opacity from a shape to another', () => {
const { up, down } = rootShapeMigrations.migrators[rootShapeVersions.HoistOpacity]
const before = {
type: 'myShape',
x: 0,
y: 0,
props: {
color: 'red',
opacity: '0.5',
},
}
const after = {
type: 'myShape',
x: 0,
y: 0,
opacity: 0.5,
props: {
color: 'red',
},
}
const afterWithNonMatchingOpacity = {
type: 'myShape',
x: 0,
y: 0,
opacity: 0.6,
props: {
color: 'red',
},
}
expect(up(before)).toEqual(after)
expect(down(after)).toEqual(before)
expect(down(afterWithNonMatchingOpacity)).toEqual(before)
})
test('hoists opacity from propsForNextShape', () => {
const { up, down } = instanceMigrations.migrators[instanceTypeVersions.HoistOpacity]
const before = {
isToolLocked: true,
propsForNextShape: {
color: 'black',
opacity: '0.5',
},
}
const after = {
isToolLocked: true,
opacityForNextShape: 0.5,
propsForNextShape: {
color: 'black',
},
}
const afterWithNonMatchingOpacity = {
isToolLocked: true,
opacityForNextShape: 0.6,
propsForNextShape: {
color: 'black',
},
}
expect(up(before)).toEqual(after)
expect(down(after)).toEqual(before)
expect(down(afterWithNonMatchingOpacity)).toEqual(before)
})
})
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
for (const migrator of allMigrators) {

View file

@ -13,7 +13,7 @@ import { fillValidator } from '../styles/TLFillStyle'
import { fontValidator } from '../styles/TLFontStyle'
import { geoValidator } from '../styles/TLGeoStyle'
import { iconValidator } from '../styles/TLIconStyle'
import { opacityValidator } from '../styles/TLOpacityStyle'
import { opacityValidator, TLOpacityType } from '../styles/TLOpacityStyle'
import { sizeValidator } from '../styles/TLSizeStyle'
import { splineValidator } from '../styles/TLSplineStyle'
import { verticalAlignValidator } from '../styles/TLVerticalAlignStyle'
@ -34,6 +34,7 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
currentPageId: TLPageId
followingUserId: string | null
brush: Box2dModel | null
opacityForNextShape: TLOpacityType
propsForNextShape: TLInstancePropsForNextShape
cursor: TLCursor
scribble: TLScribble | null
@ -62,13 +63,13 @@ export const instanceTypeValidator: T.Validator<TLInstance> = T.model(
currentPageId: pageIdValidator,
followingUserId: T.string.nullable(),
brush: T.boxModel.nullable(),
opacityForNextShape: opacityValidator,
propsForNextShape: T.object({
color: colorValidator,
labelColor: colorValidator,
dash: dashValidator,
fill: fillValidator,
size: sizeValidator,
opacity: opacityValidator,
font: fontValidator,
align: alignValidator,
verticalAlign: verticalAlignValidator,
@ -104,13 +105,14 @@ const Versions = {
AddScribbleDelay: 10,
RemoveUserId: 11,
AddIsPenModeAndIsGridMode: 12,
HoistOpacity: 13,
} as const
export { Versions as instanceTypeVersions }
/** @public */
export const instanceMigrations = defineMigrations({
currentVersion: Versions.AddIsPenModeAndIsGridMode,
currentVersion: Versions.HoistOpacity,
migrators: {
[Versions.AddTransparentExportBgs]: {
up: (instance: TLInstance) => {
@ -256,6 +258,29 @@ export const instanceMigrations = defineMigrations({
return instance
},
},
[Versions.HoistOpacity]: {
up: ({ propsForNextShape: { opacity, ...propsForNextShape }, ...instance }: any) => {
return { ...instance, opacityForNextShape: Number(opacity ?? '1'), propsForNextShape }
},
down: ({ opacityForNextShape: opacity, ...instance }: any) => {
return {
...instance,
propsForNextShape: {
...instance.propsForNextShape,
opacity:
opacity < 0.175
? '0.1'
: opacity < 0.375
? '0.25'
: opacity < 0.625
? '0.5'
: opacity < 0.875
? '0.75'
: '1',
},
}
},
},
},
})
@ -267,8 +292,8 @@ export const InstanceRecordType = createRecordType<TLInstance>('instance', {
}).withDefaultProperties(
(): Omit<TLInstance, 'typeName' | 'id' | 'currentPageId'> => ({
followingUserId: null,
opacityForNextShape: 1,
propsForNextShape: {
opacity: '1',
color: 'black',
labelColor: 'black',
dash: 'draw',

View file

@ -15,7 +15,6 @@ import { TLLineShape } from '../shapes/TLLineShape'
import { TLNoteShape } from '../shapes/TLNoteShape'
import { TLTextShape } from '../shapes/TLTextShape'
import { TLVideoShape } from '../shapes/TLVideoShape'
import { SmooshedUnionObject } from '../util-types'
import { TLPageId } from './TLPage'
/**
@ -64,8 +63,15 @@ export type TLShapePartial<T extends TLShape = TLShape> = T extends T
/** @public */
export type TLShapeId = RecordId<TLUnknownShape>
// evil type shit that will get deleted in the next PR
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
? I
: never
type Identity<T> = { [K in keyof T]: T[K] }
/** @public */
export type TLShapeProps = SmooshedUnionObject<TLShape['props']>
export type TLShapeProps = Identity<UnionToIntersection<TLDefaultShape['props']>>
/** @public */
export type TLShapeProp = keyof TLShapeProps
@ -76,13 +82,14 @@ export type TLParentId = TLPageId | TLShapeId
/** @public */
export type TLNullableShapeProps = { [K in TLShapeProp]?: TLShapeProps[K] | null }
const Versions = {
export const Versions = {
AddIsLocked: 1,
HoistOpacity: 2,
} as const
/** @internal */
export const rootShapeMigrations = defineMigrations({
currentVersion: Versions.AddIsLocked,
currentVersion: Versions.HoistOpacity,
migrators: {
[Versions.AddIsLocked]: {
up: (record) => {
@ -98,6 +105,33 @@ export const rootShapeMigrations = defineMigrations({
}
},
},
[Versions.HoistOpacity]: {
up: ({ props: { opacity, ...props }, ...record }) => {
return {
...record,
opacity: Number(opacity ?? '1'),
props,
}
},
down: ({ opacity, ...record }) => {
return {
...record,
props: {
...record.props,
opacity:
opacity < 0.175
? '0.1'
: opacity < 0.375
? '0.25'
: opacity < 0.625
? '0.5'
: opacity < 0.875
? '0.75'
: '1',
},
}
},
},
},
})

View file

@ -7,7 +7,6 @@ import { TLColorType, colorValidator } from '../styles/TLColorStyle'
import { TLDashType, dashValidator } from '../styles/TLDashStyle'
import { TLFillType, fillValidator } from '../styles/TLFillStyle'
import { TLFontType, fontValidator } from '../styles/TLFontStyle'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { SetValue } from '../util-types'
import { TLBaseShape, createShapeValidator, shapeIdValidator } from './TLBaseShape'
@ -35,7 +34,6 @@ export type TLArrowShapeProps = {
fill: TLFillType
dash: TLDashType
size: TLSizeType
opacity: TLOpacityType
arrowheadStart: TLArrowheadType
arrowheadEnd: TLArrowheadType
font: TLFontType
@ -72,7 +70,6 @@ export const arrowShapeValidator: T.Validator<TLArrowShape> = createShapeValidat
fill: fillValidator,
dash: dashValidator,
size: sizeValidator,
opacity: opacityValidator,
arrowheadStart: arrowheadValidator,
arrowheadEnd: arrowheadValidator,
font: fontValidator,

View file

@ -2,6 +2,7 @@ import { BaseRecord } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { idValidator } from '../misc/id-validator'
import { TLParentId, TLShapeId } from '../records/TLShape'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
/** @public */
export interface TLBaseShape<Type extends string, Props extends object>
@ -13,6 +14,7 @@ export interface TLBaseShape<Type extends string, Props extends object>
index: string
parentId: TLParentId
isLocked: boolean
opacity: TLOpacityType
props: Props
}
@ -42,6 +44,7 @@ export function createShapeValidator<Type extends string, Props extends object>(
parentId: parentIdValidator,
type: T.literal(type),
isLocked: T.boolean,
opacity: opacityValidator,
props,
})
}

View file

@ -2,12 +2,10 @@ import { defineMigrations } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { assetIdValidator } from '../assets/TLBaseAsset'
import { TLAssetId } from '../records/TLAsset'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
/** @public */
export type TLBookmarkShapeProps = {
opacity: TLOpacityType
w: number
h: number
assetId: TLAssetId | null
@ -21,7 +19,6 @@ export type TLBookmarkShape = TLBaseShape<'bookmark', TLBookmarkShapeProps>
export const bookmarkShapeValidator: T.Validator<TLBookmarkShape> = createShapeValidator(
'bookmark',
T.object({
opacity: opacityValidator,
w: T.nonZeroNumber,
h: T.nonZeroNumber,
assetId: assetIdValidator.nullable(),

View file

@ -4,7 +4,6 @@ import { Vec2dModel } from '../misc/geometry-types'
import { TLColorType, colorValidator } from '../styles/TLColorStyle'
import { TLDashType, dashValidator } from '../styles/TLDashStyle'
import { TLFillType, fillValidator } from '../styles/TLFillStyle'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { SetValue } from '../util-types'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
@ -30,7 +29,6 @@ export type TLDrawShapeProps = {
fill: TLFillType
dash: TLDashType
size: TLSizeType
opacity: TLOpacityType
segments: TLDrawShapeSegment[]
isComplete: boolean
isClosed: boolean
@ -48,7 +46,6 @@ export const drawShapeValidator: T.Validator<TLDrawShape> = createShapeValidator
fill: fillValidator,
dash: dashValidator,
size: sizeValidator,
opacity: opacityValidator,
segments: T.arrayOf(drawShapeSegmentValidator),
isComplete: T.boolean,
isClosed: T.boolean,

View file

@ -1,6 +1,5 @@
import { defineMigrations } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
// Only allow multiplayer embeds. If we add additional routes later for example '/help' this won't match
@ -549,7 +548,6 @@ export type TLEmbedShapePermissions = { [K in keyof typeof embedShapePermissionD
/** @public */
export type TLEmbedShapeProps = {
opacity: TLOpacityType
w: number
h: number
url: string
@ -566,7 +564,6 @@ export type TLEmbedShape = TLBaseShape<'embed', TLEmbedShapeProps>
export const embedShapeTypeValidator: T.Validator<TLEmbedShape> = createShapeValidator(
'embed',
T.object({
opacity: opacityValidator,
w: T.nonZeroNumber,
h: T.nonZeroNumber,
url: T.string,

View file

@ -1,10 +1,8 @@
import { defineMigrations } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { opacityValidator, TLOpacityType } from '../styles/TLOpacityStyle'
import { createShapeValidator, TLBaseShape } from './TLBaseShape'
type TLFrameShapeProps = {
opacity: TLOpacityType
w: number
h: number
name: string
@ -17,7 +15,6 @@ export type TLFrameShape = TLBaseShape<'frame', TLFrameShapeProps>
export const frameShapeValidator: T.Validator<TLFrameShape> = createShapeValidator(
'frame',
T.object({
opacity: opacityValidator,
w: T.nonZeroNumber,
h: T.nonZeroNumber,
name: T.string,

View file

@ -6,7 +6,6 @@ import { TLDashType, dashValidator } from '../styles/TLDashStyle'
import { TLFillType, fillValidator } from '../styles/TLFillStyle'
import { TLFontType, fontValidator } from '../styles/TLFontStyle'
import { TLGeoType, geoValidator } from '../styles/TLGeoStyle'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { TLVerticalAlignType, verticalAlignValidator } from '../styles/TLVerticalAlignStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
@ -19,7 +18,6 @@ export type TLGeoShapeProps = {
fill: TLFillType
dash: TLDashType
size: TLSizeType
opacity: TLOpacityType
font: TLFontType
align: TLAlignType
verticalAlign: TLVerticalAlignType
@ -43,7 +41,6 @@ export const geoShapeValidator: T.Validator<TLGeoShape> = createShapeValidator(
fill: fillValidator,
dash: dashValidator,
size: sizeValidator,
opacity: opacityValidator,
font: fontValidator,
align: alignValidator,
verticalAlign: verticalAlignValidator,

View file

@ -1,12 +1,9 @@
import { defineMigrations } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { opacityValidator, TLOpacityType } from '../styles/TLOpacityStyle'
import { createShapeValidator, TLBaseShape } from './TLBaseShape'
/** @public */
export type TLGroupShapeProps = {
opacity: TLOpacityType
}
export type TLGroupShapeProps = { [key in never]: undefined }
/** @public */
export type TLGroupShape = TLBaseShape<'group', TLGroupShapeProps>
@ -14,9 +11,7 @@ export type TLGroupShape = TLBaseShape<'group', TLGroupShapeProps>
/** @internal */
export const groupShapeValidator: T.Validator<TLGroupShape> = createShapeValidator(
'group',
T.object({
opacity: opacityValidator,
})
T.object({})
)
/** @internal */

View file

@ -1,7 +1,6 @@
import { defineMigrations } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { TLColorType, colorValidator } from '../styles/TLColorStyle'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
import { TLDrawShapeSegment, drawShapeSegmentValidator } from './TLDrawShape'
@ -10,7 +9,6 @@ import { TLDrawShapeSegment, drawShapeSegmentValidator } from './TLDrawShape'
export type TLHighlightShapeProps = {
color: TLColorType
size: TLSizeType
opacity: TLOpacityType
segments: TLDrawShapeSegment[]
isComplete: boolean
isPen: boolean
@ -25,7 +23,6 @@ export const highlightShapeValidator: T.Validator<TLHighlightShape> = createShap
T.object({
color: colorValidator,
size: sizeValidator,
opacity: opacityValidator,
segments: T.arrayOf(drawShapeSegmentValidator),
isComplete: T.boolean,
isPen: T.boolean,

View file

@ -3,7 +3,6 @@ import { T } from '@tldraw/validate'
import { TLColorType, colorValidator } from '../styles/TLColorStyle'
import { TLDashType, dashValidator } from '../styles/TLDashStyle'
import { TLIconType, iconValidator } from '../styles/TLIconStyle'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
@ -13,7 +12,6 @@ export type TLIconShapeProps = {
icon: TLIconType
dash: TLDashType
color: TLColorType
opacity: TLOpacityType
scale: number
}
@ -28,7 +26,6 @@ export const iconShapeValidator: T.Validator<TLIconShape> = createShapeValidator
icon: iconValidator,
dash: dashValidator,
color: colorValidator,
opacity: opacityValidator,
scale: T.number,
})
)

View file

@ -3,7 +3,6 @@ import { T } from '@tldraw/validate'
import { assetIdValidator } from '../assets/TLBaseAsset'
import { Vec2dModel } from '../misc/geometry-types'
import { TLAssetId } from '../records/TLAsset'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
/** @public */
@ -14,7 +13,6 @@ export type TLImageCrop = {
/** @public */
export type TLImageShapeProps = {
opacity: TLOpacityType
url: string
playing: boolean
w: number
@ -35,7 +33,6 @@ export const cropValidator = T.object({
export const imageShapeValidator: T.Validator<TLImageShape> = createShapeValidator(
'image',
T.object({
opacity: opacityValidator,
w: T.nonZeroNumber,
h: T.nonZeroNumber,
playing: T.boolean,

View file

@ -3,7 +3,6 @@ import { T } from '@tldraw/validate'
import { TLHandle, handleValidator } from '../misc/TLHandle'
import { TLColorType, colorValidator } from '../styles/TLColorStyle'
import { TLDashType, dashValidator } from '../styles/TLDashStyle'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { TLSplineType, splineValidator } from '../styles/TLSplineStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
@ -13,7 +12,6 @@ export type TLLineShapeProps = {
color: TLColorType
dash: TLDashType
size: TLSizeType
opacity: TLOpacityType
spline: TLSplineType
handles: {
[key: string]: TLHandle
@ -30,7 +28,6 @@ export const lineShapeValidator: T.Validator<TLLineShape> = createShapeValidator
color: colorValidator,
dash: dashValidator,
size: sizeValidator,
opacity: opacityValidator,
spline: splineValidator,
handles: T.dict(T.string, handleValidator),
})

View file

@ -3,7 +3,6 @@ import { T } from '@tldraw/validate'
import { TLAlignType, alignValidator } from '../styles/TLAlignStyle'
import { TLColorType, colorValidator } from '../styles/TLColorStyle'
import { TLFontType, fontValidator } from '../styles/TLFontStyle'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { TLVerticalAlignType, verticalAlignValidator } from '../styles/TLVerticalAlignStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
@ -15,7 +14,6 @@ export type TLNoteShapeProps = {
font: TLFontType
align: TLAlignType
verticalAlign: TLVerticalAlignType
opacity: TLOpacityType
growY: number
url: string
text: string
@ -33,7 +31,6 @@ export const noteShapeValidator: T.Validator<TLNoteShape> = createShapeValidator
font: fontValidator,
align: alignValidator,
verticalAlign: verticalAlignValidator,
opacity: opacityValidator,
growY: T.positiveNumber,
url: T.string,
text: T.string,

View file

@ -3,7 +3,6 @@ import { T } from '@tldraw/validate'
import { TLAlignType, alignValidator } from '../styles/TLAlignStyle'
import { TLColorType, colorValidator } from '../styles/TLColorStyle'
import { TLFontType, fontValidator } from '../styles/TLFontStyle'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
@ -13,7 +12,6 @@ export type TLTextShapeProps = {
size: TLSizeType
font: TLFontType
align: TLAlignType
opacity: TLOpacityType
w: number
text: string
scale: number
@ -31,7 +29,6 @@ export const textShapeValidator: T.Validator<TLTextShape> = createShapeValidator
size: sizeValidator,
font: fontValidator,
align: alignValidator,
opacity: opacityValidator,
w: T.nonZeroNumber,
text: T.string,
scale: T.nonZeroNumber,

View file

@ -2,12 +2,10 @@ import { defineMigrations } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { assetIdValidator } from '../assets/TLBaseAsset'
import { TLAssetId } from '../records/TLAsset'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
/** @public */
export type TLVideoShapeProps = {
opacity: TLOpacityType
w: number
h: number
time: number
@ -23,7 +21,6 @@ export type TLVideoShape = TLBaseShape<'video', TLVideoShapeProps>
export const videoShapeValidator: T.Validator<TLVideoShape> = createShapeValidator(
'video',
T.object({
opacity: opacityValidator,
w: T.nonZeroNumber,
h: T.nonZeroNumber,
time: T.number,

View file

@ -7,7 +7,6 @@ export const TL_STYLE_TYPES = new Set([
'dash',
'fill',
'size',
'opacity',
'font',
'align',
'verticalAlign',

View file

@ -1,18 +1,11 @@
import { T } from '@tldraw/validate'
import { SetValue } from '../util-types'
import { TLBaseStyle } from './TLBaseStyle'
/** @public */
export const TL_OPACITY_TYPES = new Set(['0.1', '0.25', '0.5', '0.75', '1'] as const)
/** @public */
export type TLOpacityType = SetValue<typeof TL_OPACITY_TYPES>
/** @public */
export interface TLOpacityStyle extends TLBaseStyle {
id: TLOpacityType
type: 'opacity'
}
export type TLOpacityType = number
/** @internal */
export const opacityValidator = T.setEnum(TL_OPACITY_TYPES)
export const opacityValidator = T.number.check((n) => {
if (n < 0 || n > 1) {
throw new T.ValidationError('Opacity must be between 0 and 1')
}
})

View file

@ -4,7 +4,6 @@ import {
TLDashStyle,
TLFillStyle,
TLFontStyle,
TLOpacityStyle,
TLSizeStyle,
TLStyleType,
} from '..'
@ -20,7 +19,6 @@ export type TLStyleItem =
| TLFillStyle
| TLDashStyle
| TLSizeStyle
| TLOpacityStyle
| TLFontStyle
| TLAlignStyle
| TLVerticalAlignStyle
@ -36,7 +34,6 @@ export interface TLStyleCollections {
fill: TLFillStyle[]
dash: TLDashStyle[]
size: TLSizeStyle[]
opacity: TLOpacityStyle[]
font: TLFontStyle[]
align: TLAlignStyle[]
verticalAlign: TLVerticalAlignStyle[]
@ -44,7 +41,6 @@ export interface TLStyleCollections {
arrowheadStart: TLArrowheadStartStyle[]
arrowheadEnd: TLArrowheadEndStyle[]
spline: TLSplineStyle[]
// icon: TLIconStyle[]
}
/** @public */

View file

@ -1,11 +1,2 @@
/** @public */
export type SmooshedUnionObject<T> = {
[K in T extends infer P ? keyof P : never]: T extends infer P
? K extends keyof P
? P[K]
: never
: never
}
/** @public */
export type SetValue<T extends Set<any>> = T extends Set<infer U> ? U : never

View file

@ -1,6 +1,7 @@
import { Editor, TLNullableShapeProps, TLStyleItem, useEditor } from '@tldraw/editor'
import React, { useCallback } from 'react'
import { minBy } from '@tldraw/utils'
import { useValue } from 'signia-react'
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { Button } from '../primitives/Button'
@ -18,6 +19,10 @@ export const StylePanel = function StylePanel({ isMobile }: StylePanelProps) {
const editor = useEditor()
const props = useValue('props', () => editor.props, [editor])
const opacity = useValue('opacity', () => editor.opacity, [editor])
const toolShapeType = useValue('toolShapeType', () => editor.root.current.value?.shapeType, [
editor,
])
const handlePointerOut = useCallback(() => {
if (!isMobile) {
@ -25,9 +30,9 @@ export const StylePanel = function StylePanel({ isMobile }: StylePanelProps) {
}
}, [editor, isMobile])
if (!props) return null
if (!props && !toolShapeType) return null
const { geo, arrowheadEnd, arrowheadStart, spline, font } = props
const { geo, arrowheadEnd, arrowheadStart, spline, font } = props ?? {}
const hideGeo = geo === undefined
const hideArrowHeads = arrowheadEnd === undefined && arrowheadStart === undefined
@ -36,13 +41,13 @@ export const StylePanel = function StylePanel({ isMobile }: StylePanelProps) {
return (
<div className="tlui-style-panel" data-ismobile={isMobile} onPointerLeave={handlePointerOut}>
<CommonStylePickerSet props={props} />
{!hideText && <TextStylePickerSet props={props} />}
<CommonStylePickerSet props={props ?? {}} opacity={opacity} />
{!hideText && <TextStylePickerSet props={props ?? {}} />}
{!(hideGeo && hideArrowHeads && hideSpline) && (
<div className="tlui-style-panel__section" aria-label="style panel styles">
<GeoStylePickerSet props={props} />
<ArrowheadStylePickerSet props={props} />
<SplineStylePickerSet props={props} />
<GeoStylePickerSet props={props ?? {}} />
<ArrowheadStylePickerSet props={props ?? {}} />
<SplineStylePickerSet props={props ?? {}} />
</div>
)}
</div>
@ -65,7 +70,15 @@ function useStyleChangeCallback() {
)
}
function CommonStylePickerSet({ props }: { props: TLNullableShapeProps }) {
const tldrawSupportedOpacities = [0.1, 0.25, 0.5, 0.75, 1] as const
function CommonStylePickerSet({
props,
opacity,
}: {
props: TLNullableShapeProps
opacity: number | null
}) {
const editor = useEditor()
const msg = useTranslation()
@ -73,14 +86,14 @@ function CommonStylePickerSet({ props }: { props: TLNullableShapeProps }) {
const handleOpacityValueChange = React.useCallback(
(value: number, ephemeral: boolean) => {
const item = styles.opacity[value]
editor.setProp(item.type, item.id, ephemeral)
const item = tldrawSupportedOpacities[value]
editor.setOpacity(item, ephemeral)
editor.isChangingStyle = true
},
[editor]
)
const { color, fill, dash, size, opacity } = props
const { color, fill, dash, size } = props
if (
color === undefined &&
@ -94,7 +107,14 @@ function CommonStylePickerSet({ props }: { props: TLNullableShapeProps }) {
const showPickers = fill !== undefined || dash !== undefined || size !== undefined
const opacityIndex = styles.opacity.findIndex((s) => s.id === opacity)
const opacityIndex =
opacity === null
? -1
: tldrawSupportedOpacities.indexOf(
minBy(tldrawSupportedOpacities, (supportedOpacity) =>
Math.abs(supportedOpacity - opacity)
)!
)
return (
<>
@ -112,10 +132,10 @@ function CommonStylePickerSet({ props }: { props: TLNullableShapeProps }) {
{opacity === undefined ? null : (
<Slider
data-testid="style.opacity"
value={opacityIndex >= 0 ? opacityIndex : styles.opacity.length - 1}
value={opacityIndex >= 0 ? opacityIndex : tldrawSupportedOpacities.length - 1}
label={opacity ? `opacity-style.${opacity}` : 'style-panel.mixed'}
onValueChange={handleOpacityValueChange}
steps={styles.opacity.length - 1}
steps={tldrawSupportedOpacities.length - 1}
title={msg('style-panel.opacity')}
/>
)}

View file

@ -79,6 +79,7 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
y: element.y,
rotation: 0,
isLocked: element.locked,
opacity: getOpacity(element.opacity),
} as const
if (element.angle !== 0) {
@ -121,7 +122,6 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
type: 'geo',
props: {
geo: element.type,
opacity: getOpacity(element.opacity),
url: element.link ?? '',
w: element.width,
h: element.height,
@ -142,7 +142,6 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
props: {
dash: getDash(element),
size: strokeWidthsToSizes[element.strokeWidth],
opacity: getOpacity(element.opacity),
color: colorsToColors[element.strokeColor] ?? 'black',
segments: [
{
@ -169,7 +168,6 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
props: {
dash: getDash(element),
size: strokeWidthsToSizes[element.strokeWidth],
opacity: getOpacity(element.opacity),
color: colorsToColors[element.strokeColor] ?? 'black',
spline: element.roundness ? 'cubic' : 'line',
handles: {
@ -234,7 +232,6 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
text,
bend: getBend(element, start, end),
dash: getDash(element),
opacity: getOpacity(element.opacity),
size: strokeWidthsToSizes[element.strokeWidth] ?? 'm',
color: colorsToColors[element.strokeColor] ?? 'black',
start: startTargetId
@ -277,7 +274,6 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
size,
scale,
font: fontFamilyToFontType[element.fontFamily] ?? 'draw',
opacity: getOpacity(element.opacity),
color: colorsToColors[element.strokeColor] ?? 'black',
text: element.text,
align: textAlignToAlignTypes[element.textAlign],
@ -308,7 +304,6 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
...base,
type: 'image',
props: {
opacity: getOpacity(element.opacity),
w: element.width,
h: element.height,
assetId,
@ -370,16 +365,16 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
const getOpacity = (opacity: number): TLOpacityType => {
const t = opacity / 100
if (t < 0.2) {
return '0.1'
return 0.1
} else if (t < 0.4) {
return '0.25'
return 0.25
} else if (t < 0.6) {
return '0.5'
return 0.5
} else if (t < 0.8) {
return '0.75'
return 0.75
}
return '1'
return 1
}
const strokeWidthsToSizes: Record<number, TLSizeType> = {