2021-07-21 01:32:09 +02:00
|
|
|
<!--
|
|
|
|
Copyright: Ankitects Pty Ltd and contributors
|
|
|
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
-->
|
2021-07-28 20:17:31 +02:00
|
|
|
<script lang="ts">
|
2021-08-05 02:58:52 +02:00
|
|
|
import HandleBackground from "./HandleBackground.svelte";
|
2021-08-04 22:25:05 +02:00
|
|
|
import HandleSelection from "./HandleSelection.svelte";
|
2021-08-04 22:58:24 +02:00
|
|
|
import HandleControl from "./HandleControl.svelte";
|
2021-08-05 03:24:04 +02:00
|
|
|
import HandleLabel from "./HandleLabel.svelte";
|
2021-07-21 16:48:02 +02:00
|
|
|
import ImageHandleFloat from "./ImageHandleFloat.svelte";
|
2021-07-21 14:13:56 +02:00
|
|
|
import ImageHandleSizeSelect from "./ImageHandleSizeSelect.svelte";
|
|
|
|
|
2021-08-04 23:03:11 +02:00
|
|
|
import { onDestroy, tick } from "svelte";
|
2021-07-21 01:32:09 +02:00
|
|
|
|
2021-07-21 14:13:56 +02:00
|
|
|
export let container: HTMLElement;
|
2021-07-28 20:17:31 +02:00
|
|
|
export let sheet: CSSStyleSheet;
|
|
|
|
export let activeImage: HTMLImageElement | null = null;
|
|
|
|
export let isRtl: boolean = false;
|
2021-07-21 02:15:10 +02:00
|
|
|
|
2021-07-28 20:17:31 +02:00
|
|
|
$: naturalWidth = activeImage?.naturalWidth;
|
|
|
|
$: naturalHeight = activeImage?.naturalHeight;
|
2021-07-26 22:31:47 +02:00
|
|
|
$: aspectRatio = naturalWidth && naturalHeight ? naturalWidth / naturalHeight : NaN;
|
2021-07-28 20:17:31 +02:00
|
|
|
$: showFloat = activeImage ? Number(activeImage!.width) >= 100 : false;
|
2021-07-21 15:52:48 +02:00
|
|
|
|
2021-07-30 20:37:10 +02:00
|
|
|
/* SIZES */
|
2021-07-21 02:15:10 +02:00
|
|
|
let containerLeft = 0;
|
2021-08-04 22:25:05 +02:00
|
|
|
let containerTop = 0;
|
2021-07-21 02:15:10 +02:00
|
|
|
|
|
|
|
let left = 0;
|
2021-08-04 22:25:05 +02:00
|
|
|
let top = 0;
|
2021-07-21 02:15:10 +02:00
|
|
|
let width = 0;
|
|
|
|
let height = 0;
|
|
|
|
|
2021-07-30 20:37:10 +02:00
|
|
|
function updateSizes() {
|
|
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
const imageRect = activeImage!.getBoundingClientRect();
|
|
|
|
|
|
|
|
containerLeft = containerRect.left;
|
2021-08-04 22:25:05 +02:00
|
|
|
containerTop = containerRect.top;
|
2021-07-30 20:37:10 +02:00
|
|
|
|
|
|
|
left = imageRect!.left - containerLeft;
|
2021-08-04 22:25:05 +02:00
|
|
|
top = imageRect!.top - containerTop;
|
2021-07-30 20:37:10 +02:00
|
|
|
width = activeImage!.clientWidth;
|
|
|
|
height = activeImage!.clientHeight;
|
|
|
|
}
|
|
|
|
|
2021-07-28 20:17:31 +02:00
|
|
|
function resetSizes() {
|
2021-08-04 19:13:37 +02:00
|
|
|
activeImage = null;
|
|
|
|
|
2021-07-28 20:17:31 +02:00
|
|
|
left = 0;
|
2021-08-04 22:25:05 +02:00
|
|
|
top = 0;
|
2021-07-28 20:17:31 +02:00
|
|
|
width = 0;
|
|
|
|
height = 0;
|
|
|
|
}
|
|
|
|
|
2021-08-05 03:24:04 +02:00
|
|
|
let customDimensions: boolean = false;
|
2021-07-30 20:37:10 +02:00
|
|
|
let actualWidth = "";
|
|
|
|
let actualHeight = "";
|
2021-07-21 18:22:09 +02:00
|
|
|
|
2021-08-05 03:24:04 +02:00
|
|
|
function updateDimensions() {
|
2021-07-26 22:31:47 +02:00
|
|
|
/* we do not want the actual width, but rather the intended display width */
|
2021-07-29 20:37:51 +02:00
|
|
|
const widthAttribute = activeImage!.getAttribute("width");
|
2021-07-21 19:03:21 +02:00
|
|
|
customDimensions = false;
|
|
|
|
|
2021-07-29 20:37:51 +02:00
|
|
|
if (widthAttribute) {
|
|
|
|
actualWidth = widthAttribute;
|
2021-07-21 19:03:21 +02:00
|
|
|
customDimensions = true;
|
2021-07-21 18:32:07 +02:00
|
|
|
} else {
|
2021-07-21 19:03:21 +02:00
|
|
|
actualWidth = String(naturalWidth);
|
2021-07-21 18:32:07 +02:00
|
|
|
}
|
2021-07-21 18:22:09 +02:00
|
|
|
|
2021-07-29 20:37:51 +02:00
|
|
|
const heightAttribute = activeImage!.getAttribute("height");
|
|
|
|
if (heightAttribute) {
|
|
|
|
actualHeight = heightAttribute;
|
2021-07-21 19:03:21 +02:00
|
|
|
customDimensions = true;
|
2021-07-29 20:37:51 +02:00
|
|
|
} else if (customDimensions) {
|
|
|
|
actualHeight = String(Math.trunc(Number(actualWidth) / aspectRatio));
|
2021-07-21 18:32:07 +02:00
|
|
|
} else {
|
2021-07-21 19:03:21 +02:00
|
|
|
actualHeight = String(naturalHeight);
|
2021-07-21 18:32:07 +02:00
|
|
|
}
|
2021-07-30 02:07:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async function updateSizesWithDimensions() {
|
2021-07-30 20:37:10 +02:00
|
|
|
updateSizes();
|
2021-08-05 03:24:04 +02:00
|
|
|
updateDimensions();
|
2021-07-30 02:07:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/* window resizing */
|
|
|
|
const resizeObserver = new ResizeObserver(async () => {
|
|
|
|
if (activeImage) {
|
|
|
|
await updateSizesWithDimensions();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
function startObserving() {
|
|
|
|
resizeObserver.observe(container);
|
|
|
|
}
|
|
|
|
|
|
|
|
function stopObserving() {
|
|
|
|
resizeObserver.unobserve(container);
|
|
|
|
}
|
|
|
|
|
|
|
|
startObserving();
|
|
|
|
|
2021-07-28 20:17:31 +02:00
|
|
|
/* memoized position of image on resize start
|
|
|
|
* prevents frantic behavior when image shift into the next/previous line */
|
|
|
|
let getDragWidth: (event: PointerEvent) => number;
|
|
|
|
let getDragHeight: (event: PointerEvent) => number;
|
2021-07-21 01:32:09 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
function setPointerCapture({ detail }: CustomEvent): void {
|
|
|
|
if (!active || detail.originalEvent.pointerId !== 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const imageRect = activeImage!.getBoundingClientRect();
|
2021-07-28 20:17:31 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
const imageLeft = imageRect!.left;
|
|
|
|
const imageRight = imageRect!.right;
|
|
|
|
const [multX, imageX] = detail.west ? [-1, imageRight] : [1, -imageLeft];
|
2021-07-28 20:17:31 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
getDragWidth = ({ clientX }) => multX * clientX + imageX;
|
2021-07-28 23:41:46 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
const imageTop = imageRect!.top;
|
|
|
|
const imageBottom = imageRect!.bottom;
|
|
|
|
const [multY, imageY] = detail.north ? [-1, imageBottom] : [1, -imageTop];
|
2021-07-28 23:41:46 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
getDragHeight = ({ clientY }) => multY * clientY + imageY;
|
2021-07-28 23:41:46 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
stopObserving();
|
2021-07-28 20:17:31 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
const target = detail.originalEvent.target as Element;
|
|
|
|
target.setPointerCapture(detail.originalEvent.pointerId);
|
|
|
|
}
|
2021-07-21 01:32:09 +02:00
|
|
|
|
2021-07-30 01:03:21 +02:00
|
|
|
$: [minResizeWidth, minResizeHeight] =
|
|
|
|
aspectRatio > 1 ? [5 * aspectRatio, 5] : [5, 5 / aspectRatio];
|
|
|
|
|
2021-07-30 02:07:41 +02:00
|
|
|
async function resize(event: PointerEvent) {
|
2021-07-21 17:37:58 +02:00
|
|
|
const element = event.target! as Element;
|
|
|
|
|
|
|
|
if (!element.hasPointerCapture(event.pointerId)) {
|
2021-07-21 01:32:09 +02:00
|
|
|
return;
|
|
|
|
}
|
2021-07-21 02:15:10 +02:00
|
|
|
|
2021-07-28 20:17:31 +02:00
|
|
|
const dragWidth = getDragWidth(event);
|
|
|
|
const dragHeight = getDragHeight(event);
|
2021-07-21 02:15:10 +02:00
|
|
|
|
2021-07-26 22:31:47 +02:00
|
|
|
const widthIncrease = dragWidth / naturalWidth!;
|
|
|
|
const heightIncrease = dragHeight / naturalHeight!;
|
2021-07-21 02:37:07 +02:00
|
|
|
|
2021-07-26 23:09:14 +02:00
|
|
|
if (widthIncrease > heightIncrease) {
|
2021-07-30 01:03:21 +02:00
|
|
|
width = Math.max(Math.trunc(dragWidth), minResizeWidth);
|
2021-07-27 16:54:23 +02:00
|
|
|
height = Math.trunc(naturalHeight! * (width / naturalWidth!));
|
2021-07-21 02:37:07 +02:00
|
|
|
} else {
|
2021-07-30 01:03:21 +02:00
|
|
|
height = Math.max(Math.trunc(dragHeight), minResizeHeight);
|
2021-07-27 16:54:23 +02:00
|
|
|
width = Math.trunc(naturalWidth! * (height / naturalHeight!));
|
2021-07-21 02:37:07 +02:00
|
|
|
}
|
|
|
|
|
2021-07-27 16:54:23 +02:00
|
|
|
showFloat = width >= 100;
|
2021-07-28 20:17:31 +02:00
|
|
|
activeImage!.width = width;
|
2021-07-30 02:07:41 +02:00
|
|
|
|
|
|
|
await updateSizesWithDimensions();
|
2021-07-21 01:32:09 +02:00
|
|
|
}
|
|
|
|
|
2021-07-21 15:52:48 +02:00
|
|
|
let sizeSelect: any;
|
2021-07-28 20:17:31 +02:00
|
|
|
let active = false;
|
2021-07-21 15:52:48 +02:00
|
|
|
|
2021-08-04 19:46:46 +02:00
|
|
|
$: if (activeImage && sizeSelect?.images.includes(activeImage)) {
|
2021-08-04 19:13:37 +02:00
|
|
|
updateSizes();
|
|
|
|
} else {
|
|
|
|
resetSizes();
|
|
|
|
}
|
|
|
|
|
2021-07-21 15:52:48 +02:00
|
|
|
function onDblclick() {
|
|
|
|
sizeSelect.toggleActualSize();
|
|
|
|
}
|
|
|
|
|
2021-07-28 20:17:31 +02:00
|
|
|
onDestroy(() => resizeObserver.disconnect());
|
2021-07-21 01:32:09 +02:00
|
|
|
</script>
|
|
|
|
|
2021-08-04 22:25:05 +02:00
|
|
|
<HandleSelection
|
|
|
|
--left="{left}px"
|
|
|
|
--top="{top}px"
|
|
|
|
--width="{width}px"
|
|
|
|
--height="{height}px"
|
2021-07-28 20:17:31 +02:00
|
|
|
>
|
|
|
|
{#if activeImage}
|
2021-08-05 02:58:52 +02:00
|
|
|
<HandleBackground on:dblclick={onDblclick} />
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
{#if sheet}
|
|
|
|
<div class="image-handle-size-select" class:is-rtl={isRtl}>
|
|
|
|
<ImageHandleSizeSelect
|
|
|
|
bind:this={sizeSelect}
|
|
|
|
bind:active
|
|
|
|
{container}
|
|
|
|
{sheet}
|
|
|
|
{activeImage}
|
|
|
|
{isRtl}
|
|
|
|
on:update={updateSizesWithDimensions}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
{/if}
|
2021-07-28 20:17:31 +02:00
|
|
|
|
2021-08-05 02:58:52 +02:00
|
|
|
{#if activeImage}
|
2021-07-27 16:54:23 +02:00
|
|
|
{#if showFloat}
|
2021-07-30 01:03:21 +02:00
|
|
|
<div
|
2021-07-30 02:07:41 +02:00
|
|
|
class="image-handle-float"
|
2021-07-30 01:03:21 +02:00
|
|
|
class:is-rtl={isRtl}
|
2021-07-30 02:07:41 +02:00
|
|
|
on:click={updateSizesWithDimensions}
|
2021-07-30 01:03:21 +02:00
|
|
|
>
|
2021-07-30 02:07:41 +02:00
|
|
|
<ImageHandleFloat {activeImage} {isRtl} />
|
2021-07-21 15:52:48 +02:00
|
|
|
</div>
|
|
|
|
{/if}
|
2021-07-30 02:07:41 +02:00
|
|
|
|
2021-08-05 03:24:04 +02:00
|
|
|
<HandleLabel {isRtl} on:mount={updateDimensions}>
|
2021-07-30 02:07:41 +02:00
|
|
|
<span>{actualWidth}×{actualHeight}</span>
|
2021-08-05 03:24:04 +02:00
|
|
|
{#if customDimensions}
|
|
|
|
<span>(Original: {naturalWidth}×{naturalHeight})</span>
|
|
|
|
{/if}
|
|
|
|
</HandleLabel>
|
2021-07-28 20:17:31 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
<HandleControl
|
|
|
|
{active}
|
|
|
|
on:pointerclick={setPointerCapture}
|
2021-07-21 17:37:58 +02:00
|
|
|
on:pointerup={startObserving}
|
2021-07-21 03:50:52 +02:00
|
|
|
on:pointermove={resize}
|
2021-07-21 03:18:33 +02:00
|
|
|
/>
|
2021-07-28 20:17:31 +02:00
|
|
|
{/if}
|
2021-08-04 22:25:05 +02:00
|
|
|
</HandleSelection>
|
2021-07-21 01:32:09 +02:00
|
|
|
|
|
|
|
<style lang="scss">
|
|
|
|
div {
|
|
|
|
position: absolute;
|
|
|
|
}
|
|
|
|
|
2021-07-21 18:35:57 +02:00
|
|
|
.image-handle-float {
|
2021-07-21 03:18:33 +02:00
|
|
|
left: 3px;
|
2021-08-05 03:24:04 +02:00
|
|
|
top: 3px;
|
2021-07-21 16:48:02 +02:00
|
|
|
|
|
|
|
&.is-rtl {
|
|
|
|
left: auto;
|
|
|
|
right: 3px;
|
|
|
|
}
|
2021-07-21 03:18:33 +02:00
|
|
|
}
|
|
|
|
|
2021-07-21 14:13:56 +02:00
|
|
|
.image-handle-size-select {
|
2021-07-21 16:48:02 +02:00
|
|
|
right: 3px;
|
2021-08-05 03:24:04 +02:00
|
|
|
top: 3px;
|
2021-07-21 16:48:02 +02:00
|
|
|
|
|
|
|
&.is-rtl {
|
|
|
|
right: auto;
|
|
|
|
left: 3px;
|
|
|
|
}
|
2021-07-21 02:15:10 +02:00
|
|
|
}
|
2021-07-21 01:32:09 +02:00
|
|
|
</style>
|