@octanejs/image-crop 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ import { PixelCrop } from '../types'
2
+
3
+ const TO_RADIANS = Math.PI / 180
4
+
5
+ export async function cropToCanvas(
6
+ image: HTMLImageElement,
7
+ canvas: HTMLCanvasElement,
8
+ crop: PixelCrop,
9
+ scale = 1,
10
+ rotate = 0
11
+ ) {
12
+ const ctx = canvas.getContext('2d')
13
+
14
+ if (!ctx) {
15
+ throw new Error('No 2d context')
16
+ }
17
+
18
+ const scaleX = image.naturalWidth / image.width
19
+ const scaleY = image.naturalHeight / image.height
20
+ // devicePixelRatio slightly increases sharpness on retina devices
21
+ // at the expense of slightly slower render times and needing to
22
+ // size the image back down if you want to download/upload and be
23
+ // true to the images natural size.
24
+ const pixelRatio = window.devicePixelRatio
25
+ // const pixelRatio = 1
26
+
27
+ canvas.width = Math.floor(crop.width * scaleX * pixelRatio)
28
+ canvas.height = Math.floor(crop.height * scaleY * pixelRatio)
29
+
30
+ ctx.scale(pixelRatio, pixelRatio)
31
+ ctx.imageSmoothingQuality = 'high'
32
+
33
+ const cropX = crop.x * scaleX
34
+ const cropY = crop.y * scaleY
35
+
36
+ const rotateRads = rotate * TO_RADIANS
37
+ const centerX = image.naturalWidth / 2
38
+ const centerY = image.naturalHeight / 2
39
+
40
+ ctx.save()
41
+
42
+ // 5) Move the crop origin to the canvas origin (0,0)
43
+ ctx.translate(-cropX, -cropY)
44
+ // 4) Move the origin to the center of the original position
45
+ ctx.translate(centerX, centerY)
46
+ // 3) Rotate around the origin
47
+ ctx.rotate(rotateRads)
48
+ // 2) Scale the image
49
+ ctx.scale(scale, scale)
50
+ // 1) Move the center of the image to the origin (0,0)
51
+ ctx.translate(-centerX, -centerY)
52
+ ctx.drawImage(image, 0, 0, image.naturalWidth, image.naturalHeight, 0, 0, image.naturalWidth, image.naturalHeight)
53
+
54
+ ctx.restore()
55
+ }
56
+
57
+ function toBlob(canvas: HTMLCanvasElement): Promise<Blob | null> {
58
+ return new Promise(resolve => {
59
+ canvas.toBlob(resolve)
60
+ })
61
+ }
62
+
63
+ let previewUrl = ''
64
+
65
+ // Returns an image source you should set to state and pass
66
+ // `{previewSrc && <img alt="Crop preview" src={previewSrc} />}`
67
+ export async function cropToImg(image: HTMLImageElement, crop: PixelCrop, scale = 1, rotate = 0) {
68
+ const canvas = document.createElement('canvas')
69
+ cropToCanvas(image, canvas, crop, scale, rotate)
70
+
71
+ const blob = await toBlob(canvas)
72
+
73
+ if (!blob) {
74
+ console.error('Failed to create blob')
75
+ return ''
76
+ }
77
+
78
+ if (previewUrl) {
79
+ URL.revokeObjectURL(previewUrl)
80
+ }
81
+
82
+ previewUrl = URL.createObjectURL(blob)
83
+ return previewUrl
84
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './types';
2
+ export * from './utils';
3
+ export * from './ReactCrop.tsrx';
4
+ export * from './addons/browserCrop';
5
+
6
+ // For compatibility with older versions.
7
+ export { ReactCrop as Component, ReactCrop as default } from './ReactCrop.tsrx';
package/src/types.ts ADDED
@@ -0,0 +1,20 @@
1
+ export type XOrds = 'e' | 'w'
2
+ export type YOrds = 'n' | 's'
3
+ export type XYOrds = 'nw' | 'ne' | 'se' | 'sw'
4
+ export type Ords = XOrds | YOrds | XYOrds
5
+
6
+ export interface Crop {
7
+ x: number
8
+ y: number
9
+ width: number
10
+ height: number
11
+ unit: 'px' | '%'
12
+ }
13
+
14
+ export interface PixelCrop extends Crop {
15
+ unit: 'px'
16
+ }
17
+
18
+ export interface PercentCrop extends Crop {
19
+ unit: '%'
20
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,347 @@
1
+ import { PixelCrop, PercentCrop, Crop, Ords } from './types'
2
+
3
+ export const defaultCrop: PixelCrop = {
4
+ x: 0,
5
+ y: 0,
6
+ width: 0,
7
+ height: 0,
8
+ unit: 'px',
9
+ }
10
+
11
+ export const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max)
12
+
13
+ export const cls = (...args: unknown[]) => args.filter(v => v && typeof v === 'string').join(' ')
14
+
15
+ export const areCropsEqual = (cropA: Partial<Crop>, cropB: Partial<Crop>) =>
16
+ cropA === cropB ||
17
+ (cropA.width === cropB.width &&
18
+ cropA.height === cropB.height &&
19
+ cropA.x === cropB.x &&
20
+ cropA.y === cropB.y &&
21
+ cropA.unit === cropB.unit)
22
+
23
+ export function makeAspectCrop(
24
+ crop: Pick<PercentCrop, 'unit'> & Partial<Omit<PercentCrop, 'unit'>>,
25
+ aspect: number,
26
+ containerWidth: number,
27
+ containerHeight: number
28
+ ): PercentCrop
29
+ export function makeAspectCrop(
30
+ crop: Pick<PixelCrop, 'unit'> & Partial<Omit<PixelCrop, 'unit'>>,
31
+ aspect: number,
32
+ containerWidth: number,
33
+ containerHeight: number
34
+ ): PixelCrop
35
+ export function makeAspectCrop(crop: Partial<Crop>, aspect: number, containerWidth: number, containerHeight: number) {
36
+ const pixelCrop = convertToPixelCrop(crop, containerWidth, containerHeight)
37
+
38
+ if (crop.width) {
39
+ pixelCrop.height = pixelCrop.width / aspect
40
+ }
41
+
42
+ if (crop.height) {
43
+ pixelCrop.width = pixelCrop.height * aspect
44
+ }
45
+
46
+ if (pixelCrop.y + pixelCrop.height > containerHeight) {
47
+ pixelCrop.height = containerHeight - pixelCrop.y
48
+ pixelCrop.width = pixelCrop.height * aspect
49
+ }
50
+
51
+ if (pixelCrop.x + pixelCrop.width > containerWidth) {
52
+ pixelCrop.width = containerWidth - pixelCrop.x
53
+ pixelCrop.height = pixelCrop.width / aspect
54
+ }
55
+
56
+ if (crop.unit === '%') {
57
+ return convertToPercentCrop(pixelCrop, containerWidth, containerHeight)
58
+ }
59
+
60
+ return pixelCrop
61
+ }
62
+
63
+ export function centerCrop(
64
+ crop: Pick<PercentCrop, 'unit'> & Partial<Omit<PercentCrop, 'unit'>>,
65
+ containerWidth: number,
66
+ containerHeight: number
67
+ ): PercentCrop
68
+ export function centerCrop(
69
+ crop: Pick<PixelCrop, 'unit'> & Partial<Omit<PixelCrop, 'unit'>>,
70
+ containerWidth: number,
71
+ containerHeight: number
72
+ ): PixelCrop
73
+ export function centerCrop(crop: Partial<Crop>, containerWidth: number, containerHeight: number) {
74
+ const pixelCrop = convertToPixelCrop(crop, containerWidth, containerHeight)
75
+
76
+ pixelCrop.x = (containerWidth - pixelCrop.width) / 2
77
+ pixelCrop.y = (containerHeight - pixelCrop.height) / 2
78
+
79
+ if (crop.unit === '%') {
80
+ return convertToPercentCrop(pixelCrop, containerWidth, containerHeight)
81
+ }
82
+
83
+ return pixelCrop
84
+ }
85
+
86
+ export function convertToPercentCrop(
87
+ crop: Partial<Crop>,
88
+ containerWidth: number,
89
+ containerHeight: number
90
+ ): PercentCrop {
91
+ if (crop.unit === '%') {
92
+ return { ...defaultCrop, ...crop, unit: '%' }
93
+ }
94
+
95
+ return {
96
+ unit: '%',
97
+ x: crop.x ? (crop.x / containerWidth) * 100 : 0,
98
+ y: crop.y ? (crop.y / containerHeight) * 100 : 0,
99
+ width: crop.width ? (crop.width / containerWidth) * 100 : 0,
100
+ height: crop.height ? (crop.height / containerHeight) * 100 : 0,
101
+ }
102
+ }
103
+
104
+ export function convertToPixelCrop(crop: Partial<Crop>, containerWidth: number, containerHeight: number): PixelCrop {
105
+ if (!crop.unit) {
106
+ return { ...defaultCrop, ...crop, unit: 'px' }
107
+ }
108
+
109
+ if (crop.unit === 'px') {
110
+ return { ...defaultCrop, ...crop, unit: 'px' }
111
+ }
112
+
113
+ return {
114
+ unit: 'px',
115
+ x: crop.x ? (crop.x * containerWidth) / 100 : 0,
116
+ y: crop.y ? (crop.y * containerHeight) / 100 : 0,
117
+ width: crop.width ? (crop.width * containerWidth) / 100 : 0,
118
+ height: crop.height ? (crop.height * containerHeight) / 100 : 0,
119
+ }
120
+ }
121
+
122
+ // Sorry.
123
+ export function containCrop(
124
+ pixelCrop: PixelCrop,
125
+ aspect: number,
126
+ ord: Ords,
127
+ containerWidth: number,
128
+ containerHeight: number,
129
+ minWidth = 0,
130
+ minHeight = 0,
131
+ maxWidth = containerWidth,
132
+ maxHeight = containerHeight
133
+ ) {
134
+ const containedCrop = { ...pixelCrop }
135
+ let _minWidth = Math.min(minWidth, containerWidth)
136
+ let _minHeight = Math.min(minHeight, containerHeight)
137
+ let _maxWidth = Math.min(maxWidth, containerWidth)
138
+ let _maxHeight = Math.min(maxHeight, containerHeight)
139
+
140
+ if (aspect) {
141
+ if (aspect > 1) {
142
+ // Landscape - increase width min + max.
143
+ _minWidth = minHeight ? minHeight * aspect : _minWidth
144
+ _minHeight = _minWidth / aspect
145
+ _maxWidth = maxWidth * aspect
146
+ } else {
147
+ // Portrait - increase height min + max.
148
+ _minHeight = minWidth ? minWidth / aspect : _minHeight
149
+ _minWidth = _minHeight * aspect
150
+ _maxHeight = maxHeight / aspect
151
+ }
152
+ }
153
+
154
+ // Stop underflow on top.
155
+ if (containedCrop.y < 0) {
156
+ containedCrop.height = Math.max(containedCrop.height + containedCrop.y, _minHeight)
157
+ containedCrop.y = 0
158
+ }
159
+
160
+ // Stop underflow on left.
161
+ if (containedCrop.x < 0) {
162
+ containedCrop.width = Math.max(containedCrop.width + containedCrop.x, _minWidth)
163
+ containedCrop.x = 0
164
+ }
165
+
166
+ // Stop overflow on right.
167
+ const xOverflow = containerWidth - (containedCrop.x + containedCrop.width)
168
+ if (xOverflow < 0) {
169
+ containedCrop.x = Math.min(containedCrop.x, containerWidth - _minWidth)
170
+ containedCrop.width += xOverflow
171
+ }
172
+
173
+ // Stop overflow on bottom.
174
+ const yOverflow = containerHeight - (containedCrop.y + containedCrop.height)
175
+ if (yOverflow < 0) {
176
+ containedCrop.y = Math.min(containedCrop.y, containerHeight - _minHeight)
177
+ containedCrop.height += yOverflow
178
+ }
179
+
180
+ // Make crop respect min width generally.
181
+ if (containedCrop.width < _minWidth) {
182
+ if (ord === 'sw' || ord == 'nw') {
183
+ // Stops box moving when min is hit.
184
+ containedCrop.x -= _minWidth - containedCrop.width
185
+ }
186
+ containedCrop.width = _minWidth
187
+ }
188
+
189
+ // Make crop respect min height generally.
190
+ if (containedCrop.height < _minHeight) {
191
+ if (ord === 'nw' || ord == 'ne') {
192
+ // Stops box moving when min is hit.
193
+ containedCrop.y -= _minHeight - containedCrop.height
194
+ }
195
+ containedCrop.height = _minHeight
196
+ }
197
+
198
+ // Make crop respect max width generally.
199
+ if (containedCrop.width > _maxWidth) {
200
+ if (ord === 'sw' || ord == 'nw') {
201
+ // Stops box moving when max is hit.
202
+ containedCrop.x -= _maxWidth - containedCrop.width
203
+ }
204
+ containedCrop.width = _maxWidth
205
+ }
206
+
207
+ // Make crop respect max height generally.
208
+ if (containedCrop.height > _maxHeight) {
209
+ if (ord === 'nw' || ord == 'ne') {
210
+ // Stops box moving when min is hit.
211
+ containedCrop.y -= _maxHeight - containedCrop.height
212
+ }
213
+ containedCrop.height = _maxHeight
214
+ }
215
+
216
+ // Maintain aspect after size fixing.
217
+ if (aspect) {
218
+ const currAspect = containedCrop.width / containedCrop.height
219
+ if (currAspect < aspect) {
220
+ // Crop is shrunk on the width so adjust the height.
221
+ const newHeight = Math.max(containedCrop.width / aspect, _minHeight)
222
+
223
+ if (ord === 'nw' || ord == 'ne') {
224
+ // Stops box moving when min is hit.
225
+ containedCrop.y -= newHeight - containedCrop.height
226
+ }
227
+
228
+ containedCrop.height = newHeight
229
+ } else if (currAspect > aspect) {
230
+ // Crop is shrunk on the height so adjust the width.
231
+ const newWidth = Math.max(containedCrop.height * aspect, _minWidth)
232
+
233
+ if (ord === 'sw' || ord == 'nw') {
234
+ // Stops box moving when max is hit.
235
+ containedCrop.x -= newWidth - containedCrop.width
236
+ }
237
+
238
+ containedCrop.width = newWidth
239
+ }
240
+ }
241
+
242
+ return containedCrop
243
+ }
244
+
245
+ export function nudgeCrop(pixelCrop: PixelCrop, key: string, offset: number, ord: Ords) {
246
+ const nextCrop = { ...pixelCrop }
247
+
248
+ if (key === 'ArrowLeft') {
249
+ if (ord === 'nw') {
250
+ nextCrop.x -= offset
251
+ nextCrop.y -= offset
252
+ nextCrop.width += offset
253
+ nextCrop.height += offset
254
+ } else if (ord === 'w') {
255
+ nextCrop.x -= offset
256
+ nextCrop.width += offset
257
+ } else if (ord === 'sw') {
258
+ nextCrop.x -= offset
259
+ nextCrop.width += offset
260
+ nextCrop.height += offset
261
+ } else if (ord === 'ne') {
262
+ nextCrop.y += offset
263
+ nextCrop.width -= offset
264
+ nextCrop.height -= offset
265
+ } else if (ord === 'e') {
266
+ nextCrop.width -= offset
267
+ } else if (ord === 'se') {
268
+ nextCrop.width -= offset
269
+ nextCrop.height -= offset
270
+ }
271
+ } else if (key === 'ArrowRight') {
272
+ if (ord === 'nw') {
273
+ nextCrop.x += offset
274
+ nextCrop.y += offset
275
+ nextCrop.width -= offset
276
+ nextCrop.height -= offset
277
+ } else if (ord === 'w') {
278
+ // Niche: Will move right if minWidth hit.
279
+ nextCrop.x += offset
280
+ nextCrop.width -= offset
281
+ } else if (ord === 'sw') {
282
+ nextCrop.x += offset
283
+ nextCrop.width -= offset
284
+ nextCrop.height -= offset
285
+ } else if (ord === 'ne') {
286
+ nextCrop.y -= offset
287
+ nextCrop.width += offset
288
+ nextCrop.height += offset
289
+ } else if (ord === 'e') {
290
+ nextCrop.width += offset
291
+ } else if (ord === 'se') {
292
+ nextCrop.width += offset
293
+ nextCrop.height += offset
294
+ }
295
+ }
296
+
297
+ if (key === 'ArrowUp') {
298
+ if (ord === 'nw') {
299
+ nextCrop.x -= offset
300
+ nextCrop.y -= offset
301
+ nextCrop.width += offset
302
+ nextCrop.height += offset
303
+ } else if (ord === 'n') {
304
+ nextCrop.y -= offset
305
+ nextCrop.height += offset
306
+ } else if (ord === 'ne') {
307
+ nextCrop.y -= offset
308
+ nextCrop.width += offset
309
+ nextCrop.height += offset
310
+ } else if (ord === 'sw') {
311
+ nextCrop.x += offset
312
+ nextCrop.width -= offset
313
+ nextCrop.height -= offset
314
+ } else if (ord === 's') {
315
+ nextCrop.height -= offset
316
+ } else if (ord === 'se') {
317
+ nextCrop.width -= offset
318
+ nextCrop.height -= offset
319
+ }
320
+ } else if (key === 'ArrowDown') {
321
+ if (ord === 'nw') {
322
+ nextCrop.x += offset
323
+ nextCrop.y += offset
324
+ nextCrop.width -= offset
325
+ nextCrop.height -= offset
326
+ } else if (ord === 'n') {
327
+ // Niche: Will move down if minHeight hit.
328
+ nextCrop.y += offset
329
+ nextCrop.height -= offset
330
+ } else if (ord === 'ne') {
331
+ nextCrop.y += offset
332
+ nextCrop.width -= offset
333
+ nextCrop.height -= offset
334
+ } else if (ord === 'sw') {
335
+ nextCrop.x -= offset
336
+ nextCrop.width += offset
337
+ nextCrop.height += offset
338
+ } else if (ord === 's') {
339
+ nextCrop.height += offset
340
+ } else if (ord === 'se') {
341
+ nextCrop.width += offset
342
+ nextCrop.height += offset
343
+ }
344
+ }
345
+
346
+ return nextCrop
347
+ }