mirror of
https://github.com/immich-app/immich.git
synced 2026-05-19 05:52:14 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8d0597359 |
@@ -284,7 +284,11 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</AdaptiveImage>
|
</AdaptiveImage>
|
||||||
|
|
||||||
{#if assetViewerManager.isFaceEditMode && assetViewerManager.imgRef}
|
{#if assetViewerManager.isFaceEditMode && assetViewerManager.imgRef && asset.width && asset.height}
|
||||||
<FaceEditor htmlElement={assetViewerManager.imgRef} {containerWidth} {containerHeight} assetId={asset.id} />
|
<FaceEditor
|
||||||
|
assetSize={{ width: asset.width, height: asset.height }}
|
||||||
|
containerSize={{ width: containerWidth, height: containerHeight }}
|
||||||
|
assetId={asset.id}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -140,9 +140,40 @@
|
|||||||
let containerHeight = $state(0);
|
let containerHeight = $state(0);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (assetViewerManager.isFaceEditMode) {
|
if (!assetViewerManager.isFaceEditMode || !videoPlayer) {
|
||||||
videoPlayer?.pause();
|
return;
|
||||||
}
|
}
|
||||||
|
videoPlayer.pause();
|
||||||
|
|
||||||
|
const { videoWidth, videoHeight } = videoPlayer;
|
||||||
|
if (videoWidth === 0 || videoHeight === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = videoWidth;
|
||||||
|
canvas.height = videoHeight;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
if (!context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.drawImage(videoPlayer, 0, 0);
|
||||||
|
const dataUrl = canvas.toDataURL('image/png');
|
||||||
|
canvas.width = 0;
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
const onLoad = () => {
|
||||||
|
assetViewerManager.imgRef = img;
|
||||||
|
};
|
||||||
|
img.addEventListener('load', onLoad);
|
||||||
|
img.src = dataUrl;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
img.removeEventListener('load', onLoad);
|
||||||
|
img.src = '';
|
||||||
|
assetViewerManager.imgRef = undefined;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -248,7 +279,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if assetViewerManager.isFaceEditMode}
|
{#if assetViewerManager.isFaceEditMode}
|
||||||
<FaceEditor htmlElement={videoPlayer} {containerWidth} {containerHeight} {assetId} />
|
<FaceEditor
|
||||||
|
assetSize={{ width: asset.width ?? 0, height: asset.height ?? 0 }}
|
||||||
|
containerSize={{ width: containerWidth, height: containerHeight }}
|
||||||
|
{assetId}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||||
import FaceCreateTagModal from '$lib/modals/CreateFaceModal.svelte';
|
import FaceCreateTagModal from '$lib/modals/CreateFaceModal.svelte';
|
||||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||||
import { getNaturalSize, scaleToFit } from '$lib/utils/container-utils';
|
import { computeContentMetrics, mapContentRectToNatural, type Size } from '$lib/utils/container-utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { createFace, getAllPeople, type PersonResponseDto } from '@immich/sdk';
|
import { createFace, getAllPeople, type PersonResponseDto } from '@immich/sdk';
|
||||||
import { Button, Input, modalManager, toastManager } from '@immich/ui';
|
import { Button, Input, modalManager, toastManager } from '@immich/ui';
|
||||||
@@ -14,13 +14,12 @@
|
|||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
htmlElement: HTMLImageElement | HTMLVideoElement;
|
assetSize: Size;
|
||||||
containerWidth: number;
|
containerSize: Size;
|
||||||
containerHeight: number;
|
|
||||||
assetId: string;
|
assetId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { htmlElement, containerWidth, containerHeight, assetId }: Props = $props();
|
let { assetSize, containerSize, assetId }: Props = $props();
|
||||||
|
|
||||||
let canvasEl: HTMLCanvasElement | undefined = $state();
|
let canvasEl: HTMLCanvasElement | undefined = $state();
|
||||||
let canvas: Canvas | undefined = $state();
|
let canvas: Canvas | undefined = $state();
|
||||||
@@ -54,7 +53,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setupCanvas = () => {
|
const setupCanvas = () => {
|
||||||
if (!canvasEl || !htmlElement) {
|
if (!canvasEl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,24 +85,14 @@
|
|||||||
searchInputEl?.focus();
|
searchInputEl?.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
const imageContentMetrics = $derived.by(() => {
|
const imageContentMetrics = $derived(computeContentMetrics(assetSize, containerSize));
|
||||||
const natural = getNaturalSize(htmlElement);
|
|
||||||
const container = { width: containerWidth, height: containerHeight };
|
|
||||||
const { width: contentWidth, height: contentHeight } = scaleToFit(natural, container);
|
|
||||||
return {
|
|
||||||
contentWidth,
|
|
||||||
contentHeight,
|
|
||||||
offsetX: (containerWidth - contentWidth) / 2,
|
|
||||||
offsetY: (containerHeight - contentHeight) / 2,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const setDefaultFaceRectanglePosition = (faceRect: Rect) => {
|
const setDefaultFaceRectanglePosition = (faceRect: Rect) => {
|
||||||
const { offsetX, offsetY } = imageContentMetrics;
|
const { offsetX, offsetY, contentWidth, contentHeight } = imageContentMetrics;
|
||||||
|
|
||||||
faceRect.set({
|
faceRect.set({
|
||||||
top: offsetY + 200,
|
top: offsetY + contentHeight / 2 - 56,
|
||||||
left: offsetX + 200,
|
left: offsetX + contentWidth / 2 - 56,
|
||||||
});
|
});
|
||||||
|
|
||||||
faceRect.setCoords();
|
faceRect.setCoords();
|
||||||
@@ -116,8 +105,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
canvas.setDimensions({
|
canvas.setDimensions({
|
||||||
width: containerWidth,
|
width: containerSize.width,
|
||||||
height: containerHeight,
|
height: containerSize.height,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!faceRect) {
|
if (!faceRect) {
|
||||||
@@ -167,6 +156,9 @@
|
|||||||
const gap = 15;
|
const gap = 15;
|
||||||
const padding = faceRect.padding ?? 0;
|
const padding = faceRect.padding ?? 0;
|
||||||
const rawBox = faceRect.getBoundingRect();
|
const rawBox = faceRect.getBoundingRect();
|
||||||
|
if (Number.isNaN(rawBox.left) || Number.isNaN(rawBox.width)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const faceBox = {
|
const faceBox = {
|
||||||
left: rawBox.left - padding,
|
left: rawBox.left - padding,
|
||||||
top: rawBox.top - padding,
|
top: rawBox.top - padding,
|
||||||
@@ -175,11 +167,11 @@
|
|||||||
};
|
};
|
||||||
const selectorWidth = faceSelectorEl.offsetWidth;
|
const selectorWidth = faceSelectorEl.offsetWidth;
|
||||||
const chromeHeight = faceSelectorEl.offsetHeight - scrollableListEl.offsetHeight;
|
const chromeHeight = faceSelectorEl.offsetHeight - scrollableListEl.offsetHeight;
|
||||||
const listHeight = Math.min(MAX_LIST_HEIGHT, containerHeight - gap * 2 - chromeHeight);
|
const listHeight = Math.min(MAX_LIST_HEIGHT, containerSize.height - gap * 2 - chromeHeight);
|
||||||
const selectorHeight = listHeight + chromeHeight;
|
const selectorHeight = listHeight + chromeHeight;
|
||||||
|
|
||||||
const clampTop = (top: number) => clamp(top, gap, containerHeight - selectorHeight - gap);
|
const clampTop = (top: number) => clamp(top, gap, containerSize.height - selectorHeight - gap);
|
||||||
const clampLeft = (left: number) => clamp(left, gap, containerWidth - selectorWidth - gap);
|
const clampLeft = (left: number) => clamp(left, gap, containerSize.width - selectorWidth - gap);
|
||||||
|
|
||||||
const overlapArea = (position: { top: number; left: number }) => {
|
const overlapArea = (position: { top: number; left: number }) => {
|
||||||
const selectorRight = position.left + selectorWidth;
|
const selectorRight = position.left + selectorWidth;
|
||||||
@@ -238,45 +230,37 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getFaceCroppedCoordinates = () => {
|
const getFaceCroppedCoordinates = () => {
|
||||||
if (!faceRect || !htmlElement) {
|
if (!faceRect || imageContentMetrics.contentWidth === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { left, top, width, height } = faceRect.getBoundingRect();
|
const imageRect = mapContentRectToNatural(faceRect.getBoundingRect(), imageContentMetrics, assetSize);
|
||||||
const { offsetX, offsetY, contentWidth, contentHeight } = imageContentMetrics;
|
|
||||||
const natural = getNaturalSize(htmlElement);
|
|
||||||
|
|
||||||
const scaleX = natural.width / contentWidth;
|
|
||||||
const scaleY = natural.height / contentHeight;
|
|
||||||
const imageX = (left - offsetX) * scaleX;
|
|
||||||
const imageY = (top - offsetY) * scaleY;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
imageWidth: natural.width,
|
imageWidth: assetSize.width,
|
||||||
imageHeight: natural.height,
|
imageHeight: assetSize.height,
|
||||||
x: Math.floor(imageX),
|
x: Math.floor(imageRect.left),
|
||||||
y: Math.floor(imageY),
|
y: Math.floor(imageRect.top),
|
||||||
width: Math.floor(width * scaleX),
|
width: Math.floor(imageRect.width),
|
||||||
height: Math.floor(height * scaleY),
|
height: Math.floor(imageRect.height),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type FaceCoordinates = NonNullable<ReturnType<typeof getFaceCroppedCoordinates>>;
|
type FaceCoordinates = NonNullable<ReturnType<typeof getFaceCroppedCoordinates>>;
|
||||||
|
|
||||||
const getFacePreviewUrl = (data: FaceCoordinates) => {
|
const getFacePreviewUrl = (data: FaceCoordinates) => {
|
||||||
if (!htmlElement) {
|
const imgRef = assetViewerManager.imgRef;
|
||||||
|
if (!imgRef || imageContentMetrics.contentWidth === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const natural = getNaturalSize(htmlElement);
|
const scaleX = imgRef.naturalWidth / assetSize.width;
|
||||||
if (natural.width <= 0 || natural.height <= 0) {
|
const scaleY = imgRef.naturalHeight / assetSize.height;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const x = clamp(data.x, 0, natural.width - 1);
|
const x = clamp(Math.floor(data.x * scaleX), 0, imgRef.naturalWidth - 1);
|
||||||
const y = clamp(data.y, 0, natural.height - 1);
|
const y = clamp(Math.floor(data.y * scaleY), 0, imgRef.naturalHeight - 1);
|
||||||
const width = clamp(data.width, 1, natural.width - x);
|
const width = clamp(Math.floor(data.width * scaleX), 1, imgRef.naturalWidth - x);
|
||||||
const height = clamp(data.height, 1, natural.height - y);
|
const height = clamp(Math.floor(data.height * scaleY), 1, imgRef.naturalHeight - y);
|
||||||
|
|
||||||
if (width <= 0 || height <= 0) {
|
if (width <= 0 || height <= 0) {
|
||||||
return;
|
return;
|
||||||
@@ -292,7 +276,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
context.drawImage(htmlElement, x, y, width, height, 0, 0, width, height);
|
context.drawImage(imgRef, x, y, width, height, 0, 0, width, height);
|
||||||
return canvas.toDataURL('image/png');
|
return canvas.toDataURL('image/png');
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
getContentMetrics,
|
computeContentMetrics,
|
||||||
getNaturalSize,
|
getNaturalSize,
|
||||||
|
mapContentRectToNatural,
|
||||||
mapNormalizedRectToContent,
|
mapNormalizedRectToContent,
|
||||||
mapNormalizedToContent,
|
mapNormalizedToContent,
|
||||||
scaleToCover,
|
scaleToCover,
|
||||||
scaleToFit,
|
scaleToFit,
|
||||||
} from '$lib/utils/container-utils';
|
} from '$lib/utils/container-utils';
|
||||||
|
|
||||||
const mockImage = (props: {
|
const mockImage = (props: { naturalWidth: number; naturalHeight: number }): HTMLImageElement =>
|
||||||
naturalWidth: number;
|
props as unknown as HTMLImageElement;
|
||||||
naturalHeight: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}): HTMLImageElement => props as unknown as HTMLImageElement;
|
|
||||||
|
|
||||||
const mockVideo = (props: {
|
const mockVideo = (props: {
|
||||||
videoWidth: number;
|
videoWidth: number;
|
||||||
@@ -49,48 +46,85 @@ describe('scaleToFit', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getContentMetrics', () => {
|
describe('computeContentMetrics', () => {
|
||||||
it('should compute zero offsets when aspect ratios match', () => {
|
it('should return zero metrics for zero-width content', () => {
|
||||||
const img = mockImage({ naturalWidth: 1600, naturalHeight: 900, width: 800, height: 450 });
|
expect(computeContentMetrics({ width: 0, height: 1080 }, { width: 800, height: 600 })).toEqual({
|
||||||
expect(getContentMetrics(img)).toEqual({
|
contentWidth: 0,
|
||||||
|
contentHeight: 0,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return zero metrics for zero-height content', () => {
|
||||||
|
expect(computeContentMetrics({ width: 1920, height: 0 }, { width: 800, height: 600 })).toEqual({
|
||||||
|
contentWidth: 0,
|
||||||
|
contentHeight: 0,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should center wide content vertically', () => {
|
||||||
|
expect(computeContentMetrics({ width: 2000, height: 1000 }, { width: 800, height: 600 })).toEqual({
|
||||||
|
contentWidth: 800,
|
||||||
|
contentHeight: 400,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should center tall content horizontally', () => {
|
||||||
|
expect(computeContentMetrics({ width: 1000, height: 2000 }, { width: 800, height: 600 })).toEqual({
|
||||||
|
contentWidth: 300,
|
||||||
|
contentHeight: 600,
|
||||||
|
offsetX: 250,
|
||||||
|
offsetY: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should produce zero offsets when aspect ratios match', () => {
|
||||||
|
expect(computeContentMetrics({ width: 1600, height: 900 }, { width: 800, height: 450 })).toEqual({
|
||||||
contentWidth: 800,
|
contentWidth: 800,
|
||||||
contentHeight: 450,
|
contentHeight: 450,
|
||||||
offsetX: 0,
|
offsetX: 0,
|
||||||
offsetY: 0,
|
offsetY: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should compute horizontal letterbox offsets for tall image', () => {
|
describe('mapContentRectToNatural', () => {
|
||||||
const img = mockImage({ naturalWidth: 1000, naturalHeight: 2000, width: 800, height: 600 });
|
it('should map a full-content rect back to natural size', () => {
|
||||||
const metrics = getContentMetrics(img);
|
const metrics = { contentWidth: 800, contentHeight: 400, offsetX: 0, offsetY: 100 };
|
||||||
expect(metrics.contentWidth).toBe(300);
|
const rect = mapContentRectToNatural({ left: 0, top: 100, width: 800, height: 400 }, metrics, {
|
||||||
expect(metrics.contentHeight).toBe(600);
|
width: 2000,
|
||||||
expect(metrics.offsetX).toBe(250);
|
height: 1000,
|
||||||
expect(metrics.offsetY).toBe(0);
|
});
|
||||||
|
expect(rect).toEqual({ left: 0, top: 0, width: 2000, height: 1000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compute vertical letterbox offsets for wide image', () => {
|
it('should map a centered sub-rect to natural coordinates', () => {
|
||||||
const img = mockImage({ naturalWidth: 2000, naturalHeight: 1000, width: 800, height: 600 });
|
const metrics = { contentWidth: 800, contentHeight: 400, offsetX: 0, offsetY: 100 };
|
||||||
const metrics = getContentMetrics(img);
|
const rect = mapContentRectToNatural({ left: 200, top: 200, width: 400, height: 200 }, metrics, {
|
||||||
expect(metrics.contentWidth).toBe(800);
|
width: 2000,
|
||||||
expect(metrics.contentHeight).toBe(400);
|
height: 1000,
|
||||||
expect(metrics.offsetX).toBe(0);
|
});
|
||||||
expect(metrics.offsetY).toBe(100);
|
expect(rect).toEqual({ left: 500, top: 250, width: 1000, height: 500 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use clientWidth/clientHeight for video elements', () => {
|
it('should handle letterboxed content with horizontal offset', () => {
|
||||||
const video = mockVideo({ videoWidth: 1920, videoHeight: 1080, clientWidth: 800, clientHeight: 600 });
|
const metrics = { contentWidth: 300, contentHeight: 600, offsetX: 250, offsetY: 0 };
|
||||||
const metrics = getContentMetrics(video);
|
const rect = mapContentRectToNatural({ left: 250, top: 0, width: 300, height: 600 }, metrics, {
|
||||||
expect(metrics.contentWidth).toBe(800);
|
width: 1000,
|
||||||
expect(metrics.contentHeight).toBe(450);
|
height: 2000,
|
||||||
expect(metrics.offsetX).toBe(0);
|
});
|
||||||
expect(metrics.offsetY).toBe(75);
|
expect(rect).toEqual({ left: 0, top: 0, width: 1000, height: 2000 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getNaturalSize', () => {
|
describe('getNaturalSize', () => {
|
||||||
it('should return naturalWidth/naturalHeight for images', () => {
|
it('should return naturalWidth/naturalHeight for images', () => {
|
||||||
const img = mockImage({ naturalWidth: 4000, naturalHeight: 3000, width: 800, height: 600 });
|
const img = mockImage({ naturalWidth: 4000, naturalHeight: 3000 });
|
||||||
expect(getNaturalSize(img)).toEqual({ width: 4000, height: 3000 });
|
expect(getNaturalSize(img)).toEqual({ width: 4000, height: 3000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -49,13 +49,6 @@ export const scaleToFit = (dimensions: Size, container: Size): Size => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getElementSize = (element: HTMLImageElement | HTMLVideoElement): Size => {
|
|
||||||
if (element instanceof HTMLVideoElement) {
|
|
||||||
return { width: element.clientWidth, height: element.clientHeight };
|
|
||||||
}
|
|
||||||
return { width: element.width, height: element.height };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNaturalSize = (element: HTMLImageElement | HTMLVideoElement): Size => {
|
export const getNaturalSize = (element: HTMLImageElement | HTMLVideoElement): Size => {
|
||||||
if (element instanceof HTMLVideoElement) {
|
if (element instanceof HTMLVideoElement) {
|
||||||
return { width: element.videoWidth, height: element.videoHeight };
|
return { width: element.videoWidth, height: element.videoHeight };
|
||||||
@@ -63,17 +56,18 @@ export const getNaturalSize = (element: HTMLImageElement | HTMLVideoElement): Si
|
|||||||
return { width: element.naturalWidth, height: element.naturalHeight };
|
return { width: element.naturalWidth, height: element.naturalHeight };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getContentMetrics = (element: HTMLImageElement | HTMLVideoElement): ContentMetrics => {
|
export function computeContentMetrics(content: Size, container: Size): ContentMetrics {
|
||||||
const natural = getNaturalSize(element);
|
if (content.width === 0 || content.height === 0) {
|
||||||
const client = getElementSize(element);
|
return { contentWidth: 0, contentHeight: 0, offsetX: 0, offsetY: 0 };
|
||||||
const { width: contentWidth, height: contentHeight } = scaleToFit(natural, client);
|
}
|
||||||
|
const { width: contentWidth, height: contentHeight } = scaleToFit(content, container);
|
||||||
return {
|
return {
|
||||||
contentWidth,
|
contentWidth,
|
||||||
contentHeight,
|
contentHeight,
|
||||||
offsetX: (client.width - contentWidth) / 2,
|
offsetX: (container.width - contentWidth) / 2,
|
||||||
offsetY: (client.height - contentHeight) / 2,
|
offsetY: (container.height - contentHeight) / 2,
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
export function mapNormalizedToContent(point: Point, sizeOrMetrics: Size | ContentMetrics): Point {
|
export function mapNormalizedToContent(point: Point, sizeOrMetrics: Size | ContentMetrics): Point {
|
||||||
if ('contentWidth' in sizeOrMetrics) {
|
if ('contentWidth' in sizeOrMetrics) {
|
||||||
@@ -109,3 +103,25 @@ export function mapNormalizedRectToContent(
|
|||||||
height: br.y - tl.y,
|
height: br.y - tl.y,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapContentToNatural(point: Point, metrics: ContentMetrics, naturalSize: Size): Point {
|
||||||
|
return {
|
||||||
|
x: ((point.x - metrics.offsetX) / metrics.contentWidth) * naturalSize.width,
|
||||||
|
y: ((point.y - metrics.offsetY) / metrics.contentHeight) * naturalSize.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapContentRectToNatural(rect: Rect, metrics: ContentMetrics, naturalSize: Size): Rect {
|
||||||
|
const topLeft = mapContentToNatural({ x: rect.left, y: rect.top }, metrics, naturalSize);
|
||||||
|
const bottomRight = mapContentToNatural(
|
||||||
|
{ x: rect.left + rect.width, y: rect.top + rect.height },
|
||||||
|
metrics,
|
||||||
|
naturalSize,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
top: topLeft.y,
|
||||||
|
left: topLeft.x,
|
||||||
|
width: bottomRight.x - topLeft.x,
|
||||||
|
height: bottomRight.y - topLeft.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user