mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-27 00:05:39 +00:00
Merge pull request #499 from webadderallorg/fix-motion-blur-regressions
fix motion blur regressions
This commit is contained in:
@@ -2734,7 +2734,7 @@ export function SettingsPanel({
|
||||
.maxDirectionalBlurPx
|
||||
}
|
||||
min={0}
|
||||
max={32}
|
||||
max={96}
|
||||
step={0.1}
|
||||
onChange={(value) =>
|
||||
onZoomMotionBlurTuningChange?.({
|
||||
@@ -2778,7 +2778,7 @@ export function SettingsPanel({
|
||||
.maxRadialBlurStrength
|
||||
}
|
||||
min={0}
|
||||
max={0.5}
|
||||
max={1.5}
|
||||
step={0.005}
|
||||
onChange={(value) =>
|
||||
onZoomMotionBlurTuningChange?.({
|
||||
|
||||
@@ -135,6 +135,7 @@ import {
|
||||
fromFileUrl,
|
||||
normalizeProjectEditor,
|
||||
resolveVideoUrl,
|
||||
stripPersistedDevMotionBlurSettings,
|
||||
toFileUrl,
|
||||
validateProjectData,
|
||||
} from "./projectPersistence";
|
||||
@@ -1774,7 +1775,7 @@ export default function VideoEditor() {
|
||||
defaultSourceAudioTrackSettings: SourceAudioTrackSettings;
|
||||
}>,
|
||||
) => {
|
||||
return editor;
|
||||
return stripPersistedDevMotionBlurSettings(editor);
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -2038,7 +2039,9 @@ export default function VideoEditor() {
|
||||
|
||||
const project = candidate;
|
||||
const sourcePath = fromFileUrl(project.videoPath);
|
||||
const normalizedEditor = normalizeProjectEditor(project.editor);
|
||||
const normalizedEditor = normalizeProjectEditor(
|
||||
stripPersistedDevMotionBlurSettings(project.editor ?? {}),
|
||||
);
|
||||
|
||||
try {
|
||||
videoPlaybackRef.current?.pause();
|
||||
|
||||
@@ -135,6 +135,7 @@ import {
|
||||
DEFAULT_CURSOR_CLICK_BOUNCE_DURATION,
|
||||
DEFAULT_CURSOR_MOTION_BLUR,
|
||||
DEFAULT_CURSOR_SIZE,
|
||||
DEFAULT_CURSOR_STYLE,
|
||||
DEFAULT_CURSOR_SMOOTHING,
|
||||
DEFAULT_CURSOR_SWAY,
|
||||
DEFAULT_PADDING,
|
||||
@@ -429,7 +430,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
onAnnotationSizeChange,
|
||||
cursorTelemetry = [],
|
||||
showCursor = false,
|
||||
cursorStyle = "tahoe",
|
||||
cursorStyle = DEFAULT_CURSOR_STYLE,
|
||||
cursorSize = DEFAULT_CURSOR_SIZE,
|
||||
cursorSmoothing = DEFAULT_CURSOR_SMOOTHING,
|
||||
cursorSpringStiffnessMultiplier = 1,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import * as fc from "fast-check";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { CURSOR_MOTION_PRESETS } from "./cursorMotionPresets";
|
||||
import { fromFileUrl, normalizeProjectEditor, toFileUrl } from "./projectPersistence";
|
||||
import {
|
||||
fromFileUrl,
|
||||
normalizeProjectEditor,
|
||||
stripPersistedDevMotionBlurSettings,
|
||||
toFileUrl,
|
||||
} from "./projectPersistence";
|
||||
|
||||
describe("Audio path handling", () => {
|
||||
describe("toFileUrl produces valid file:// URLs for audio paths", () => {
|
||||
@@ -288,3 +293,24 @@ describe("Motion preset normalization", () => {
|
||||
expect(result.cursorClickBounceDuration).toBe(focused.cursorClickBounceDuration);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Dev-only motion blur persistence", () => {
|
||||
it("strips legacy dev-only blur tuning from project data before normalization", () => {
|
||||
const defaults = normalizeProjectEditor({} as any);
|
||||
const stripped = stripPersistedDevMotionBlurSettings({
|
||||
zoomMotionBlurTuning: {
|
||||
...defaults.zoomMotionBlurTuning,
|
||||
maxDirectionalBlurPx: 96,
|
||||
maxRadialBlurStrength: 1.5,
|
||||
},
|
||||
wallpaper: "/wallpapers/wallpaper2.jpg",
|
||||
} as any);
|
||||
|
||||
expect(stripped).not.toHaveProperty("zoomMotionBlurTuning");
|
||||
|
||||
const result = normalizeProjectEditor(stripped as any);
|
||||
|
||||
expect(result.wallpaper).toBe("/wallpapers/wallpaper2.jpg");
|
||||
expect(result.zoomMotionBlurTuning).toEqual(defaults.zoomMotionBlurTuning);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,8 +69,8 @@ describe("editorPreferences", () => {
|
||||
expect(DEFAULT_EDITOR_PREFERENCES.exportQuality).toBe("source");
|
||||
});
|
||||
|
||||
it("defaults cursor preferences to Tahoe at 2.5x with lighter sway", () => {
|
||||
expect(DEFAULT_EDITOR_PREFERENCES.cursorStyle).toBe("tahoe");
|
||||
it("defaults cursor preferences to macOS at 2.5x with lighter sway", () => {
|
||||
expect(DEFAULT_EDITOR_PREFERENCES.cursorStyle).toBe("macos");
|
||||
expect(DEFAULT_EDITOR_PREFERENCES.cursorSize).toBe(2.5);
|
||||
expect(DEFAULT_EDITOR_PREFERENCES.cursorSway).toBe(0.25);
|
||||
});
|
||||
@@ -79,6 +79,57 @@ describe("editorPreferences", () => {
|
||||
expect(DEFAULT_EDITOR_PREFERENCES.exportPipelineModel).toBe("modern");
|
||||
});
|
||||
|
||||
it("bakes in the stronger split motion blur defaults", () => {
|
||||
expect(DEFAULT_EDITOR_PREFERENCES.zoomMotionBlurTuning).toMatchObject({
|
||||
panVelocityThreshold: 0,
|
||||
zoomVelocityThreshold: 0,
|
||||
maxDirectionalBlurPx: 41.8,
|
||||
maxRadialBlurStrength: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores legacy persisted dev-only motion blur overrides", () => {
|
||||
vi.stubGlobal(
|
||||
"localStorage",
|
||||
createStorageMock({
|
||||
[EDITOR_PREFERENCES_STORAGE_KEY]: JSON.stringify({
|
||||
zoomMotionBlurTuning: {
|
||||
panVelocityThreshold: 40,
|
||||
zoomVelocityThreshold: 0.2,
|
||||
maxDirectionalBlurPx: 96,
|
||||
maxRadialBlurStrength: 1.5,
|
||||
panResponsePerSecond: 22,
|
||||
zoomResponsePerSecond: 18,
|
||||
zoomSafeZoneRadiusPx: 40,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const loaded = loadEditorPreferences();
|
||||
|
||||
expect(loaded.zoomMotionBlurTuning).toEqual(DEFAULT_EDITOR_PREFERENCES.zoomMotionBlurTuning);
|
||||
});
|
||||
|
||||
it("does not save dev-only split blur tuning overrides to editor preferences", () => {
|
||||
const localStorage = createStorageMock();
|
||||
vi.stubGlobal("localStorage", localStorage);
|
||||
|
||||
saveEditorPreferences({
|
||||
zoomMotionBlurTuning: {
|
||||
...DEFAULT_EDITOR_PREFERENCES.zoomMotionBlurTuning,
|
||||
maxDirectionalBlurPx: 96,
|
||||
maxRadialBlurStrength: 1.5,
|
||||
},
|
||||
});
|
||||
|
||||
const stored = JSON.parse(
|
||||
localStorage.getItem(EDITOR_PREFERENCES_STORAGE_KEY) ?? "{}",
|
||||
) as Record<string, unknown>;
|
||||
|
||||
expect(stored).not.toHaveProperty("zoomMotionBlurTuning");
|
||||
});
|
||||
|
||||
it("loads stored editor control preferences", () => {
|
||||
vi.stubGlobal(
|
||||
"localStorage",
|
||||
@@ -212,8 +263,7 @@ describe("editorPreferences", () => {
|
||||
autoApplyFreshRecordingAutoZooms: false,
|
||||
});
|
||||
|
||||
expect(loadEditorPreferences()).toEqual({
|
||||
...DEFAULT_EDITOR_PREFERENCES,
|
||||
expect(loadEditorPreferences()).toMatchObject({
|
||||
wallpaper: "linear-gradient(to right, #000000, #ffffff)",
|
||||
shadowIntensity: 0.4,
|
||||
backgroundBlur: 1.5,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
normalizeExportMp4FrameRate,
|
||||
normalizeExportPipelineModel,
|
||||
normalizeProjectEditor,
|
||||
stripPersistedDevMotionBlurSettings,
|
||||
type ProjectEditorState,
|
||||
} from "./projectPersistence";
|
||||
|
||||
@@ -267,71 +268,74 @@ function normalizeEditorControls(
|
||||
raw: Partial<EditorPreferences>,
|
||||
fallback: EditorPreferences,
|
||||
): PersistedEditorControls {
|
||||
const sanitizedRaw = stripPersistedDevMotionBlurSettings(raw);
|
||||
const candidate: PartialEditorControls = {
|
||||
wallpaper: raw.wallpaper ?? fallback.wallpaper,
|
||||
shadowIntensity: raw.shadowIntensity ?? fallback.shadowIntensity,
|
||||
backgroundBlur: raw.backgroundBlur ?? fallback.backgroundBlur,
|
||||
zoomMotionBlur: raw.zoomMotionBlur ?? fallback.zoomMotionBlur,
|
||||
zoomMotionBlurTuning: raw.zoomMotionBlurTuning ?? fallback.zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur: raw.zoomTemporalMotionBlur ?? fallback.zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount:
|
||||
raw.zoomMotionBlurSampleCount ?? fallback.zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction:
|
||||
raw.zoomMotionBlurShutterFraction ?? fallback.zoomMotionBlurShutterFraction,
|
||||
connectZooms: raw.connectZooms ?? fallback.connectZooms,
|
||||
zoomInDurationMs: raw.zoomInDurationMs ?? fallback.zoomInDurationMs,
|
||||
zoomInOverlapMs: raw.zoomInOverlapMs ?? fallback.zoomInOverlapMs,
|
||||
zoomOutDurationMs: raw.zoomOutDurationMs ?? fallback.zoomOutDurationMs,
|
||||
connectedZoomGapMs: raw.connectedZoomGapMs ?? fallback.connectedZoomGapMs,
|
||||
connectedZoomDurationMs: raw.connectedZoomDurationMs ?? fallback.connectedZoomDurationMs,
|
||||
zoomInEasing: raw.zoomInEasing ?? fallback.zoomInEasing,
|
||||
zoomOutEasing: raw.zoomOutEasing ?? fallback.zoomOutEasing,
|
||||
connectedZoomEasing: raw.connectedZoomEasing ?? fallback.connectedZoomEasing,
|
||||
showCursor: raw.showCursor ?? fallback.showCursor,
|
||||
loopCursor: raw.loopCursor ?? fallback.loopCursor,
|
||||
cursorStyle: raw.cursorStyle ?? fallback.cursorStyle,
|
||||
cursorSize: raw.cursorSize ?? fallback.cursorSize,
|
||||
cursorSmoothing: raw.cursorSmoothing ?? fallback.cursorSmoothing,
|
||||
wallpaper: sanitizedRaw.wallpaper ?? fallback.wallpaper,
|
||||
shadowIntensity: sanitizedRaw.shadowIntensity ?? fallback.shadowIntensity,
|
||||
backgroundBlur: sanitizedRaw.backgroundBlur ?? fallback.backgroundBlur,
|
||||
zoomMotionBlur: sanitizedRaw.zoomMotionBlur ?? fallback.zoomMotionBlur,
|
||||
connectZooms: sanitizedRaw.connectZooms ?? fallback.connectZooms,
|
||||
zoomInDurationMs: sanitizedRaw.zoomInDurationMs ?? fallback.zoomInDurationMs,
|
||||
zoomInOverlapMs: sanitizedRaw.zoomInOverlapMs ?? fallback.zoomInOverlapMs,
|
||||
zoomOutDurationMs: sanitizedRaw.zoomOutDurationMs ?? fallback.zoomOutDurationMs,
|
||||
connectedZoomGapMs:
|
||||
sanitizedRaw.connectedZoomGapMs ?? fallback.connectedZoomGapMs,
|
||||
connectedZoomDurationMs:
|
||||
sanitizedRaw.connectedZoomDurationMs ?? fallback.connectedZoomDurationMs,
|
||||
zoomInEasing: sanitizedRaw.zoomInEasing ?? fallback.zoomInEasing,
|
||||
zoomOutEasing: sanitizedRaw.zoomOutEasing ?? fallback.zoomOutEasing,
|
||||
connectedZoomEasing:
|
||||
sanitizedRaw.connectedZoomEasing ?? fallback.connectedZoomEasing,
|
||||
showCursor: sanitizedRaw.showCursor ?? fallback.showCursor,
|
||||
loopCursor: sanitizedRaw.loopCursor ?? fallback.loopCursor,
|
||||
cursorStyle: sanitizedRaw.cursorStyle ?? fallback.cursorStyle,
|
||||
cursorSize: sanitizedRaw.cursorSize ?? fallback.cursorSize,
|
||||
cursorSmoothing: sanitizedRaw.cursorSmoothing ?? fallback.cursorSmoothing,
|
||||
cursorSpringStiffnessMultiplier:
|
||||
raw.cursorSpringStiffnessMultiplier ?? fallback.cursorSpringStiffnessMultiplier,
|
||||
sanitizedRaw.cursorSpringStiffnessMultiplier ??
|
||||
fallback.cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier:
|
||||
raw.cursorSpringDampingMultiplier ?? fallback.cursorSpringDampingMultiplier,
|
||||
sanitizedRaw.cursorSpringDampingMultiplier ??
|
||||
fallback.cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier:
|
||||
raw.cursorSpringMassMultiplier ?? fallback.cursorSpringMassMultiplier,
|
||||
sanitizedRaw.cursorSpringMassMultiplier ?? fallback.cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier:
|
||||
raw.cameraSpringStiffnessMultiplier ?? fallback.cameraSpringStiffnessMultiplier,
|
||||
sanitizedRaw.cameraSpringStiffnessMultiplier ??
|
||||
fallback.cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier:
|
||||
raw.cameraSpringDampingMultiplier ?? fallback.cameraSpringDampingMultiplier,
|
||||
sanitizedRaw.cameraSpringDampingMultiplier ??
|
||||
fallback.cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier:
|
||||
raw.cameraSpringMassMultiplier ?? fallback.cameraSpringMassMultiplier,
|
||||
cursorMotionBlur: raw.cursorMotionBlur ?? fallback.cursorMotionBlur,
|
||||
cursorClickBounce: raw.cursorClickBounce ?? fallback.cursorClickBounce,
|
||||
sanitizedRaw.cameraSpringMassMultiplier ?? fallback.cameraSpringMassMultiplier,
|
||||
cursorMotionBlur: sanitizedRaw.cursorMotionBlur ?? fallback.cursorMotionBlur,
|
||||
cursorClickBounce: sanitizedRaw.cursorClickBounce ?? fallback.cursorClickBounce,
|
||||
cursorClickBounceDuration:
|
||||
raw.cursorClickBounceDuration ?? fallback.cursorClickBounceDuration,
|
||||
cursorSway: raw.cursorSway ?? fallback.cursorSway,
|
||||
borderRadius: raw.borderRadius ?? fallback.borderRadius,
|
||||
padding: raw.padding ?? fallback.padding,
|
||||
frame: raw.frame !== undefined ? raw.frame : fallback.frame,
|
||||
webcam: raw.webcam ?? fallback.webcam,
|
||||
aspectRatio: raw.aspectRatio ?? fallback.aspectRatio,
|
||||
exportEncodingMode: raw.exportEncodingMode ?? fallback.exportEncodingMode,
|
||||
sanitizedRaw.cursorClickBounceDuration ?? fallback.cursorClickBounceDuration,
|
||||
cursorSway: sanitizedRaw.cursorSway ?? fallback.cursorSway,
|
||||
borderRadius: sanitizedRaw.borderRadius ?? fallback.borderRadius,
|
||||
padding: sanitizedRaw.padding ?? fallback.padding,
|
||||
frame: sanitizedRaw.frame !== undefined ? sanitizedRaw.frame : fallback.frame,
|
||||
webcam: sanitizedRaw.webcam ?? fallback.webcam,
|
||||
aspectRatio: sanitizedRaw.aspectRatio ?? fallback.aspectRatio,
|
||||
exportEncodingMode:
|
||||
sanitizedRaw.exportEncodingMode ?? fallback.exportEncodingMode,
|
||||
exportBackendPreference:
|
||||
raw.exportBackendPreference === undefined
|
||||
sanitizedRaw.exportBackendPreference === undefined
|
||||
? fallback.exportBackendPreference
|
||||
: normalizeExportBackendPreference(raw.exportBackendPreference),
|
||||
: normalizeExportBackendPreference(sanitizedRaw.exportBackendPreference),
|
||||
exportPipelineModel:
|
||||
raw.exportPipelineModel === undefined
|
||||
sanitizedRaw.exportPipelineModel === undefined
|
||||
? fallback.exportPipelineModel
|
||||
: normalizeExportPipelineModel(raw.exportPipelineModel),
|
||||
exportQuality: raw.exportQuality ?? fallback.exportQuality,
|
||||
: normalizeExportPipelineModel(sanitizedRaw.exportPipelineModel),
|
||||
exportQuality: sanitizedRaw.exportQuality ?? fallback.exportQuality,
|
||||
mp4FrameRate:
|
||||
raw.mp4FrameRate === undefined
|
||||
sanitizedRaw.mp4FrameRate === undefined
|
||||
? fallback.mp4FrameRate
|
||||
: normalizeExportMp4FrameRate(raw.mp4FrameRate),
|
||||
exportFormat: raw.exportFormat ?? fallback.exportFormat,
|
||||
gifFrameRate: raw.gifFrameRate ?? fallback.gifFrameRate,
|
||||
gifLoop: raw.gifLoop ?? fallback.gifLoop,
|
||||
gifSizePreset: raw.gifSizePreset ?? fallback.gifSizePreset,
|
||||
: normalizeExportMp4FrameRate(sanitizedRaw.mp4FrameRate),
|
||||
exportFormat: sanitizedRaw.exportFormat ?? fallback.exportFormat,
|
||||
gifFrameRate: sanitizedRaw.gifFrameRate ?? fallback.gifFrameRate,
|
||||
gifLoop: sanitizedRaw.gifLoop ?? fallback.gifLoop,
|
||||
gifSizePreset: sanitizedRaw.gifSizePreset ?? fallback.gifSizePreset,
|
||||
};
|
||||
|
||||
const normalized = normalizeProjectEditor(candidate);
|
||||
@@ -442,7 +446,10 @@ export function saveEditorPreferences(preferences: Partial<EditorPreferences>):
|
||||
try {
|
||||
const current = loadEditorPreferences();
|
||||
const merged = normalizeEditorPreferences({ ...current, ...preferences }, current);
|
||||
globalThis.localStorage.setItem(EDITOR_PREFERENCES_STORAGE_KEY, JSON.stringify(merged));
|
||||
globalThis.localStorage.setItem(
|
||||
EDITOR_PREFERENCES_STORAGE_KEY,
|
||||
JSON.stringify(stripPersistedDevMotionBlurSettings(merged)),
|
||||
);
|
||||
} catch {
|
||||
// Ignore storage failures so editor controls still work.
|
||||
}
|
||||
|
||||
@@ -156,6 +156,21 @@ function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
type PersistedDevMotionBlurSettings = {
|
||||
zoomMotionBlurTuning?: unknown;
|
||||
};
|
||||
|
||||
export function stripPersistedDevMotionBlurSettings<T extends PersistedDevMotionBlurSettings>(
|
||||
editor: T,
|
||||
): Omit<T, keyof PersistedDevMotionBlurSettings> {
|
||||
const {
|
||||
zoomMotionBlurTuning: _zoomMotionBlurTuning,
|
||||
...persistedEditor
|
||||
} = editor;
|
||||
|
||||
return persistedEditor;
|
||||
}
|
||||
|
||||
export function normalizeExportEncodingMode(value: unknown): ExportEncodingMode {
|
||||
if (value === "fast" || value === "balanced" || value === "quality") {
|
||||
return value;
|
||||
@@ -360,10 +375,10 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
? clamp(rawZoomMotionBlurTuning.zoomVelocityThreshold, 0, 0.4)
|
||||
: DEFAULT_ZOOM_MOTION_BLUR_TUNING.zoomVelocityThreshold,
|
||||
maxDirectionalBlurPx: isFiniteNumber(rawZoomMotionBlurTuning.maxDirectionalBlurPx)
|
||||
? clamp(rawZoomMotionBlurTuning.maxDirectionalBlurPx, 0, 32)
|
||||
? clamp(rawZoomMotionBlurTuning.maxDirectionalBlurPx, 0, 96)
|
||||
: DEFAULT_ZOOM_MOTION_BLUR_TUNING.maxDirectionalBlurPx,
|
||||
maxRadialBlurStrength: isFiniteNumber(rawZoomMotionBlurTuning.maxRadialBlurStrength)
|
||||
? clamp(rawZoomMotionBlurTuning.maxRadialBlurStrength, 0, 0.5)
|
||||
? clamp(rawZoomMotionBlurTuning.maxRadialBlurStrength, 0, 1.5)
|
||||
: DEFAULT_ZOOM_MOTION_BLUR_TUNING.maxRadialBlurStrength,
|
||||
panResponsePerSecond: isFiniteNumber(rawZoomMotionBlurTuning.panResponsePerSecond)
|
||||
? clamp(rawZoomMotionBlurTuning.panResponsePerSecond, 1, 30)
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface CursorVisualSettings {
|
||||
}
|
||||
|
||||
export type CursorStyle = "macos" | "tahoe" | "tahoe-inverted" | "dot" | "figma" | (string & {}); // extension-contributed cursor styles
|
||||
export const DEFAULT_CURSOR_STYLE: CursorStyle = "tahoe";
|
||||
export const DEFAULT_CURSOR_STYLE: CursorStyle = "macos";
|
||||
|
||||
export type EditorEffectSection =
|
||||
| "scene"
|
||||
@@ -116,9 +116,9 @@ export interface ZoomMotionBlurTuning {
|
||||
|
||||
export const DEFAULT_ZOOM_MOTION_BLUR_TUNING: ZoomMotionBlurTuning = {
|
||||
panVelocityThreshold: 0,
|
||||
zoomVelocityThreshold: 0.025,
|
||||
maxDirectionalBlurPx: 11,
|
||||
maxRadialBlurStrength: 0.175,
|
||||
zoomVelocityThreshold: 0,
|
||||
maxDirectionalBlurPx: 41.8,
|
||||
maxRadialBlurStrength: 1,
|
||||
panResponsePerSecond: 11,
|
||||
zoomResponsePerSecond: 9,
|
||||
zoomSafeZoneRadiusPx: 6,
|
||||
|
||||
Reference in New Issue
Block a user