tgui-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +10 -0
- package/.eslintrc.cjs +78 -0
- package/.gitattributes +4 -0
- package/.prettierrc.yml +1 -0
- package/.vscode/extensions.json +6 -0
- package/.vscode/settings.json +5 -0
- package/README.md +1 -0
- package/global.d.ts +173 -0
- package/package.json +25 -0
- package/src/assets.ts +43 -0
- package/src/backend.ts +369 -0
- package/src/common/collections.ts +349 -0
- package/src/common/color.ts +94 -0
- package/src/common/events.ts +45 -0
- package/src/common/exhaustive.ts +19 -0
- package/src/common/fp.ts +38 -0
- package/src/common/keycodes.ts +86 -0
- package/src/common/keys.ts +39 -0
- package/src/common/math.ts +98 -0
- package/src/common/perf.ts +72 -0
- package/src/common/random.ts +32 -0
- package/src/common/react.ts +65 -0
- package/src/common/redux.ts +196 -0
- package/src/common/storage.js +196 -0
- package/src/common/string.ts +173 -0
- package/src/common/timer.ts +68 -0
- package/src/common/type-utils.ts +41 -0
- package/src/common/types.ts +9 -0
- package/src/common/uuid.ts +24 -0
- package/src/common/vector.ts +51 -0
- package/src/components/AnimatedNumber.tsx +185 -0
- package/src/components/Autofocus.tsx +23 -0
- package/src/components/Blink.jsx +69 -0
- package/src/components/BlockQuote.tsx +15 -0
- package/src/components/BodyZoneSelector.tsx +149 -0
- package/src/components/Box.tsx +255 -0
- package/src/components/Button.tsx +415 -0
- package/src/components/ByondUi.jsx +121 -0
- package/src/components/Chart.tsx +160 -0
- package/src/components/Collapsible.tsx +45 -0
- package/src/components/ColorBox.tsx +30 -0
- package/src/components/Dialog.tsx +85 -0
- package/src/components/Dimmer.tsx +19 -0
- package/src/components/Divider.tsx +26 -0
- package/src/components/DmIcon.tsx +72 -0
- package/src/components/DraggableControl.jsx +282 -0
- package/src/components/Dropdown.tsx +246 -0
- package/src/components/FakeTerminal.jsx +52 -0
- package/src/components/FitText.tsx +99 -0
- package/src/components/Flex.tsx +105 -0
- package/src/components/Grid.tsx +44 -0
- package/src/components/Icon.tsx +91 -0
- package/src/components/Image.tsx +63 -0
- package/src/components/InfinitePlane.jsx +192 -0
- package/src/components/Input.tsx +181 -0
- package/src/components/KeyListener.tsx +40 -0
- package/src/components/Knob.tsx +185 -0
- package/src/components/LabeledControls.tsx +50 -0
- package/src/components/LabeledList.tsx +130 -0
- package/src/components/MenuBar.tsx +238 -0
- package/src/components/Modal.tsx +25 -0
- package/src/components/NoticeBox.tsx +48 -0
- package/src/components/NumberInput.tsx +328 -0
- package/src/components/Popper.tsx +100 -0
- package/src/components/ProgressBar.tsx +79 -0
- package/src/components/RestrictedInput.jsx +301 -0
- package/src/components/RoundGauge.tsx +189 -0
- package/src/components/Section.tsx +125 -0
- package/src/components/Slider.tsx +173 -0
- package/src/components/Stack.tsx +101 -0
- package/src/components/StyleableSection.tsx +30 -0
- package/src/components/Table.tsx +90 -0
- package/src/components/Tabs.tsx +90 -0
- package/src/components/TextArea.tsx +198 -0
- package/src/components/TimeDisplay.jsx +64 -0
- package/src/components/Tooltip.tsx +147 -0
- package/src/components/TrackOutsideClicks.tsx +35 -0
- package/src/components/VirtualList.tsx +69 -0
- package/src/constants.ts +355 -0
- package/src/debug/KitchenSink.jsx +56 -0
- package/src/debug/actions.js +11 -0
- package/src/debug/hooks.js +10 -0
- package/src/debug/index.ts +10 -0
- package/src/debug/middleware.js +86 -0
- package/src/debug/reducer.js +22 -0
- package/src/debug/selectors.js +7 -0
- package/src/drag.ts +280 -0
- package/src/events.ts +237 -0
- package/src/focus.ts +25 -0
- package/src/format.ts +173 -0
- package/src/hotkeys.ts +212 -0
- package/src/http.ts +16 -0
- package/src/layouts/Layout.tsx +75 -0
- package/src/layouts/NtosWindow.tsx +162 -0
- package/src/layouts/Pane.tsx +56 -0
- package/src/layouts/Window.tsx +227 -0
- package/src/renderer.ts +50 -0
- package/styles/base.scss +32 -0
- package/styles/colors.scss +92 -0
- package/styles/components/BlockQuote.scss +20 -0
- package/styles/components/Button.scss +175 -0
- package/styles/components/ColorBox.scss +12 -0
- package/styles/components/Dialog.scss +105 -0
- package/styles/components/Dimmer.scss +22 -0
- package/styles/components/Divider.scss +27 -0
- package/styles/components/Dropdown.scss +72 -0
- package/styles/components/Flex.scss +31 -0
- package/styles/components/Icon.scss +25 -0
- package/styles/components/Input.scss +68 -0
- package/styles/components/Knob.scss +131 -0
- package/styles/components/LabeledList.scss +49 -0
- package/styles/components/MenuBar.scss +75 -0
- package/styles/components/Modal.scss +14 -0
- package/styles/components/NoticeBox.scss +65 -0
- package/styles/components/NumberInput.scss +76 -0
- package/styles/components/ProgressBar.scss +63 -0
- package/styles/components/RoundGauge.scss +88 -0
- package/styles/components/Section.scss +143 -0
- package/styles/components/Slider.scss +54 -0
- package/styles/components/Stack.scss +59 -0
- package/styles/components/Table.scss +44 -0
- package/styles/components/Tabs.scss +144 -0
- package/styles/components/TextArea.scss +84 -0
- package/styles/components/Tooltip.scss +24 -0
- package/styles/functions.scss +79 -0
- package/styles/layouts/Layout.scss +57 -0
- package/styles/layouts/NtosHeader.scss +20 -0
- package/styles/layouts/NtosWindow.scss +26 -0
- package/styles/layouts/TitleBar.scss +111 -0
- package/styles/layouts/Window.scss +103 -0
- package/styles/main.scss +97 -0
- package/styles/reset.scss +68 -0
- package/styles/themes/abductor.scss +68 -0
- package/styles/themes/admin.scss +38 -0
- package/styles/themes/cardtable.scss +57 -0
- package/styles/themes/hackerman.scss +70 -0
- package/styles/themes/malfunction.scss +67 -0
- package/styles/themes/neutral.scss +50 -0
- package/styles/themes/ntOS95.scss +166 -0
- package/styles/themes/ntos.scss +44 -0
- package/styles/themes/ntos_cat.scss +148 -0
- package/styles/themes/ntos_darkmode.scss +44 -0
- package/styles/themes/ntos_lightmode.scss +67 -0
- package/styles/themes/ntos_spooky.scss +69 -0
- package/styles/themes/ntos_synth.scss +99 -0
- package/styles/themes/ntos_terminal.scss +112 -0
- package/styles/themes/paper.scss +184 -0
- package/styles/themes/retro.scss +72 -0
- package/styles/themes/spookyconsole.scss +73 -0
- package/styles/themes/syndicate.scss +67 -0
- package/styles/themes/wizard.scss +68 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* N-dimensional vector manipulation functions.
|
|
3
|
+
*
|
|
4
|
+
* Vectors are plain number arrays, i.e. [x, y, z].
|
|
5
|
+
*
|
|
6
|
+
* @file
|
|
7
|
+
* @copyright 2020 Aleksej Komarov
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { map, reduce, zip } from './collections';
|
|
12
|
+
|
|
13
|
+
const ADD = (a: number, b: number): number => a + b;
|
|
14
|
+
const SUB = (a: number, b: number): number => a - b;
|
|
15
|
+
const MUL = (a: number, b: number): number => a * b;
|
|
16
|
+
const DIV = (a: number, b: number): number => a / b;
|
|
17
|
+
|
|
18
|
+
export type Vector = number[];
|
|
19
|
+
|
|
20
|
+
export const vecAdd = (...vecs: Vector[]): Vector => {
|
|
21
|
+
return map(zip(...vecs), (x) => reduce(x, ADD));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const vecSubtract = (...vecs: Vector[]): Vector => {
|
|
25
|
+
return map(zip(...vecs), (x) => reduce(x, SUB));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const vecMultiply = (...vecs: Vector[]): Vector => {
|
|
29
|
+
return map(zip(...vecs), (x) => reduce(x, MUL));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const vecDivide = (...vecs: Vector[]): Vector => {
|
|
33
|
+
return map(zip(...vecs), (x) => reduce(x, DIV));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const vecScale = (vec: Vector, n: number): Vector => {
|
|
37
|
+
return map(vec, (x) => x * n);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const vecInverse = (vec: Vector): Vector => {
|
|
41
|
+
return map(vec, (x) => -x);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const vecLength = (vec: Vector): number => {
|
|
45
|
+
return Math.sqrt(reduce(vecMultiply(vec, vec), ADD));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const vecNormalize = (vec: Vector): Vector => {
|
|
49
|
+
const length = vecLength(vec);
|
|
50
|
+
return map(vec, (c) => c / length);
|
|
51
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
* @copyright 2020 Aleksej Komarov
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { clamp, toFixed } from '../common/math';
|
|
8
|
+
import { Component, createRef } from 'react';
|
|
9
|
+
|
|
10
|
+
const isSafeNumber = (value: number) => {
|
|
11
|
+
return (
|
|
12
|
+
typeof value === 'number' && Number.isFinite(value) && !Number.isNaN(value)
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type AnimatedNumberProps = {
|
|
17
|
+
/**
|
|
18
|
+
* The target value to approach.
|
|
19
|
+
*/
|
|
20
|
+
value: number;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* If provided, the initial value displayed. By default, the same as `value`.
|
|
24
|
+
* If `initial` and `value` are different, the component immediately starts
|
|
25
|
+
* animating.
|
|
26
|
+
*/
|
|
27
|
+
initial?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* If provided, a function that formats the inner string. By default,
|
|
31
|
+
* attempts to match the numeric precision of `value`.
|
|
32
|
+
*/
|
|
33
|
+
format?: (value: number) => string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Animated numbers are animated at roughly 60 frames per second.
|
|
38
|
+
*/
|
|
39
|
+
const SIXTY_HZ = 1_000.0 / 60.0;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The exponential moving average coefficient. Larger values result in a faster
|
|
43
|
+
* convergence.
|
|
44
|
+
*/
|
|
45
|
+
const Q = 0.8333;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A small number.
|
|
49
|
+
*/
|
|
50
|
+
const EPSILON = 10e-4;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* An animated number label. Shows a number, formatted with an optionally
|
|
54
|
+
* provided function, and animates it towards its target value.
|
|
55
|
+
*/
|
|
56
|
+
export class AnimatedNumber extends Component<AnimatedNumberProps> {
|
|
57
|
+
/**
|
|
58
|
+
* The inner `<span/>` being updated sixty times per second.
|
|
59
|
+
*/
|
|
60
|
+
ref = createRef<HTMLSpanElement>();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The interval being used to update the inner span.
|
|
64
|
+
*/
|
|
65
|
+
interval?: NodeJS.Timeout;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The current value. This values approaches the target value.
|
|
69
|
+
*/
|
|
70
|
+
currentValue: number = 0;
|
|
71
|
+
|
|
72
|
+
constructor(props: AnimatedNumberProps) {
|
|
73
|
+
super(props);
|
|
74
|
+
|
|
75
|
+
const { initial, value } = props;
|
|
76
|
+
|
|
77
|
+
if (initial !== undefined && isSafeNumber(initial)) {
|
|
78
|
+
this.currentValue = initial;
|
|
79
|
+
} else if (isSafeNumber(value)) {
|
|
80
|
+
this.currentValue = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
componentDidMount() {
|
|
85
|
+
if (this.currentValue !== this.props.value) {
|
|
86
|
+
this.startTicking();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
componentWillUnmount() {
|
|
91
|
+
// Stop animating when the component is unmounted.
|
|
92
|
+
this.stopTicking();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
shouldComponentUpdate(newProps: AnimatedNumberProps) {
|
|
96
|
+
if (newProps.value !== this.props.value) {
|
|
97
|
+
// The target value has been adjusted; start animating if we aren't
|
|
98
|
+
// already.
|
|
99
|
+
this.startTicking();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Starts animating the inner span. If the inner span is already animating,
|
|
107
|
+
* this is a no-op.
|
|
108
|
+
*/
|
|
109
|
+
startTicking() {
|
|
110
|
+
if (this.interval !== undefined) {
|
|
111
|
+
// We're already ticking; do nothing.
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.interval = setInterval(() => this.tick(), SIXTY_HZ);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Stops animating the inner span.
|
|
120
|
+
*/
|
|
121
|
+
stopTicking() {
|
|
122
|
+
if (this.interval === undefined) {
|
|
123
|
+
// We're not ticking; do nothing.
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
clearInterval(this.interval);
|
|
128
|
+
|
|
129
|
+
this.interval = undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Steps forward one frame.
|
|
134
|
+
*/
|
|
135
|
+
tick() {
|
|
136
|
+
const { currentValue } = this;
|
|
137
|
+
const { value } = this.props;
|
|
138
|
+
|
|
139
|
+
if (isSafeNumber(value)) {
|
|
140
|
+
// Converge towards the value.
|
|
141
|
+
this.currentValue = currentValue * Q + value * (1 - Q);
|
|
142
|
+
} else {
|
|
143
|
+
// If the value is unsafe, we're never going to converge, so stop ticking.
|
|
144
|
+
this.stopTicking();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
Math.abs(value - this.currentValue) < Math.max(EPSILON, EPSILON * value)
|
|
149
|
+
) {
|
|
150
|
+
// We're about as close as we're going to get--snap to the value and
|
|
151
|
+
// stop ticking.
|
|
152
|
+
this.currentValue = value;
|
|
153
|
+
this.stopTicking();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (this.ref.current) {
|
|
157
|
+
this.ref.current.textContent = this.getText();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Gets the inner text of the span.
|
|
163
|
+
*/
|
|
164
|
+
getText() {
|
|
165
|
+
const { props, currentValue } = this;
|
|
166
|
+
const { format, value } = props;
|
|
167
|
+
|
|
168
|
+
if (!isSafeNumber(value)) {
|
|
169
|
+
return String(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (format) {
|
|
173
|
+
return format(this.currentValue);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const fraction = String(value).split('.')[1];
|
|
177
|
+
const precision = fraction ? fraction.length : 0;
|
|
178
|
+
|
|
179
|
+
return toFixed(currentValue, clamp(precision, 0, 8));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
render() {
|
|
183
|
+
return <span ref={this.ref}>{this.getText()}</span>;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PropsWithChildren, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/** Used to force the window to steal focus on load. Children optional */
|
|
4
|
+
export function Autofocus(props: PropsWithChildren) {
|
|
5
|
+
const { children } = props;
|
|
6
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const timer = setTimeout(() => {
|
|
10
|
+
ref.current?.focus();
|
|
11
|
+
}, 1);
|
|
12
|
+
|
|
13
|
+
return () => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
};
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div ref={ref} tabIndex={-1}>
|
|
20
|
+
{children}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_BLINKING_INTERVAL = 1000;
|
|
4
|
+
const DEFAULT_BLINKING_TIME = 1000;
|
|
5
|
+
|
|
6
|
+
export class Blink extends Component {
|
|
7
|
+
constructor(props) {
|
|
8
|
+
super(props);
|
|
9
|
+
this.state = {
|
|
10
|
+
hidden: false,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
createTimer() {
|
|
15
|
+
const {
|
|
16
|
+
interval = DEFAULT_BLINKING_INTERVAL,
|
|
17
|
+
time = DEFAULT_BLINKING_TIME,
|
|
18
|
+
} = this.props;
|
|
19
|
+
|
|
20
|
+
clearInterval(this.interval);
|
|
21
|
+
clearTimeout(this.timer);
|
|
22
|
+
|
|
23
|
+
this.setState({
|
|
24
|
+
hidden: false,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
this.interval = setInterval(() => {
|
|
28
|
+
this.setState({
|
|
29
|
+
hidden: true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this.timer = setTimeout(() => {
|
|
33
|
+
this.setState({
|
|
34
|
+
hidden: false,
|
|
35
|
+
});
|
|
36
|
+
}, time);
|
|
37
|
+
}, interval + time);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
componentDidMount() {
|
|
41
|
+
this.createTimer();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
componentDidUpdate(prevProps) {
|
|
45
|
+
if (
|
|
46
|
+
prevProps.interval !== this.props.interval ||
|
|
47
|
+
prevProps.time !== this.props.time
|
|
48
|
+
) {
|
|
49
|
+
this.createTimer();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
componentWillUnmount() {
|
|
54
|
+
clearInterval(this.interval);
|
|
55
|
+
clearTimeout(this.timer);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
render() {
|
|
59
|
+
return (
|
|
60
|
+
<span
|
|
61
|
+
style={{
|
|
62
|
+
visibility: this.state.hidden ? 'hidden' : 'visible',
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{this.props.children}
|
|
66
|
+
</span>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
* @copyright 2020 Aleksej Komarov
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { classes } from '../common/react';
|
|
8
|
+
|
|
9
|
+
import { Box, BoxProps } from './Box';
|
|
10
|
+
|
|
11
|
+
export function BlockQuote(props: BoxProps) {
|
|
12
|
+
const { className, ...rest } = props;
|
|
13
|
+
|
|
14
|
+
return <Box className={classes(['BlockQuote', className])} {...rest} />;
|
|
15
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Component, createRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { resolveAsset } from '../assets';
|
|
4
|
+
import { Image } from './Image';
|
|
5
|
+
|
|
6
|
+
export enum BodyZone {
|
|
7
|
+
Head = 'head',
|
|
8
|
+
Chest = 'chest',
|
|
9
|
+
LeftArm = 'l_arm',
|
|
10
|
+
RightArm = 'r_arm',
|
|
11
|
+
LeftLeg = 'l_leg',
|
|
12
|
+
RightLeg = 'r_leg',
|
|
13
|
+
Eyes = 'eyes',
|
|
14
|
+
Mouth = 'mouth',
|
|
15
|
+
Groin = 'groin',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const bodyZonePixelToZone = (x: number, y: number): BodyZone | null => {
|
|
19
|
+
// TypeScript translation of /atom/movable/screen/zone_sel/proc/get_zone_at
|
|
20
|
+
if (y < 1) {
|
|
21
|
+
return null;
|
|
22
|
+
} else if (y < 10) {
|
|
23
|
+
if (x > 10 && x < 15) {
|
|
24
|
+
return BodyZone.RightLeg;
|
|
25
|
+
} else if (x > 17 && x < 22) {
|
|
26
|
+
return BodyZone.LeftLeg;
|
|
27
|
+
}
|
|
28
|
+
} else if (y < 13) {
|
|
29
|
+
if (x > 8 && x < 11) {
|
|
30
|
+
return BodyZone.RightArm;
|
|
31
|
+
} else if (x > 12 && x < 20) {
|
|
32
|
+
return BodyZone.Groin;
|
|
33
|
+
} else if (x > 21 && x < 24) {
|
|
34
|
+
return BodyZone.LeftArm;
|
|
35
|
+
}
|
|
36
|
+
} else if (y < 22) {
|
|
37
|
+
if (x > 8 && x < 11) {
|
|
38
|
+
return BodyZone.RightArm;
|
|
39
|
+
} else if (x > 12 && x < 20) {
|
|
40
|
+
return BodyZone.Chest;
|
|
41
|
+
} else if (x > 21 && x < 24) {
|
|
42
|
+
return BodyZone.LeftArm;
|
|
43
|
+
}
|
|
44
|
+
} else if (y < 30 && x > 12 && x < 20) {
|
|
45
|
+
if (y > 23 && y < 24 && x > 15 && x < 17) {
|
|
46
|
+
return BodyZone.Mouth;
|
|
47
|
+
} else if (y > 25 && y < 27 && x > 14 && x < 18) {
|
|
48
|
+
return BodyZone.Eyes;
|
|
49
|
+
} else {
|
|
50
|
+
return BodyZone.Head;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type BodyZoneSelectorProps = {
|
|
58
|
+
onClick?: (zone: BodyZone) => void;
|
|
59
|
+
scale?: number;
|
|
60
|
+
selectedZone: BodyZone | null;
|
|
61
|
+
theme?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type BodyZoneSelectorState = {
|
|
65
|
+
hoverZone: BodyZone | null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export class BodyZoneSelector extends Component<
|
|
69
|
+
BodyZoneSelectorProps,
|
|
70
|
+
BodyZoneSelectorState
|
|
71
|
+
> {
|
|
72
|
+
ref = createRef<HTMLDivElement>();
|
|
73
|
+
state: BodyZoneSelectorState = {
|
|
74
|
+
hoverZone: null,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
render() {
|
|
78
|
+
const { hoverZone } = this.state;
|
|
79
|
+
const { scale = 3, selectedZone, theme = 'midnight' } = this.props;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
ref={this.ref}
|
|
84
|
+
style={{
|
|
85
|
+
width: `${32 * scale}px`,
|
|
86
|
+
height: `${32 * scale}px`,
|
|
87
|
+
position: 'relative',
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<Image
|
|
91
|
+
src={resolveAsset(`body_zones.base_${theme}.png`)}
|
|
92
|
+
onClick={() => {
|
|
93
|
+
const onClick = this.props.onClick;
|
|
94
|
+
if (onClick && this.state.hoverZone) {
|
|
95
|
+
onClick(this.state.hoverZone);
|
|
96
|
+
}
|
|
97
|
+
}}
|
|
98
|
+
onMouseMove={(event) => {
|
|
99
|
+
if (!this.props.onClick) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rect = this.ref.current?.getBoundingClientRect();
|
|
104
|
+
if (!rect) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const x = event.clientX - rect.left;
|
|
109
|
+
const y = 32 * scale - (event.clientY - rect.top);
|
|
110
|
+
|
|
111
|
+
this.setState({
|
|
112
|
+
hoverZone: bodyZonePixelToZone(x / scale, y / scale),
|
|
113
|
+
});
|
|
114
|
+
}}
|
|
115
|
+
style={{
|
|
116
|
+
position: 'absolute',
|
|
117
|
+
width: `${32 * scale}px`,
|
|
118
|
+
height: `${32 * scale}px`,
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
|
|
122
|
+
{selectedZone && (
|
|
123
|
+
<Image
|
|
124
|
+
src={resolveAsset(`body_zones.${selectedZone}.png`)}
|
|
125
|
+
style={{
|
|
126
|
+
pointerEvents: 'none',
|
|
127
|
+
position: 'absolute',
|
|
128
|
+
width: `${32 * scale}px`,
|
|
129
|
+
height: `${32 * scale}px`,
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{hoverZone && hoverZone !== selectedZone && (
|
|
135
|
+
<Image
|
|
136
|
+
src={resolveAsset(`body_zones.${hoverZone}.png`)}
|
|
137
|
+
style={{
|
|
138
|
+
opacity: '0.5',
|
|
139
|
+
pointerEvents: 'none',
|
|
140
|
+
position: 'absolute',
|
|
141
|
+
width: `${32 * scale}px`,
|
|
142
|
+
height: `${32 * scale}px`,
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|