Profectus/src/components/board/BoardNode.vue

401 lines
13 KiB
Vue
Raw Normal View History

<template>
<g
class="boardnode"
:style="{ opacity: dragging?.id === node.id && hasDragged ? 0.5 : 1 }"
:transform="`translate(${position.x},${position.y})`"
>
<transition name="actions" appear>
<g v-if="selected && actions">
<!-- TODO move to separate file -->
<g
v-for="(action, index) in actions"
:key="action.id"
class="action"
:class="{ selected: selectedAction === action }"
:transform="
`translate(
${(-size - 30) *
Math.sin(((actions.length - 1) / 2 - index) * actionDistance)},
${(size + 30) *
Math.cos(((actions.length - 1) / 2 - index) * actionDistance)}
)`
"
@mousedown="e => performAction(e, action)"
@touchstart="e => performAction(e, action)"
@mouseup="e => actionMouseUp(e, action)"
@touchend.stop="e => actionMouseUp(e, action)"
>
<circle
:fill="
action.fillColor
? typeof action.fillColor === 'function'
? action.fillColor(node)
: action.fillColor
: fillColor
"
r="20"
:stroke-width="selectedAction === action ? 4 : 0"
:stroke="outlineColor"
/>
<text :fill="titleColor" class="material-icons">{{
typeof action.icon === "function" ? action.icon(node) : action.icon
}}</text>
</g>
</g>
</transition>
<g
class="node-container"
@mouseenter="mouseEnter"
@mouseleave="mouseLeave"
@mousedown="mouseDown"
@touchstart="mouseDown"
@mouseup="mouseUp"
@touchend="mouseUp"
>
<g v-if="shape === Shape.Circle">
<circle
v-if="canAccept"
:r="size + 8"
:fill="backgroundColor"
:stroke="receivingNode ? '#0F0' : '#0F03'"
:stroke-width="2"
/>
<circle :r="size" :fill="fillColor" :stroke="outlineColor" :stroke-width="4" />
<circle
v-if="progressDisplay === ProgressDisplay.Fill"
:r="Math.max(size * progress - 2, 0)"
:fill="progressColor"
/>
<circle
v-else
:r="size + 4.5"
class="progressRing"
fill="transparent"
:stroke-dasharray="(size + 4.5) * 2 * Math.PI"
:stroke-width="5"
:stroke-dashoffset="
(size + 4.5) * 2 * Math.PI - progress * (size + 4.5) * 2 * Math.PI
"
:stroke="progressColor"
/>
</g>
<g v-else-if="shape === Shape.Diamond" transform="rotate(45, 0, 0)">
<rect
v-if="canAccept"
:width="size * sqrtTwo + 16"
:height="size * sqrtTwo + 16"
:transform="
`translate(${-(size * sqrtTwo + 16) / 2}, ${-(size * sqrtTwo + 16) /
2})`
"
:fill="backgroundColor"
:stroke="receivingNode ? '#0F0' : '#0F03'"
:stroke-width="2"
/>
2021-08-20 23:21:13 -05:00
<rect
:width="size * sqrtTwo"
:height="size * sqrtTwo"
:transform="`translate(${(-size * sqrtTwo) / 2}, ${(-size * sqrtTwo) / 2})`"
:fill="fillColor"
:stroke="outlineColor"
:stroke-width="4"
/>
<rect
v-if="progressDisplay === ProgressDisplay.Fill"
:width="Math.max(size * sqrtTwo * progress - 2, 0)"
:height="Math.max(size * sqrtTwo * progress - 2, 0)"
:transform="
`translate(${-Math.max(size * sqrtTwo * progress - 2, 0) /
2}, ${-Math.max(size * sqrtTwo * progress - 2, 0) / 2})`
"
:fill="progressColor"
/>
<rect
v-else
:width="size * sqrtTwo + 9"
:height="size * sqrtTwo + 9"
:transform="
`translate(${-(size * sqrtTwo + 9) / 2}, ${-(size * sqrtTwo + 9) / 2})`
"
fill="transparent"
:stroke-dasharray="(size * sqrtTwo + 9) * 4"
:stroke-width="5"
:stroke-dashoffset="
(size * sqrtTwo + 9) * 4 - progress * (size * sqrtTwo + 9) * 4
"
:stroke="progressColor"
/>
</g>
<text :fill="titleColor" class="node-title">{{ title }}</text>
</g>
<transition name="fade" appear>
<text
v-if="label"
:fill="label.color || titleColor"
class="node-title"
:class="{ pulsing: label.pulsing }"
:y="-size - 20"
>{{ label.text }}</text
>
</transition>
<transition name="fade" appear>
<text
:fill="titleColor"
class="node-title"
:y="size + 75"
v-if="selected && selectedAction"
>Tap again to confirm</text
>
</transition>
</g>
</template>
<script lang="ts">
import themes from "@/data/themes";
2021-08-20 23:21:13 -05:00
import { ProgressDisplay, Shape } from "@/game/enums";
import { layers } from "@/game/layers";
import player from "@/game/player";
import { BoardNode, BoardNodeAction, NodeLabel, NodeType } from "@/typings/features/board";
import { getNodeTypeProperty } from "@/util/features";
import { defineComponent, PropType } from "vue";
export default defineComponent({
name: "BoardNode",
data() {
return {
ProgressDisplay,
2021-08-20 23:21:13 -05:00
Shape,
hovering: false,
sqrtTwo: Math.sqrt(2)
};
},
emits: ["mouseDown", "endDragging"],
props: {
node: {
type: Object as PropType<BoardNode>,
required: true
},
nodeType: {
type: Object as PropType<NodeType>,
required: true
},
dragging: {
type: Object as PropType<BoardNode>
},
dragged: {
type: Object as PropType<{ x: number; y: number }>,
required: true
},
hasDragged: {
type: Boolean,
default: false
},
receivingNode: {
type: Boolean,
default: false
}
},
computed: {
board() {
return layers[this.nodeType.layer].boards!.data[this.nodeType.id];
},
selected() {
return this.board.selectedNode === this.node;
},
selectedAction() {
return this.board.selectedAction;
},
actions(): BoardNodeAction[] | null | undefined {
return getNodeTypeProperty(this.nodeType, this.node, "actions");
},
draggable(): boolean {
return getNodeTypeProperty(this.nodeType, this.node, "draggable");
},
position(): { x: number; y: number } {
return this.draggable && this.dragging?.id === this.node.id
? {
x: this.node.position.x + Math.round(this.dragged.x / 25) * 25,
y: this.node.position.y + Math.round(this.dragged.y / 25) * 25
}
: this.node.position;
},
2021-08-20 23:21:13 -05:00
shape(): Shape {
return getNodeTypeProperty(this.nodeType, this.node, "shape");
},
size(): number {
let size: number = getNodeTypeProperty(this.nodeType, this.node, "size");
if (this.receivingNode) {
size *= 1.25;
} else if (this.hovering || this.selected) {
size *= 1.15;
}
return size;
},
title(): string {
return getNodeTypeProperty(this.nodeType, this.node, "title");
},
label(): NodeLabel | null | undefined {
return getNodeTypeProperty(this.nodeType, this.node, "label");
},
progress(): number {
return getNodeTypeProperty(this.nodeType, this.node, "progress") || 0;
},
backgroundColor(): string {
return themes[player.theme].variables["--background"];
},
outlineColor(): string {
return (
getNodeTypeProperty(this.nodeType, this.node, "outlineColor") ||
themes[player.theme].variables["--separator"]
);
},
fillColor(): string {
return (
getNodeTypeProperty(this.nodeType, this.node, "fillColor") ||
themes[player.theme].variables["--secondary-background"]
);
},
progressColor(): string {
return getNodeTypeProperty(this.nodeType, this.node, "progressColor") || "none";
},
titleColor(): string {
return (
getNodeTypeProperty(this.nodeType, this.node, "titleColor") ||
themes[player.theme].variables["--color"]
);
},
progressDisplay(): ProgressDisplay {
return (
getNodeTypeProperty(this.nodeType, this.node, "progressDisplay") ||
ProgressDisplay.Outline
);
},
canAccept(): boolean {
if (this.dragging == null || !this.hasDragged) {
return false;
}
return typeof this.nodeType.canAccept === "boolean"
? this.nodeType.canAccept
: this.nodeType.canAccept(this.node, this.dragging);
},
actionDistance(): number {
return getNodeTypeProperty(this.nodeType, this.node, "actionDistance");
}
},
methods: {
2021-08-24 19:19:55 -05:00
mouseDown(e: MouseEvent | TouchEvent) {
this.$emit("mouseDown", e, this.node.id, this.draggable);
},
mouseUp() {
if (!this.hasDragged) {
this.nodeType.onClick?.(this.node);
2021-08-20 23:53:40 -05:00
}
},
mouseEnter() {
this.hovering = true;
},
mouseLeave() {
this.hovering = false;
},
2021-08-24 19:19:55 -05:00
performAction(e: MouseEvent | TouchEvent, action: BoardNodeAction) {
// If the onClick function made this action selected,
// don't propagate the event (which will deselect everything)
2021-08-24 08:18:55 -05:00
if (action.onClick(this.node) || this.board.selectedAction === action) {
e.preventDefault();
e.stopPropagation();
}
},
2021-08-24 19:19:55 -05:00
actionMouseUp(e: MouseEvent | TouchEvent, action: BoardNodeAction) {
if (this.board.selectedAction === action) {
e.preventDefault();
e.stopPropagation();
}
}
},
watch: {
onDraggableChanged() {
if (this.dragging === this.node && !this.draggable) {
this.$emit("endDragging", this.node.id);
}
}
}
});
</script>
<style scoped>
.boardnode {
cursor: pointer;
transition-duration: 0s;
}
.node-title {
text-anchor: middle;
dominant-baseline: middle;
font-family: monospace;
font-size: 200%;
pointer-events: none;
}
.progressRing {
transform: rotate(-90deg);
}
.action:hover circle,
.action.selected circle {
r: 25;
}
.action:hover text,
.action.selected text {
font-size: 187.5%; /* 150% * 1.25 */
}
.action text {
text-anchor: middle;
dominant-baseline: central;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.pulsing {
animation: pulsing 2s ease-in infinite;
}
@keyframes pulsing {
0% {
opacity: 0.25;
}
50% {
opacity: 1;
}
100% {
opacity: 0.25;
}
}
</style>
<style>
.actions-enter-from .action,
.actions-leave-to .action {
transform: translate(0, 0);
}
.grow-enter-from .node-container,
.grow-leave-to .node-container {
transform: scale(0);
}
</style>