From e0925d5b97ab0804222195eb4231c63b33703942 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Fri, 5 Apr 2024 17:17:38 +0200 Subject: [PATCH] draggable tracker icons --- package-lock.json | 48 ++++++++++ package.json | 29 +++--- resources/js/components/common/AspectBox.js | 21 +++++ resources/js/components/tracker/Canvas.js | 94 ++++++++++++++++++- resources/js/components/tracker/ToggleIcon.js | 11 ++- resources/js/hooks/tracker.js | 27 +++++- resources/sass/common.scss | 12 +++ 7 files changed, 224 insertions(+), 18 deletions(-) create mode 100644 resources/js/components/common/AspectBox.js diff --git a/package-lock.json b/package-lock.json index f0de661..e86ecd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@uiw/react-codemirror": "^4.21.9", "apng-js": "^1.1.1", "crc-32": "^1.2.2", + "d3-drag": "^3.0.0", "file-saver": "^2.0.5", "formik": "^2.2.9", "i18next": "^23.4.9", @@ -6525,6 +6526,26 @@ "node": ">=12" } }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -6575,6 +6596,14 @@ "node": ">=12" } }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", @@ -22502,6 +22531,20 @@ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" }, + "d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + }, + "d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } + }, "d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -22537,6 +22580,11 @@ "d3-time-format": "2 - 4" } }, + "d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + }, "d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", diff --git a/package.json b/package.json index 1fdb362..00152a8 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "tabWidth": 4 } ], - "no-use-before-define": "error", + "no-use-before-define": "error", "no-extra-parens": [ "warn", "all", @@ -68,23 +68,23 @@ "env": { "jest": true }, - "settings": { - "import/resolver": { - "node": { - "paths": [ - "resources/js" - ] - } - } - } + "settings": { + "import/resolver": { + "node": { + "paths": [ + "resources/js" + ] + } + } + } } ] }, "jest": { - "moduleDirectories": [ - "node_modules", - "resources/js" - ], + "moduleDirectories": [ + "node_modules", + "resources/js" + ], "roots": [ "/resources/js", "/tests/js" @@ -133,6 +133,7 @@ "@uiw/react-codemirror": "^4.21.9", "apng-js": "^1.1.1", "crc-32": "^1.2.2", + "d3-drag": "^3.0.0", "file-saver": "^2.0.5", "formik": "^2.2.9", "i18next": "^23.4.9", diff --git a/resources/js/components/common/AspectBox.js b/resources/js/components/common/AspectBox.js new file mode 100644 index 0000000..52709c1 --- /dev/null +++ b/resources/js/components/common/AspectBox.js @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +const AspectBox = ({ children, ratio }) => +
+
+ {children} +
+
; + +AspectBox.propTypes = { + children: PropTypes.node, + ratio: PropTypes.number, +}; + +AspectBox.defaultProps = { + children: null, + ratio: 1, +}; + +export default AspectBox; diff --git a/resources/js/components/tracker/Canvas.js b/resources/js/components/tracker/Canvas.js index 392f2a7..fab1942 100644 --- a/resources/js/components/tracker/Canvas.js +++ b/resources/js/components/tracker/Canvas.js @@ -1,8 +1,12 @@ +import { drag } from 'd3-drag'; +import { select } from 'd3-selection'; import React from 'react'; import Dungeons from './Dungeons'; import Items from './Items'; import Map from './Map'; +import ToggleIcon from './ToggleIcon'; +import ZeldaIcon from '../common/ZeldaIcon'; import { shouldShowDungeonItem } from '../../helpers/tracker'; import { useTracker } from '../../hooks/tracker'; @@ -34,7 +38,8 @@ const LAYOUTS = { }; const Canvas = () => { - const { config } = useTracker(); + const [dragging, setDragging] = React.useState(null); + const { addPin, config, pins, removePin } = useTracker(); const layout = React.useMemo(() => { if (config.mapLayout === 'vertical') { @@ -57,6 +62,62 @@ const Canvas = () => { } }, [config]); + React.useEffect(() => { + const canvas = select('.canvas'); + const bbox = canvas.select('.background'); + const start = { x: 0, y: 0 }; + const onStart = function (e) { + const bounds = bbox.node().getBoundingClientRect(); + start.x = e.x; + start.y = e.y; + setDragging({ + icon: this.dataset['icon'], + x: (e.x - bounds.x) / bounds.width, + y: (e.y - bounds.y) / bounds.height, + }); + }; + const onDrag = function (e) { + const bounds = bbox.node().getBoundingClientRect(); + setDragging({ + icon: this.dataset['icon'], + x: (e.x - bounds.x) / bounds.width, + y: (e.y - bounds.y) / bounds.height, + }); + }; + const onEnd = function (e) { + const bounds = bbox.node().getBoundingClientRect(); + setDragging(null); + const distance = Math.max(Math.abs(e.x - start.x), Math.abs(e.y - start.y)); + if (distance > 5) { + addPin({ + icon: this.dataset['icon'], + x: (e.x - bounds.x) / bounds.width, + y: (e.y - bounds.y) / bounds.height, + }); + if (this.classList.contains('map-pin')) { + let id = 0; + this.classList.forEach(name => { + if (name.startsWith('map-pin-')) { + id = parseInt(name.substr(8), 10); + } + }); + removePin({ id }); + } + } + }; + const selection = canvas.selectAll('.toggle-icon'); + const draggable = drag() + .container(bbox) + .clickDistance(5) + .on('start', onStart) + .on('drag', onDrag) + .on('end', onEnd); + selection.call(draggable); + return () => { + selection.on('.drag', null); + }; + }, [pins, removePin]); + return { e.stopPropagation(); }} > + @@ -77,6 +145,30 @@ const Canvas = () => { + + {pins.map(pin => + + + + )} + + {dragging ? + + + + : null} ; }; diff --git a/resources/js/components/tracker/ToggleIcon.js b/resources/js/components/tracker/ToggleIcon.js index 98ae932..0d8afa4 100644 --- a/resources/js/components/tracker/ToggleIcon.js +++ b/resources/js/components/tracker/ToggleIcon.js @@ -24,6 +24,7 @@ const ToggleIcon = ({ controller, className, icons, svg }) => { const activeController = controller || ToggleIcon.nullController; const active = activeController.getActive(state, icons); const defaultIcon = activeController.getDefault(state, icons); + const icon = active || defaultIcon || icons[0]; const classNames = ['toggle-icon']; if (active) { classNames.push('active'); @@ -36,6 +37,7 @@ const ToggleIcon = ({ controller, className, icons, svg }) => { if (svg) { return { activeController.handlePrimary(state, setManualState, icons); e.preventDefault(); @@ -47,7 +49,7 @@ const ToggleIcon = ({ controller, className, icons, svg }) => { e.stopPropagation(); }} > - + ; } return ({ + getActive: firstIcon, + getDefault: firstIcon, + handlePrimary: doNothing, + handleSecondary: () => removePin(pin), +}); + ToggleIcon.simpleController = { getActive: highestActive, getDefault: firstIcon, diff --git a/resources/js/hooks/tracker.js b/resources/js/hooks/tracker.js index ce01c4b..d0fb6b8 100644 --- a/resources/js/hooks/tracker.js +++ b/resources/js/hooks/tracker.js @@ -21,6 +21,7 @@ export const TrackerProvider = ({ children }) => { const [manualState, setManualState] = React.useState(makeEmptyState()); const [dungeons, setDungeons] = React.useState(DUNGEONS); const [logic, setLogic] = React.useState({}); + const [pins, setPins] = React.useState([]); const saveConfig = React.useCallback((values) => { setConfig(s => { @@ -30,6 +31,17 @@ export const TrackerProvider = ({ children }) => { }); }, []); + const addPin = React.useCallback((pin) => { + setPins(ps => { + const id = ps.length ? ps[ps.length - 1].id + 1 : 1; + return [...ps, { ...pin, id }]; + }); + }, []); + + const removePin = React.useCallback((pin) => { + setPins(ps => ps.filter(p => p.id !== pin.id)); + }, []); + React.useEffect(() => { const savedConfig = localStorage.getItem('tracker.config'); if (savedConfig) { @@ -51,8 +63,19 @@ export const TrackerProvider = ({ children }) => { }, [config, dungeons, state]); const value = React.useMemo(() => { - return { config, dungeons, logic, saveConfig, setAutoState, setManualState, state }; - }, [config, dungeons, logic, state]); + return { + addPin, + config, + dungeons, + logic, + pins, + removePin, + saveConfig, + setAutoState, + setManualState, + state, + }; + }, [config, dungeons, logic, pins, state]); return {children} diff --git a/resources/sass/common.scss b/resources/sass/common.scss index 0d61af3..1f73fb4 100644 --- a/resources/sass/common.scss +++ b/resources/sass/common.scss @@ -11,6 +11,18 @@ h1 { margin-bottom: 2rem; } +.aspect-box-container { + position: relative; + padding-top: 100%; +} +.aspect-box-content { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + .button-bar { margin: -0.5ex; > * { -- 2.39.2