pcm-shared-components 0.0.44 → 0.0.47
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.
- package/README.md +23 -0
- package/dist/components/Buttons/CanvasPaint/index.js +207 -0
- package/dist/components/Buttons/ImageCanvasDraw/components/Controls.js +191 -0
- package/dist/components/Buttons/ImageCanvasDraw/constants/index.js +1 -0
- package/dist/components/Buttons/ImageCanvasDraw/constants/types.js +4 -0
- package/dist/components/Buttons/ImageCanvasDraw/index.js +183 -0
- package/dist/components/Buttons/ImageCanvasDraw/service/image_tools.js +37 -0
- package/dist/components/Buttons/ImageCanvasDraw/service/local_storage.js +20 -0
- package/dist/components/Buttons/ImageCanvasDraw/testMap.test.js +1 -0
- package/dist/styles/tailwind.css +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
1
|
# Shared Components between client and admin portal
|
|
2
2
|
|
|
3
3
|
The Library is published to NPM on the github build.
|
|
4
|
+
|
|
5
|
+
# Deploying
|
|
6
|
+
|
|
7
|
+
- When the code is checked into Github it will publish it automatically if the package.json version is bumped.
|
|
8
|
+
|
|
9
|
+
# Manual Deploy
|
|
10
|
+
|
|
11
|
+
Manually deploying is faster then committing the code as it doesn't need to create a ful build.
|
|
12
|
+
|
|
13
|
+
```shell
|
|
14
|
+
# Must build the Storybook dist folder before manually publishing
|
|
15
|
+
> npm run build
|
|
16
|
+
|
|
17
|
+
# Edit the package.json file and up he version.
|
|
18
|
+
> npm version patch --force
|
|
19
|
+
|
|
20
|
+
# Login if not already logged in
|
|
21
|
+
> npm login
|
|
22
|
+
|
|
23
|
+
# Publish your new library
|
|
24
|
+
> npm publish
|
|
25
|
+
|
|
26
|
+
```
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import tinycolor from "tinycolor2"; // Brush colour and size
|
|
3
|
+
// // Geometry
|
|
4
|
+
// // Drawing functions
|
|
5
|
+
// // Event helpers
|
|
6
|
+
// // Event handlers
|
|
7
|
+
|
|
8
|
+
export const CanvasPain = () => {
|
|
9
|
+
const canvas = useRef();
|
|
10
|
+
let context; // // Set up our drawing context
|
|
11
|
+
// // const canvas = document.getElementById("canvas");
|
|
12
|
+
// canvas.current.width = window.innerWidth;
|
|
13
|
+
// canvas.current.height = window.innerHeight - 10;
|
|
14
|
+
|
|
15
|
+
let colour;
|
|
16
|
+
const strokeWidth = 40;
|
|
17
|
+
const varyBrightness = 5; // Drawing state
|
|
18
|
+
|
|
19
|
+
let latestPoint;
|
|
20
|
+
let drawing = false;
|
|
21
|
+
let currentAngle;
|
|
22
|
+
|
|
23
|
+
const mouseDown = evt => {
|
|
24
|
+
if (drawing) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
evt.preventDefault();
|
|
29
|
+
canvas.current.addEventListener("mousemove", mouseMove, false);
|
|
30
|
+
startStroke([evt.offsetX, evt.offsetY]);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const mouseEnter = evt => {
|
|
34
|
+
if (!mouseButtonIsDown(evt.buttons) || drawing) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
mouseDown(evt);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const endStroke = evt => {
|
|
42
|
+
if (!drawing) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
drawing = false;
|
|
47
|
+
evt.currentTarget.removeEventListener("mousemove", mouseMove, false);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const continueStroke = newPoint => {
|
|
51
|
+
const newAngle = getNewAngle(latestPoint, newPoint, currentAngle);
|
|
52
|
+
drawStroke(currentBrush, latestPoint, newPoint, currentAngle, newAngle);
|
|
53
|
+
currentAngle = newAngle % (Math.PI * 2);
|
|
54
|
+
latestPoint = newPoint;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const startStroke = point => {
|
|
58
|
+
colour = document.getElementById("colourInput").value;
|
|
59
|
+
currentAngle = undefined;
|
|
60
|
+
currentBrush = makeBrush(strokeWidth);
|
|
61
|
+
drawing = true;
|
|
62
|
+
latestPoint = point;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const touchStart = evt => {
|
|
66
|
+
if (drawing) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
evt.preventDefault();
|
|
71
|
+
startStroke(getTouchPoint(evt));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const mouseMove = evt => {
|
|
75
|
+
if (!drawing) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
continueStroke([evt.offsetX, evt.offsetY]);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const touchMove = evt => {
|
|
83
|
+
if (!drawing) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
continueStroke(getTouchPoint(evt));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const touchEnd = evt => {
|
|
91
|
+
drawing = false;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const varyColour = sourceColour => {
|
|
95
|
+
const amount = Math.round(Math.random() * 2 * varyBrightness);
|
|
96
|
+
const c = tinycolor(sourceColour);
|
|
97
|
+
const varied = amount > varyBrightness ? c.brighten(amount - varyBrightness) : c.darken(amount);
|
|
98
|
+
return varied.toHexString();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const makeBrush = size => {
|
|
102
|
+
const brush = [];
|
|
103
|
+
let bristleCount = Math.round(size / 3);
|
|
104
|
+
const gap = strokeWidth / bristleCount;
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i < bristleCount; i++) {
|
|
107
|
+
const distance = i === 0 ? 0 : gap * i + Math.random() * gap / 2 - gap / 2;
|
|
108
|
+
brush.push({
|
|
109
|
+
distance,
|
|
110
|
+
thickness: Math.random() * 2 + 2,
|
|
111
|
+
colour: varyColour(colour)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return brush;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
let currentBrush = makeBrush(strokeWidth);
|
|
119
|
+
|
|
120
|
+
const getNewAngle = (origin, destination, oldAngle) => {
|
|
121
|
+
const bearing = getBearing(origin, destination);
|
|
122
|
+
|
|
123
|
+
if (typeof oldAngle === "undefined") {
|
|
124
|
+
console.log(bearing);
|
|
125
|
+
return bearing;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return oldAngle - angleDiff(oldAngle, bearing);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const rotatePoint = (distance, angle, origin) => [origin[0] + distance * Math.cos(angle), origin[1] + distance * Math.sin(angle)];
|
|
132
|
+
|
|
133
|
+
const getBearing = (origin, destination) => (Math.atan2(destination[1] - origin[1], destination[0] - origin[0]) - Math.PI / 2) % (Math.PI * 2);
|
|
134
|
+
|
|
135
|
+
const angleDiff = (angleA, angleB) => {
|
|
136
|
+
const twoPi = Math.PI * 2;
|
|
137
|
+
const diff = (angleA - (angleB > 0 ? angleB : angleB + twoPi) + Math.PI) % twoPi - Math.PI;
|
|
138
|
+
return diff < -Math.PI ? diff + twoPi : diff;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const drawStroke = (bristles, origin, destination, oldAngle, newAngle) => {
|
|
142
|
+
bristles.forEach(bristle => {
|
|
143
|
+
context.beginPath();
|
|
144
|
+
let bristleOrigin = rotatePoint(bristle.distance - strokeWidth / 2, oldAngle, origin);
|
|
145
|
+
let bristleDestination = rotatePoint(bristle.distance - strokeWidth / 2, newAngle, destination);
|
|
146
|
+
const controlPoint = rotatePoint(bristle.distance - strokeWidth / 2, newAngle, origin);
|
|
147
|
+
bristleDestination = rotatePoint(bristle.distance - strokeWidth / 2, newAngle, destination);
|
|
148
|
+
strokeBristle(bristleOrigin, bristleDestination, bristle, controlPoint);
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const strokeBristle = (origin, destination, bristle, controlPoint) => {
|
|
153
|
+
context.beginPath();
|
|
154
|
+
context.moveTo(origin[0], origin[1]);
|
|
155
|
+
context.strokeStyle = bristle.colour;
|
|
156
|
+
context.lineWidth = bristle.thickness;
|
|
157
|
+
context.lineCap = "round";
|
|
158
|
+
context.lineJoin = "round";
|
|
159
|
+
context.shadowColor = bristle.colour;
|
|
160
|
+
context.shadowBlur = bristle.thickness / 2;
|
|
161
|
+
context.quadraticCurveTo(controlPoint[0], controlPoint[1], destination[0], destination[1]);
|
|
162
|
+
context.lineTo(destination[0], destination[1]);
|
|
163
|
+
context.stroke();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const getTouchPoint = evt => {
|
|
167
|
+
if (!evt.currentTarget) {
|
|
168
|
+
return [0, 0];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const rect = evt.currentTarget.getBoundingClientRect();
|
|
172
|
+
const touch = evt.targetTouches[0];
|
|
173
|
+
console.log(rect, touch);
|
|
174
|
+
return [touch.clientX - rect.left, touch.clientY - rect.top];
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const BUTTON = 0b01;
|
|
178
|
+
|
|
179
|
+
const mouseButtonIsDown = buttons => (BUTTON & buttons) === BUTTON;
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
// Register event handlers
|
|
183
|
+
canvas.current.addEventListener("touchstart", touchStart, false);
|
|
184
|
+
canvas.current.addEventListener("touchend", touchEnd, false);
|
|
185
|
+
canvas.current.addEventListener("touchcancel", touchEnd, false);
|
|
186
|
+
canvas.current.addEventListener("touchmove", touchMove, false);
|
|
187
|
+
canvas.current.addEventListener("mousedown", mouseDown, false);
|
|
188
|
+
canvas.current.addEventListener("mouseup", endStroke, false);
|
|
189
|
+
canvas.current.addEventListener("mouseout", endStroke, false);
|
|
190
|
+
canvas.current.addEventListener("mouseenter", mouseEnter, false);
|
|
191
|
+
context = canvas.current.getContext("2d");
|
|
192
|
+
}, []);
|
|
193
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("canvas", {
|
|
194
|
+
id: "canvas",
|
|
195
|
+
ref: canvas,
|
|
196
|
+
height: "450",
|
|
197
|
+
width: "800",
|
|
198
|
+
style: {
|
|
199
|
+
backgroundColor: 'yellow'
|
|
200
|
+
}
|
|
201
|
+
}), /*#__PURE__*/React.createElement("input", {
|
|
202
|
+
type: "color",
|
|
203
|
+
id: "colourInput",
|
|
204
|
+
value: "#3d34a5"
|
|
205
|
+
}));
|
|
206
|
+
};
|
|
207
|
+
export default CanvasPain;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Slider from '@mui/material/Slider';
|
|
3
|
+
import Typography from '@mui/material/Typography';
|
|
4
|
+
import { combineDrawing } from '../service/image_tools';
|
|
5
|
+
import imageCompression from 'browser-image-compression';
|
|
6
|
+
import { ChromePicker } from 'react-color';
|
|
7
|
+
import Button from '@mui/material/Button';
|
|
8
|
+
import { OUTPUT_TYPES } from '.././constants';
|
|
9
|
+
const imageScaleMarks = [{
|
|
10
|
+
value: 1.5,
|
|
11
|
+
label: 'Full'
|
|
12
|
+
}, {
|
|
13
|
+
value: 2,
|
|
14
|
+
label: 'Medium'
|
|
15
|
+
}, {
|
|
16
|
+
value: 2.5,
|
|
17
|
+
label: 'Small'
|
|
18
|
+
}];
|
|
19
|
+
export const downloadImage = (blob, filename = `pitchcamp_${new Date().toString()}.jpg`) => {
|
|
20
|
+
//Used for testing
|
|
21
|
+
const a = document.createElement('a');
|
|
22
|
+
document.body.appendChild(a);
|
|
23
|
+
a.style = 'display: none';
|
|
24
|
+
const url = window.URL.createObjectURL(blob);
|
|
25
|
+
a.href = url;
|
|
26
|
+
a.download = filename;
|
|
27
|
+
a.click();
|
|
28
|
+
window.URL.revokeObjectURL(url);
|
|
29
|
+
};
|
|
30
|
+
export const toBase64 = async blob => {
|
|
31
|
+
try {
|
|
32
|
+
const reader = new FileReader();
|
|
33
|
+
reader.readAsDataURL(blob);
|
|
34
|
+
return await new Promise(resolve => {
|
|
35
|
+
reader.onloadend = () => {
|
|
36
|
+
resolve(reader.result);
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Error in toBase64 function of hte ImageCanvasDraw component: ', error);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function valueLabelFormat(value) {
|
|
45
|
+
return imageScaleMarks.findIndex(mark => mark.value === value) + 0.5;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const exportImage = async (thisFunction, saveableCanvas, outputFormat, imageCompressionOptions) => {
|
|
49
|
+
try {
|
|
50
|
+
const {
|
|
51
|
+
blob,
|
|
52
|
+
dataUri
|
|
53
|
+
} = combineDrawing(saveableCanvas);
|
|
54
|
+
imageCompression(blob, imageCompressionOptions).then(function (compressedBlob) {
|
|
55
|
+
if (OUTPUT_TYPES.BLOB === outputFormat.toLowerCase()) {
|
|
56
|
+
//Return the blob object
|
|
57
|
+
thisFunction(compressedBlob);
|
|
58
|
+
} else {
|
|
59
|
+
//Return the base64 url
|
|
60
|
+
toBase64(compressedBlob).then(base64Data => {
|
|
61
|
+
thisFunction(base64Data);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}).catch(function (error) {
|
|
65
|
+
console.log(error.message);
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.log('error in exportImage in ImageCanvasDraw comp: ', error);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const Controls = props => {
|
|
73
|
+
const {
|
|
74
|
+
state,
|
|
75
|
+
setState,
|
|
76
|
+
saveableCanvas,
|
|
77
|
+
onSave,
|
|
78
|
+
outputFormat,
|
|
79
|
+
showDownloadButton
|
|
80
|
+
} = props;
|
|
81
|
+
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
|
|
82
|
+
className: "flex flex-col"
|
|
83
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
84
|
+
className: "flex flex-row justify-between "
|
|
85
|
+
}, /*#__PURE__*/React.createElement(Button, {
|
|
86
|
+
color: "secondary",
|
|
87
|
+
onClick: () => {
|
|
88
|
+
saveableCanvas.current.eraseAll();
|
|
89
|
+
}
|
|
90
|
+
}, "Erase"), /*#__PURE__*/React.createElement(Button, {
|
|
91
|
+
color: "secondary",
|
|
92
|
+
onClick: () => {
|
|
93
|
+
saveableCanvas.current.undo();
|
|
94
|
+
}
|
|
95
|
+
}, "Undo"), /*#__PURE__*/React.createElement(Button, {
|
|
96
|
+
variant: "outlined",
|
|
97
|
+
onClick: () => {
|
|
98
|
+
exportImage(onSave, saveableCanvas, outputFormat, state.imageCompressionOptions);
|
|
99
|
+
}
|
|
100
|
+
}, "Save")), /*#__PURE__*/React.createElement("div", {
|
|
101
|
+
className: "m-auto my-12"
|
|
102
|
+
}, /*#__PURE__*/React.createElement(ChromePicker, {
|
|
103
|
+
className: "drop-shadow-none",
|
|
104
|
+
disableAlpha: true,
|
|
105
|
+
color: state.color,
|
|
106
|
+
onChange: (color, event) => {
|
|
107
|
+
setState(previousState => {
|
|
108
|
+
return { ...previousState,
|
|
109
|
+
color: color.hex
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
})), /*#__PURE__*/React.createElement("div", {
|
|
114
|
+
className: "my-6"
|
|
115
|
+
}, /*#__PURE__*/React.createElement(Typography, {
|
|
116
|
+
id: "image-scale"
|
|
117
|
+
}, "Pull String Length"), /*#__PURE__*/React.createElement("div", {
|
|
118
|
+
className: "mx-8"
|
|
119
|
+
}, /*#__PURE__*/React.createElement(Slider, {
|
|
120
|
+
className: "w-full",
|
|
121
|
+
defaultValue: 1.5,
|
|
122
|
+
"aria-labelledby": "image-scale",
|
|
123
|
+
valueLabelDisplay: "auto",
|
|
124
|
+
value: state.lazyRadius,
|
|
125
|
+
onChange: event => {
|
|
126
|
+
setState(previousState => {
|
|
127
|
+
return { ...previousState,
|
|
128
|
+
lazyRadius: Number(event.target.value)
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
step: 1,
|
|
133
|
+
min: 1,
|
|
134
|
+
max: 30
|
|
135
|
+
}))), /*#__PURE__*/React.createElement("div", {
|
|
136
|
+
className: "my-6"
|
|
137
|
+
}, /*#__PURE__*/React.createElement(Typography, {
|
|
138
|
+
id: "image-scale"
|
|
139
|
+
}, "Brush Size"), /*#__PURE__*/React.createElement("div", {
|
|
140
|
+
className: "mx-8"
|
|
141
|
+
}, /*#__PURE__*/React.createElement(Slider, {
|
|
142
|
+
className: "w-full",
|
|
143
|
+
defaultValue: 1.5,
|
|
144
|
+
"aria-labelledby": "image-scale",
|
|
145
|
+
valueLabelDisplay: "auto",
|
|
146
|
+
value: state.brushRadius,
|
|
147
|
+
onChange: event => {
|
|
148
|
+
setState(previousState => {
|
|
149
|
+
return { ...previousState,
|
|
150
|
+
brushRadius: Number(event.target.value)
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
step: 1,
|
|
155
|
+
min: 1,
|
|
156
|
+
max: 10
|
|
157
|
+
}))), /*#__PURE__*/React.createElement("div", {
|
|
158
|
+
className: "my-6"
|
|
159
|
+
}, /*#__PURE__*/React.createElement(Typography, {
|
|
160
|
+
id: "image-scale"
|
|
161
|
+
}, "Image Size"), /*#__PURE__*/React.createElement("div", {
|
|
162
|
+
className: "mx-8"
|
|
163
|
+
}, /*#__PURE__*/React.createElement(Slider, {
|
|
164
|
+
className: "w-full",
|
|
165
|
+
defaultValue: 1.5,
|
|
166
|
+
valueLabelFormat: valueLabelFormat,
|
|
167
|
+
"aria-labelledby": "image-scale",
|
|
168
|
+
step: null // valueLabelDisplay="auto"
|
|
169
|
+
,
|
|
170
|
+
value: state.imageScale,
|
|
171
|
+
onChange: event => {
|
|
172
|
+
setState(previousState => {
|
|
173
|
+
return { ...previousState,
|
|
174
|
+
imageScale: Number(event.target.value)
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
marks: imageScaleMarks,
|
|
179
|
+
min: 1.5,
|
|
180
|
+
max: 2.5
|
|
181
|
+
})))), /*#__PURE__*/React.createElement("div", {
|
|
182
|
+
className: "flex flex-row justify-center mt-12"
|
|
183
|
+
}, showDownloadButton && /*#__PURE__*/React.createElement(Button, {
|
|
184
|
+
variant: "outlined",
|
|
185
|
+
onClick: () => {
|
|
186
|
+
exportImage(downloadImage, saveableCanvas, OUTPUT_TYPES.BLOB, state.imageCompressionOptions);
|
|
187
|
+
}
|
|
188
|
+
}, "Download")));
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export default Controls;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './types';
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import CanvasDraw from "react-canvas-draw";
|
|
3
|
+
import Controls from './components/Controls';
|
|
4
|
+
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
|
5
|
+
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
|
6
|
+
import Button from '@mui/material/Button';
|
|
7
|
+
import { getStorage, setStorage } from './service/local_storage';
|
|
8
|
+
import PropTypes from 'prop-types';
|
|
9
|
+
/**
|
|
10
|
+
* Displays an image that can is used to draw on to generate maps with highlighted routes.
|
|
11
|
+
*
|
|
12
|
+
* - Requires the ```b64DataUrl``` props to contain a dataUrl string.
|
|
13
|
+
* - Returns the ```data``` inside the onSave(data) function passed to the component.
|
|
14
|
+
*
|
|
15
|
+
* ### Importing the component
|
|
16
|
+
|
|
17
|
+
```//PitchCamp shared component
|
|
18
|
+
import {ImageCanvasDraw} from 'pcm-shared-components';
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
*
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const ImageCanvasDraw = props => {
|
|
25
|
+
const {
|
|
26
|
+
onSave,
|
|
27
|
+
outputFormat,
|
|
28
|
+
showDownloadButton,
|
|
29
|
+
b64DataUrl
|
|
30
|
+
} = props;
|
|
31
|
+
const [state, setState] = useState({
|
|
32
|
+
drawerOpen: true,
|
|
33
|
+
color: "darkorange",
|
|
34
|
+
width: 0,
|
|
35
|
+
height: 0,
|
|
36
|
+
brushRadius: 5,
|
|
37
|
+
lazyRadius: 2,
|
|
38
|
+
imageScale: 1.5,
|
|
39
|
+
imageQuality: 0.5,
|
|
40
|
+
b64DataUrl: b64DataUrl,
|
|
41
|
+
imageCompressionOptions: {
|
|
42
|
+
maxSizeMB: 0.4,
|
|
43
|
+
maxWidthOrHeight: 1920,
|
|
44
|
+
useWebWorker: true,
|
|
45
|
+
maxIteration: 10
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const saveableCanvas = useRef();
|
|
49
|
+
|
|
50
|
+
const setImageSize = imageUrl => {
|
|
51
|
+
try {
|
|
52
|
+
const img = new Image();
|
|
53
|
+
|
|
54
|
+
img.onload = () => {
|
|
55
|
+
//Scale the image down
|
|
56
|
+
const new_width = Number(img.width) / Number(state.imageScale);
|
|
57
|
+
const new_height = Number(img.height) / Number(state.imageScale); // Create a temporary canvas to draw the downscaled image on.
|
|
58
|
+
|
|
59
|
+
let canvas = document.createElement("canvas");
|
|
60
|
+
canvas.width = new_width;
|
|
61
|
+
canvas.height = new_height; // Draw the downscaled image on the canvas and return the new data URL.
|
|
62
|
+
|
|
63
|
+
let ctx = canvas.getContext("2d");
|
|
64
|
+
ctx.drawImage(img, 0, 0, new_width, new_height);
|
|
65
|
+
const newDataUrl = canvas.toDataURL("image/jpeg", state.imageQuality);
|
|
66
|
+
setState(previousState => {
|
|
67
|
+
return { ...previousState,
|
|
68
|
+
...getStorage(),
|
|
69
|
+
height: new_height,
|
|
70
|
+
width: new_width,
|
|
71
|
+
b64DataUrl: newDataUrl
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
img.src = imageUrl;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('error in setImageSize', error);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
//Get the scaled down image and width, height
|
|
84
|
+
setImageSize(state.b64DataUrl);
|
|
85
|
+
}, []);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (saveableCanvas && saveableCanvas.current) {
|
|
88
|
+
//Refresh the canvas each time a props changes
|
|
89
|
+
saveableCanvas.current.drawImage();
|
|
90
|
+
setStorage(state);
|
|
91
|
+
}
|
|
92
|
+
}, [state]);
|
|
93
|
+
|
|
94
|
+
const DrawerButtonOpenClose = ({
|
|
95
|
+
children
|
|
96
|
+
}) => /*#__PURE__*/React.createElement(Button, {
|
|
97
|
+
style: {
|
|
98
|
+
minWidth: 6,
|
|
99
|
+
padding: 0,
|
|
100
|
+
margin: 0
|
|
101
|
+
},
|
|
102
|
+
color: "secondary",
|
|
103
|
+
onClick: () => {
|
|
104
|
+
setState(previousState => {
|
|
105
|
+
return { ...previousState,
|
|
106
|
+
drawerOpen: !previousState.drawerOpen
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}, children);
|
|
111
|
+
|
|
112
|
+
if (state.width && state.width !== 0) {
|
|
113
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
114
|
+
className: "flex flex-row overflow-hidden"
|
|
115
|
+
}, state.drawerOpen ? /*#__PURE__*/React.createElement("div", {
|
|
116
|
+
className: "flex flex-row",
|
|
117
|
+
style: {
|
|
118
|
+
width: 'auto'
|
|
119
|
+
}
|
|
120
|
+
}, /*#__PURE__*/React.createElement(Controls, {
|
|
121
|
+
state: state,
|
|
122
|
+
setState: setState,
|
|
123
|
+
saveableCanvas: saveableCanvas,
|
|
124
|
+
onSave: onSave,
|
|
125
|
+
outputFormat: outputFormat,
|
|
126
|
+
showDownloadButton: showDownloadButton
|
|
127
|
+
}), /*#__PURE__*/React.createElement(DrawerButtonOpenClose, null, /*#__PURE__*/React.createElement(ChevronLeftIcon, null))) : /*#__PURE__*/React.createElement("div", {
|
|
128
|
+
className: "flex flex-row justify-end"
|
|
129
|
+
}, /*#__PURE__*/React.createElement(DrawerButtonOpenClose, null, /*#__PURE__*/React.createElement(ChevronRightIcon, null))), state.width !== 0 && /*#__PURE__*/React.createElement("div", {
|
|
130
|
+
className: "overflow-scroll"
|
|
131
|
+
}, /*#__PURE__*/React.createElement(CanvasDraw, {
|
|
132
|
+
className: "",
|
|
133
|
+
ref: saveableCanvas,
|
|
134
|
+
onChange: evt => console.log("onChange", evt) // enablePanAndZoom //Don't enable pan and zoom or else it gets too complicated when saving...
|
|
135
|
+
// zoomExtents={{ min: 0.33, max: 10 }}
|
|
136
|
+
,
|
|
137
|
+
clampLinesToDocument: true,
|
|
138
|
+
hideInterface: false,
|
|
139
|
+
hideGrid: false,
|
|
140
|
+
backgroundColor: 'rgba(34,34,34,0.2)',
|
|
141
|
+
brushColor: state.color,
|
|
142
|
+
brushRadius: state.brushRadius,
|
|
143
|
+
lazyRadius: state.lazyRadius,
|
|
144
|
+
canvasWidth: Number(state.width) / Number(state.imageScale),
|
|
145
|
+
canvasHeight: Number(state.height) / Number(state.imageScale),
|
|
146
|
+
catenaryColor: 'black',
|
|
147
|
+
gridColor: "#ccc",
|
|
148
|
+
imgSrc: state.b64DataUrl
|
|
149
|
+
})));
|
|
150
|
+
} else {
|
|
151
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, "loading...");
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export default ImageCanvasDraw;
|
|
156
|
+
ImageCanvasDraw.defaultProps = {
|
|
157
|
+
onSave: undefined,
|
|
158
|
+
outputFormat: 'blob',
|
|
159
|
+
showDownloadButton: undefined,
|
|
160
|
+
b64DataUrl: undefined
|
|
161
|
+
};
|
|
162
|
+
ImageCanvasDraw.propTypes = {
|
|
163
|
+
/** On save function to execute returns either base64 or blob format based on the output format specified
|
|
164
|
+
*
|
|
165
|
+
* - onSave(data) data is either a **``blob``** or a **``base64``**
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
onSave = (data) =>{
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
*/
|
|
173
|
+
onSave: PropTypes.func,
|
|
174
|
+
|
|
175
|
+
/** Specified the type of data the onSave function will return */
|
|
176
|
+
outputFormat: PropTypes.oneOf(['blob', 'base64']),
|
|
177
|
+
|
|
178
|
+
/** If specified then we show the download button which is mainly used for testing to display what the canvas is exporting */
|
|
179
|
+
showDownloadButton: PropTypes.bool,
|
|
180
|
+
|
|
181
|
+
/** This is the image in a base 64 url format to use as the base image to draw on */
|
|
182
|
+
b64DataUrl: PropTypes.string.isRequired
|
|
183
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const combineDrawing = canvasRef => {
|
|
2
|
+
try {
|
|
3
|
+
const width = canvasRef.current.props.canvasWidth;
|
|
4
|
+
const height = canvasRef.current.props.canvasHeight;
|
|
5
|
+
const background = canvasRef.current.canvasContainer.children[0]; // const test = canvasRef.current.canvasContainer.children[0];
|
|
6
|
+
|
|
7
|
+
const drawing = canvasRef.current.canvasContainer.children[1];
|
|
8
|
+
const canvas = document.createElement('canvas');
|
|
9
|
+
canvas.width = width;
|
|
10
|
+
canvas.height = height; // composite now
|
|
11
|
+
|
|
12
|
+
canvas.getContext('2d').drawImage(background, 0, 0);
|
|
13
|
+
canvas.getContext('2d').globalAlpha = 1.0;
|
|
14
|
+
canvas.getContext('2d').drawImage(drawing, 0, 0); // canvas.getContext('2d').drawImage(test, 0, 0);
|
|
15
|
+
|
|
16
|
+
const dataUri = canvas.toDataURL('image/jpeg', 1.0);
|
|
17
|
+
const data = dataUri.split(',')[1];
|
|
18
|
+
const mimeType = dataUri.split(';')[0].slice(5);
|
|
19
|
+
const bytes = window.atob(data);
|
|
20
|
+
const buf = new ArrayBuffer(bytes.length);
|
|
21
|
+
const arr = new Uint8Array(buf);
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
24
|
+
arr[i] = bytes.charCodeAt(i);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const blob = new Blob([arr], {
|
|
28
|
+
type: mimeType
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
blob: blob,
|
|
32
|
+
dataUri: dataUri
|
|
33
|
+
};
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Error in combineDrawing: ', error);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const STORAGE_NAME = 'PC_IMAGE_DRAW_STATES';
|
|
2
|
+
export const getStorage = () => {
|
|
3
|
+
try {
|
|
4
|
+
return JSON.parse(localStorage.getItem(STORAGE_NAME));
|
|
5
|
+
} catch (error) {
|
|
6
|
+
console.error('Error in getting storage for IMAGE Draw:', error);
|
|
7
|
+
setStorage('');
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
export const setStorage = value => {
|
|
11
|
+
try {
|
|
12
|
+
//Need to filter out the b64DataUrl object from the state or else it might slow down the browser...
|
|
13
|
+
let new_value = Object.fromEntries(Object.entries(value).filter(([key, value]) => key !== 'b64DataUrl'));
|
|
14
|
+
console.log('debug new_value', new_value);
|
|
15
|
+
localStorage.setItem(STORAGE_NAME, JSON.stringify(new_value));
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error('Error in setting storage for IMAGE Draw:', error);
|
|
18
|
+
setStorage('');
|
|
19
|
+
}
|
|
20
|
+
};
|