-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEventMaintenancePopupCropWindow.jsx
More file actions
350 lines (292 loc) · 20.1 KB
/
EventMaintenancePopupCropWindow.jsx
File metadata and controls
350 lines (292 loc) · 20.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
import React, { useRef } from 'react';
function EventMaintenancePopupCropWindow(props) {
// Component to:
// 1. display the input graphic supplied in props.temporaryBackgroundFilename under a draggable and resizable
// "crop window".
// 2. provide a "crop" button that, when clicked, calls the props.returnDataUrl function to return a scaled
// copy of the portion of the input graphic framed by the "crop window" to the component's parent.
const pageContainerRef = useRef(null);
const baseImageRef = useRef(null);
const imageContainerRef = useRef(null);
const scaledBaseImageRef = useRef(null);
const scaledBaseImageCanvasRef = useRef(null)
const cropWindowRef = useRef(null);
const cropWindowResizeBoxRef = useRef(null);
const scaledCropImageCanvasRef = useRef(null);
var mouseDownX, mouseDownY, mouseDownLeft, mouseDownTop, mouseDownWidth, mouseDownHeight,
xChangeLeftPermissibleMax, xChangeRightPermissibleMax, yChangeUpPermissibleMax, yChangeDownPermissibleMax,
xChangeWidthPermissibleMax, yChangeHeightPermissibleMax,
xChange, yChange;
// Sizing for the "imageContainer", the square window within the CropWindow Popup that displays the user
// supplied graphic is defined relative to the "rem" sizing of the popup itself. It needs to be square
// because the graphic is of unkniown format - it may be either landscape or portrait and it needs to be
// as large as possible because, in the current design, the higest user-supplied image is going to be
// scaled into the pixel dimensions of "imageContainer" and the Cropped graphic snapped out of this. So
// resolution will be lost if it's too small. Other designs might be possible, (for instance, you might
// run a "hidden" large-scale canvas" behind the visible "imageContainer" and do your snapping from that),
// but things are complicated enough as they are!
const imageContainerWidth = "28rem"; // following through on the use of rem units to define the CropWindow popup
const imageContainerHeight = "28rem";
const imageContainerWidthInPx = parseInt(imageContainerWidth, 10) * 16;
const imageContainerHeightInPx = parseInt(imageContainerHeight, 10) * 16;
// The EventCard component is referenced by the EventMaintenancePopup, Home and MobileHome comonents and
// is sized by these using vw-based dimensions. The first two use 23vw*14vw and the last uses 75vw*46vw.
// The important thing is that the aspect ration is consistent, otherwise distortion may occur when the
// thumbnail graphic is applied by EventCard itself. It applies this as a background with "backGroundSize"
// style set to "cover". This last ensures that width and height are "tugged" and cropped as necessary to
// make sure that the background covers the <div> completely.
// CropWindow is thus designed to deliver a graphic image of 23*14 aspect ratio with a resolution that's
// adeqate but not excessive (so that performace is not degraded). It does this by arbitraril choosin
// 23rem as the width and assuming that a rem is 16px
const eventCardWidth = "23rem";
const eventCardHeight = "14rem";
const aspectRatio = parseInt(eventCardWidth, 10) / parseInt(eventCardHeight, 10)
const eventCardWidthInPx = parseInt(eventCardWidth, 10) * 16
const eventCardHeightInPx = parseInt(eventCardHeight, 10) * 16
// to get the cropping window started with a generally suitable width, this is initialised as 250px.
// Height then naturally follows things by reference to the 23/14 aspectRatio
const cropWindowWidth = "250px"
const cropWindowHeight = parseInt(cropWindowWidth, 10) / aspectRatio + "px";
var isDragging = false;
var isResizing = false;
// The EventMaintenancePopupCropWindow is launched by parent components once the local image file supplied
// by the user has been successfully uploaded to it temporary location in the Cloud Storage
// event_backgrounds folder. It then renders a series of <div>s, <img>s and <Canvas>es as defined below.
// But additionally, because we don't know the dimensions of the usersupplied file until it actually
// arrives, the code needs to set appropriate widths, heights, tops and lefts for many of these elements.
// In a classic React implementation this would be achieved by using a useEffect function to "reach into"
// the Dom and manipulate properties as necessary but in practice this just didn't seem to work reliably.
// What was wanted was for the useEffect to declare an "onload" function for the baseImage <img> (this
// being the component that contains all the information we need) but for some reason this didn't work
// reliably. The solution seemed simply to be to define this onload function as an "onload=" structure on
// the <img> itself. Here's the structure that this initialises
// imageContainer - the outer (bordered) square "holder" for the cropping arrangement
// baseImage - within this a hidden <img> rectangle that may overhand imageContainer to
// right or left. The src for this comes from props.temporaryBackgroundFilename
// scaledBaseImage - a hidden scaled copy of baseImage sized to fit into imageContainer (but really
// just used to store scaled image height and width values)
// scaledBaseImageCanvas - a <canvas> copy of baseImage scaled to fit within imageContainer
// cropWindow - the draggable, resizable window displayed over scaledBaseImageCanvas
// scaledCropImageCanvas - a hidden <canvas> containing a copy of the subset of imageContainer defined
// by cropWindow
function mouseMoveOnImageContainer(e) {
// console.log("x,y in ImageContainer are " + e.clientX + " and " + e.clientY)
}
function mouseDownOnCropWindow(e) {
//console.log("In mouseDownOnCropWindow")
e.stopPropagation();
// stopPropagation() is used on all component mouse event functions to ensure that they don't bubble
// up and fire events on its parents
isDragging = true
mouseDownX = e.clientX;
mouseDownY = e.clientY;
// "left for an element is measured from the left of its border.
// "width" for an element also does not include its border.
// All "saved" settings are numbers rather than "px"-suffixed strings required to specify left,
// height etc in style
mouseDownLeft = parseInt(cropWindowRef.current.style.left, 10);
mouseDownTop = parseInt(cropWindowRef.current.style.top, 10);
xChangeLeftPermissibleMax = -parseInt(cropWindowRef.current.style.left, 10) - 1;
xChangeRightPermissibleMax = (scaledBaseImageRef.current.width) - (parseInt(cropWindowRef.current.style.left, 10) + parseInt(cropWindowRef.current.style.width, 10))
yChangeUpPermissibleMax = -parseInt(cropWindowRef.current.style.top, 10) - 1;
yChangeDownPermissibleMax = (scaledBaseImageRef.current.height + 1) - (parseInt(cropWindowRef.current.style.top, 10) + parseInt(cropWindowRef.current.style.height, 10));
}
function dragCropWindow(e) {
//console.log("In dragCropWindow")
e.stopPropagation();
if (isResizing) resizeCropWindow(e);
if (isDragging) {
// constrain movement past the image margins
let xChangeRequest = e.clientX - mouseDownX; // negative value indicates movement left
let yChangeRequest = e.clientY - mouseDownY; // negative value indicates movement upward
xChange = (xChangeRequest < 0) ? Math.max(xChangeRequest, xChangeLeftPermissibleMax) : Math.min(xChangeRequest, xChangeRightPermissibleMax)
yChange = (yChangeRequest < 0) ? Math.max(yChangeRequest, yChangeUpPermissibleMax) : Math.min(yChangeRequest, yChangeDownPermissibleMax)
// apply the "X/Y changes since MouseDown"
cropWindowRef.current.style.left = (mouseDownLeft + xChange) + "px";
cropWindowRef.current.style.top = (mouseDownTop + yChange) + "px";
}
}
function mouseUpOnCropWindow(e) {
//console.log("In mouseUpOnCropWindow")
e.stopPropagation();
clearWindowSettings();
}
function mouseDownOnResizeBox(e) {
//console.log("In mouseDownOnResizeBox")
e.stopPropagation();
isResizing = true
mouseDownX = e.clientX;
mouseDownY = e.clientY;
mouseDownLeft = parseInt(cropWindowRef.current.style.left, 10); // all "saved" settings are numbers rather than "px"-suffixed strings
mouseDownWidth = parseInt(cropWindowRef.current.style.width, 10);
mouseDownTop = parseInt(cropWindowRef.current.style.top, 10);
mouseDownHeight = parseInt(cropWindowRef.current.style.height, 10);
xChangeLeftPermissibleMax = -parseInt(cropWindowRef.current.style.left, 10) - 1;
xChangeWidthPermissibleMax = parseInt(cropWindowRef.current.style.width, 10);
yChangeUpPermissibleMax = -parseInt(cropWindowRef.current.style.top, 10) - 1;
yChangeHeightPermissibleMax = parseInt(cropWindowRef.current.style.height, 10);
}
function resizeCropWindow(e) {
//console.log("In resizeCropWindow where isResizing is " + isResizing)
e.stopPropagation();
if (isResizing) {
let xChangeRequest = e.clientX - mouseDownX // negative change indicates magnification
let yChangeRequest = xChangeRequest / aspectRatio
let changePermitted = true;
if (xChangeRequest < 0 && xChangeRequest < xChangeLeftPermissibleMax) changePermitted = false;
if (xChangeRequest >= 0 && xChangeRequest > xChangeWidthPermissibleMax) changePermitted = false;
if (yChangeRequest < 0 && yChangeRequest < yChangeUpPermissibleMax) changePermitted = false;
if (yChangeRequest >= 0 && yChangeRequest > yChangeHeightPermissibleMax) changePermitted = false;
// console.log("xChangeRequest = " + xChangeRequest + " " + " yChangeRequest = " + yChangeRequest + " xChangeLeftPermissibleMax = " + xChangeLeftPermissibleMax + " yChangeUpPermissibleMax = " + yChangeUpPermissibleMax + " changePermitted = " + changePermitted)
if (changePermitted) {
xChange = (xChangeRequest < 0) ? Math.max(xChangeRequest, xChangeLeftPermissibleMax) : Math.min(xChangeRequest, xChangeWidthPermissibleMax)
yChange = (yChangeRequest < 0) ? Math.max(yChangeRequest, yChangeUpPermissibleMax) : Math.min(yChangeRequest, yChangeHeightPermissibleMax)
cropWindowRef.current.style.left = (mouseDownLeft + xChange - 1) + "px";
cropWindowRef.current.style.width = (mouseDownWidth - xChange) + "px";
cropWindowRef.current.style.top = (mouseDownTop + yChange) + "px";
cropWindowRef.current.style.height = (mouseDownHeight - yChange) + "px";
}
}
}
function mouseUpOnResizeBox(e) {
//console.log("In mouseUpOnPageContainer")
clearWindowSettings();
}
function mouseMoveOnPageContainer(e) {
//console.log("In mouseUpOnPageContainer")
if (isResizing) resizeCropWindow(e)
}
function mouseUpOnPageContainer(e) {
// "on container rather than on ResizeBox because the pointer may wander off the image completely
console.log("In mouseUpOnImageContainer")
clearWindowSettings();
}
function clearWindowSettings() {
isDragging = false;
isResizing = false;
cropWindowRef.current.style.cursor = "move";
cropWindowResizeBoxRef.current.style.cursor = "nw-resize";
imageContainerRef.current.style.cursor = "default";
}
function crop() {
// get the section of the image defined on scaledBaseimageCanvas by the current cropwindow and write
// it to a new scaledCropImageCanvas scaled at 23rem*14rem.
// "left for an element excludes its border. "width" for an element also excludes its border.
const scaledCropImageCanvasCtx = scaledCropImageCanvasRef.current.getContext("2d");
const sx = parseInt(cropWindowRef.current.style.left, 10) + 1;
const sy = parseInt(cropWindowRef.current.style.top, 10) + 1;
const sw = parseInt(cropWindowRef.current.style.width, 10);
const sh = parseInt(cropWindowRef.current.style.height, 10);
// see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage for parameter docs
scaledCropImageCanvasCtx.drawImage(scaledBaseImageCanvasRef.current, sx, sy, sw, sh, 0, 0, eventCardWidthInPx, eventCardHeightInPx);
// finally, turn it into a dataimage and get its length. Using chatGpt recommendations for file type
// and quality to minimise string size while maximising image qualityminisin
const dataUrl = scaledCropImageCanvasRef.current.toDataURL('image/jpg', 0.7)
props.returnDataUrl(dataUrl);
}
return (
<div ref={pageContainerRef} style={{ width: "100%", height: "100%" }}
onMouseMove={(e) => mouseMoveOnPageContainer(e)}
onMouseUp={(e) => mouseUpOnPageContainer(e)}>
<div style={{ display: 'flex', width: '90%', justifyContent: 'center', userSelect: 'none' }}>
<div style={{ width: "50vh" }}>
<p style={{ width: "80%", marginLeft: "auto", marginRight: "auto" }}>
Use the "draggable" and "resizable" cropwindow displayed at bottom/right to select the
portion of your graphic to be used as the event's "thumnail". Click the
"Crop" button, below, to preview the thumbnail and adjust label colours, as necessary
</p>
<button type='button' className='selectedapparchbutton'
style={{
marginLeft: 'auto',
marginRight: 'auto',
}}
title="Crop"
onClick={() => crop()}
>Crop</button>
</div>
<div ref={imageContainerRef} style={{
position: "relative", border: '1px solid black', padding: '2px', marginRight: "auto"
}}
onMouseMove={(e) => mouseMoveOnImageContainer(e)}>
<div style={{ position: "relative" }}>
<img
ref={baseImageRef}
src={props.temporaryBackgroundFilename}
crossOrigin="anonymous"
style={{ display: 'none' }}
alt="Hidden unscaled display of the user-supplied graphic "
onLoad={function () {
console.log("In useEffect onload)")
const scaledBaseImageCanvas = scaledBaseImageCanvasRef.current;
const ctx = scaledBaseImageCanvas.getContext("2d");
// we're going to have to compute the scaling factor necessary to fit the user-supplied graphic
// into the imageContainer in an optimal fashion - which dimension is going to determine the
// factor - height or width?
// set the width and height in the style for the imageContainer, scaledBaseImageCanvas and scaledCropImageCanvas
imageContainerRef.current.style.width = imageContainerWidthInPx + "px"
imageContainerRef.current.style.height = imageContainerHeightInPx + "px"
scaledBaseImageCanvasRef.current.width = imageContainerWidthInPx
scaledBaseImageCanvasRef.current.height = imageContainerHeightInPx
scaledCropImageCanvasRef.current.width = eventCardWidthInPx
scaledCropImageCanvasRef.current.height = eventCardHeightInPx
var hRatio = imageContainerWidthInPx / baseImageRef.current.width;// "width" doesn't include "border" or padding
var vRatio = imageContainerHeightInPx / baseImageRef.current.height;
var ratio = Math.min(hRatio, vRatio);
scaledBaseImageRef.current.width = baseImageRef.current.width * ratio;
scaledBaseImageRef.current.height = baseImageRef.current.height * ratio;
ctx.drawImage(baseImageRef.current, 0, 0, baseImageRef.current.width, baseImageRef.current.height, 0, 0, scaledBaseImageRef.current.width, scaledBaseImageRef.current.height);
// set the position of the cropWindow so that it sits at the extreme right/bottom of the baseImage
cropWindowRef.current.style.left = ((baseImageRef.current.width * ratio) - parseInt(cropWindowWidth, 10) + 1) + "px" ///leve room for the right border
cropWindowRef.current.style.top = ((baseImageRef.current.height * ratio) - parseInt(cropWindowHeight, 10) + 11) + "px" //leave room for the resizeBox and the top border
}}
/>
<div
>
<img ref={scaledBaseImageRef} style={{ display: 'none' }}
alt="Hidden scaled display of the user-supplied graphic " />
<canvas
ref={scaledBaseImageCanvasRef}
style={{ position: "absolute", left: 0, top: 0 }}
/>
<div
ref={cropWindowRef}
style={{
position: "absolute",
width: (cropWindowWidth),
height: (cropWindowHeight),
border: "1px solid white",
cursor: "move",
transformOrigin: "bottom right"
}}
onMouseDown={(e) => mouseDownOnCropWindow(e)}
onMouseMove={(e) => dragCropWindow(e)}
onMouseUp={(e) => mouseUpOnCropWindow(e)}
>
<span
ref={cropWindowResizeBoxRef}
style={{
position: "absolute",
left: "-10px",
top: "-10px",
cursor: "nw-resize",
width: "10px",
height: "10px",
background: 'cyan',
border: '1px solid black'
}}
onMouseDown={(e) => mouseDownOnResizeBox(e)}
onMouseMove={(e) => resizeCropWindow(e)}
onMouseUp={(e) => mouseUpOnResizeBox(e)}
>
<span></span>
</span>
</div>
</div>
<canvas ref={scaledCropImageCanvasRef} style={{ display: 'none' }}></canvas>
</div>
</div>
</div>
</div >
);
};
export { EventMaintenancePopupCropWindow };