From 119f8c552aca7a4d02a3349aa01c1f029431654d Mon Sep 17 00:00:00 2001 From: Justin Lin Date: Fri, 9 Oct 2026 23:23:02 +1100 Subject: [PATCH] refactor: replace svg rect with html button Only use svg to draw lines --- app/src/components/QuestMap.vue | 206 ++++++++++++-------------------- 1 file changed, 78 insertions(+), 128 deletions(-) diff --git a/app/src/components/QuestMap.vue b/app/src/components/QuestMap.vue index 3956b14..93d203d 100644 --- a/app/src/components/QuestMap.vue +++ b/app/src/components/QuestMap.vue @@ -11,49 +11,11 @@ const { t, locale } = useI18n() const store = useUserProfile() const toast = useToast() -const svgWidth = ref(window.innerWidth) -const svgHeight = ref(window.innerHeight) - const marginX = 120 const marginY = 120 const nodeWidth = 160 const nodeHeight = 50 -const maxTextWidth = nodeWidth - 20 -const fontSize = 16 -const maxLines = 2 - -// Approximate glyph width: wide (CJK etc.) chars take a full em, others about half -function charWidth(ch: string) { - return ch.charCodeAt(0) > 0x2e80 ? fontSize : fontSize * 0.55 -} - -function textWidth(s: string) { - return [...s].reduce((w, ch) => w + charWidth(ch), 0) -} - -// SVG text does not wrap, so split the title into lines that fit the node -function wrapTitle(title: string): string[] { - const lines: string[] = [] - let current = '' - // Keep whole words together for spaced languages; CJK splits per character - const tokens = title.match(/[⺀-￿]|\S+\s*/g) ?? [] - for (const token of tokens) { - if (current && textWidth((current + token).trimEnd()) > maxTextWidth) { - lines.push(current.trimEnd()) - current = '' - } - current += token - } - if (current) lines.push(current.trimEnd()) - - if (lines.length <= maxLines) return lines - const kept = lines.slice(0, maxLines) - let last = kept[maxLines - 1] - while (last && textWidth(last + '…') > maxTextWidth) last = last.slice(0, -1) - kept[maxLines - 1] = last + '…' - return kept -} const opened = ref(null) type Node = { @@ -82,14 +44,18 @@ const dragStartY = ref(0) const offsetX = ref(0) const offsetY = ref(0) -function startDrag(event: MouseEvent) { +function startDrag(event: PointerEvent) { isDragging.value = true dragStartX.value = event.clientX - offsetX.value dragStartY.value = event.clientY - offsetY.value } -function drag(event: MouseEvent) { +function drag(event: PointerEvent) { if (!isDragging.value) return + // Capture only once dragging starts so plain clicks still reach the node buttons + const el = event.currentTarget as HTMLElement + if (!el.hasPointerCapture(event.pointerId)) + el.setPointerCapture(event.pointerId) offsetX.value = event.clientX - dragStartX.value offsetY.value = event.clientY - dragStartY.value } @@ -111,10 +77,8 @@ async function computeGraphData() { id: node.id, title: node.title, x: marginX * node.layer * 2, - y: - marginY * node.index - - (marginX * dag.getLayer(node.id)) / 2 + - svgHeight.value / 2, + // Centered on 0; the world container is anchored at the viewport's vertical center + y: (node.index - (dag.getLayer(node.id) + 1) / 2) * marginY, completed: store.progress[node.id]?.completed || false, unlocked: node.requirements.every( (req: string) => store.progress[req]?.completed @@ -159,14 +123,10 @@ watch(locale, () => { computeGraphData() }) -const curvedPath = computed(() => { +const edgePath = computed(() => { return (source: Node, target: Node) => { - // const dx = target.x - source.x - // const dy = target.y - source.y - // const dr = Math.sqrt(dx * dx + dy * dy) - const dr = 0 - const sweep = target.index > source.index ? 0 : 1 // Determines if the arc should curve up or down based on node index - return `M${source.x},${source.y}A${dr},${dr} 0 0,${sweep} ${target.x},${target.y}` + const mx = (source.x + target.x) / 2 + return `M${source.x},${source.y}C${mx},${source.y} ${mx},${target.y} ${target.x},${target.y}` } }) @@ -196,58 +156,44 @@ const edgeStyle = computed(() => { > {{ t('quest_map.intro') }} - - - +
+ - - + - - + {{ node.title }} + +
+ {