DrawCanvas.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. import React, { useState, useEffect } from 'react';
  2. import Paper, { Path } from 'paper';
  3. import { Modal, ModalBody, ModalFooter, Button, Form, FormGroup, Label, Input, Fade, ModalHeader, ButtonGroup } from 'reactstrap';
  4. import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
  5. import { faReply, faTrashAlt, faEraser, faSave, faEdit, faPen } from '@fortawesome/free-solid-svg-icons';
  6. import { remove, update } from 'ramda';
  7. const COLORS = [
  8. '#FFC6BC',
  9. '#FEDEA2',
  10. '#FFF9B8',
  11. '#D3DBB2',
  12. '#A7D3D2',
  13. '#EFE3F3',
  14. '#C4D0F5',
  15. '#FFC6BC',
  16. ];
  17. const mkBgColor = (color: string) => {
  18. const _color = new Paper.Color(color);
  19. _color.alpha = 0.5;
  20. return _color;
  21. };
  22. const mkPath = (color: string, point?: paper.Point) => {
  23. let style = {
  24. strokeColor: color,
  25. strokeWidth: 5,
  26. fillColor: mkBgColor(color)
  27. }
  28. let path = new Path(style);
  29. if (point) {
  30. path.add(point);
  31. }
  32. return path;
  33. }
  34. const mkText = (color: string) => {
  35. const _text = new Paper.PointText({
  36. fillColor: color,
  37. strokeColor: '#4b505a',
  38. strokeWidth: 1,
  39. justification: 'center',
  40. fontWeight: 'bold',
  41. fontSize: '16px',
  42. });
  43. return _text;
  44. }
  45. export type CanvasData = {
  46. form: FormData,
  47. path: paper.Path,
  48. text: paper.PointText
  49. };
  50. type CanvasState = {
  51. data: CanvasData[],
  52. current: number,
  53. openModal: boolean,
  54. drawTool?: paper.Tool,
  55. editTool?: paper.Tool,
  56. pathSelected?: number,
  57. _path?: CanvasData,
  58. editMode: boolean
  59. };
  60. type CanvasProps = {
  61. saveData: (data: CanvasData[]) => void
  62. };
  63. type FormData = {
  64. name: string,
  65. soundExample: string,
  66. associations: string,
  67. };
  68. const PathQuestions: React.FunctionComponent<{
  69. saveData: (data: FormData, editing?: boolean) => void,
  70. cancel: () => void,
  71. initialData?: {
  72. name: string,
  73. soundExample: string,
  74. associations: string,
  75. }
  76. }> = ({ saveData, cancel, initialData }) => {
  77. const [ data, setData ] = useState<FormData>({
  78. name: '',
  79. soundExample: '',
  80. associations: '',
  81. });
  82. useEffect(() => {
  83. if (initialData) {
  84. setData(initialData);
  85. }
  86. }, [ initialData ]);
  87. const handleChange = (key: string) => (e: React.ChangeEvent<HTMLSelectElement|HTMLInputElement>) => {
  88. setData({
  89. ...data,
  90. [key]: e.target.value
  91. });
  92. }
  93. return (
  94. <>
  95. <ModalBody>
  96. <Form>
  97. <FormGroup>
  98. <Label for="path-name">Please name the accent spoken in this area</Label>
  99. <Input type="text" id="path-name" value={data.name} onChange={handleChange('name')} />
  100. </FormGroup>
  101. <FormGroup>
  102. <Label for="path-sound-example">Please provide an example of how this accent sounds</Label>
  103. <Input type="text" id="path-sound-example" value={data.soundExample} onChange={handleChange('soundExample')} />
  104. </FormGroup>
  105. <FormGroup>
  106. <Label for="path-associations">Please provide any associations (ideas, judgements, opinions, etc.) that come to mind when you encounter this accent/a speaker with this accent</Label>
  107. <Input type="text" id="path-associations" value={data.associations} onChange={handleChange('associations')} />
  108. </FormGroup>
  109. </Form>
  110. </ModalBody>
  111. <ModalFooter>
  112. <Button onClick={() => { cancel() }}>Cancel</Button>
  113. <Button color="primary" onClick={() => { saveData(data, !!initialData) }}>Save</Button>
  114. </ModalFooter>
  115. </>
  116. )
  117. };
  118. class Canvas extends React.Component<CanvasProps, CanvasState> {
  119. constructor(props: any){
  120. super(props);
  121. this.state = {
  122. data: [],
  123. current: 0,
  124. openModal: false,
  125. drawTool: undefined,
  126. editTool: undefined,
  127. pathSelected: undefined,
  128. _path: undefined,
  129. editMode: false
  130. }
  131. }
  132. addPoint = (event: any) => {
  133. const { current, _path } = this.state;
  134. if (_path) {
  135. _path.path.add(event.point)
  136. } else {
  137. this.setState({
  138. _path: {
  139. form: {
  140. name: '',
  141. associations: '',
  142. soundExample: '',
  143. },
  144. path: mkPath(COLORS[current], event.point),
  145. text: mkText(COLORS[current])
  146. }
  147. })
  148. }
  149. }
  150. closePath = (event: any) => {
  151. const { _path } = this.state;
  152. if (_path) {
  153. _path.path.add(_path.path.firstSegment);
  154. _path.path.closed = true;
  155. _path.path.simplify();
  156. }
  157. }
  158. getPathId = (name: string): string => {
  159. return name.slice(-1);
  160. }
  161. addPathLabel = (pathName: string, path: paper.Path, update?: number) => {
  162. const { _path } = this.state;
  163. if (update !== undefined) {
  164. const { data } = this.state;
  165. data[update].text.content = pathName;
  166. }
  167. if ( _path) {
  168. _path.text.content = pathName;
  169. _path.text.position = path.bounds.center;
  170. }
  171. }
  172. toggleModal = (value: boolean) => {
  173. this.setState({
  174. openModal: value
  175. })
  176. }
  177. saveData = (formData: FormData, editing?: boolean) => {
  178. const { data, current, _path } = this.state;
  179. if (editing) {
  180. const { pathSelected } = this.state;
  181. this.addPathLabel(formData.name, data[pathSelected!].path, pathSelected);
  182. this.setState({
  183. openModal: false,
  184. data: update(pathSelected!, {
  185. ...data[pathSelected!],
  186. form: formData
  187. }, data)
  188. })
  189. }
  190. if (_path) {
  191. this.addPathLabel(formData.name, _path.path);
  192. this.setState({
  193. openModal: false,
  194. current: current < 7 ? current + 1 : current,
  195. data: [
  196. ...data,
  197. {
  198. ..._path,
  199. form: formData,
  200. }
  201. ],
  202. _path: undefined
  203. })
  204. }
  205. }
  206. cancel = () => {
  207. const { _path } = this.state;
  208. _path?.path.remove();
  209. _path?.text.remove();
  210. this.setState({
  211. _path: undefined,
  212. openModal: false
  213. })
  214. }
  215. removeFromCanvas = (data: CanvasData) => {
  216. data.path.remove();
  217. data.text.remove();
  218. }
  219. clearCanvas = () => {
  220. this.setState(({ data }) => {
  221. data.forEach(this.removeFromCanvas)
  222. return {
  223. current: 0,
  224. data: []
  225. }
  226. })
  227. }
  228. mkDrawTool = () => {
  229. let Tool = new Paper.Tool();
  230. Tool.onMouseDown = this.addPoint;
  231. Tool.onMouseDrag = this.addPoint;
  232. Tool.onMouseUp = (event: any) => {
  233. this.closePath(event);
  234. this.toggleModal(true);
  235. if (this.state.current === 7) {
  236. Tool.remove();
  237. }
  238. }
  239. Tool.activate();
  240. return Tool;
  241. }
  242. getDataByPath = (item: paper.Item) => {
  243. return this.state.data.findIndex(data => data.path.id === item.id)
  244. }
  245. mkEditTool = () => {
  246. const EditTool = new Paper.Tool();
  247. EditTool.onMouseDown = ({ item }: paper.ToolEvent) => {
  248. if (item) {
  249. if (item.selected) {
  250. item.selected = false;
  251. this.setState({
  252. pathSelected: undefined,
  253. })
  254. } else {
  255. let itemId = this.getDataByPath(item);
  256. item.bringToFront()
  257. item.selected = true;
  258. this.setState({
  259. pathSelected: itemId,
  260. })
  261. }
  262. }
  263. }
  264. return EditTool;
  265. }
  266. removePath = () => {
  267. const { data, pathSelected } = this.state;
  268. if (pathSelected !== undefined) {
  269. this.removeFromCanvas(data[pathSelected]);
  270. const newData = remove(pathSelected, 1, data);
  271. if (!newData.length) {
  272. this.toggleEditMode(false);
  273. }
  274. this.setState({
  275. pathSelected: undefined,
  276. data: newData,
  277. current: newData.length,
  278. })
  279. }
  280. }
  281. toggleEditMode = (showEdit: boolean) => {
  282. const { editTool, drawTool, data, pathSelected } = this.state
  283. if (showEdit) {
  284. editTool?.activate()
  285. this.setState({
  286. editMode: showEdit,
  287. })
  288. } else {
  289. drawTool?.activate();
  290. if (pathSelected !== undefined) {
  291. data[pathSelected].path.selected = false;
  292. }
  293. this.setState({
  294. editMode: showEdit,
  295. pathSelected: undefined
  296. })
  297. }
  298. }
  299. componentDidMount = () => {
  300. Paper.setup('magic-canvas');
  301. this.setState({
  302. drawTool: this.mkDrawTool(),
  303. editTool: this.mkEditTool(),
  304. })
  305. }
  306. render = () => {
  307. const { pathSelected, data } = this.state;
  308. return (
  309. <>
  310. <div id="vom-canvas-tools">
  311. <ButtonGroup className="mb-3 ">
  312. <Button onClick={() => this.clearCanvas()} color="danger">
  313. <FontAwesomeIcon icon={faTrashAlt} />
  314. </Button>
  315. {
  316. this.state.editMode ? (
  317. <Button onClick={() => this.toggleEditMode(false)} color="warning">
  318. <FontAwesomeIcon icon={faReply} />
  319. </Button>
  320. ) : null
  321. }
  322. {
  323. this.state.data.length && !this.state.editMode ? (
  324. <Button color="success" onClick={() => this.toggleEditMode(true)} disabled={this.state.current === 0}>
  325. <FontAwesomeIcon icon={faPen} />
  326. </Button>
  327. ) : null
  328. }
  329. </ButtonGroup>
  330. {
  331. this.state.pathSelected !== undefined ? (
  332. <ButtonGroup className="mb-3">
  333. <Button onClick={() => this.removePath()}>
  334. <FontAwesomeIcon icon={faEraser} />
  335. </Button>
  336. <Button onClick={() => this.toggleModal(true)}>
  337. <FontAwesomeIcon icon={faEdit} />
  338. </Button>
  339. <Button onClick={() => this.toggleEditMode(false)}>
  340. <FontAwesomeIcon icon={faSave} />
  341. </Button>
  342. </ButtonGroup>
  343. ) : null
  344. }
  345. </div>
  346. <canvas id="magic-canvas" className={`mb-4 border rounded ${this.state.editMode && 'border-success'}`}/>
  347. <Button
  348. color="primary"
  349. onClick={() => this.props.saveData(this.state.data)}
  350. disabled={this.state.current === 0}
  351. >Finish</Button>
  352. <Modal isOpen={this.state.openModal}>
  353. <PathQuestions
  354. saveData={this.saveData}
  355. cancel={this.cancel}
  356. initialData={pathSelected !== undefined ? data[pathSelected].form : undefined}
  357. />
  358. </Modal>
  359. <Modal isOpen={this.state.current === 7}>
  360. <ModalBody>Maximun drawings reached!</ModalBody>
  361. <ModalFooter>
  362. <Button onClick={() => {}}>Edit drawings</Button>
  363. <Button onClick={() => this.props.saveData(this.state.data)}>Next step</Button>
  364. </ModalFooter>
  365. </Modal>
  366. </>
  367. )
  368. }
  369. }
  370. export const DrawCanvas: React.FunctionComponent<{
  371. saveData: (data: CanvasData[]) => void
  372. }> = ({ saveData }) => {
  373. const [showInstructions, setShowInstructions] = useState(true)
  374. return (
  375. <Fade tag="section" id="vom-drawing-canvas">
  376. <Modal isOpen={showInstructions}>
  377. <ModalHeader>
  378. Instructions
  379. </ModalHeader>
  380. <ModalBody>
  381. <ol>
  382. <li>Please draw a shape around an area where you think a certain accent or dialect is spoken in Merseyside.</li>
  383. <li>Please name the accent, give an example of how it sounds, write down any associations (ideas, judgements, opinions etc.) that come to mind when you encounter this accent/a speaker with this accent.</li>
  384. <li>You are free to edit your map using the edit function.</li>
  385. <li>Repeat stages 1 and 2 until you have finished your map. Once you have finished, click ‘finished’. It will not possible to make changes after your map has been submitted.</li>
  386. </ol>
  387. </ModalBody>
  388. <ModalFooter>
  389. <Button outline color="primary" onClick={() => setShowInstructions(false)}>Start</Button>
  390. </ModalFooter>
  391. </Modal>
  392. <Canvas
  393. saveData={saveData}
  394. />
  395. </Fade>
  396. )
  397. };
  398. export default DrawCanvas;