206 lines
7.0 KiB
Vue
206 lines
7.0 KiB
Vue
<template>
|
|
<div
|
|
class="z-[1] select-none touch-none"
|
|
:class="selected ? 'outline outline-2 outline-accent outline-offset-2' : ''"
|
|
:style="computedStyle"
|
|
@mousedown.stop="$emit('drag-start', { id: widget.id, event: $event })"
|
|
@touchstart.stop="$emit('drag-start', { id: widget.id, event: $event })"
|
|
@click.stop="$emit('select')"
|
|
>
|
|
<div class="absolute inset-0" :style="powerFlowBgStyle"></div>
|
|
<div v-if="widget.text" class="absolute left-4 top-3 text-[13px] uppercase tracking-[0.08em]" :style="{ color: widget.textColor }">
|
|
{{ widget.text }}
|
|
</div>
|
|
<svg class="absolute inset-0 pointer-events-none" :width="widget.w * scale" :height="widget.h * scale">
|
|
<path
|
|
v-for="link in powerFlowLinks"
|
|
:key="`link-${link.id}`"
|
|
:d="link.path"
|
|
:stroke="link.color"
|
|
:stroke-width="link.width"
|
|
stroke-linecap="round"
|
|
fill="none"
|
|
:opacity="link.opacity"
|
|
/>
|
|
<circle
|
|
v-for="link in powerFlowLinks"
|
|
:key="`dot-${link.id}`"
|
|
:r="link.dotRadius"
|
|
:fill="link.color"
|
|
:opacity="link.opacity"
|
|
>
|
|
<animateMotion
|
|
:dur="`${link.duration}s`"
|
|
repeatCount="indefinite"
|
|
:path="link.path"
|
|
/>
|
|
</circle>
|
|
</svg>
|
|
<div v-if="!powerNodes.length" class="absolute inset-0 grid place-items-center text-[12px] text-muted">
|
|
Power Nodes hinzufuegen
|
|
</div>
|
|
<WidgetElement
|
|
v-for="child in powerFlowChildren"
|
|
:key="child.id"
|
|
:widget="child"
|
|
:scale="scale"
|
|
:selected="store.selectedWidgetId === child.id"
|
|
@select="store.selectedWidgetId = child.id"
|
|
@drag-start="$emit('drag-start', $event)"
|
|
@resize-start="$emit('resize-start', $event)"
|
|
/>
|
|
|
|
<!-- Resize Handle -->
|
|
<div
|
|
v-if="selected"
|
|
class="absolute -right-1.5 -bottom-1.5 w-3.5 h-3.5 rounded-[4px] bg-accent border-[2px] border-[#1b1308] shadow-[0_4px_12px_rgba(0,0,0,0.35)] cursor-se-resize z-10"
|
|
data-resize-handle
|
|
@mousedown.stop="$emit('resize-start', { id: widget.id, event: $event })"
|
|
@touchstart.stop="$emit('resize-start', { id: widget.id, event: $event })"
|
|
>
|
|
<span class="absolute right-[2px] bottom-[2px] w-[6px] h-[6px] border-r-2 border-b-2 border-[#1b1308a6] rounded-[2px]"></span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, defineAsyncComponent } from 'vue';
|
|
import { useEditorStore } from '../../../stores/editor';
|
|
import { WIDGET_TYPES } from '../../../constants';
|
|
import { getBaseStyle, getShadowStyle, getBorderStyle, clamp, hexToRgba } from '../shared/utils';
|
|
|
|
const WidgetElement = defineAsyncComponent(() => import('../../WidgetElement.vue'));
|
|
|
|
const props = defineProps({
|
|
widget: { type: Object, required: true },
|
|
scale: { type: Number, default: 1 },
|
|
selected: { type: Boolean, default: false }
|
|
});
|
|
|
|
defineEmits(['select', 'drag-start', 'resize-start']);
|
|
|
|
const store = useEditorStore();
|
|
|
|
const children = computed(() => {
|
|
if (!store.activeScreen) return [];
|
|
return store.activeScreen.widgets.filter(w => w.parentId === props.widget.id);
|
|
});
|
|
|
|
const powerNodes = computed(() => {
|
|
return children.value.filter((child) => child.type === WIDGET_TYPES.POWERNODE && child.visible !== false);
|
|
});
|
|
|
|
const powerLinkWidgets = computed(() => {
|
|
return children.value.filter((child) => child.type === WIDGET_TYPES.POWERLINK && child.visible !== false);
|
|
});
|
|
|
|
const powerFlowChildren = computed(() => {
|
|
return children.value.filter((child) => child.type !== WIDGET_TYPES.POWERLINK);
|
|
});
|
|
|
|
const powerFlowLinks = computed(() => {
|
|
if (powerNodes.value.length < 2 || !powerLinkWidgets.value.length) return [];
|
|
|
|
const s = props.scale;
|
|
const nodeMap = new Map(powerNodes.value.map((node) => [node.id, node]));
|
|
|
|
return powerLinkWidgets.value.flatMap((link, idx) => {
|
|
const fromNode = nodeMap.get(link.x);
|
|
const toNode = nodeMap.get(link.y);
|
|
if (!fromNode || !toNode) return [];
|
|
|
|
const lineWidth = Math.max(3, link.w || 3);
|
|
const fromCenter = {
|
|
x: (fromNode.x + fromNode.w / 2) * s,
|
|
y: (fromNode.y + fromNode.h / 2) * s
|
|
};
|
|
const toCenter = {
|
|
x: (toNode.x + toNode.w / 2) * s,
|
|
y: (toNode.y + toNode.h / 2) * s
|
|
};
|
|
|
|
const dx = toCenter.x - fromCenter.x;
|
|
const dy = toCenter.y - fromCenter.y;
|
|
const len = Math.hypot(dx, dy) || 1;
|
|
const ux = dx / len;
|
|
const uy = dy / len;
|
|
const fromRadius = Math.max(0, (Math.min(fromNode.w, fromNode.h) * 0.5 - lineWidth * 0.5) * s);
|
|
const toRadius = Math.max(0, (Math.min(toNode.w, toNode.h) * 0.5 - lineWidth * 0.5) * s);
|
|
|
|
let startX = fromCenter.x + ux * fromRadius;
|
|
let startY = fromCenter.y + uy * fromRadius;
|
|
let endX = toCenter.x - ux * toRadius;
|
|
let endY = toCenter.y - uy * toRadius;
|
|
|
|
if (len <= fromRadius + toRadius + 1) {
|
|
startX = fromCenter.x;
|
|
startY = fromCenter.y;
|
|
endX = toCenter.x;
|
|
endY = toCenter.y;
|
|
}
|
|
|
|
const dxTrim = endX - startX;
|
|
const dyTrim = endY - startY;
|
|
const lenTrim = Math.hypot(dxTrim, dyTrim) || 1;
|
|
const nx = -dyTrim / lenTrim;
|
|
const ny = dxTrim / lenTrim;
|
|
const midX = (startX + endX) / 2;
|
|
const midY = (startY + endY) / 2;
|
|
const curveSign = idx % 2 === 0 ? 1 : -1;
|
|
const curve = Math.min(42 * s, lenTrim * 0.3) * curveSign;
|
|
const cpx = midX + nx * curve;
|
|
const cpy = midY + ny * curve;
|
|
const dotRadius = Math.min(8, Math.max(4, lineWidth * 1.6));
|
|
const rawValue = parseFloat(link.text);
|
|
const hasRaw = Number.isFinite(rawValue);
|
|
const isStatic = (link.textSrc ?? 0) === 0;
|
|
const factor = hasRaw ? rawValue : (isStatic ? 60 : 1);
|
|
const previewValue = 50;
|
|
const speed = Math.max(5, isStatic ? factor : Math.abs(previewValue) * factor);
|
|
const duration = Math.max(2, Math.min(10, lenTrim / speed));
|
|
|
|
return [{
|
|
id: link.id,
|
|
path: `M ${startX} ${startY} Q ${cpx} ${cpy} ${endX} ${endY}`,
|
|
color: link.bgColor || '#6fa7d8',
|
|
opacity: clamp((link.bgOpacity ?? 255) / 255, 0.1, 1),
|
|
width: lineWidth,
|
|
dotRadius,
|
|
duration
|
|
}];
|
|
});
|
|
});
|
|
|
|
const powerFlowBgStyle = computed(() => {
|
|
const alpha = clamp((props.widget.bgOpacity ?? 255) / 255, 0, 1);
|
|
const base = hexToRgba(props.widget.bgColor, alpha);
|
|
const dot = hexToRgba('#9aa7b4', 0.25);
|
|
const dotSize = 16 * props.scale;
|
|
|
|
return {
|
|
backgroundColor: base,
|
|
backgroundImage: `radial-gradient(${dot} 0.9px, transparent 1px), linear-gradient(140deg, rgba(255,255,255,0.9) 0%, ${base} 70%)`,
|
|
backgroundSize: `${dotSize}px ${dotSize}px, 100% 100%`,
|
|
backgroundPosition: '0 0, 0 0'
|
|
};
|
|
});
|
|
|
|
const computedStyle = computed(() => {
|
|
const w = props.widget;
|
|
const s = props.scale;
|
|
|
|
const style = getBaseStyle(w, s);
|
|
style.borderRadius = `${w.radius * s}px`;
|
|
style.overflow = 'hidden';
|
|
if ((w.borderWidth || 0) > 0 && (w.borderOpacity ?? 0) > 0) {
|
|
Object.assign(style, getBorderStyle(w, s));
|
|
} else {
|
|
style.border = `1px solid ${hexToRgba('#94a3b8', 0.35)}`;
|
|
}
|
|
|
|
Object.assign(style, getShadowStyle(w, s));
|
|
|
|
return style;
|
|
});
|
|
</script>
|