Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 77 additions & 22 deletions app/lib/cubits/transform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ part 'transform.freezed.dart';

const kMinZoom = 0.1;
const kMaxZoom = 10.0;
const kInfiniteZoomFlag = 'infiniteZoom';
const kRoundPrecision = 3;
const kDrag = 0.001;
const kBoundsSnapBackDuration = 0.18;

double? getZoomUpperBound(ButterflySettings settings) =>
settings.hasFlag(kInfiniteZoomFlag) ? null : kMaxZoom;

@freezed
sealed class FrictionState with _$FrictionState {
const factory FrictionState(
Expand Down Expand Up @@ -51,9 +55,16 @@ sealed class CameraTransform with _$CameraTransform {
CameraTransform withPointPosition(Point<double> position) =>
CameraTransform(pixelRatio, position.toOffset(), size, rotation);

CameraTransform withSize(double size, [Offset cursor = Offset.zero]) {
CameraTransform withSize(
double size, [
Offset cursor = Offset.zero,
double? maxZoom = kMaxZoom,
]) {
// Set size and focus on cursor if provided
final double newSize = size.clamp(kMinZoom, kMaxZoom);
final requestedSize = size.isFinite ? size : this.size;
final double newSize = maxZoom == null
? max(requestedSize, kMinZoom)
: requestedSize.clamp(kMinZoom, maxZoom);
return CameraTransform(
pixelRatio,
localToGlobal(cursor) - cursor.rotate(Offset.zero, -rotation) / newSize,
Expand Down Expand Up @@ -128,6 +139,7 @@ sealed class CameraTransform with _$CameraTransform {
Offset velocityPosition,
double velocitySize, {
Rect? positionBounds,
double? maxZoom = kMaxZoom,
}) {
final simX = _getSimulation(velocityPosition.dx);
final finalX = simX.finalX;
Expand All @@ -141,7 +153,11 @@ sealed class CameraTransform with _$CameraTransform {
final finalPosition = positionBounds == null
? position - finalPos
: _clampPosition(position - finalPos, positionBounds);
final finalScale = (size + finalSize).clamp(kMinZoom, kMaxZoom);
final simulatedScale = size + finalSize;
final requestedScale = simulatedScale.isFinite ? simulatedScale : size;
final finalScale = maxZoom == null
? max(requestedScale, kMinZoom)
: requestedScale.clamp(kMinZoom, maxZoom);
var duration = max(durationPosition, durationSize);
if (!duration.isFinite) {
duration = 0;
Expand Down Expand Up @@ -189,15 +205,27 @@ class TransformCubit extends Cubit<CameraTransform> {

void move(Offset delta) => emit(state.withPosition(state.position + delta));

void teleport(Offset position, [double? scale, double? rotation]) => emit(
void teleport(
Offset position, [
double? scale,
double? rotation,
double? maxZoom = kMaxZoom,
]) => emit(
state
.withPosition(position)
.withSize(scale ?? state.size)
.withSize(
scale ?? state.size,
Offset.zero,
scale == null ? null : maxZoom,
)
.withRotation(rotation ?? state.rotation),
);

void zoom(double delta, [Offset cursor = Offset.zero]) =>
emit(state.withSize(state.size * delta, cursor));
void zoom(
double delta, [
Offset cursor = Offset.zero,
double? maxZoom = kMaxZoom,
]) => emit(state.withSize(state.size * delta, cursor, maxZoom));

void focus(Offset cursor) => emit(state.withSize(state.size, cursor));

Expand All @@ -206,40 +234,54 @@ class TransformCubit extends Cubit<CameraTransform> {

void reset() => emit(CameraTransform(state.pixelRatio));

void size(double size, [Offset cursor = Offset.zero]) =>
emit(state.withSize(size, cursor));

void teleportToWaypoint(Waypoint waypoint) =>
teleport(waypoint.position.toOffset(), waypoint.scale ?? state.size);
void size(
double size, [
Offset cursor = Offset.zero,
double? maxZoom = kMaxZoom,
]) => emit(state.withSize(size, cursor, maxZoom));

void teleportToWaypoint(Waypoint waypoint, {double? maxZoom = kMaxZoom}) =>
teleport(
waypoint.position.toOffset(),
waypoint.scale ?? state.size,
null,
maxZoom,
);

void teleportToArea(
Area area, [
Size? screen,
RenderResolution resolution = RenderResolution.performance,
double? maxZoom = kMaxZoom,
]) {
if (screen == null || area.width <= 0 || area.height <= 0) {
teleport(area.position.toOffset(), state.size);
teleport(area.position.toOffset(), state.size, null, maxZoom);
return;
}

final effectiveScreen = screen / resolution.multiplier;
final width = effectiveScreen.width / area.width;
final height = effectiveScreen.height / area.height;
final size = min(width, height).clamp(kMinZoom, kMaxZoom);
final requestedSize = min(width, height);
final size = maxZoom == null
? max(requestedSize, kMinZoom)
: requestedSize.clamp(kMinZoom, maxZoom);
final position =
area.rect.center - effectiveScreen.center(Offset.zero) / size;
teleport(position, size);
teleport(position, size, null, maxZoom);
}

void slide(
Offset velocityPosition,
double velocitySize, {
Rect? positionBounds,
double? maxZoom = kMaxZoom,
}) => emit(
state.withFriction(
velocityPosition,
velocitySize,
positionBounds: positionBounds,
maxZoom: maxZoom,
),
);

Expand Down Expand Up @@ -558,12 +600,16 @@ class TransformCubit extends Cubit<CameraTransform> {
return;
}
if (force) {
zoom(delta, cursor);
zoom(delta, cursor, getZoomUpperBound(runtime.settingsCubit.state));
return;
}
final transform = state.withSize(state.size * delta, cursor);
final transform = state.withSize(
state.size * delta,
cursor,
getZoomUpperBound(runtime.settingsCubit.state),
);
final clamped = _clampTransform(transform: transform, runtime: runtime);
teleport(clamped.position, clamped.size);
emit(clamped);
}

void rotateConstrained(
Expand All @@ -587,14 +633,18 @@ class TransformCubit extends Cubit<CameraTransform> {
final locks = runtime.viewCubit.state.locks;
if (locks.lockZoom && !force) return;
if (force) {
this.size(size, cursor);
this.size(size, cursor, getZoomUpperBound(runtime.settingsCubit.state));
return;
}
final transform = _clampTransform(
transform: state.withSize(size, cursor),
transform: state.withSize(
size,
cursor,
getZoomUpperBound(runtime.settingsCubit.state),
),
runtime: runtime,
);
teleport(transform.position, transform.size);
emit(transform);
}

void slideConstrained(
Expand Down Expand Up @@ -655,6 +705,11 @@ class TransformCubit extends Cubit<CameraTransform> {
!outOfBounds) {
return;
}
slide(positionVelocity, sizeVelocity, positionBounds: bounds);
slide(
positionVelocity,
sizeVelocity,
positionBounds: bounds,
maxZoom: getZoomUpperBound(runtime.settingsCubit.state),
);
}
}
8 changes: 7 additions & 1 deletion app/lib/handlers/presentation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,13 @@ mixin GeneralPresentationHandler {
final zoom = animation.interpolateCameraZoom(currentFrame);
if (position == null && zoom == null) return;
if (position != null) transformCubit.teleport(position.toOffset());
if (zoom != null) transformCubit.size(zoom);
if (zoom != null) {
transformCubit.size(
zoom,
Offset.zero,
getZoomUpperBound(cubit.settingsCubit.state),
);
}
bloc.delayedBake(testTransform: true);
}

Expand Down
5 changes: 5 additions & 0 deletions app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,10 @@
"@edgePanAreaSwitching": {
"description": "Switch areas when you pan near the edge of the canvas"
},
"infiniteZoom": "Infinite zoom",
"@infiniteZoom": {
"description": "Allow zooming beyond the standard maximum"
},
"useAndroidSaf": "Use Android SAF",
"exact": "Exact",
"@exact": {
Expand Down Expand Up @@ -1553,6 +1557,7 @@
"onStartupNewNoteDescription": "Creates a new note when Butterfly starts.",
"smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.",
"edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.",
"infiniteZoomDescription": "Removes the standard maximum zoom level. Embedded raster content may stop getting sharper at very high zoom levels.",
"collaborationDescription": "Allows multiple people to edit the same note together in real time.",
"showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.",
"bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.",
Expand Down
6 changes: 4 additions & 2 deletions app/lib/renderers/elements/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,15 @@ abstract class GenericTextRenderer<T extends LabelElement> extends Renderer<T> {
final span = paragraph.textSpans[i];
if (span is text.MathTextSpan && !_renderedLatex.containsKey(i)) {
final widget = _buildLatexElement(styleSheet, paragraphStyle, span);
const maxRasterScale = 16.0;
final pixelRatio = transform.pixelRatio * transform.size;
final renderPixelRatio = min(scale * pixelRatio, maxRasterScale);
try {
final image = await renderWidget(
widget,
pixelRatio: scale * pixelRatio,
pixelRatio: renderPixelRatio,
);
_renderedLatex[i] = (image, pixelRatio);
_renderedLatex[i] = (image, renderPixelRatio / scale);
} catch (_) {}
}
}
Expand Down
1 change: 1 addition & 0 deletions app/lib/settings/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:butterfly/actions/shortcuts.dart';
import 'package:butterfly/api/file_system.dart';
import 'package:butterfly/api/open.dart';
import 'package:butterfly/cubits/settings.dart';
import 'package:butterfly/cubits/transform.dart';
import 'package:butterfly/dialogs/input.dart';
import 'package:butterfly/main.dart';
import 'package:butterfly/repositories/document_state.dart';
Expand Down
10 changes: 10 additions & 0 deletions app/lib/settings/pages/experiments.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ final _experimentsSettingsPage = SettingsLeapPage<ButterflySettings>(
AppLocalizations.of(context).collaboration,
AppLocalizations.of(context).smoothNavigation,
AppLocalizations.of(context).edgePanAreaSwitching,
AppLocalizations.of(context).infiniteZoom,
],
appBarBuilder: _butterflyAppBar,
onReset: (context, state) => context.read<SettingsCubit>().resetFlags(),
Expand Down Expand Up @@ -43,6 +44,15 @@ final _experimentsSettingsPage = SettingsLeapPage<ButterflySettings>(
write: (context, value) =>
_changeFlag(context, 'edgePanAreaSwitching', value),
),
SettingsLeapBoolSetting(
displayName: (context) => AppLocalizations.of(context).infiniteZoom,
hintBuilder: (context) =>
AppLocalizations.of(context).infiniteZoomDescription,
icon: PhosphorIconsLight.infinity,
read: (state) => state.hasFlag(kInfiniteZoomFlag),
write: (context, value) =>
_changeFlag(context, kInfiniteZoomFlag, value),
),
],
),
},
Expand Down
6 changes: 5 additions & 1 deletion app/lib/views/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ class _ProjectPageState extends State<ProjectPage> {
),
initialSession.camera.zoom,
initialSession.camera.rotation,
getZoomUpperBound(settingsCubit.state),
);
final editorSessionCubit = EditorSessionCubit(
repository: documentStateRepository,
Expand Down Expand Up @@ -488,7 +489,10 @@ class _ProjectPageState extends State<ProjectPage> {
return;
}
if (restoredSession == null) {
transformCubit.teleportToWaypoint(page.getOriginWaypoint());
transformCubit.teleportToWaypoint(
page.getOriginWaypoint(),
maxZoom: getZoomUpperBound(settingsCubit.state),
);
}
final editorController = EditorController(
settingsCubit,
Expand Down
1 change: 1 addition & 0 deletions app/lib/views/navigator/areas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ class _AreasViewState extends State<AreasView> {
area,
viewport.toSize(),
viewport.resolution,
getZoomUpperBound(context.read<SettingsCubit>().state),
);
bloc.add(CurrentAreaChanged(area.name));
}
Expand Down
27 changes: 25 additions & 2 deletions app/lib/views/navigator/waypoints.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:butterfly/bloc/document_bloc.dart';
import 'package:butterfly/cubits/settings.dart';
import 'package:butterfly_api/butterfly_api.dart';
import 'package:flutter/material.dart';
import 'package:butterfly/src/generated/i18n/app_localizations.dart';
Expand Down Expand Up @@ -85,7 +86,14 @@ class _WaypointsViewState extends State<WaypointsView> {
onTap: () {
context
.read<TransformCubit>()
.teleportToWaypoint(origin);
.teleportToWaypoint(
origin,
maxZoom: getZoomUpperBound(
context
.read<SettingsCubit>()
.state,
),
);
context.read<DocumentBloc>().bake();
},
trailing: button,
Expand Down Expand Up @@ -159,6 +167,14 @@ class _WaypointsViewState extends State<WaypointsView> {
bloc.transformCubit
.teleportToWaypoint(
Waypoint.defaultOrigin,
maxZoom:
getZoomUpperBound(
context
.read<
SettingsCubit
>()
.state,
),
);
bloc.delayedBake();
},
Expand Down Expand Up @@ -218,7 +234,14 @@ class _WaypointsViewState extends State<WaypointsView> {
onTap: () {
context
.read<TransformCubit>()
.teleportToWaypoint(waypoint);
.teleportToWaypoint(
waypoint,
maxZoom: getZoomUpperBound(
context
.read<SettingsCubit>()
.state,
),
);
context.read<DocumentBloc>().bake();
},
onSaved: (value) =>
Expand Down
Loading
Loading