Flip images (#4113)
This PR adds the ability to flip images. ### Change type - [x] `improvement` ### Test plan 1. Resize an image shape 2. Select an image shape and use the flip X / flip Y options in the context menu. - [x] Unit tests ### Release notes - Adds the ability to flip images.
This commit is contained in:
parent
7e561e54e5
commit
9a3afa2e2a
10 changed files with 89 additions and 15 deletions
|
@ -1926,10 +1926,10 @@ export function releasePointerCapture(element: Element, event: PointerEvent | Re
|
|||
export type RequiredKeys<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function resizeBox(shape: TLBaseBoxShape, info: {
|
||||
export function resizeBox<T extends TLBaseBoxShape>(shape: T, info: {
|
||||
handle: TLResizeHandle;
|
||||
initialBounds: Box;
|
||||
initialShape: TLBaseBoxShape;
|
||||
initialShape: T;
|
||||
mode: TLResizeMode;
|
||||
newPoint: VecModel;
|
||||
scaleX: number;
|
||||
|
@ -1939,14 +1939,7 @@ export function resizeBox(shape: TLBaseBoxShape, info: {
|
|||
maxWidth: number;
|
||||
minHeight: number;
|
||||
minWidth: number;
|
||||
}>): {
|
||||
props: {
|
||||
h: number;
|
||||
w: number;
|
||||
};
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}>): T;
|
||||
|
||||
// @public (undocumented)
|
||||
export type ResizeBoxOptions = Partial<{
|
||||
|
|
|
@ -608,6 +608,16 @@ input,
|
|||
border-radius: var(--radius-1);
|
||||
}
|
||||
|
||||
.tl-flip-x {
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
.tl-flip-y {
|
||||
transform: scale(1, -1);
|
||||
}
|
||||
.tl-flip-xy {
|
||||
transform: scale(-1, -1);
|
||||
}
|
||||
|
||||
/* --------------------- Nametag -------------------- */
|
||||
|
||||
.tl-collaborator-cursor {
|
||||
|
|
|
@ -14,8 +14,8 @@ export type ResizeBoxOptions = Partial<{
|
|||
}>
|
||||
|
||||
/** @public */
|
||||
export function resizeBox(
|
||||
shape: TLBaseBoxShape,
|
||||
export function resizeBox<T extends TLBaseBoxShape>(
|
||||
shape: T,
|
||||
info: {
|
||||
newPoint: VecModel
|
||||
handle: TLResizeHandle
|
||||
|
@ -23,10 +23,10 @@ export function resizeBox(
|
|||
scaleX: number
|
||||
scaleY: number
|
||||
initialBounds: Box
|
||||
initialShape: TLBaseBoxShape
|
||||
initialShape: T
|
||||
},
|
||||
opts = {} as ResizeBoxOptions
|
||||
) {
|
||||
): T {
|
||||
const { newPoint, handle, scaleX, scaleY } = info
|
||||
const { minWidth = 1, maxWidth = Infinity, minHeight = 1, maxHeight = Infinity } = opts
|
||||
|
||||
|
@ -118,6 +118,7 @@ export function resizeBox(
|
|||
const { x, y } = offset.rot(shape.rotation).add(newPoint)
|
||||
|
||||
return {
|
||||
...shape,
|
||||
x,
|
||||
y,
|
||||
props: {
|
||||
|
|
|
@ -1022,12 +1022,16 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
|||
// (undocumented)
|
||||
onDoubleClickEdge: TLOnDoubleClickHandler<TLImageShape>;
|
||||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<any>;
|
||||
// (undocumented)
|
||||
static props: {
|
||||
assetId: Validator<TLAssetId | null>;
|
||||
crop: Validator< {
|
||||
bottomRight: VecModel;
|
||||
topLeft: VecModel;
|
||||
} | null>;
|
||||
flipX: Validator<boolean>;
|
||||
flipY: Validator<boolean>;
|
||||
h: Validator<number>;
|
||||
playing: Validator<boolean>;
|
||||
url: Validator<string>;
|
||||
|
|
|
@ -7,14 +7,17 @@ import {
|
|||
MediaHelpers,
|
||||
TLImageShape,
|
||||
TLOnDoubleClickHandler,
|
||||
TLOnResizeHandler,
|
||||
TLShapePartial,
|
||||
Vec,
|
||||
fetch,
|
||||
imageShapeMigrations,
|
||||
imageShapeProps,
|
||||
resizeBox,
|
||||
structuredClone,
|
||||
toDomPrecision,
|
||||
} from '@tldraw/editor'
|
||||
import classNames from 'classnames'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { BrokenAssetIcon } from '../shared/BrokenAssetIcon'
|
||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||
|
@ -44,9 +47,26 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
|||
playing: true,
|
||||
url: '',
|
||||
crop: null,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
}
|
||||
}
|
||||
|
||||
override onResize: TLOnResizeHandler<any> = (shape: TLImageShape, info) => {
|
||||
let resized: TLImageShape = resizeBox(shape, info)
|
||||
const { flipX, flipY } = info.initialShape.props
|
||||
|
||||
resized = {
|
||||
...resized,
|
||||
props: {
|
||||
...resized.props,
|
||||
flipX: info.scaleX < 0 !== flipX,
|
||||
flipY: info.scaleY < 0 !== flipY,
|
||||
},
|
||||
}
|
||||
return resized
|
||||
}
|
||||
|
||||
isAnimated(shape: TLImageShape) {
|
||||
const asset = shape.props.assetId ? this.editor.getAsset(shape.props.assetId) : undefined
|
||||
|
||||
|
@ -177,7 +197,11 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
|||
>
|
||||
<div className="tl-image-container" style={containerStyle}>
|
||||
<img
|
||||
className="tl-image"
|
||||
className={classNames('tl-image', {
|
||||
'tl-flip-x': shape.props.flipX && !shape.props.flipY,
|
||||
'tl-flip-y': shape.props.flipY && !shape.props.flipX,
|
||||
'tl-flip-xy': shape.props.flipY && shape.props.flipX,
|
||||
})}
|
||||
// We don't set crossOrigin for non-animated images because
|
||||
// for Cloudflare we don't currenly have that set up.
|
||||
crossOrigin={this.isAnimated(shape) ? 'anonymous' : undefined}
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
TLArrowShape,
|
||||
TLDrawShape,
|
||||
TLGroupShape,
|
||||
TLImageShape,
|
||||
TLLineShape,
|
||||
TLTextShape,
|
||||
useEditor,
|
||||
|
@ -195,6 +196,7 @@ export function useOnlyFlippableShape() {
|
|||
return (
|
||||
shape &&
|
||||
(editor.isShapeOfType<TLGroupShape>(shape, 'group') ||
|
||||
editor.isShapeOfType<TLImageShape>(shape, 'image') ||
|
||||
editor.isShapeOfType<TLArrowShape>(shape, 'arrow') ||
|
||||
editor.isShapeOfType<TLLineShape>(shape, 'line') ||
|
||||
editor.isShapeOfType<TLDrawShape>(shape, 'draw'))
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
TLArrowShape,
|
||||
TLArrowShapeProps,
|
||||
TLBindingCreate,
|
||||
TLImageShape,
|
||||
TLShapeId,
|
||||
TLShapePartial,
|
||||
createBindingId,
|
||||
|
@ -624,3 +625,14 @@ describe('When flipping shapes that include arrows', () => {
|
|||
expect(editor.getSelectionRotatedPageBounds()).toCloselyMatchObject(boundsBefore)
|
||||
})
|
||||
})
|
||||
|
||||
it('Updates the image shape flip properties when flipped', () => {
|
||||
editor.createShape({
|
||||
type: 'image',
|
||||
})
|
||||
editor.select(editor.getLastCreatedShape())
|
||||
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
|
||||
expect(editor.getLastCreatedShape<TLImageShape>().props.flipX).toBe(true)
|
||||
editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
|
||||
expect(editor.getLastCreatedShape<TLImageShape>().props.flipY).toBe(true)
|
||||
})
|
||||
|
|
|
@ -600,6 +600,8 @@ export const imageShapeProps: {
|
|||
bottomRight: VecModel;
|
||||
topLeft: VecModel;
|
||||
} | null>;
|
||||
flipX: T.Validator<boolean>;
|
||||
flipY: T.Validator<boolean>;
|
||||
h: T.Validator<number>;
|
||||
playing: T.Validator<boolean>;
|
||||
url: T.Validator<string>;
|
||||
|
|
|
@ -1974,6 +1974,18 @@ describe('Add scale to line shape', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Add flipX, flipY to image shape', () => {
|
||||
const { up, down } = getTestMigration(imageShapeVersions.AddFlipProps)
|
||||
|
||||
test('up works as expected', () => {
|
||||
expect(up({ props: {} })).toEqual({ props: { flipX: false, flipY: false } })
|
||||
})
|
||||
|
||||
test('down works as expected', () => {
|
||||
expect(down({ props: { flipX: false, flipY: false } })).toEqual({ props: {} })
|
||||
})
|
||||
})
|
||||
|
||||
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
||||
|
||||
// check that all migrator fns were called at least once
|
||||
|
|
|
@ -21,6 +21,8 @@ export const imageShapeProps = {
|
|||
url: T.linkUrl,
|
||||
assetId: assetIdValidator.nullable(),
|
||||
crop: ImageShapeCrop.nullable(),
|
||||
flipX: T.boolean,
|
||||
flipY: T.boolean,
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -33,6 +35,7 @@ const Versions = createShapePropsMigrationIds('image', {
|
|||
AddUrlProp: 1,
|
||||
AddCropProp: 2,
|
||||
MakeUrlsValid: 3,
|
||||
AddFlipProps: 4,
|
||||
})
|
||||
|
||||
export { Versions as imageShapeVersions }
|
||||
|
@ -67,5 +70,16 @@ export const imageShapeMigrations = createShapePropsMigrationSequence({
|
|||
// noop
|
||||
},
|
||||
},
|
||||
{
|
||||
id: Versions.AddFlipProps,
|
||||
up: (props) => {
|
||||
props.flipX = false
|
||||
props.flipY = false
|
||||
},
|
||||
down: (props) => {
|
||||
delete props.flipX
|
||||
delete props.flipY
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue