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