@thinkpixellab-public/px-vue 3.0.67 → 3.0.69
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/components/PxArrow.vue +128 -0
- package/components/PxArrowScroller.vue +1 -1
- package/components/PxIconButton.vue +8 -0
- package/components/PxMenu.vue +240 -0
- package/components/PxMenuItem.vue +147 -0
- package/components/PxSvg.vue +5 -1
- package/components/PxTransition.vue +241 -0
- package/dev/demos/Demo-PxFloat.vue +1 -1
- package/dev/demos/Demo-PxMenu.vue +90 -0
- package/dev/dev.vue +3 -1
- package/package.json +2 -2
- package/utils/points.js +110 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Generates an SVG arrow based on props.
|
|
3
|
+
-->
|
|
4
|
+
<template>
|
|
5
|
+
<svg
|
|
6
|
+
version="1.1"
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
9
|
+
overflow="visible"
|
|
10
|
+
:viewBox="svgData.viewBox"
|
|
11
|
+
>
|
|
12
|
+
<g
|
|
13
|
+
:stroke-width="svgData.strokeWidth"
|
|
14
|
+
stroke="currentColor"
|
|
15
|
+
fill="none"
|
|
16
|
+
fill-rule="evenodd"
|
|
17
|
+
stroke-linecap="round"
|
|
18
|
+
>
|
|
19
|
+
<polyline :points="svgData.pointer"></polyline>
|
|
20
|
+
<polyline v-if="stemVisible" :points="svgData.stem"></polyline>
|
|
21
|
+
</g>
|
|
22
|
+
</svg>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
import PxBaseResize from '@thinkpixellab-public/px-vue/components/PxBaseResize.vue';
|
|
27
|
+
import { getBoundingBox, rotatePoints } from '../utils/points.js';
|
|
28
|
+
import { contain } from '@thinkpixellab-public/px-vue/utils/fit.js';
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
name: 'px-arrow',
|
|
32
|
+
mixins: [PxBaseResize],
|
|
33
|
+
props: {
|
|
34
|
+
// the aspect of the arrow "rectangle"
|
|
35
|
+
pointerAspect: { type: [Number, String], default: 2 },
|
|
36
|
+
|
|
37
|
+
// the relative height of the arrow
|
|
38
|
+
pointerSize: { type: Number, default: 0.4 },
|
|
39
|
+
|
|
40
|
+
// the relative size of the stem
|
|
41
|
+
stemVisible: { type: Boolean, default: true },
|
|
42
|
+
|
|
43
|
+
// the angle in which the arrow is pointing (in degrees)
|
|
44
|
+
angle: { type: Number, default: 0 },
|
|
45
|
+
|
|
46
|
+
// the stroke thickness of the arrow
|
|
47
|
+
strokeWidth: { type: Number, default: 1 },
|
|
48
|
+
|
|
49
|
+
// when true, we scale the strokeWidth in the internal SVG to keep it at the specified value
|
|
50
|
+
// regardless of render size
|
|
51
|
+
scaleStrokeWidth: { type: Boolean, default: true },
|
|
52
|
+
},
|
|
53
|
+
data() {
|
|
54
|
+
return {
|
|
55
|
+
elementSize: { width: 100, height: 100 },
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
computed: {
|
|
59
|
+
svgData() {
|
|
60
|
+
const pointerHeight = this.pointerSize;
|
|
61
|
+
const pointerWidth = this.pointerAspect * pointerHeight;
|
|
62
|
+
|
|
63
|
+
// basic arrow (bottom left corner anchored to origin)
|
|
64
|
+
let points = [
|
|
65
|
+
// pointer
|
|
66
|
+
{ x: 0, y: pointerHeight },
|
|
67
|
+
{ x: pointerWidth / 2, y: 0 },
|
|
68
|
+
{ x: pointerWidth, y: pointerHeight },
|
|
69
|
+
|
|
70
|
+
// stem
|
|
71
|
+
{ x: pointerWidth / 2, y: 0 },
|
|
72
|
+
{ x: pointerWidth / 2, y: 1 },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// rotate
|
|
76
|
+
points = rotatePoints(points, this.angle);
|
|
77
|
+
|
|
78
|
+
// scale by 100 so the viewbox isn't ridiculous
|
|
79
|
+
points = points.map(p => {
|
|
80
|
+
return {
|
|
81
|
+
x: p.x * 100,
|
|
82
|
+
y: p.y * 100,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// get viewbox bounds
|
|
87
|
+
const bounds = getBoundingBox(points);
|
|
88
|
+
|
|
89
|
+
// get stroke scale
|
|
90
|
+
const strokeScale = this.scaleStrokeWidth ? contain(this.elementSize, bounds).scale : 1;
|
|
91
|
+
|
|
92
|
+
// convert points to a string
|
|
93
|
+
points = points.map(p => {
|
|
94
|
+
return `${p.x} ${p.y}`;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const svgData = {
|
|
98
|
+
viewBox: [bounds.x, bounds.y, bounds.width, bounds.height]
|
|
99
|
+
.map(n => Math.round(n))
|
|
100
|
+
.join(' '),
|
|
101
|
+
pointer: [points[0], points[1], points[2]].join(' '),
|
|
102
|
+
stem: [points[3], points[4]].join(' '),
|
|
103
|
+
strokeWidth: this.strokeWidth * strokeScale,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return svgData;
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
methods: {
|
|
110
|
+
resize() {
|
|
111
|
+
if (this.$el) {
|
|
112
|
+
const r = this.$el.getBoundingClientRect();
|
|
113
|
+
this.elementSize = {
|
|
114
|
+
width: r.width,
|
|
115
|
+
height: r.height,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<style lang="scss">
|
|
124
|
+
@import '@/styles/include.scss';
|
|
125
|
+
.px-arrow {
|
|
126
|
+
position: relative;
|
|
127
|
+
}
|
|
128
|
+
</style>
|
|
@@ -55,7 +55,7 @@ ar
|
|
|
55
55
|
// import Component from '~/components/Component.vue';
|
|
56
56
|
// import Mixin from '~/components/Mixin.vue';
|
|
57
57
|
import PxBaseResize from './PxBaseResize.vue';
|
|
58
|
-
import PxIcon from '
|
|
58
|
+
import PxIcon from './PxIcon.vue';
|
|
59
59
|
|
|
60
60
|
export default {
|
|
61
61
|
name: 'px-arrow-scroller',
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
:viewBox="svgViewBox"
|
|
30
30
|
:height="calcHeight"
|
|
31
31
|
:width="calcWidth"
|
|
32
|
+
:title="title"
|
|
32
33
|
/>
|
|
33
34
|
</button>
|
|
34
35
|
</template>
|
|
@@ -111,5 +112,12 @@ export default {
|
|
|
111
112
|
color: accent(-2);
|
|
112
113
|
}
|
|
113
114
|
}
|
|
115
|
+
|
|
116
|
+
&--subtle {
|
|
117
|
+
opacity: 0.33;
|
|
118
|
+
&:hover {
|
|
119
|
+
opacity: 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
114
122
|
}
|
|
115
123
|
</style>
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The general idea here is that you define a context menu and then use the v-context-menu
|
|
3
|
+
directive to assign it to an element and pass in optional context.
|
|
4
|
+
|
|
5
|
+
The directive creates a handler for the element's contextmenu event and shows the appropriate
|
|
6
|
+
context menu with the provided context.
|
|
7
|
+
|
|
8
|
+
Here is what all of that looks like:
|
|
9
|
+
|
|
10
|
+
// define the context menu
|
|
11
|
+
|
|
12
|
+
<px-menu ref="mymenu" :items="[
|
|
13
|
+
{
|
|
14
|
+
id: 'copy',
|
|
15
|
+
icon: require('myicon.svg'),
|
|
16
|
+
label: 'Copy',
|
|
17
|
+
|
|
18
|
+
// function with a single arg which will be the context from the directive
|
|
19
|
+
handler: 'onContextMenuCopy',
|
|
20
|
+
|
|
21
|
+
// visible / enabled can be a function or bool
|
|
22
|
+
enabled: (ctx) => ctx.isSomething,
|
|
23
|
+
visible: true
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'copy',
|
|
27
|
+
icon: require('myicon.svg'),
|
|
28
|
+
label: 'Copy',
|
|
29
|
+
|
|
30
|
+
// function with a single arg which will be the context from the directive
|
|
31
|
+
handler: 'onContextMenuCopy',
|
|
32
|
+
|
|
33
|
+
// visible / enabled can be a function or bool
|
|
34
|
+
enabled: (ctx) => ctx.isSomething,
|
|
35
|
+
visible: true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
isSeparator: true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'advanced',
|
|
42
|
+
label: 'Advanced',
|
|
43
|
+
items: [],
|
|
44
|
+
},
|
|
45
|
+
]" />
|
|
46
|
+
|
|
47
|
+
// use the context menu (no context)
|
|
48
|
+
<div v-context-menu="'mymenu'" />
|
|
49
|
+
|
|
50
|
+
// use the context menu (pass dynamic mydata as context)
|
|
51
|
+
<div v-context-menu:[mydata]="'mymenu'" />
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
-->
|
|
55
|
+
<template>
|
|
56
|
+
<px-float
|
|
57
|
+
v-bind="{
|
|
58
|
+
placement: 'right-start',
|
|
59
|
+
...floatProps,
|
|
60
|
+
}"
|
|
61
|
+
:popup-class="bem('popup')"
|
|
62
|
+
ref="popup"
|
|
63
|
+
variant="none"
|
|
64
|
+
@hide="afterHide"
|
|
65
|
+
>
|
|
66
|
+
<div :class="bem()">
|
|
67
|
+
<slot v-for="item in items" :item="item" :data="data" :done="hide">
|
|
68
|
+
<PxMenuItem
|
|
69
|
+
:key="item.id"
|
|
70
|
+
:separator="item.separator"
|
|
71
|
+
:label="item.label"
|
|
72
|
+
:hint="item.hint"
|
|
73
|
+
:items="item.items"
|
|
74
|
+
:icon="item.icon"
|
|
75
|
+
:visible="item.visible"
|
|
76
|
+
:disabled="item.disabled"
|
|
77
|
+
:handler="item.handler"
|
|
78
|
+
:iconSize="calcIconSize"
|
|
79
|
+
:iconClass="iconClass"
|
|
80
|
+
:data="data"
|
|
81
|
+
@done="hide"
|
|
82
|
+
@iconsized="iconSized"
|
|
83
|
+
/>
|
|
84
|
+
</slot>
|
|
85
|
+
</div>
|
|
86
|
+
</px-float>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<script>
|
|
90
|
+
import PxFloat from './PxFloat.vue';
|
|
91
|
+
import PxMenuItem from './PxMenuItem.vue';
|
|
92
|
+
|
|
93
|
+
// ================================================================================
|
|
94
|
+
// Component
|
|
95
|
+
// ================================================================================
|
|
96
|
+
|
|
97
|
+
const PxMenu = {
|
|
98
|
+
name: 'px-menu',
|
|
99
|
+
components: { PxFloat, PxMenuItem },
|
|
100
|
+
mixins: [],
|
|
101
|
+
props: {
|
|
102
|
+
items: { type: Array, default: null },
|
|
103
|
+
iconSize: { type: [String, Number], default: 1 },
|
|
104
|
+
iconClass: { type: String, default: null },
|
|
105
|
+
floatProps: { type: Object, default: null },
|
|
106
|
+
},
|
|
107
|
+
data() {
|
|
108
|
+
return {
|
|
109
|
+
data: null,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
computed: {
|
|
113
|
+
calcIconSize() {
|
|
114
|
+
if (this.items.find(item => !!item.icon)) {
|
|
115
|
+
return this.iconSize;
|
|
116
|
+
}
|
|
117
|
+
return 0;
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
methods: {
|
|
121
|
+
showAtCursor(mouseEvent, data) {
|
|
122
|
+
this.data = data;
|
|
123
|
+
this.$refs.popup?.showAtCursor(mouseEvent);
|
|
124
|
+
},
|
|
125
|
+
show(data) {
|
|
126
|
+
this.data = data;
|
|
127
|
+
this.$refs.popup?.show();
|
|
128
|
+
},
|
|
129
|
+
hide() {
|
|
130
|
+
this.$refs.popup?.hide();
|
|
131
|
+
},
|
|
132
|
+
afterHide() {
|
|
133
|
+
this.data = null;
|
|
134
|
+
},
|
|
135
|
+
iconSized(e) {
|
|
136
|
+
console.log(e);
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// ================================================================================
|
|
142
|
+
// Directives
|
|
143
|
+
// ================================================================================
|
|
144
|
+
|
|
145
|
+
const contextMenuDirective = {
|
|
146
|
+
//bind: (el, binding, vnode) => {
|
|
147
|
+
bind(el, binding, vnode) {
|
|
148
|
+
if (!binding || !vnode) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
binding.el = vnode?.elm;
|
|
153
|
+
binding.contextMenuHandler = e => {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
e.stopPropagation();
|
|
156
|
+
|
|
157
|
+
const name = binding?.value;
|
|
158
|
+
const data = binding?.arg;
|
|
159
|
+
const $refs = vnode?.context?.$refs;
|
|
160
|
+
|
|
161
|
+
if ($refs && name in $refs) {
|
|
162
|
+
const menu = $refs[name];
|
|
163
|
+
|
|
164
|
+
if (typeof menu?.show == 'function') {
|
|
165
|
+
menu.showAtCursor(e, data);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (binding.el) {
|
|
171
|
+
binding.el.addEventListener('contextmenu', binding.contextMenuHandler);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
unbind(el, binding) {
|
|
175
|
+
if (binding.el) {
|
|
176
|
+
binding.el.removeEventListener('contextmenu', binding.contextMenuHandler);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const menuDirective = {
|
|
182
|
+
//bind: (el, binding, vnode) => {
|
|
183
|
+
bind(el, binding, vnode) {
|
|
184
|
+
if (!binding || !vnode) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
binding.el = vnode?.elm;
|
|
189
|
+
binding.contextMenuHandler = e => {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
e.stopPropagation();
|
|
192
|
+
|
|
193
|
+
const name = binding?.value;
|
|
194
|
+
const data = binding?.arg;
|
|
195
|
+
const $refs = vnode?.context?.$refs;
|
|
196
|
+
|
|
197
|
+
if ($refs && name in $refs) {
|
|
198
|
+
const menu = $refs[name];
|
|
199
|
+
|
|
200
|
+
if (typeof menu?.show == 'function') {
|
|
201
|
+
menu.show(e, data);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (binding.el) {
|
|
207
|
+
binding.el.addEventListener('click', binding.contextMenuHandler);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
unbind(el, binding) {
|
|
211
|
+
if (binding.el) {
|
|
212
|
+
binding.el.removeEventListener('click', binding.contextMenuHandler);
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export {
|
|
218
|
+
PxMenu as default,
|
|
219
|
+
contextMenuDirective as contextMenuDirective,
|
|
220
|
+
menuDirective as menuDirective,
|
|
221
|
+
};
|
|
222
|
+
</script>
|
|
223
|
+
|
|
224
|
+
<style lang="scss">
|
|
225
|
+
@use '../styles/px.scss' as *;
|
|
226
|
+
.px-menu {
|
|
227
|
+
font-family: get('font-family');
|
|
228
|
+
background-color: clr(page-bg);
|
|
229
|
+
color: clr(page-fg);
|
|
230
|
+
border-radius: get('controls:border-radius');
|
|
231
|
+
overflow: hidden;
|
|
232
|
+
display: flex;
|
|
233
|
+
flex-direction: column;
|
|
234
|
+
justify-content: stretch;
|
|
235
|
+
|
|
236
|
+
&__popup {
|
|
237
|
+
@include css-map(popup());
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
</style>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Describe this component...
|
|
3
|
+
-->
|
|
4
|
+
<template>
|
|
5
|
+
<button
|
|
6
|
+
v-if="!separator && isVisible"
|
|
7
|
+
:class="bem({ clicked, disabled: isDisabled })"
|
|
8
|
+
:title="hint"
|
|
9
|
+
:disabled="isDisabled"
|
|
10
|
+
@click="onClick"
|
|
11
|
+
>
|
|
12
|
+
<span
|
|
13
|
+
ref="icon"
|
|
14
|
+
v-if="iconSize"
|
|
15
|
+
:class="[bem('icon'), iconClass]"
|
|
16
|
+
:style="{ minWidth: cssEmFallback(iconSize) }"
|
|
17
|
+
>
|
|
18
|
+
<PxIcon :src="icon" :size="cssEmFallback(iconSize)" size-bias="width" />
|
|
19
|
+
</span>
|
|
20
|
+
<span :class="bem('label')">
|
|
21
|
+
{{ label }}
|
|
22
|
+
</span>
|
|
23
|
+
</button>
|
|
24
|
+
|
|
25
|
+
<hr v-else-if="visible" :class="bem({ separator: true })" :visible="isVisible" />
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script>
|
|
29
|
+
import PxIcon from './PxIcon.vue';
|
|
30
|
+
|
|
31
|
+
// import { mapState } from 'vuex';
|
|
32
|
+
// import Component from '~/components/Component.vue';
|
|
33
|
+
// import Mixin from '~/components/Mixin.vue';
|
|
34
|
+
|
|
35
|
+
export default {
|
|
36
|
+
name: 'px-menu-item',
|
|
37
|
+
components: { PxIcon },
|
|
38
|
+
// mixins: [ Mixin ],
|
|
39
|
+
props: {
|
|
40
|
+
separator: { type: Boolean, default: false },
|
|
41
|
+
label: { type: String, default: null },
|
|
42
|
+
hint: { type: String, default: null },
|
|
43
|
+
items: { type: Array, default: null },
|
|
44
|
+
icon: { type: String, default: null },
|
|
45
|
+
iconSize: { type: [String, Number], default: 1 },
|
|
46
|
+
iconClass: { type: String, default: null },
|
|
47
|
+
disabled: { type: [Function, Boolean], default: false },
|
|
48
|
+
visible: { type: [Function, Boolean], default: true },
|
|
49
|
+
handler: { type: Function, default: null },
|
|
50
|
+
data: { default: null },
|
|
51
|
+
},
|
|
52
|
+
data() {
|
|
53
|
+
return {
|
|
54
|
+
// used only to track visual state
|
|
55
|
+
clicked: false,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
computed: {
|
|
59
|
+
isVisible() {
|
|
60
|
+
if (typeof this.visible == 'boolean') {
|
|
61
|
+
return this.visible;
|
|
62
|
+
}
|
|
63
|
+
if (typeof this.visible == 'function') {
|
|
64
|
+
return this.visible(this.data);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
},
|
|
68
|
+
isDisabled() {
|
|
69
|
+
if (typeof this.disabled == 'boolean') {
|
|
70
|
+
return this.disabled;
|
|
71
|
+
}
|
|
72
|
+
if (typeof this.disabled == 'function') {
|
|
73
|
+
return this.disabled(this.data);
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
// watch: {},
|
|
79
|
+
mounted() {
|
|
80
|
+
this.clicked = false;
|
|
81
|
+
},
|
|
82
|
+
methods: {
|
|
83
|
+
onClick() {
|
|
84
|
+
this.clicked = true;
|
|
85
|
+
|
|
86
|
+
if (typeof this.handler == 'function') {
|
|
87
|
+
this.handler(this.data);
|
|
88
|
+
}
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
this.$emit('done');
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<style lang="scss">
|
|
98
|
+
@use '../styles/px.scss' as *;
|
|
99
|
+
|
|
100
|
+
.px-menu-item {
|
|
101
|
+
@include control-reset();
|
|
102
|
+
font-size: inherit;
|
|
103
|
+
background-color: inherit;
|
|
104
|
+
color: inherit;
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
gap: 0.5em;
|
|
108
|
+
min-width: 7em;
|
|
109
|
+
max-width: 20em;
|
|
110
|
+
white-space: wrap;
|
|
111
|
+
line-height: 1.33;
|
|
112
|
+
text-align: left;
|
|
113
|
+
padding: 0.33em 1em;
|
|
114
|
+
|
|
115
|
+
&__icon {
|
|
116
|
+
margin-left: -0.25em;
|
|
117
|
+
flex: none;
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
&__label {
|
|
123
|
+
white-space: nowrap;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
&--clicked,
|
|
127
|
+
&:hover {
|
|
128
|
+
background-color: clr(select);
|
|
129
|
+
color: contrast-color(clr(select));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
&:disabled {
|
|
133
|
+
background-color: transparent !important;
|
|
134
|
+
color: inherit !important;
|
|
135
|
+
opacity: 0.5;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
&--separator {
|
|
139
|
+
background-color: currentColor;
|
|
140
|
+
opacity: 0.25;
|
|
141
|
+
height: 1px;
|
|
142
|
+
min-height: 1px;
|
|
143
|
+
max-height: 1px;
|
|
144
|
+
padding: 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
</style>
|
package/components/PxSvg.vue
CHANGED
|
@@ -23,10 +23,11 @@
|
|
|
23
23
|
<inline-svg
|
|
24
24
|
class="px-svg"
|
|
25
25
|
:transformSource="transform"
|
|
26
|
-
:src="src"
|
|
26
|
+
:src="src || ''"
|
|
27
27
|
:viewBox="svgViewBox"
|
|
28
28
|
:height="calcHeight"
|
|
29
29
|
:width="calcWidth"
|
|
30
|
+
:title="title"
|
|
30
31
|
v-on="$listeners"
|
|
31
32
|
/>
|
|
32
33
|
</template>
|
|
@@ -59,6 +60,9 @@ export default {
|
|
|
59
60
|
|
|
60
61
|
// string replacements to be made on the svg (we replace black with currentColor by default)
|
|
61
62
|
replacements: { type: Object, default: null },
|
|
63
|
+
|
|
64
|
+
// set the title for accessibility
|
|
65
|
+
title: { type: String, default: null },
|
|
62
66
|
},
|
|
63
67
|
data() {
|
|
64
68
|
return { svgWidth: null, svgHeight: null, svgViewBox: null };
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
<PxTransition animation="fade up" />
|
|
3
|
+
-->
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<transition @enter="enter" @leave="leave">
|
|
7
|
+
<slot />
|
|
8
|
+
</transition>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script>
|
|
12
|
+
import gsap from 'gsap';
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
// components: { Component },
|
|
16
|
+
// mixins: [ Mixin ],
|
|
17
|
+
props: {
|
|
18
|
+
/*
|
|
19
|
+
* Specifies the show/hide animation. Should contain a mix of the following keywords
|
|
20
|
+
*
|
|
21
|
+
* - nofade - disables the standard fade (why would you want that?)
|
|
22
|
+
* - up - translate y: 50 -> 0 / 0 -> -50
|
|
23
|
+
* - down - translate y: 0 -> -50 / 0 -> 50
|
|
24
|
+
* - left - translate x: from -50 to 0 / 0 -> 50
|
|
25
|
+
* - right - translate x: from 50 to 0 / 0 -> -50
|
|
26
|
+
* - zoomin - scale x: 0.66 -> 1 / 1 -> 1.33
|
|
27
|
+
* - zoomout - scale x: 1.33 -> 1 / 1 -> 0.66
|
|
28
|
+
* - lg - scales values by 1.5
|
|
29
|
+
* - sm - scales values by 0.5
|
|
30
|
+
* - xs - scales values by 0.25
|
|
31
|
+
*
|
|
32
|
+
* Examples:
|
|
33
|
+
*
|
|
34
|
+
* <px-transition show="fade zoomin" hide="fade zoomout" />
|
|
35
|
+
*
|
|
36
|
+
* // if only one of show or hide are specified then we reverse missing transition so the following are equivalent
|
|
37
|
+
* <px-transition show="fade up" />
|
|
38
|
+
* <px-transition show="fade up" hide="fade down" />
|
|
39
|
+
*
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
animation: {
|
|
43
|
+
type: [String, Object],
|
|
44
|
+
default: 'fade',
|
|
45
|
+
},
|
|
46
|
+
duration: {
|
|
47
|
+
type: Number,
|
|
48
|
+
default: 0.5,
|
|
49
|
+
},
|
|
50
|
+
hideAnimation: {
|
|
51
|
+
type: [String, Object],
|
|
52
|
+
default: null,
|
|
53
|
+
},
|
|
54
|
+
easing: {
|
|
55
|
+
type: String,
|
|
56
|
+
default: 'power.out',
|
|
57
|
+
},
|
|
58
|
+
hideDuration: {
|
|
59
|
+
type: Number,
|
|
60
|
+
default: null,
|
|
61
|
+
},
|
|
62
|
+
hideEasing: {
|
|
63
|
+
type: String,
|
|
64
|
+
default: null,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
// data() { return { b: 0 }; },f
|
|
68
|
+
computed: {
|
|
69
|
+
transitionName() {
|
|
70
|
+
return `pxtx-${this.name}`;
|
|
71
|
+
},
|
|
72
|
+
showDur() {
|
|
73
|
+
return this.duration;
|
|
74
|
+
},
|
|
75
|
+
showEase() {
|
|
76
|
+
return this.easing;
|
|
77
|
+
},
|
|
78
|
+
hideDur() {
|
|
79
|
+
return this.hideDuration || this.duration;
|
|
80
|
+
},
|
|
81
|
+
hideEase() {
|
|
82
|
+
return this.hideEasing || this.easing;
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
// watch: {},
|
|
86
|
+
// mounted() {},
|
|
87
|
+
methods: {
|
|
88
|
+
keywordsToParameters(keywords, direction, reverse) {
|
|
89
|
+
const words = keywords.split(/[, ]+/).map(s => s.trim());
|
|
90
|
+
const fr = { x: 0, y: 0, scale: 1 };
|
|
91
|
+
const to = { x: 0, y: 0, scale: 1 };
|
|
92
|
+
const isHide = direction == 'hide';
|
|
93
|
+
const isShow = !isHide;
|
|
94
|
+
|
|
95
|
+
// parameter scale and percent
|
|
96
|
+
let s = 0.33;
|
|
97
|
+
s = words.includes('xl') ? 0.75 : s;
|
|
98
|
+
s = words.includes('lg') ? 0.5 : s;
|
|
99
|
+
s = words.includes('sm') ? 0.15 : s;
|
|
100
|
+
s = words.includes('xs') ? 0.05 : s;
|
|
101
|
+
|
|
102
|
+
if (words && words.length) {
|
|
103
|
+
// fade - animates opacity: 0 -> 1 / 1 -> 0
|
|
104
|
+
if (!words.includes('nofade')) {
|
|
105
|
+
fr['opacity'] = isShow ? 0 : 1;
|
|
106
|
+
to['opacity'] = isShow ? 1 : 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// up - translate y: 50 -> 0 / 0 -> -50
|
|
110
|
+
if (words.includes('up')) {
|
|
111
|
+
fr['y'] = isShow ? s : 0;
|
|
112
|
+
to['y'] = isShow ? 0 : -s;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// down - translate y: -50 -> 0 / 0 -> 50
|
|
116
|
+
if (words.includes('down')) {
|
|
117
|
+
fr['y'] = isShow ? -s : 0;
|
|
118
|
+
to['y'] = isShow ? 0 : s;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// left - translate x: from -100 to 0 / 0 -> 100
|
|
122
|
+
if (words.includes('right')) {
|
|
123
|
+
fr['x'] = isShow ? -s : 0;
|
|
124
|
+
to['x'] = isShow ? 0 : s;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// right - translate x: from 0.33 to 0 / 0 -> -0.33
|
|
128
|
+
if (words.includes('left')) {
|
|
129
|
+
fr['x'] = isShow ? s : 0;
|
|
130
|
+
to['x'] = isShow ? 0 : -s;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// zoomin - scale x: 0.66 -> 1 / 1 -> 1.33
|
|
134
|
+
if (words.includes('zoomin')) {
|
|
135
|
+
fr['scale'] = isShow ? 1 - s : 1;
|
|
136
|
+
to['scale'] = isShow ? 1 : 1 + s;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// zoomout - scale x: 1.33 -> 1 / 1 -> 0.66
|
|
140
|
+
if (words.includes('zoomout')) {
|
|
141
|
+
fr['scale'] = isShow ? 1 + s : 1;
|
|
142
|
+
to['scale'] = isShow ? 1 : 1 + s;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// convert tranlsate to percentages
|
|
147
|
+
|
|
148
|
+
to.y = this.cssPcnt(to.y);
|
|
149
|
+
fr.y = this.cssPcnt(fr.y);
|
|
150
|
+
to.x = this.cssPcnt(to.x);
|
|
151
|
+
fr.x = this.cssPcnt(fr.x);
|
|
152
|
+
|
|
153
|
+
if (reverse) {
|
|
154
|
+
return {
|
|
155
|
+
from: to,
|
|
156
|
+
to: fr,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
from: fr,
|
|
161
|
+
to: to,
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
enter(element, done) {
|
|
165
|
+
const params = this.keywordsToParameters(this.animation, 'show');
|
|
166
|
+
if (params) {
|
|
167
|
+
gsap.fromTo(element, params.from, {
|
|
168
|
+
...params.to,
|
|
169
|
+
duration: this.showDur,
|
|
170
|
+
ease: this.showEase,
|
|
171
|
+
onComplete: done,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
leave(element, done) {
|
|
176
|
+
const params = this.hideAnimation
|
|
177
|
+
? this.keywordsToParameters(this.hideAnimation, 'hide')
|
|
178
|
+
: this.keywordsToParameters(this.animation, 'show', true);
|
|
179
|
+
if (params) {
|
|
180
|
+
gsap.to(element, {
|
|
181
|
+
...params.to,
|
|
182
|
+
duration: this.hideDur,
|
|
183
|
+
ease: this.hideEase,
|
|
184
|
+
onComplete: done,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
<style lang="scss">
|
|
193
|
+
@use '../styles/px.scss' as *;
|
|
194
|
+
|
|
195
|
+
// @at-root {
|
|
196
|
+
// body {
|
|
197
|
+
// --pxtx-transition-dur: 0.5s;
|
|
198
|
+
// }
|
|
199
|
+
// }
|
|
200
|
+
|
|
201
|
+
// $pxtx-dur: var(--pxtx-transition-dur);
|
|
202
|
+
|
|
203
|
+
// @include vue-transition(pxtx-fade) {
|
|
204
|
+
// // define the hidden state (the non-hidden state is implicty -- the normal state)
|
|
205
|
+
// @include vt-transition {
|
|
206
|
+
// @include transition(opacity, $dur: $pxtx-dur);
|
|
207
|
+
// }
|
|
208
|
+
|
|
209
|
+
// the hidden state before entering
|
|
210
|
+
// @include vt-hidden-enter {
|
|
211
|
+
// //transform: translateY(-100px);
|
|
212
|
+
// opacity: 0;
|
|
213
|
+
// }
|
|
214
|
+
|
|
215
|
+
// // the hidden after exiting
|
|
216
|
+
// @include vt-hidden-leave {
|
|
217
|
+
// //transform: translateY(100px);
|
|
218
|
+
// opacity: 0;
|
|
219
|
+
// }
|
|
220
|
+
// }
|
|
221
|
+
|
|
222
|
+
// @include vue-transition(pxtx-fade-up) {
|
|
223
|
+
// // define the hidden state (the non-hidden state is implicty -- the normal state)
|
|
224
|
+
// @include vt-transition {
|
|
225
|
+
// @include transition(opacity transform, $dur: 3s);
|
|
226
|
+
// //transform: translateY(0px);
|
|
227
|
+
// }
|
|
228
|
+
|
|
229
|
+
// // the hidden state before entering
|
|
230
|
+
// @include vt-hidden-enter {
|
|
231
|
+
// transform: scale(0.5);
|
|
232
|
+
// opacity: 0;
|
|
233
|
+
// }
|
|
234
|
+
|
|
235
|
+
// // the hidden after exiting
|
|
236
|
+
// @include vt-hidden-leave {
|
|
237
|
+
// transform: scale(0.5);
|
|
238
|
+
// opacity: 0;
|
|
239
|
+
// }
|
|
240
|
+
// } ;
|
|
241
|
+
</style>
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
<!-- mouse -->
|
|
43
43
|
<div :class="bem('button')" @click="showMouseFloat">Mouse</div>
|
|
44
|
-
<px-float ref="mouseFloat" :placement="'bottom-start'" variant="
|
|
44
|
+
<px-float ref="mouseFloat" :placement="'bottom-start'" variant="dialog sm">
|
|
45
45
|
Positioned with showAtCursor! (Note: needs to be done pogramatically because we only
|
|
46
46
|
have access to mouse coordinates in a mouse event.)
|
|
47
47
|
</px-float>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Describe this component...
|
|
3
|
+
-->
|
|
4
|
+
<template>
|
|
5
|
+
<div :class="bem()">
|
|
6
|
+
<PxMenu
|
|
7
|
+
ref="menu"
|
|
8
|
+
:items="[
|
|
9
|
+
{
|
|
10
|
+
id: 'copy',
|
|
11
|
+
label: 'Copy',
|
|
12
|
+
icon: require('../../assets/feather/copy.svg'),
|
|
13
|
+
iconSize: 1,
|
|
14
|
+
handler: onCopy,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'paste',
|
|
18
|
+
label: 'Paste',
|
|
19
|
+
icon: require('../../assets/feather/clipboard.svg'),
|
|
20
|
+
iconSize: 1,
|
|
21
|
+
handler: onPaste,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'sep1',
|
|
25
|
+
separator: true,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'paste2',
|
|
29
|
+
label: 'Paste',
|
|
30
|
+
|
|
31
|
+
iconSize: 1,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'paste3',
|
|
35
|
+
label: 'Paste',
|
|
36
|
+
icon: require('../../assets/feather/clipboard.svg'),
|
|
37
|
+
iconSize: 1.5,
|
|
38
|
+
},
|
|
39
|
+
]"
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<div :class="bem('target')" v-context-menu:[mydata]="'menu'" v-if="visible">
|
|
43
|
+
Right Click Me!
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<button @click="visible = !visible">{{ visible ? 'Hide' : 'Show' }}</button>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script>
|
|
51
|
+
// import { mapState } from 'vuex';
|
|
52
|
+
// import Component from '~/components/Component.vue';
|
|
53
|
+
// import Mixin from '~/components/Mixin.vue';
|
|
54
|
+
|
|
55
|
+
import { contextMenuDirective } from '../../components/PxMenu.vue';
|
|
56
|
+
import PxMenu from '../../components/PxMenu.vue';
|
|
57
|
+
|
|
58
|
+
export default {
|
|
59
|
+
name: 'demo-px-menu',
|
|
60
|
+
components: { PxMenu },
|
|
61
|
+
directives: { contextMenu: contextMenuDirective },
|
|
62
|
+
// mixins: [ Mixin ],
|
|
63
|
+
// props: { a: { type: Boolean, default: false } },
|
|
64
|
+
data() {
|
|
65
|
+
return { visible: true, mydata: 'This is some data.' };
|
|
66
|
+
},
|
|
67
|
+
// computed: {},
|
|
68
|
+
// watch: {},
|
|
69
|
+
// mounted() {},
|
|
70
|
+
methods: {
|
|
71
|
+
onCopy(data) {
|
|
72
|
+
console.log('copy: ' + data);
|
|
73
|
+
},
|
|
74
|
+
onPaste(data) {
|
|
75
|
+
console.log('paste: ' + data);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<style lang="scss">
|
|
82
|
+
@use '../../styles/px.scss' as *;
|
|
83
|
+
.demo-px-menu {
|
|
84
|
+
&__target {
|
|
85
|
+
width: 300px;
|
|
86
|
+
height: 300px;
|
|
87
|
+
background-color: orange;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
</style>
|
package/dev/dev.vue
CHANGED
|
@@ -18,6 +18,7 @@ import DemoPxStylesPreview from './demos/Demo-PxStylesPreview.vue';
|
|
|
18
18
|
import DemoPxPivot from './demos/Demo-PxPivot.vue';
|
|
19
19
|
import DemoPxFloat from './demos/Demo-PxFloat.vue';
|
|
20
20
|
import DemoPxContain from './demos/Demo-PxContain.vue';
|
|
21
|
+
import DemoPxMenu from './demos/Demo-PxMenu.vue';
|
|
21
22
|
|
|
22
23
|
export default Vue.extend({
|
|
23
24
|
components: {
|
|
@@ -32,6 +33,7 @@ export default Vue.extend({
|
|
|
32
33
|
DemoPxPivot,
|
|
33
34
|
DemoPxFloat,
|
|
34
35
|
DemoPxContain,
|
|
36
|
+
DemoPxMenu,
|
|
35
37
|
},
|
|
36
38
|
name: 'ServeDev',
|
|
37
39
|
data() {
|
|
@@ -44,7 +46,7 @@ export default Vue.extend({
|
|
|
44
46
|
const params = new Proxy(new URLSearchParams(window.location.search), {
|
|
45
47
|
get: (searchParams, prop) => searchParams.get(prop),
|
|
46
48
|
});
|
|
47
|
-
this.componentToRender = params.component || '
|
|
49
|
+
this.componentToRender = params.component || 'DemoPxMenu';
|
|
48
50
|
console.log(`this.componentToRender: ${this.componentToRender}`);
|
|
49
51
|
},
|
|
50
52
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinkpixellab-public/px-vue",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.69",
|
|
4
4
|
"description": "General purpose Vue components and helpers that can be used across projects.",
|
|
5
5
|
"author": "Pixel Lab",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"url": "git+https://github.com/thinkpixellab/px-vue.git"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"serve": "vue-cli-service serve dev/serve.js"
|
|
12
|
+
"serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve dev/serve.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@floating-ui/dom": "^1.0.1",
|
package/utils/points.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// rounds things to 5 decimals (removes a lot of weird sin/cos noise)
|
|
2
|
+
function fineRound(v) {
|
|
3
|
+
return Math.round(v * 10000) / 10000;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the bounding box for an array of points {x,y}
|
|
8
|
+
*/
|
|
9
|
+
export function getBoundingBox(points) {
|
|
10
|
+
if (points.length === 0) {
|
|
11
|
+
return null; // Return null for an empty array
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let minX = points[0].x;
|
|
15
|
+
let maxX = points[0].x;
|
|
16
|
+
let minY = points[0].y;
|
|
17
|
+
let maxY = points[0].y;
|
|
18
|
+
|
|
19
|
+
// Find the minimum and maximum x and y values
|
|
20
|
+
for (let i = 1; i < points.length; i++) {
|
|
21
|
+
const point = points[i];
|
|
22
|
+
minX = Math.min(minX, point.x);
|
|
23
|
+
maxX = Math.max(maxX, point.x);
|
|
24
|
+
minY = Math.min(minY, point.y);
|
|
25
|
+
maxY = Math.max(maxY, point.y);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Calculate the width and height of the bounding box
|
|
29
|
+
const width = maxX - minX;
|
|
30
|
+
const height = maxY - minY;
|
|
31
|
+
|
|
32
|
+
// Return the bounding box rectangle
|
|
33
|
+
return {
|
|
34
|
+
x: minX,
|
|
35
|
+
y: minY,
|
|
36
|
+
width: width,
|
|
37
|
+
height: height,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @returns A new array contain all points in the points array after having been rotated angleDeg
|
|
43
|
+
* degrees around the center of the combined points bounding box.
|
|
44
|
+
*/
|
|
45
|
+
export function rotatePoints(points, angleDeg) {
|
|
46
|
+
if (angleDeg == 0) {
|
|
47
|
+
return points;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let bounds = getBoundingBox(points);
|
|
51
|
+
|
|
52
|
+
// distance to the origin
|
|
53
|
+
const dx = (bounds.x + bounds.width) / 2;
|
|
54
|
+
const dy = (bounds.y + bounds.height) / 2;
|
|
55
|
+
|
|
56
|
+
points = points.map(p => {
|
|
57
|
+
const x = p.x - dx;
|
|
58
|
+
const y = p.y - dy;
|
|
59
|
+
const angleRad = (angleDeg * Math.PI) / 180;
|
|
60
|
+
const cosTheta = Math.cos(angleRad);
|
|
61
|
+
const sinTheta = Math.sin(angleRad);
|
|
62
|
+
const newX = x * cosTheta - y * sinTheta;
|
|
63
|
+
const newY = x * sinTheta + y * cosTheta;
|
|
64
|
+
return { x: fineRound(newX + dx), y: fineRound(newY + dy) };
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// shift back to the origin
|
|
68
|
+
bounds = getBoundingBox(points);
|
|
69
|
+
points = shiftPoints(points, bounds.x, bounds.y);
|
|
70
|
+
|
|
71
|
+
return points;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Shifts each point in an array of points {x,y} by dx, dy
|
|
76
|
+
*/
|
|
77
|
+
export function shiftPoints(points, dx, dy) {
|
|
78
|
+
return points.map(p => {
|
|
79
|
+
return {
|
|
80
|
+
x: p.x - dx,
|
|
81
|
+
y: p.y - dy,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Takes an angle in degrees and a line length and returns a point representing the end of that line
|
|
88
|
+
* relative to the origin
|
|
89
|
+
* @param {*} angleInDegrees an angle in degrees
|
|
90
|
+
* @param {*} lineLength a length
|
|
91
|
+
* @returns a point {x, y}
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
export function getLineEndPoint(angleInDegrees, lineLength) {
|
|
95
|
+
const angleInRadians = (angleInDegrees * Math.PI) / 180;
|
|
96
|
+
return {
|
|
97
|
+
x: fineRound(lineLength * Math.cos(angleInRadians)),
|
|
98
|
+
y: fineRound(lineLength * Math.sin(angleInRadians)),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getLineRect(angleInDegrees, lineLength) {
|
|
103
|
+
const endPoint = getLineEndPoint(angleInDegrees, lineLength);
|
|
104
|
+
return {
|
|
105
|
+
x: 0,
|
|
106
|
+
y: 0,
|
|
107
|
+
width: Math.abs(endPoint.x),
|
|
108
|
+
height: Math.abs(endPoint.y),
|
|
109
|
+
};
|
|
110
|
+
}
|