Skip to content

Commit

Permalink
replace croppr with custom cropper
Browse files Browse the repository at this point in the history
  • Loading branch information
jrmajor committed Dec 16, 2024
1 parent fe63505 commit 04ad357
Show file tree
Hide file tree
Showing 11 changed files with 335 additions and 134 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"dependencies": {
"@inertiajs/svelte": "2.0.0-beta.3",
"@tailwindcss/forms": "^0.5.9",
"croppr": "^2.3",
"esm-env": "^1.2",
"pretty-bytes": "^6.1",
"svelte": "^5.7",
Expand Down
1 change: 0 additions & 1 deletion resources/css/style.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@import 'croppr/src/css/croppr.css';
@import './base.css';
@import './headers.css';
@import './placeholders.css';
Expand Down
60 changes: 0 additions & 60 deletions resources/js/Components/Cropper.svelte

This file was deleted.

193 changes: 193 additions & 0 deletions resources/js/Components/Cropper/Cropper.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<script module lang="ts">
import type { Box } from './box';
export type CropValue = Box;
</script>

<script lang="ts">
import clamp from '@/helpers/clamp';
import { round, constrainToBoundary, constrainToRatio } from './box';
let {
src,
crop = $bindable(),
aspectRatio = null,
}: {
src: string;
crop: CropValue;
aspectRatio?: number | null;
} = $props();
let el: HTMLImageElement;
type Direction = 'nw' | 'ne' | 'sw' | 'se' | 'n' | 's' | 'w' | 'e';
type Handle = { name: Direction; position: [number, number]; cursor: string };
const handles: Handle[] = [
{ name: 'n', position: [0.5, 0], cursor: 'ns' },
{ name: 's', position: [0.5, 1], cursor: 'ns' },
{ name: 'w', position: [0, 0.5], cursor: 'ew' },
{ name: 'e', position: [1, 0.5], cursor: 'ew' },
{ name: 'nw', position: [0, 0], cursor: 'nwse' },
{ name: 'ne', position: [1, 0], cursor: 'nesw' },
{ name: 'sw', position: [0, 1], cursor: 'nesw' },
{ name: 'se', position: [1, 1], cursor: 'nwse' },
];
function scaled(value: number) {
return el.clientWidth / el.naturalWidth * value;
}
function unscaled(value: number) {
return el.naturalWidth / el.clientWidth * value;
}
let currentlyDragging: Handle | 'anchor' | null = $state(null);
let oldCrop: CropValue;
let dragStart: { x: number; y: number };
function onmousedown(event: MouseEvent, handle: Handle | 'anchor') {
currentlyDragging = handle;
oldCrop = crop;
dragStart = { x: event.clientX, y: event.clientY };
event.preventDefault();
}
function onmousemove(event: MouseEvent) {
if (currentlyDragging === 'anchor') {
onAnchorMove(event);
} else if (currentlyDragging) {
onHandleMove(event, currentlyDragging);
}
}
function onmouseup() {
currentlyDragging = null;
}
function onHandleMove(event: MouseEvent, handle: Handle) {
const { name: direction } = handle;
const origin: [number, number] = [
1 - handle.position[0],
1 - handle.position[1],
];
const bounds = el.getBoundingClientRect();
const mouseX = clamp(bounds.left, event.clientX, bounds.right);
const mouseY = clamp(bounds.top, event.clientY, bounds.bottom);
const dragsHorizontal = direction.includes('w') || direction.includes('e');
const dragsVertical = direction.includes('n') || direction.includes('s');
const x = dragsHorizontal ? unscaled(mouseX - dragStart.x) : 0;
const y = dragsVertical ? unscaled(mouseY - dragStart.y) : 0;
let newBox = {
x: direction.includes('w') ? oldCrop.x + x : oldCrop.x,
y: direction.includes('n') ? oldCrop.y + y : oldCrop.y,
width: oldCrop.width + (direction.includes('e') ? x : -x),
height: oldCrop.height + (direction.includes('s') ? y : -y),
};
if (aspectRatio) {
const primaryDirection = direction.length === 1
? direction
: direction[Math.abs(x) > Math.abs(y) ? 1 : 0];
const ratioMode = ['w', 'e'].includes(primaryDirection) ? 'height' : 'width';
newBox = constrainToRatio(newBox, aspectRatio, origin, ratioMode);
}
newBox = constrainToBoundary(newBox, el.naturalWidth, el.naturalHeight, origin);
crop = round(newBox);
}
function onAnchorMove(event: MouseEvent) {
const x = unscaled(event.clientX - dragStart.x);
const y = unscaled(event.clientY - dragStart.y);
crop = round({
x: clamp(0, oldCrop.x + x, el.naturalWidth - oldCrop.width),
y: clamp(0, oldCrop.y + y, el.naturalHeight - oldCrop.height),
width: oldCrop.width,
height: oldCrop.height,
});
}
const cursor = $derived.by(() => {
if (!currentlyDragging) return;
if (currentlyDragging === 'anchor') return 'move';
return `${currentlyDragging.cursor}-resize`;
});
</script>

<svelte:document {onmousemove} {onmouseup}/>

<div class="cropper" style:cursor>
<img
bind:this={el}
{src}
alt=""
class="original"
>
<div
class="border"
style:top={`${scaled(crop.y) - 1}px`}
style:left={`${scaled(crop.x) - 1}px`}
style:width={`${scaled(crop.width) + 2}px`}
style:height={`${scaled(crop.height) + 2}px`}
></div>
<div class="handles">
{#each handles as handle}
{@const { position: [x, y], cursor } = handle}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
onmousedown={(e) => onmousedown(e, handle)}
style:cursor="{cursor}-resize"
style:left="{scaled(crop.x + (crop.width * x))}px"
style:top="{scaled(crop.y + (crop.height * y))}px"
></div>
{/each}
</div>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<img
{src}
alt=""
onmousedown={(e) => onmousedown(e, 'anchor')}
class="cropped"
style:clip="rect({scaled(crop.y)}px, {scaled(crop.x + crop.width)}px, {scaled(crop.y + crop.height)}px, {scaled(crop.x)}px)"
>
</div>

<style lang="postcss">
.cropper {
position: relative;
}
.original {
filter: brightness(0.5);
}
.cropped {
position: absolute;
top: 0;
left: 0;
cursor: move;
}
.border {
position: absolute;
border: 1px dashed #000;
}
.handles > * {
position: absolute;
width: 10px;
height: 10px;
background-color: #fff;
border: 1px solid #000;
border-radius: 5px;
translate: -50% -50%;
z-index: 1;
}
</style>
99 changes: 99 additions & 0 deletions resources/js/Components/Cropper/box.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
export interface Box {
x: number;
y: number;
width: number;
height: number;
}

type Coordinates = [number, number];

export function round(box: Box): Box {
return {
x: Math.round(box.x),
y: Math.round(box.y),
width: Math.round(box.width),
height: Math.round(box.height),
};
}

function resize(
box: Box,
newWidth: number,
newHeight: number,
origin: Coordinates,
): Box {
const fromX = box.x + (box.width * origin[0]);
const fromY = box.y + (box.height * origin[1]);

return {
x: fromX - (newWidth * origin[0]),
y: fromY - (newHeight * origin[1]),
width: newWidth,
height: newHeight,
};
}

function scale(box: Box, factor: number, origin: Coordinates): Box {
const newWidth = box.width * factor;
const newHeight = box.height * factor;

return resize(box, newWidth, newHeight, origin);
}

function getAbsolutePoint(box: Box, [x, y]: Coordinates): Coordinates {
return [
box.x + (box.width * x),
box.y + (box.height * y),
];
}

export function constrainToRatio(
box: Box,
ratio: number,
origin: Coordinates,
grow: 'width' | 'height',
): Box {
return grow === 'width'
? resize(box, box.height / ratio, box.height, origin)
: resize(box, box.width, box.width * ratio, origin);
}

export function constrainToBoundary(
box: Box,
boundaryWidth: number,
boundaryHeight: number,
origin: Coordinates,
): Box {
const [originX, originY] = getAbsolutePoint(box, origin);
const maxIfLeft = originX;
const maxIfTop = originY;
const maxIfRight = boundaryWidth - originX;
const maxIfBottom = boundaryHeight - originY;

let maxWidth: number;
if (origin[0] > 0.5) {
maxWidth = maxIfLeft;
} else if (origin[0] < 0.5) {
maxWidth = maxIfRight;
} else {
maxWidth = Math.min(maxIfLeft, maxIfRight) * 2;
}
let maxHeight: number;
if (origin[1] > 0.5) {
maxHeight = maxIfTop;
} else if (origin[1] < 0.5) {
maxHeight = maxIfBottom;
} else {
maxHeight = Math.min(maxIfTop, maxIfBottom) * 2;
}

if (box.width > maxWidth) {
return scale(box, maxWidth / box.width, origin);
}

if (box.height > maxHeight) {
return scale(box, maxHeight / box.height, origin);
}

return box;
}
Loading

0 comments on commit 04ad357

Please sign in to comment.