]> git.localhorst.tv Git - alttp.git/blob - resources/js/components/tracker/Canvas.js
89339abb346eedcebbb870d7e3044bac2705bf29
[alttp.git] / resources / js / components / tracker / Canvas.js
1 import { drag } from 'd3-drag';
2 import { select } from 'd3-selection';
3 import React from 'react';
4
5 import Dungeons from './Dungeons';
6 import Items from './Items';
7 import Map from './Map';
8 import ToggleIcon from './ToggleIcon';
9 import ZeldaIcon from '../common/ZeldaIcon';
10 import { shouldShowDungeonItem } from '../../helpers/tracker';
11 import { useTracker } from '../../hooks/tracker';
12
13 const LAYOUTS = {
14         defaultHorizontal: {
15                 width: 100,
16                 height: 60,
17                 itemsTransform: 'translate(1 1) scale(22)',
18                 dungeonColumns: 4,
19                 dungeonsTransform: 'translate(1 39) scale(98)',
20                 mapTransform: 'translate(24 0) scale(76)',
21         },
22         defaultVertical: {
23                 width: 100,
24                 height: 100,
25                 itemsTransform: 'translate(10 1) scale(30)',
26                 dungeonColumns: 2,
27                 dungeonsTransform: 'translate(1 51) scale(48)',
28                 mapTransform: 'translate(50 0) scale(50)',
29         },
30         manyDungeonItemsVertical: {
31                 width: 80,
32                 height: 100,
33                 itemsTransform: 'translate(1 1) scale(27)',
34                 dungeonColumns: 1,
35                 dungeonsTransform: 'translate(1 48) scale(24)',
36                 mapTransform: 'translate(30 0) scale(50)',
37         },
38 };
39
40 const Canvas = () => {
41         const [dragging, setDragging] = React.useState(null);
42         const { addPin, config, pins, removePin } = useTracker();
43
44         const layout = React.useMemo(() => {
45                 if (config.mapLayout === 'vertical') {
46                         let count = 0;
47                         if (shouldShowDungeonItem(config, 'Map')) {
48                                 ++count;
49                         }
50                         if (shouldShowDungeonItem(config, 'Compass')) {
51                                 ++count;
52                         }
53                         if (shouldShowDungeonItem(config, 'Small')) {
54                                 ++count;
55                         }
56                         if (shouldShowDungeonItem(config, 'Big')) {
57                                 ++count;
58                         }
59                         return count > 2 ? LAYOUTS.manyDungeonItemsVertical : LAYOUTS.defaultVertical;
60                 } else {
61                         return LAYOUTS.defaultHorizontal;
62                 }
63         }, [config]);
64
65         React.useEffect(() => {
66                 const canvas = select('.canvas');
67                 const bbox = canvas.select('.background');
68                 const start = { x: 0, y: 0 };
69                 const onStart = function (e) {
70                         start.x = e.x;
71                         start.y = e.y;
72                 };
73                 const onDrag = function (e) {
74                         const bounds = bbox.node().getBoundingClientRect();
75                         const distance = Math.max(Math.abs(e.x - start.x), Math.abs(e.y - start.y));
76                         if (distance > 5) {
77                                 setDragging({
78                                         icon: this.dataset['icon'],
79                                         x: (e.x - bounds.x) / bounds.width,
80                                         y: (e.y - bounds.y) / bounds.height,
81                                 });
82                         } else {
83                                 setDragging(null);
84                         }
85                 };
86                 const onEnd = function (e) {
87                         const bounds = bbox.node().getBoundingClientRect();
88                         setDragging(null);
89                         const distance = Math.max(Math.abs(e.x - start.x), Math.abs(e.y - start.y));
90                         if (distance > 5) {
91                                 addPin({
92                                         icon: this.dataset['icon'],
93                                         x: (e.x - bounds.x) / bounds.width,
94                                         y: (e.y - bounds.y) / bounds.height,
95                                 });
96                                 if (this.classList.contains('map-pin')) {
97                                         let id = 0;
98                                         this.classList.forEach(name => {
99                                                 if (name.startsWith('map-pin-')) {
100                                                         id = parseInt(name.substr(8), 10);
101                                                 }
102                                         });
103                                         removePin({ id });
104                                 }
105                         }
106                 };
107                 const selection = canvas.selectAll('.toggle-icon');
108                 const draggable = drag()
109                         .container(bbox)
110                         .clickDistance(5)
111                         .on('start', onStart)
112                         .on('drag', onDrag)
113                         .on('end', onEnd);
114                 selection.call(draggable);
115                 return () => {
116                         selection.on('.drag', null);
117                 };
118         }, [pins, removePin]);
119
120         return <svg
121                 xmlns="http://www.w3.org/2000/svg"
122                 className="canvas"
123                 width={layout.width}
124                 height={layout.height}
125                 viewBox={`0 0 ${layout.width} ${layout.height}`}
126                 onContextMenu={(e) => {
127                         e.preventDefault();
128                         e.stopPropagation();
129                 }}
130         >
131                 <rect
132                         className="background"
133                         fill="transparent"
134                         x="0" y="0"
135                         width={layout.width}
136                         height={layout.height}
137                 />
138                 <g className="items" transform={layout.itemsTransform}>
139                         <Items />
140                 </g>
141                 <g className="dungeons" transform={layout.dungeonsTransform}>
142                         <Dungeons columns={layout.dungeonColumns} />
143                 </g>
144                 <g className="tracker-map" transform={layout.mapTransform}>
145                         <Map />
146                 </g>
147                 <g className="pins">
148                         {pins.map(pin =>
149                                 <ToggleIcon
150                                         key={pin.id}
151                                         className={`map-pin map-pin-${pin.id}`}
152                                         controller={ToggleIcon.pinController(pin, removePin)}
153                                         icons={[pin.icon]}
154                                         svg
155                                         transform={
156                                                 `translate(${pin.x * layout.width} ${pin.y * layout.height}) scale(3)`
157                                         }
158                                 />
159                         )}
160                 </g>
161                 {dragging ?
162                         <g transform={
163                                 `translate(${dragging.x * layout.width} ${dragging.y * layout.height}) scale(4)`
164                         }>
165                                 <ZeldaIcon name={dragging.icon} svg />
166                         </g>
167                 : null}
168         </svg>;
169 };
170
171 export default Canvas;