From a220b2eff1d1db19678f833c79b994e9cc1f3e1b Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Tue, 30 May 2023 16:22:49 +0100 Subject: [PATCH] [feature] reduce motion (#1485) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds a user preference to reduce motion. When enabled the app will not animate the camera (and perhaps skip other animations in the future). It's actual implementation is as an `animateSpeed` property, so we can also use it to speed up or slow down our animations if that's something we want to do! ### Change Type - [x] `minor` — New Feature ### Test Plan 1. Turn on reduce motion 2. Use minimap / camera features to zoom in / out / etc - [x] Unit Tests ### Release Notes - [editor] Add `reduceMotion` user preference - Add reduce motion option to preferences --- assets/translations/main.json | 2 + packages/editor/api-report.md | 6 ++- packages/editor/src/lib/app/App.ts | 39 ++++++++++++++++--- .../app/managers/UserPreferencesManager.ts | 4 ++ .../src/lib/config/TLUserPreferences.ts | 27 ++++++++++++- .../lib/test/commands/animationSpeed.test.ts | 38 ++++++++++++++++++ packages/ui/api-report.md | 2 +- packages/ui/src/lib/hooks/useActions.tsx | 11 ++++++ .../ui/src/lib/hooks/useEventsProvider.tsx | 1 + packages/ui/src/lib/hooks/useMenuSchema.tsx | 3 ++ .../hooks/useTranslation/TLTranslationKey.ts | 2 + .../useTranslation/defaultTranslation.ts | 2 + 12 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 packages/editor/src/lib/test/commands/animationSpeed.test.ts diff --git a/assets/translations/main.json b/assets/translations/main.json index eb096550d..a87b96075 100644 --- a/assets/translations/main.json +++ b/assets/translations/main.json @@ -71,6 +71,8 @@ "action.toggle-auto-size": "Toggle auto size", "action.toggle-dark-mode.menu": "Dark mode", "action.toggle-dark-mode": "Toggle dark mode", + "action.toggle-reduce-motion.menu": "Reduce motion", + "action.toggle-reduce-motion": "Toggle reduce motion", "action.toggle-debug-mode.menu": "Debug mode", "action.toggle-debug-mode": "Toggle debug mode", "action.toggle-focus-mode.menu": "Focus mode", diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 8b04c42ca..89e59af1a 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -135,6 +135,8 @@ export class App extends EventEmitter { }): this; // (undocumented) animateToShape(shapeId: TLShapeId, opts?: AnimationOptions): this; + // (undocumented) + get animationSpeed(): number; // @internal (undocumented) annotateError(error: unknown, { origin, willCrashApp, tags, extras, }: { origin: string; @@ -449,6 +451,8 @@ export class App extends EventEmitter { selectNone(): this; sendBackward(ids?: TLShapeId[]): this; sendToBack(ids?: TLShapeId[]): this; + // (undocumented) + setAnimationSpeed(animationSpeed: number): this; setBrush(brush?: Box2dModel | null): this; setCamera(x: number, y: number, z?: number, { stopFollowing }?: ViewportOptions): this; // (undocumented) @@ -492,7 +496,7 @@ export class App extends EventEmitter { direction: Vec2d; friction: number; speedThreshold?: number | undefined; - }): this; + }): this | undefined; readonly snaps: SnapManager; get sortedShapesArray(): TLShape[]; stackShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[], gap?: number): this; diff --git a/packages/editor/src/lib/app/App.ts b/packages/editor/src/lib/app/App.ts index 772ebba2e..eca1865bc 100644 --- a/packages/editor/src/lib/app/App.ts +++ b/packages/editor/src/lib/app/App.ts @@ -1516,6 +1516,17 @@ export class App extends EventEmitter { return this } + get animationSpeed() { + return this.user.animationSpeed + } + + setAnimationSpeed(animationSpeed: number) { + if (animationSpeed !== this.animationSpeed) { + this.user.updateUserPreferences({ animationSpeed }) + } + return this + } + get isFocusMode() { return this.instanceState.isFocusMode } @@ -8378,21 +8389,33 @@ export class App extends EventEmitter { /** @internal */ private _animateToViewport(targetViewportPage: Box2d, opts = {} as AnimationOptions) { const { duration = 0, easing = EASINGS.easeInOutCubic } = opts - const startViewport = this.viewportPageBounds.clone() + const { animationSpeed, viewportPageBounds } = this + // If we have an existing animation, then stop it; also stop following any user this.stopCameraAnimation() if (this.instanceState.followingUserId) { this.stopFollowingUser() } + if (duration === 0 || animationSpeed === 0) { + // If we have no animation, then skip the animation and just set the camera + return this._setCamera( + -targetViewportPage.x, + -targetViewportPage.y, + this.viewportScreenBounds.width / targetViewportPage.width + ) + } + + // Set our viewport animation this._viewportAnimation = { elapsed: 0, - duration, + duration: duration / animationSpeed, easing, - start: startViewport, + start: viewportPageBounds.clone(), end: targetViewportPage, } + // On each tick, animate the viewport this.addListener('tick', this._animateViewport) return this @@ -8408,11 +8431,15 @@ export class App extends EventEmitter { ) { if (!this.canMoveCamera) return this - const { speed, direction, friction, speedThreshold = 0.01 } = opts - let currentSpeed = speed - this.stopCameraAnimation() + const { animationSpeed } = this + + if (animationSpeed === 0) return + + const { speed, friction, direction, speedThreshold = 0.01 } = opts + let currentSpeed = Math.min(speed, 1) + const cancel = () => { this.removeListener('tick', moveCamera) this.removeListener('stop-camera-animation', cancel) diff --git a/packages/editor/src/lib/app/managers/UserPreferencesManager.ts b/packages/editor/src/lib/app/managers/UserPreferencesManager.ts index 33f1cd6e6..cef9ad9c2 100644 --- a/packages/editor/src/lib/app/managers/UserPreferencesManager.ts +++ b/packages/editor/src/lib/app/managers/UserPreferencesManager.ts @@ -15,6 +15,10 @@ export class UserPreferencesManager { return this.editor.config.userPreferences.value.isDarkMode } + get animationSpeed() { + return this.editor.config.userPreferences.value.animationSpeed + } + get id() { return this.editor.config.userPreferences.value.id } diff --git a/packages/editor/src/lib/config/TLUserPreferences.ts b/packages/editor/src/lib/config/TLUserPreferences.ts index 629330733..e502c10ef 100644 --- a/packages/editor/src/lib/config/TLUserPreferences.ts +++ b/packages/editor/src/lib/config/TLUserPreferences.ts @@ -17,6 +17,7 @@ export interface TLUserPreferences { locale: string color: string isDarkMode: boolean + animationSpeed: number } interface UserDataSnapshot { @@ -30,15 +31,35 @@ interface UserChangeBroadcastMessage { data: UserDataSnapshot } -const userTypeValidator: T.Validator = T.object({ +const userTypeValidator: T.Validator = T.object({ id: T.string, name: T.string, locale: T.string, color: T.string, isDarkMode: T.boolean, + animationSpeed: T.number, }) -const userTypeMigrations = defineMigrations({}) +const Versions = { + AddAnimationSpeed: 1, +} as const + +const userTypeMigrations = defineMigrations({ + currentVersion: 1, + migrators: { + [Versions.AddAnimationSpeed]: { + up: (user) => { + return { + ...user, + animationSpeed: 1, + } + }, + down: ({ animationSpeed: _, ...user }) => { + return user + }, + }, + }, +}) /** @internal */ export const USER_COLORS = [ @@ -68,8 +89,10 @@ function getFreshUserPreferences(): TLUserPreferences { color: getRandomColor(), // TODO: detect dark mode isDarkMode: false, + animationSpeed: 1, } } + function migrateUserPreferences(userData: unknown) { if (userData === null || typeof userData !== 'object') { return getFreshUserPreferences() diff --git a/packages/editor/src/lib/test/commands/animationSpeed.test.ts b/packages/editor/src/lib/test/commands/animationSpeed.test.ts new file mode 100644 index 000000000..2342e68df --- /dev/null +++ b/packages/editor/src/lib/test/commands/animationSpeed.test.ts @@ -0,0 +1,38 @@ +import { TestApp } from '../TestApp' + +let app: TestApp + +beforeEach(() => { + app = new TestApp() +}) + +jest.useFakeTimers() + +it('zooms in gradually when duration is present and animtion speed is default', () => { + expect(app.zoomLevel).toBe(1) + app.setAnimationSpeed(1) // default + app.zoomIn(undefined, { duration: 100 }) + app.emit('tick', 25) // <-- quarter way + expect(app.zoomLevel).not.toBe(2) + app.emit('tick', 25) // 50 <-- half way + expect(app.zoomLevel).not.toBe(2) + app.emit('tick', 50) // 50 <-- done! + expect(app.zoomLevel).toBe(2) +}) + +it('zooms in gradually when duration is present and animtion speed is off', () => { + expect(app.zoomLevel).toBe(1) + app.setAnimationSpeed(0) // none + app.zoomIn(undefined, { duration: 100 }) + expect(app.zoomLevel).toBe(2) // <-- Should skip! +}) + +it('zooms in gradually when duration is present and animtion speed is double', () => { + expect(app.zoomLevel).toBe(1) + app.setAnimationSpeed(2) // default + app.zoomIn(undefined, { duration: 100 }) + app.emit('tick', 25) // <-- half way + expect(app.zoomLevel).not.toBe(2) + app.emit('tick', 25) // 50 <-- should finish + expect(app.zoomLevel).toBe(2) +}) diff --git a/packages/ui/api-report.md b/packages/ui/api-report.md index be8005687..a865c92c2 100644 --- a/packages/ui/api-report.md +++ b/packages/ui/api-report.md @@ -712,7 +712,7 @@ export type TLTranslation = { }; // @public (undocumented) -export type TLTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'; +export type TLTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'; // @public (undocumented) export type TLTranslationLocale = TLTranslations[number]['locale']; diff --git a/packages/ui/src/lib/hooks/useActions.tsx b/packages/ui/src/lib/hooks/useActions.tsx index 9d15042d5..e05d41110 100644 --- a/packages/ui/src/lib/hooks/useActions.tsx +++ b/packages/ui/src/lib/hooks/useActions.tsx @@ -797,6 +797,17 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) { }, checkbox: true, }, + { + id: 'toggle-reduce-motion', + label: 'action.toggle-reduce-motion', + menuLabel: 'action.toggle-reduce-motion.menu', + readonlyOk: true, + onSelect(source) { + trackEvent('toggle-reduce-motion', { source }) + app.setAnimationSpeed(app.animationSpeed === 0 ? 1 : 0) + }, + checkbox: true, + }, { id: 'toggle-transparent', label: 'action.toggle-transparent', diff --git a/packages/ui/src/lib/hooks/useEventsProvider.tsx b/packages/ui/src/lib/hooks/useEventsProvider.tsx index bd2349e55..f3abf0326 100644 --- a/packages/ui/src/lib/hooks/useEventsProvider.tsx +++ b/packages/ui/src/lib/hooks/useEventsProvider.tsx @@ -77,6 +77,7 @@ export interface TLUiEventMap { 'toggle-dark-mode': null 'toggle-focus-mode': null 'toggle-debug-mode': null + 'toggle-reduce-motion': null 'exit-pen-mode': null 'stop-following': null } diff --git a/packages/ui/src/lib/hooks/useMenuSchema.tsx b/packages/ui/src/lib/hooks/useMenuSchema.tsx index f5de9705a..994ef0244 100644 --- a/packages/ui/src/lib/hooks/useMenuSchema.tsx +++ b/packages/ui/src/lib/hooks/useMenuSchema.tsx @@ -50,6 +50,7 @@ export function MenuSchemaProvider({ overrides, children }: MenuSchemaProviderPr const isMobile = breakpoint < 5 const isDarkMode = useValue('isDarkMode', () => app.isDarkMode, [app]) + const animationSpeed = useValue('animationSpeed', () => app.animationSpeed, [app]) const isGridMode = useValue('isGridMode', () => app.userDocumentSettings.isGridMode, [app]) const isSnapMode = useValue('isSnapMode', () => app.userDocumentSettings.isSnapMode, [app]) const isToolLock = useValue('isToolLock', () => app.instanceState.isToolLocked, [app]) @@ -171,6 +172,7 @@ export function MenuSchemaProvider({ overrides, children }: MenuSchemaProviderPr menuItem(actions['toggle-grid'], { checked: isGridMode }), menuItem(actions['toggle-dark-mode'], { checked: isDarkMode }), menuItem(actions['toggle-focus-mode'], { checked: isFocusMode }), + menuItem(actions['toggle-reduce-motion'], { checked: animationSpeed === 0 }), menuItem(actions['toggle-debug-mode'], { checked: isDebugMode }) ) ), @@ -206,6 +208,7 @@ export function MenuSchemaProvider({ overrides, children }: MenuSchemaProviderPr noneSelected, canUndo, canRedo, + animationSpeed, isDarkMode, isGridMode, isSnapMode, diff --git a/packages/ui/src/lib/hooks/useTranslation/TLTranslationKey.ts b/packages/ui/src/lib/hooks/useTranslation/TLTranslationKey.ts index 0dd6ded42..2c179dc5e 100644 --- a/packages/ui/src/lib/hooks/useTranslation/TLTranslationKey.ts +++ b/packages/ui/src/lib/hooks/useTranslation/TLTranslationKey.ts @@ -75,6 +75,8 @@ export type TLTranslationKey = | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' + | 'action.toggle-reduce-motion.menu' + | 'action.toggle-reduce-motion' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' diff --git a/packages/ui/src/lib/hooks/useTranslation/defaultTranslation.ts b/packages/ui/src/lib/hooks/useTranslation/defaultTranslation.ts index bd9899ca5..68495669a 100644 --- a/packages/ui/src/lib/hooks/useTranslation/defaultTranslation.ts +++ b/packages/ui/src/lib/hooks/useTranslation/defaultTranslation.ts @@ -75,6 +75,8 @@ export const DEFAULT_TRANSLATION = { 'action.toggle-auto-size': 'Toggle auto size', 'action.toggle-dark-mode.menu': 'Dark mode', 'action.toggle-dark-mode': 'Toggle dark mode', + 'action.toggle-reduce-motion.menu': 'Reduce motion', + 'action.toggle-reduce-motion': 'Toggle reduce motion', 'action.toggle-debug-mode.menu': 'Debug mode', 'action.toggle-debug-mode': 'Toggle debug mode', 'action.toggle-focus-mode.menu': 'Focus mode',