]> git.localhorst.tv Git - alttp.git/blob - resources/js/components/tracker/Canvas.js
1258776809c000152abb72187398cf62e46f6b3b
[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                         const bounds = bbox.node().getBoundingClientRect();
71                         start.x = e.x;
72                         start.y = e.y;
73                         setDragging({
74                                 icon: this.dataset['icon'],
75                                 x: (e.x - bounds.x) / bounds.width,
76                                 y: (e.y - bounds.y) / bounds.height,
77                         });
78                 };
79                 const onDrag = function (e) {
80                         const bounds = bbox.node().getBoundingClientRect();
81                         setDragging({
82                                 icon: this.dataset['icon'],
83                                 x: (e.x - bounds.x) / bounds.width,
84                                 y: (e.y - bounds.y) / bounds.height,
85                         });
86                 };
87                 const onEnd = function (e) {
88                         const bounds = bbox.node().getBoundingClientRect();
89                         setDragging(null);
90                         const distance = Math.max(Math.abs(e.x - start.x), Math.abs(e.y - start.y));
91                         if (distance > 5) {
92                                 addPin({
93                                         icon: this.dataset['icon'],
94                                         x: (e.x - bounds.x) / bounds.width,
95                                         y: (e.y - bounds.y) / bounds.height,
96                                 });
97                                 if (this.classList.contains('map-pin')) {
98                                         let id = 0;
99                                         this.classList.forEach(name => {
100                                                 if (name.startsWith('map-pin-')) {
101                                                         id = parseInt(name.substr(8), 10);
102                                                 }
103                                         });
104                                         removePin({ id });
105                                 }
106                         }
107                 };
108                 const selection = canvas.selectAll('.toggle-icon');
109                 const draggable = drag()
110                         .container(bbox)
111                         .clickDistance(5)
112                         .on('start', onStart)
113                         .on('drag', onDrag)
114                         .on('end', onEnd);
115                 selection.call(draggable);
116                 return () => {
117                         selection.on('.drag', null);
118                 };
119         }, [pins, removePin]);
120
121         return <svg
122                 xmlns="http://www.w3.org/2000/svg"
123                 className="canvas"
124                 width={layout.width}
125                 height={layout.height}
126                 viewBox={`0 0 ${layout.width} ${layout.height}`}
127                 onContextMenu={(e) => {
128                         e.preventDefault();
129                         e.stopPropagation();
130                 }}
131         >
132                 <rect
133                         className="background"
134                         fill="transparent"
135                         x="0" y="0"
136                         width={layout.width}
137                         height={layout.height}
138                 />
139                 <g className="items" transform={layout.itemsTransform}>
140                         <Items />
141                 </g>
142                 <g className="dungeons" transform={layout.dungeonsTransform}>
143                         <Dungeons columns={layout.dungeonColumns} />
144                 </g>
145                 <g className="tracker-map" transform={layout.mapTransform}>
146                         <Map />
147                 </g>
148                 <g className="pins">
149                         {pins.map(pin =>
150                                 <ToggleIcon
151                                         key={pin.id}
152                                         className={`map-pin map-pin-${pin.id}`}
153                                         controller={ToggleIcon.pinController(pin, removePin)}
154                                         icons={[pin.icon]}
155                                         svg
156                                         transform={
157                                                 `translate(${pin.x * layout.width} ${pin.y * layout.height}) scale(3)`
158                                         }
159                                 />
160                         )}
161                 </g>
162                 {dragging ?
163                         <g transform={
164                                 `translate(${dragging.x * layout.width} ${dragging.y * layout.height}) scale(4)`
165                         }>
166                                 <ZeldaIcon name={dragging.icon} svg />
167                         </g>
168                 : null}
169         </svg>;
170 };
171
172 export default Canvas;