Skip to content

Commit

Permalink
feat: Add crop image utility 💞 (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
willnguyen1312 authored May 16, 2023
1 parent 92bd8e6 commit 5809c27
Show file tree
Hide file tree
Showing 11 changed files with 134 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/tall-melons-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zoom-image/core": minor
---

Add crop image utility 💞
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export default defineConfig({
{ text: "createZoomImageHover", link: "/api/createZoomImageHover" },
{ text: "createZoomImageMove", link: "/api/createZoomImageMove" },
{ text: "createZoomImageWheel", link: "/api/createZoomImageWheel" },
{ text: "cropImage", link: "/api/cropImage" },
{ text: "makeCalculateZoom", link: "/api/makeCalculateZoom" },
{ text: "makeCalculatePercentage", link: "/api/makeCalculatePercentage" },
],
Expand Down
3 changes: 3 additions & 0 deletions docs/api/createZoomImageWheel.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ cleanup()
```ts
type ZoomImageWheelState = {
currentZoom: number
enable: boolean
currentPositionX: number
currentPositionY: number
}

type Listener = (state: ZoomImageWheelState) => void
Expand Down
35 changes: 35 additions & 0 deletions docs/api/cropImage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script setup>
import BundleSize from '../components/BundleSize.vue'
</script>

<BundleSize func="cropImage" />

Crop the current image based on the zoom level

### Basic Usage

```ts
const croppedImage = cropImage({
currentZoom: number
image: HTMLImageElement
positionX: number
positionY: number
})
```

### Type Declaration

```ts
type CropImageArg = {
// Zoom image element
image: HTMLImageElement
// Current zoom positionX returned from createZoomImageWheel
positionX: number
// Current zoom positionY returned from createZoomImageWheel
positionY: number
// Current zoom level from createZoomImageWheel
currentZoom: number
}

function cropImage = (arg: CropImageArg) => string
```
32 changes: 29 additions & 3 deletions docs/components/HomePageShow.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<script setup lang="ts">
import { createZoomImageHover, createZoomImageWheel, createZoomImageMove, createZoomImageClick } from "@zoom-image/core"
import {
createZoomImageHover,
createZoomImageWheel,
createZoomImageMove,
createZoomImageClick,
cropImage,
} from "@zoom-image/core"
import { computed, nextTick, onUnmounted, ref, watch } from "vue"
let cleanup: () => void = () => {}
Expand Down Expand Up @@ -36,6 +42,8 @@ const handleTabClick = (tab: { name: string; href: string; current: boolean }) =
tab.current = true
}
let handleCropWheelZoomImage = () => {}
watch(
zoomType,
async () => {
Expand All @@ -60,6 +68,18 @@ watch(
const result = createZoomImageWheel(imageContainer)
cleanup = result.cleanup
handleCropWheelZoomImage = () => {
const state = result.getState()
const croppedImage = cropImage({
currentZoom: state.currentZoom,
image: imageContainer.querySelector("img") as HTMLImageElement,
positionX: state.currentPositionX,
positionY: state.currentPositionY,
})
const cropImageElement = document.getElementById("crop-image") as HTMLImageElement
cropImageElement.src = croppedImage
}
}
if (zoomType.value === "move") {
Expand Down Expand Up @@ -108,9 +128,15 @@ onUnmounted(() => {

<div class="space-y-4" v-if="zoomType === 'wheel'">
<p>Scroll / Pinch inside the image to see zoom in-out effect</p>
<div ref="imageWheelContainerRef" class="mt-1 h-[300px] w-[300px] cursor-crosshair">
<img class="h-full w-full" alt="Large Pic" src="https://nam-assets.netlify.app/static/large.webp" />
<div class="mt-1 flex space-x-2">
<div ref="imageWheelContainerRef" class="h-[300px] w-[300px] cursor-crosshair">
<img class="h-full w-full" crossorigin="anonymous" alt="Large Pic" src="/large.webp" />
</div>
<img class="h-[300px] w-[300px]" id="crop-image" />
</div>
<button class="text-dark-500 rounded bg-gray-100 p-2 font-medium" @click="handleCropWheelZoomImage">
Crop image
</button>
</div>

<div class="space-y-4" v-if="zoomType === 'hover'">
Expand Down
Binary file added docs/public/large.webp
Binary file not shown.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"solid": "pnpm --filter \"./examples/solid-ts\"",
"qwik": "pnpm --filter \"./examples/qwik-ts\"",
"core": "pnpm --filter \"./packages/core\"",
"start:preact": "pnpm vanilla dev --open --host",
"start:preact": "pnpm preact dev --open --host",
"start-preact": "run-p dev start:preact",
"start:react": "pnpm react dev --open --host",
"start-react": "run-p dev start:react",
Expand Down Expand Up @@ -109,6 +109,12 @@
"import": "{ createZoomImageClick }",
"limit": "1.5KB"
},
{
"name": "cropImage",
"path": "packages/core/dist/index.mjs",
"import": "{ cropImage }",
"limit": "1.5KB"
},
{
"name": "makeCalculatePercentage",
"path": "packages/core/dist/index.mjs",
Expand Down
30 changes: 17 additions & 13 deletions packages/core/src/createZoomImageWheel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const ZOOM_DELTA = 0.5
export type ZoomImageWheelState = {
currentZoom: number
enable: boolean
currentPositionX: number
currentPositionY: number
}

type StateUpdate = { enable: boolean }
Expand All @@ -24,8 +26,12 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW
const store = createStore<ZoomImageWheelState>({
currentZoom: 1,
enable: true,
currentPositionX: 0,
currentPositionY: 0,
})

const state = store.getState()

const sourceImgElement = getSourceImage(container)
const finalOptions: Required<ZoomImageWheelOptions> = {
maxZoom: options.maxZoom || 4,
Expand Down Expand Up @@ -53,9 +59,7 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW
const pointerMap = new Map<number, { x: number; y: number }>()

let isOnMove = false
let currentPositionX = 0
let lastPositionX = 0
let currentPositionY = 0
let lastPositionY = 0
let startX = 0
let startY = 0
Expand All @@ -64,7 +68,7 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW
sourceImgElement.style.transformOrigin = "0 0"

function updateZoom() {
sourceImgElement.style.transform = `translate(${currentPositionX}px, ${currentPositionY}px) scale(${
sourceImgElement.style.transform = `translate(${state.currentPositionX}px, ${state.currentPositionY}px) scale(${
store.getState().currentZoom
})`
}
Expand All @@ -75,17 +79,17 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW
const zoomPointY = y - containerRect.top
const { currentZoom } = store.getState()

const zoomTargetX = (zoomPointX - currentPositionX) / currentZoom
const zoomTargetY = (zoomPointY - currentPositionY) / currentZoom
const zoomTargetX = (zoomPointX - state.currentPositionX) / currentZoom
const zoomTargetY = (zoomPointY - state.currentPositionY) / currentZoom

const newCurrentZoom = clamp(
currentZoom + delta * finalOptions.wheelZoomRatio * currentZoom,
1,
finalOptions.maxZoom,
)

currentPositionX = calculatePositionX(-zoomTargetX * newCurrentZoom + zoomPointX, newCurrentZoom)
currentPositionY = calculatePositionY(-zoomTargetY * newCurrentZoom + zoomPointY, newCurrentZoom)
state.currentPositionX = calculatePositionX(-zoomTargetX * newCurrentZoom + zoomPointX, newCurrentZoom)
state.currentPositionY = calculatePositionY(-zoomTargetY * newCurrentZoom + zoomPointY, newCurrentZoom)
store.update({ currentZoom: newCurrentZoom })
}

Expand Down Expand Up @@ -135,8 +139,8 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW
const offsetX = clientX - startX
const offsetY = clientY - startY
const { currentZoom } = store.getState()
currentPositionX = calculatePositionX(lastPositionX + offsetX, currentZoom)
currentPositionY = calculatePositionY(lastPositionY + offsetY, currentZoom)
state.currentPositionX = calculatePositionX(lastPositionX + offsetX, currentZoom)
state.currentPositionY = calculatePositionY(lastPositionY + offsetY, currentZoom)
updateZoom()
}
}
Expand All @@ -155,8 +159,8 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW

const { clientX, clientY, pointerId } = event
isOnMove = true
lastPositionX = currentPositionX
lastPositionY = currentPositionY
lastPositionX = state.currentPositionX
lastPositionY = state.currentPositionY
startX = clientX
startY = clientY
pointerMap.set(pointerId, { x: clientX, y: clientY })
Expand All @@ -183,8 +187,8 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW
zoomType = ""
}

lastPositionX = currentPositionX
lastPositionY = currentPositionY
lastPositionX = state.currentPositionX
lastPositionY = state.currentPositionY
}

function _handlePointerLeave() {
Expand Down
33 changes: 33 additions & 0 deletions packages/core/src/cropImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
type CropImageArg = {
currentZoom: number
image: HTMLImageElement
positionX: number
positionY: number
}

export const cropImage = ({ image, positionX, positionY, currentZoom }: CropImageArg) => {
const canvas = document.createElement("canvas")
const scale = image.naturalWidth / (image.clientWidth * currentZoom)
const croppedImageWidth = image.clientWidth * scale
const croppedImageHeight = image.clientHeight * scale
canvas.width = croppedImageWidth
canvas.height = croppedImageHeight
const canvasContext = canvas.getContext("2d") as CanvasRenderingContext2D

const sx = Math.max(0, Math.abs(positionX) * scale)
const sy = Math.max(0, Math.abs(positionY) * scale)

canvasContext.drawImage(
image,
sx,
sy,
croppedImageWidth,
croppedImageHeight,
0,
0,
croppedImageWidth,
croppedImageHeight,
)

return canvas.toDataURL()
}
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type { ZoomImageClickOptions }
// Start importing utils
import { makeCalculatePercentage } from "./makeCalculatePercentage"
import { makeCalculateZoom } from "./makeCalculateZoom"
import { cropImage } from "./cropImage"

export { makeCalculatePercentage, makeCalculateZoom }
export { makeCalculatePercentage, makeCalculateZoom, cropImage }
// Stop importing utils
3 changes: 2 additions & 1 deletion size.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"createZoomImageClick": "1.01 KB",
"createZoomImageHover": "1.4 KB",
"createZoomImageMove": "1020 B",
"createZoomImageWheel": "1.46 KB",
"createZoomImageWheel": "1.49 KB",
"cropImage": "250 B",
"makeCalculatePercentage": "145 B",
"makeCalculateZoom": "141 B"
}

0 comments on commit 5809c27

Please sign in to comment.