From fb6d50961a8ed153ccd88e047d0caa79013e596f Mon Sep 17 00:00:00 2001 From: Akurosia Kamo Date: Sat, 23 May 2026 21:10:19 +0200 Subject: [PATCH] better moving of skills on the timeline --- css/planner.css | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ js/planner.js | 74 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/css/planner.css b/css/planner.css index 689f9ea..acc205d 100644 --- a/css/planner.css +++ b/css/planner.css @@ -838,6 +838,14 @@ border-top: 1px solid var(--border); } +.timeline-player-row--drop-ok .timeline-track { + background-color: rgba(88,180,116,.08); +} + +.timeline-player-row--drop-bad .timeline-track { + background-color: rgba(224,92,92,.08); +} + .timeline-boss-action { position: absolute; top: 8px; @@ -931,6 +939,77 @@ background: linear-gradient(to right, rgba(74,158,255,.30) 0%, rgba(74,158,255,.30) var(--active-width), rgba(74,158,255,.06) var(--active-width), rgba(74,158,255,.06) 100%); } +.timeline-drag-preview { + position: absolute; + top: 6px; + width: var(--cd-width); + min-width: 28px; + height: 26px; + display: flex; + align-items: center; + gap: 4px; + padding: 0 6px; + border: 1px solid rgba(200,168,75,.85); + border-radius: var(--r); + background: linear-gradient(to right, rgba(200,168,75,.42) 0%, rgba(200,168,75,.42) var(--active-width), rgba(200,168,75,.14) var(--active-width), rgba(200,168,75,.14) 100%); + color: var(--gold); + font-size: 11px; + pointer-events: none; + z-index: 6; + box-shadow: 0 0 0 1px rgba(0,0,0,.25), 0 0 16px rgba(200,168,75,.18); + overflow: hidden; +} + +.timeline-drag-preview::before { + content: ""; + position: absolute; + left: 0; + top: -6px; + bottom: -6px; + width: 1px; + background: var(--gold); + box-shadow: 0 0 10px rgba(200,168,75,.55); +} + +.timeline-drag-preview--bad { + border-color: rgba(224,92,92,.85); + background: rgba(224,92,92,.18); + color: var(--red); +} + +.timeline-drag-preview--bad::before { + background: var(--red); + box-shadow: 0 0 10px rgba(224,92,92,.55); +} + +.timeline-drag-preview img { + width: 18px; + height: 18px; + object-fit: contain; + flex-shrink: 0; + position: relative; + z-index: 1; +} + +.timeline-drag-preview span { + position: relative; + z-index: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.timeline-drag-preview-active { + position: absolute !important; + left: 0; + top: 0; + bottom: 0; + width: var(--active-width); + background: currentColor; + opacity: .20; + z-index: 0 !important; +} + .timeline-axis { min-height: 28px; } diff --git a/js/planner.js b/js/planner.js index f5f6dee..a106d5c 100644 --- a/js/planner.js +++ b/js/planner.js @@ -1004,6 +1004,50 @@ function initTimeline(planId) { let timelinePan = null; let suppressNextTimelineClick = false; + let timelineDrag = null; + + function removeDragPreview() { + document.getElementById('timeline-drag-preview')?.remove(); + timeline.querySelectorAll('.timeline-player-row--drop-ok, .timeline-player-row--drop-bad') + .forEach(row => row.classList.remove('timeline-player-row--drop-ok', 'timeline-player-row--drop-bad')); + } + + function updateDragPreview(event) { + if (!timelineDrag) return; + removeDragPreview(); + + const track = event.target.closest('.timeline-player-row .timeline-track'); + const row = event.target.closest('.timeline-player-row'); + if (!track || !row) return; + + const plan = getPlan(planId); + if (!plan) return; + const rect = track.getBoundingClientRect(); + const duration = planDurationMs(plan); + const deltaPx = event.clientX - timelineDrag.startClientX; + const timestamp = Math.max(0, timelineDrag.startTimestamp + (deltaPx / rect.width) * duration); + const left = Math.max(0, Math.min(100, (timestamp / duration) * 100)); + const durationPct = Math.max(1.2, Math.min(100 - left, (timelineDrag.durationSec * 1000 / duration) * 100)); + const cooldownPct = Math.max(durationPct, Math.min(100 - left, (timelineDrag.cooldownSec * 1000 / duration) * 100)); + const activePct = Math.min(100, (durationPct / cooldownPct) * 100); + const valid = row.dataset.ability === timelineDrag.ability + && jobCanUseAbility(row.dataset.job, timelineDrag.ability); + + row.classList.add(valid ? 'timeline-player-row--drop-ok' : 'timeline-player-row--drop-bad'); + + const preview = document.createElement('div'); + preview.id = 'timeline-drag-preview'; + preview.className = `timeline-drag-preview ${valid ? '' : 'timeline-drag-preview--bad'}`; + preview.style.left = `${left}%`; + preview.style.setProperty('--cd-width', `${cooldownPct}%`); + preview.style.setProperty('--active-width', `${activePct}%`); + preview.innerHTML = ` + + ${timelineDrag.icon ? `` : ''} + ${escHtml(timelineDrag.label)} + `; + track.appendChild(preview); + } timeline.addEventListener('pointerdown', e => { if (e.button !== 0) return; @@ -1122,6 +1166,20 @@ function initTimeline(planId) { job: block.dataset.job, }); if (!plan || !found) return; + const transparent = document.createElement('canvas'); + transparent.width = 1; + transparent.height = 1; + e.dataTransfer.setDragImage(transparent, 0, 0); + timelineDrag = { + ability: block.dataset.ability, + job: block.dataset.job, + label: block.querySelector('span:last-child')?.textContent ?? block.dataset.ability, + icon: block.querySelector('img')?.getAttribute('src') ?? '', + startClientX: e.clientX, + startTimestamp: assignmentStartMs(found.mechanic, found.assignment), + durationSec: assignmentDurationSeconds(found.assignment), + cooldownSec: assignmentCooldownSeconds(found.assignment), + }; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', JSON.stringify({ mechanicId: block.dataset.mechanicId, @@ -1133,10 +1191,18 @@ function initTimeline(planId) { }); timeline.addEventListener('dragover', e => { - if (e.target.closest('.timeline-player-row .timeline-track')) e.preventDefault(); + if (e.target.closest('.timeline-player-row .timeline-track')) { + e.preventDefault(); + updateDragPreview(e); + } + }); + + timeline.addEventListener('dragleave', e => { + if (!timeline.contains(e.relatedTarget)) removeDragPreview(); }); timeline.addEventListener('drop', e => { + removeDragPreview(); const track = e.target.closest('.timeline-player-row .timeline-track'); const row = e.target.closest('.timeline-player-row'); if (!track || !row) return; @@ -1152,6 +1218,12 @@ function initTimeline(planId) { const timestamp = (Number(data.startTimestamp) || 0) + timestampDelta; if (row.dataset.ability && data.ability !== row.dataset.ability) return; updateTimelineAssignmentPosition(planId, data.mechanicId, data.ability, data.job, row.dataset.job, timestamp); + timelineDrag = null; + }); + + timeline.addEventListener('dragend', () => { + timelineDrag = null; + removeDragPreview(); }); settings.addEventListener('change', e => {