Skip to content

Commit

Permalink
Merge pull request #23 from Glazzes/dev
Browse files Browse the repository at this point in the history
Improve CropZoom component rendering
  • Loading branch information
Glazzes authored Jun 6, 2024
2 parents cf439ed + 6e6c108 commit 88fa385
Show file tree
Hide file tree
Showing 13 changed files with 854 additions and 1,171 deletions.
2 changes: 1 addition & 1 deletion docs/docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default defineConfig({
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Home', link: '/' },
{text: '1.2.1', items: [
{ text: '1.2.3', items: [
{text: 'Releases', link: 'https://github.com/Glazzes/react-native-zoom-toolkit/releases'},
{text: 'Contributing', link: 'https://github.com/Glazzes/react-native-zoom-toolkit/blob/main/CONTRIBUTING.md'},
]}
Expand Down
4 changes: 3 additions & 1 deletion docs/docs/components/gallery.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ The next video footage is taken from the [Example app](https://github.com/Glazze
The following example is a full screen image gallery.

::: tip Remember
Follow React Native's [performance recommendations](https://reactnative.dev/docs/optimizing-flatlist-configuration#list-items) for list components.
- Follow React Native's [performance recommendations](https://reactnative.dev/docs/optimizing-flatlist-configuration#list-items) for list components.

- Each cell is as big as the size of the `Gallery` component itself.
:::

::: code-group
Expand Down
34 changes: 17 additions & 17 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,28 @@
"web": "expo start --web"
},
"dependencies": {
"@expo/metro-runtime": "~3.1.3",
"@expo/metro-runtime": "~3.2.1",
"@react-navigation/drawer": "^6.6.15",
"@shopify/react-native-skia": "0.1.221",
"expo": "50.0.17",
"expo-av": "~13.10.6",
"expo-constants": "~15.4.6",
"expo-image": "~1.10.6",
"expo-image-manipulator": "~11.8.0",
"expo-linking": "~6.2.2",
"expo-media-library": "~15.9.2",
"expo-router": "~3.4.10",
"expo-screen-orientation": "~6.4.1",
"expo-status-bar": "~1.11.1",
"@shopify/react-native-skia": "1.2.3",
"expo": "^51.0.8",
"expo-av": "~14.0.5",
"expo-constants": "~16.0.1",
"expo-image": "~1.12.9",
"expo-image-manipulator": "~12.0.5",
"expo-linking": "~6.3.1",
"expo-media-library": "~16.0.3",
"expo-router": "~3.5.14",
"expo-screen-orientation": "~7.0.5",
"expo-status-bar": "~1.12.1",
"fbemitter": "^3.0.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.73.6",
"react-native-gesture-handler": "~2.14.0",
"react-native": "0.74.1",
"react-native-gesture-handler": "~2.16.1",
"react-native-image-viewing": "^0.2.2",
"react-native-reanimated": "~3.6.2",
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "~3.29.0",
"react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "4.10.1",
"react-native-screens": "3.31.1",
"react-native-web": "~0.19.6"
},
"devDependencies": {
Expand Down
8 changes: 7 additions & 1 deletion example/src/gallery/GalleryImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ const GalleryImage: React.FC<GalleryImageProps> = ({
[activeIndex]
);

return <Image source={asset.uri} style={size} allowDownscaling={downScale} />;
return (
<Image
source={{ uri: asset.uri }}
style={size}
allowDownscaling={downScale}
/>
);
};

export default GalleryImage;
47 changes: 0 additions & 47 deletions src/__tests__/components/CropZoom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { CropMode } from '../../components/crop/types';
import { render } from '@testing-library/react-native';
import type { SizeVector } from '../../commons/types';
import { getInvalidChildrenMessage } from '../../commons/utils/messages';
import { getInvalidCropSizeMessage } from '../../components/crop/utils/messages';

const componentName = 'CropZoom';
const cropSize: SizeVector<number> = { width: 100, height: 100 };
Expand Down Expand Up @@ -117,50 +116,4 @@ describe('CropZoom', () => {

expect(() => render(cropzoom)).toThrow();
});

test('should warn the user about invalid crop size height', () => {
const exceededCropSize: SizeVector<number> = { width: 100, height: 400 };
const spyConsole = jest.spyOn(global.console, 'warn');

const message = getInvalidCropSizeMessage({
dimension: 'height',
actual: exceededCropSize.height,
expected: resolution.height,
});

expect(
render(
<CropZoom cropSize={exceededCropSize} resolution={resolution}>
<View />
</CropZoom>
)
).toBeDefined();

expect(spyConsole).toBeCalledWith(message);
});

test('should warn the user about invalid crop size width', () => {
const horizontalResolution: SizeVector<number> = {
width: 200,
height: 100,
};
const exceededCropSize: SizeVector<number> = { width: 400, height: 100 };

const spyConsole = jest.spyOn(global.console, 'warn');
const message = getInvalidCropSizeMessage({
dimension: 'width',
actual: exceededCropSize.width,
expected: horizontalResolution.width,
});

expect(
render(
<CropZoom cropSize={exceededCropSize} resolution={horizontalResolution}>
<View />
</CropZoom>
)
).toBeDefined();

expect(spyConsole).toBeCalledWith(message);
});
});
41 changes: 2 additions & 39 deletions src/commons/hoc/withCropValidation.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import React, { forwardRef } from 'react';
import { CropMode, type CropZoomProps } from '../../components/crop/types';
import type { SizeVector } from '../types';
import getAspectRatioSize from '../../utils/getAspectRatioSize';
import { getInvalidChildrenMessage } from '../utils/messages';
import { getInvalidCropSizeMessage } from '../../components/crop/utils/messages';

export default function withCropValidation<T, P extends CropZoomProps>(
WrappedComponent: React.ComponentType<P>
) {
return forwardRef<T, P>((props, ref) => {
const { mode, resolution, minScale, maxScale, children } = props;
const { mode, minScale, maxScale, children } = props;

const childrenCount = React.Children.count(children);
if (childrenCount !== 1 && mode === CropMode.MANAGED) {
Expand Down Expand Up @@ -50,40 +47,6 @@ export default function withCropValidation<T, P extends CropZoomProps>(
throw new Error('minScale must not be greater than or equals maxScale');
}

/*
* Infers the opposite dimension while clamping the ones provided for the user in order to prevent
* errors that could lead to an unusuable component for not so common use cases
*/
const isPortrait = resolution.height >= resolution.width;
let cropSize: SizeVector<number> = props.cropSize;
const { width, height } = getAspectRatioSize({
aspectRatio: resolution.width / resolution.height,
width: isPortrait ? cropSize.width : undefined,
height: isPortrait ? undefined : cropSize.height,
});

if (isPortrait && cropSize.height > height) {
const message = getInvalidCropSizeMessage({
dimension: 'height',
actual: cropSize.height,
expected: height,
});

console.warn(message);
cropSize.height = height;
}

if (!isPortrait && cropSize.width > width) {
const message = getInvalidCropSizeMessage({
dimension: 'width',
actual: cropSize.width,
expected: width,
});

console.warn(message);
cropSize.width = width;
}

return <WrappedComponent {...props} cropSize={cropSize} reference={ref} />;
return <WrappedComponent {...props} reference={ref} />;
});
}
78 changes: 78 additions & 0 deletions src/commons/utils/crop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { interpolate } from 'react-native-reanimated';
import type { SizeVector, Vector } from '../types';
import type { CropContextResult } from '../../components/crop/types';

type CanvasToSizeOptions = {
context: CropContextResult['context'];
cropSize: SizeVector<number>;
canvas: SizeVector<number>;
resolution: SizeVector<number>;
position: Vector<number>;
scale: number;
fixedWidth?: number;
};

const flipVector = (vector: SizeVector<number>): SizeVector<number> => {
return {
width: vector.height,
height: vector.width,
};
};

export const crop = ({
cropSize,
canvas,
resolution,
position,
scale,
fixedWidth,
context,
}: CanvasToSizeOptions): CropContextResult => {
const isFlipped = context.rotationAngle % 180 !== 0;

let currentCanvas = canvas;
let currentResolution = resolution;
if (isFlipped) {
currentCanvas = flipVector(canvas);
currentResolution = flipVector(resolution);
}

const offsetX = (currentCanvas.width * scale - cropSize.width) / 2;
const offsetY = (currentCanvas.height * scale - cropSize.height) / 2;

const normalizedX = Math.abs(offsetX) + position.x;
const normalizedY = Math.abs(offsetY) + position.y;

const relativeX = cropSize.width / (currentCanvas.width * scale);
const relativeY = cropSize.height / (currentCanvas.height * scale);

// (1 - relative) - (1 - normalized / (2 * offset)) I just do not like NaN checks
const posX = interpolate(normalizedX, [0, 2 * offsetX], [1 - relativeX, 0]);
const posY = interpolate(normalizedY, [0, 2 * offsetY], [1 - relativeY, 0]);

const x = currentResolution.width * posX;
const y = currentResolution.height * posY;
const width = currentResolution.width * relativeX;
const height = currentResolution.height * relativeY;
let resize: SizeVector<number> | undefined;

let fixer = 1;
if (fixedWidth !== undefined) {
fixer = fixedWidth / width;
resize = {
width: Math.ceil(resolution.width * fixer),
height: Math.ceil(resolution.height * fixer),
};
}

return {
crop: {
originX: x * fixer,
originY: y * fixer,
width: Math.round(width * fixer),
height: Math.round(height * fixer),
},
context,
resize,
};
};
37 changes: 27 additions & 10 deletions src/commons/utils/getCropRotatedSize.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
import getAspectRatioSize from '../../utils/getAspectRatioSize';
import type { SizeVector } from '../types';
import getAspectRatioSize from '../../utils/getAspectRatioSize';

type Options = {
size: SizeVector<number>;
crop: SizeVector<number>;
resolution: SizeVector<number>;
angle: number;
aspectRatio: number;
};

export const getCropRotatedSize = (options: Options): SizeVector<number> => {
'worklet';
const { size, angle, aspectRatio } = options;
const { crop, angle, resolution } = options;
const cropAspectRatio = crop.width / crop.height;
let base = crop;

const flipped = angle % Math.PI === 0;
const aspectRatio = resolution.width / resolution.height;
const inverseAspectRatio = resolution.height / resolution.width;

base = getAspectRatioSize({
aspectRatio: flipped ? aspectRatio : inverseAspectRatio,
width: cropAspectRatio >= 1 ? undefined : crop.width,
height: cropAspectRatio >= 1 ? crop.height : undefined,
});

const sinWidth = Math.abs(size.height * Math.sin(angle));
const cosWidth = Math.abs(size.width * Math.cos(angle));
let resizer = 1;
if (base.height < crop.height) resizer = crop.height / base.height;
if (base.width < crop.width) resizer = crop.width / base.width;
base.width = base.width * resizer;
base.height = base.height * resizer;

const sinHeight = Math.abs(size.height * Math.cos(angle));
const cosHeight = Math.abs(size.width * Math.sin(angle));
const maxWidth =
Math.abs(base.height * Math.sin(angle)) +
Math.abs(base.width * Math.cos(angle));

const maxWidth = sinWidth + cosWidth;
const maxHeight = sinHeight + cosHeight;
const maxHeight =
Math.abs(base.height * Math.cos(angle)) +
Math.abs(base.width * Math.sin(angle));

return getAspectRatioSize({
aspectRatio: aspectRatio,
Expand Down
8 changes: 4 additions & 4 deletions src/components/crop/CropZoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { usePinchCommons } from '../../commons/hooks/usePinchCommons';
import { getMaxScale } from '../../commons/utils/getMaxScale';
import { useVector } from '../../commons/hooks/useVector';
import { PanMode, type BoundsFuction, ScaleMode } from '../../commons/types';
import { canvasToSize } from './utils/canvasToSize';
import { crop } from '../../commons/utils/crop';
import {
CropMode,
type CropZoomProps,
Expand Down Expand Up @@ -94,8 +94,8 @@ const CropZoom: React.FC<CropZoomProps> = (props) => {

useDerivedValue(() => {
const size = getCropRotatedSize({
size: cropSize,
aspectRatio: resolution.width / resolution.height,
crop: cropSize,
resolution: resolution,
angle: sizeAngle.value,
});

Expand Down Expand Up @@ -304,7 +304,7 @@ const CropZoom: React.FC<CropZoomProps> = (props) => {
};

const handleCrop = (fixedWidth?: number): CropContextResult => {
return canvasToSize({
return crop({
cropSize: cropSize,
resolution: resolution,
canvas: { width: container.width.value, height: container.height.value },
Expand Down
Loading

0 comments on commit 88fa385

Please sign in to comment.