diff --git a/src/features/bars/Bar.vue b/src/features/bars/Bar.vue index 3c4b91c..89315bb 100644 --- a/src/features/bars/Bar.vue +++ b/src/features/bars/Bar.vue @@ -179,5 +179,6 @@ export default defineComponent({ margin-left: -0.5px; transition-duration: 0.2s; z-index: 2; + transition-duration: 0.05s; } </style> diff --git a/src/features/boards/Board.vue b/src/features/boards/Board.vue index f1f8b83..2605aaa 100644 --- a/src/features/boards/Board.vue +++ b/src/features/boards/Board.vue @@ -1,7 +1,6 @@ <template> <panZoom v-if="isVisible(visibility)" - v-show="isHidden(visibility)" :style="[ { width, @@ -25,7 +24,10 @@ <svg class="stage" width="100%" height="100%"> <g class="g1"> <transition-group name="link" appear> - <g v-for="(link, i) in unref(links) || []" :key="i"> + <g + v-for="link in unref(links) || []" + :key="`${link.startNode.id}-${link.endNode.id}`" + > <BoardLinkVue :link="link" /> </g> </transition-group> @@ -35,7 +37,7 @@ :node="node" :nodeType="types[node.type]" :dragging="draggingNode" - :dragged="dragged" + :dragged="draggingNode === node ? dragged : undefined" :hasDragged="hasDragged" :receivingNode="receivingNode?.id === node.id" :selectedNode="unref(selectedNode)" @@ -60,9 +62,9 @@ import type { } from "features/boards/board"; import { getNodeProperty } from "features/boards/board"; import type { StyleValue } from "features/feature"; -import { isHidden, isVisible, Visibility } from "features/feature"; +import { Visibility, isVisible } from "features/feature"; import type { ProcessedComputable } from "util/computed"; -import { computed, ref, Ref, toRefs, unref } from "vue"; +import { Ref, computed, ref, toRefs, unref } from "vue"; import BoardLinkVue from "./BoardLink.vue"; import BoardNodeVue from "./BoardNode.vue"; @@ -138,6 +140,7 @@ const receivingNode = computed(() => { // eslint-disable-next-line @typescript-eslint/no-explicit-any function onInit(panzoomInstance: any) { panzoomInstance.setTransformOrigin(null); + panzoomInstance.moveTo(stage.value.$el.clientWidth / 2, stage.value.$el.clientHeight / 2); } function mouseDown(e: MouseEvent | TouchEvent, nodeID: number | null = null, draggable = false) { @@ -222,8 +225,7 @@ function endDragging(nodeID: number | null) { draggingNode.value.position.y += Math.round(dragged.value.y / 25) * 25; const nodes = props.nodes.value; - nodes.splice(nodes.indexOf(draggingNode.value), 1); - nodes.push(draggingNode.value); + nodes.push(nodes.splice(nodes.indexOf(draggingNode.value), 1)[0]); if (receivingNode.value) { props.types.value[receivingNode.value.type].onDrop?.( diff --git a/src/features/boards/BoardLink.vue b/src/features/boards/BoardLink.vue index cb61914..441d996 100644 --- a/src/features/boards/BoardLink.vue +++ b/src/features/boards/BoardLink.vue @@ -39,6 +39,10 @@ const endPosition = computed(() => { </script> <style scoped> +.link { + transition-duration: 0s; +} + .link.pulsing { animation: pulsing 2s ease-in infinite; } diff --git a/src/features/boards/BoardNode.vue b/src/features/boards/BoardNode.vue index 931c6ee..e493f93 100644 --- a/src/features/boards/BoardNode.vue +++ b/src/features/boards/BoardNode.vue @@ -1,50 +1,19 @@ <template> <g class="boardnode" - :class="node.type" + :class="{ [node.type]: true, isSelected, isDraggable }" :style="{ opacity: dragging?.id === node.id && hasDragged ? 0.5 : 1 }" :transform="`translate(${position.x},${position.y})`" > - <transition name="actions" appear> - <g v-if="isSelected && actions"> - <!-- TODO move to separate file --> - <g - v-for="(action, index) in actions" - :key="action.id" - class="action" - :class="{ selected: selectedAction?.id === action.id }" - :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="getNodeProperty(action.fillColor, node)" - r="20" - :stroke-width="selectedAction?.id === action.id ? 4 : 0" - :stroke="outlineColor" - /> - <text :fill="titleColor" class="material-icons">{{ - getNodeProperty(action.icon, node) - }}</text> - </g> - </g> - </transition> + <BoardNodeAction + :actions="actions ?? []" + :is-selected="isSelected" + :node="node" + :node-type="nodeType" + /> <g class="node-container" - @mouseenter="isHovering = true" - @mouseleave="isHovering = false" @mousedown="mouseDown" @touchstart.passive="mouseDown" @mouseup="mouseUp" @@ -69,7 +38,7 @@ /> <circle - class="progressFill" + class="progress progressFill" v-if="progressDisplay === ProgressDisplay.Fill" :r="Math.max(size * progress - 2, 0)" :fill="progressColor" @@ -77,7 +46,7 @@ <circle v-else :r="size + 4.5" - class="progressRing" + class="progress progressRing" fill="transparent" :stroke-dasharray="(size + 4.5) * 2 * Math.PI" :stroke-width="5" @@ -113,7 +82,7 @@ <rect v-if="progressDisplay === ProgressDisplay.Fill" - class="progressFill" + class="progress progressFill" :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}, ${ @@ -123,7 +92,7 @@ /> <rect v-else - class="progressDiamond" + class="progress progressDiamond" :width="size * sqrtTwo + 9" :height="size * sqrtTwo + 9" :transform="`translate(${-(size * sqrtTwo + 9) / 2}, ${ @@ -173,6 +142,7 @@ import { ProgressDisplay, getNodeProperty, Shape } from "features/boards/board"; import { isVisible } from "features/feature"; import settings from "game/settings"; import { computed, ref, toRefs, unref, watch } from "vue"; +import BoardNodeAction from "./BoardNodeAction.vue"; const sqrtTwo = Math.sqrt(2); @@ -195,7 +165,6 @@ const emit = defineEmits<{ (e: "endDragging", node: number): void; }>(); -const isHovering = ref(false); const isSelected = computed(() => unref(props.selectedNode) === unref(props.node)); const isDraggable = computed(() => getNodeProperty(props.nodeType.value.draggable, unref(props.node)) @@ -217,16 +186,20 @@ const actions = computed(() => { const position = computed(() => { const node = unref(props.node); - const dragged = unref(props.dragged); - return getNodeProperty(props.nodeType.value.draggable, node) && + if ( + getNodeProperty(props.nodeType.value.draggable, node) && unref(props.dragging)?.id === node.id && - dragged - ? { - x: node.position.x + Math.round(dragged.x / 25) * 25, - y: node.position.y + Math.round(dragged.y / 25) * 25 - } - : node.position; + unref(props.dragged) != null + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { x, y } = unref(props.dragged)!; + return { + x: node.position.x + Math.round(x / 25) * 25, + y: node.position.y + Math.round(y / 25) * 25 + }; + } + return node.position; }); const shape = computed(() => getNodeProperty(props.nodeType.value.shape, unref(props.node))); @@ -264,32 +237,14 @@ const canAccept = computed( unref(props.hasDragged) && getNodeProperty(props.nodeType.value.canAccept, unref(props.node)) ); -const actionDistance = computed(() => - getNodeProperty(props.nodeType.value.actionDistance, unref(props.node)) -); function mouseDown(e: MouseEvent | TouchEvent) { emit("mouseDown", e, props.node.value.id, isDraggable.value); } -function mouseUp() { +function mouseUp(e: MouseEvent | TouchEvent) { if (!props.hasDragged?.value) { props.nodeType.value.onClick?.(props.node.value); - } -} - -function performAction(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) { - // If the onClick function made this action selected, - // don't propagate the event (which will deselect everything) - if (action.onClick(unref(props.node)) || unref(props.selectedAction)?.id === action.id) { - e.preventDefault(); - e.stopPropagation(); - } -} - -function actionMouseUp(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) { - if (unref(props.selectedAction)?.id === action.id) { - e.preventDefault(); e.stopPropagation(); } } @@ -301,6 +256,22 @@ function actionMouseUp(e: MouseEvent | TouchEvent, action: GenericBoardNodeActio transition-duration: 0s; } +.boardnode:hover .body { + fill: var(--highlighted); +} + +.boardnode.isSelected { + transform: scale(1.2); +} + +.boardnode.isSelected .body { + fill: var(--accent1) !important; +} + +.boardnode:not(.isDraggable) .body { + fill: var(--locked); +} + .node-title { text-anchor: middle; dominant-baseline: middle; @@ -309,25 +280,14 @@ function actionMouseUp(e: MouseEvent | TouchEvent, action: GenericBoardNodeActio pointer-events: none; } +.progress { + transition-duration: 0.05s; +} + .progressRing { transform: rotate(-90deg); } -.action:not(.boardnode):hover circle, -.action:not(.boardnode).selected circle { - r: 25; -} - -.action:not(.boardnode):hover text, -.action:not(.boardnode).selected text { - font-size: 187.5%; /* 150% * 1.25 */ -} - -.action:not(.boardnode) text { - text-anchor: middle; - dominant-baseline: central; -} - .fade-enter-from, .fade-leave-to { opacity: 0; @@ -353,11 +313,6 @@ function actionMouseUp(e: MouseEvent | TouchEvent, action: GenericBoardNodeActio </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); diff --git a/src/features/boards/BoardNodeAction.vue b/src/features/boards/BoardNodeAction.vue new file mode 100644 index 0000000..c67a802 --- /dev/null +++ b/src/features/boards/BoardNodeAction.vue @@ -0,0 +1,109 @@ +<template> + <transition name="actions" appear> + <g v-if="isSelected && actions"> + <!-- TODO move to separate file --> + <g + v-for="(action, index) in actions" + :key="action.id" + class="action" + :class="{ selected: selectedAction?.id === action.id }" + :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="getNodeProperty(action.fillColor, node)" + r="20" + :stroke-width="selectedAction?.id === action.id ? 4 : 0" + :stroke="outlineColor" + /> + <text :fill="titleColor" class="material-icons">{{ + getNodeProperty(action.icon, node) + }}</text> + </g> + </g> + </transition> +</template> + +<script setup lang="ts"> +import themes from "data/themes"; +import type { BoardNode, GenericBoardNodeAction, GenericNodeType } from "features/boards/board"; +import { getNodeProperty } from "features/boards/board"; +import settings from "game/settings"; +import { computed, toRefs, unref } from "vue"; + +const _props = defineProps<{ + node: BoardNode; + nodeType: GenericNodeType; + actions?: GenericBoardNodeAction[]; + isSelected: boolean; + selectedAction?: GenericBoardNodeAction; +}>(); +const props = toRefs(_props); + +const size = computed(() => getNodeProperty(props.nodeType.value.size, unref(props.node))); +const outlineColor = computed( + () => + getNodeProperty(props.nodeType.value.outlineColor, unref(props.node)) ?? + themes[settings.theme].variables["--outline"] +); +const titleColor = computed( + () => + getNodeProperty(props.nodeType.value.titleColor, unref(props.node)) ?? + themes[settings.theme].variables["--foreground"] +); +const actionDistance = computed(() => + getNodeProperty(props.nodeType.value.actionDistance, unref(props.node)) +); + +function performAction(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) { + // If the onClick function made this action selected, + // don't propagate the event (which will deselect everything) + if (action.onClick(unref(props.node)) || unref(props.selectedAction)?.id === action.id) { + e.preventDefault(); + e.stopPropagation(); + } +} + +function actionMouseUp(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) { + if (unref(props.selectedAction)?.id === action.id) { + e.preventDefault(); + e.stopPropagation(); + } +} +</script> + +<style scoped> +.action:not(.boardnode):hover circle, +.action:not(.boardnode).selected circle { + r: 25; +} + +.action:not(.boardnode):hover text, +.action:not(.boardnode).selected text { + font-size: 187.5%; /* 150% * 1.25 */ +} + +.action:not(.boardnode) text { + text-anchor: middle; + dominant-baseline: central; +} +</style> + +<style> +.actions-enter-from .action, +.actions-leave-to .action { + transform: translate(0, 0); +} +</style> diff --git a/src/features/boards/board.ts b/src/features/boards/board.ts index e10bf67..e26be13 100644 --- a/src/features/boards/board.ts +++ b/src/features/boards/board.ts @@ -63,6 +63,8 @@ export interface BoardNode { export interface BoardNodeLink extends Omit<Link, "startNode" | "endNode"> { startNode: BoardNode; endNode: BoardNode; + stroke: string; + strokeWidth: number; pulsing?: boolean; } @@ -365,7 +367,7 @@ export function createBoard<T extends BoardOptions>( processComputable(board as T, "width"); setDefault(board, "width", "100%"); processComputable(board as T, "height"); - setDefault(board, "height", "400px"); + setDefault(board, "height", "100%"); processComputable(board as T, "classes"); processComputable(board as T, "style");