Phase 6D with the SVG rendering
This commit is contained in:
parent
ba5960f907
commit
5831172b1d
@ -819,7 +819,8 @@ else
|
|||||||
{
|
{
|
||||||
if (Model.PlotLanes.Any() && Model.OrderedScenes.Any())
|
if (Model.PlotLanes.Any() && Model.OrderedScenes.Any())
|
||||||
{
|
{
|
||||||
<div class="plot-lane-board" data-timeline-section="plot-lines" style="--scene-count:@timelineColumns.Count">
|
<div class="plot-lane-board" data-timeline-section="plot-lines" data-plot-line-svg-board style="--scene-count:@timelineColumns.Count">
|
||||||
|
<svg class="plot-lane-svg-overlay" data-plot-line-svg aria-hidden="true"></svg>
|
||||||
<div class="lane-board-heading plot-lane-board-heading">
|
<div class="lane-board-heading plot-lane-board-heading">
|
||||||
<button type="button" data-collapse-toggle="plot-lines">Plot line lanes</button>
|
<button type="button" data-collapse-toggle="plot-lines">Plot line lanes</button>
|
||||||
<small>Thread event markers by scene</small>
|
<small>Thread event markers by scene</small>
|
||||||
@ -834,7 +835,7 @@ else
|
|||||||
{
|
{
|
||||||
if (lane.PlotLine.PlotLineTypeName != group.Key) { continue; }
|
if (lane.PlotLine.PlotLineTypeName != group.Key) { continue; }
|
||||||
var laneFocusClass = Model.Filter.FocusType == "plotline" && Model.Filter.FocusID.HasValue && Model.Filter.FocusID != lane.PlotLine.PlotLineID ? "lane-dim" : "";
|
var laneFocusClass = Model.Filter.FocusType == "plotline" && Model.Filter.FocusID.HasValue && Model.Filter.FocusID != lane.PlotLine.PlotLineID ? "lane-dim" : "";
|
||||||
<div class="plot-lane-row @laneFocusClass" style="--plot-colour:@lane.PlotLine.Colour">
|
<div class="plot-lane-row @laneFocusClass" data-plot-line-row data-plot-line-id="@lane.PlotLine.PlotLineID" data-plot-colour="@lane.PlotLine.Colour" style="--plot-colour:@lane.PlotLine.Colour">
|
||||||
<a class="plot-lane-label" asp-controller="PlotLines" asp-action="Edit" asp-route-id="@lane.PlotLine.PlotLineID">
|
<a class="plot-lane-label" asp-controller="PlotLines" asp-action="Edit" asp-route-id="@lane.PlotLine.PlotLineID">
|
||||||
<span class="colour-swatch" style="background:@lane.PlotLine.Colour"></span>
|
<span class="colour-swatch" style="background:@lane.PlotLine.Colour"></span>
|
||||||
<strong>@lane.PlotLine.PlotLineName</strong>
|
<strong>@lane.PlotLine.PlotLineName</strong>
|
||||||
@ -849,7 +850,7 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
var slot = lane.SceneSlots.FirstOrDefault(x => x.Scene.SceneID == realPlotScene.SceneID);
|
var slot = lane.SceneSlots.FirstOrDefault(x => x.Scene.SceneID == realPlotScene.SceneID);
|
||||||
<div class="plot-lane-slot" title="Scene @realPlotScene.SceneNumber: @realPlotScene.SceneTitle">
|
<div class="plot-lane-slot" data-plot-line-slot data-scene-id="@realPlotScene.SceneID" title="Scene @realPlotScene.SceneNumber: @realPlotScene.SceneTitle">
|
||||||
@foreach (var threadEvent in slot?.Events ?? Enumerable.Empty<ThreadEvent>())
|
@foreach (var threadEvent in slot?.Events ?? Enumerable.Empty<ThreadEvent>())
|
||||||
{
|
{
|
||||||
var markerClass = threadEvent.EventTypeName is "Resolved" or "Payoff"
|
var markerClass = threadEvent.EventTypeName is "Resolved" or "Payoff"
|
||||||
@ -858,6 +859,8 @@ else
|
|||||||
? "contradicted"
|
? "contradicted"
|
||||||
: "";
|
: "";
|
||||||
<a class="thread-marker @markerClass"
|
<a class="thread-marker @markerClass"
|
||||||
|
data-plot-event-node
|
||||||
|
data-plot-event-type="@threadEvent.PlotEventTypeName"
|
||||||
asp-controller="Timeline"
|
asp-controller="Timeline"
|
||||||
asp-action="Index"
|
asp-action="Index"
|
||||||
asp-route-ProjectID="@Model.Project.ProjectID"
|
asp-route-ProjectID="@Model.Project.ProjectID"
|
||||||
|
|||||||
@ -827,6 +827,7 @@ body.timeline-inspector-resizing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.plot-lane-board {
|
.plot-lane-board {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
@ -836,6 +837,42 @@ body.timeline-inspector-resizing {
|
|||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plot-lane-svg-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto auto 0;
|
||||||
|
z-index: 4;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot-line-svg-path {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 4;
|
||||||
|
stroke-linecap: round;
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot-event-svg-node circle,
|
||||||
|
.plot-event-svg-node path {
|
||||||
|
stroke: #ffffff;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot-event-svg-node line {
|
||||||
|
stroke: #8b1e2d;
|
||||||
|
stroke-width: 4;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot-event-svg-node-reveal circle {
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot-event-svg-node-resolve circle:last-child {
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.lane-board-heading,
|
.lane-board-heading,
|
||||||
.metric-lane-board-heading,
|
.metric-lane-board-heading,
|
||||||
.plot-lane-board-heading {
|
.plot-lane-board-heading {
|
||||||
@ -927,6 +964,10 @@ body.timeline-inspector-resizing {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plot-lane-board [data-plot-event-node] {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.thread-marker:hover {
|
.thread-marker:hover {
|
||||||
color: var(--plotline-ink);
|
color: var(--plotline-ink);
|
||||||
background: color-mix(in srgb, var(--plot-colour), #ffffff 58%);
|
background: color-mix(in srgb, var(--plot-colour), #ffffff 58%);
|
||||||
|
|||||||
2
PlotLine/wwwroot/css/site.min.css
vendored
2
PlotLine/wwwroot/css/site.min.css
vendored
File diff suppressed because one or more lines are too long
@ -1307,6 +1307,7 @@
|
|||||||
densitySelect.value = nextValue;
|
densitySelect.value = nextValue;
|
||||||
}
|
}
|
||||||
localStorage.setItem(densityKey, nextValue);
|
localStorage.setItem(densityKey, nextValue);
|
||||||
|
window.requestAnimationFrame(renderPlotLineSvg);
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyZoom = (value) => {
|
const applyZoom = (value) => {
|
||||||
@ -1316,6 +1317,7 @@
|
|||||||
zoomSelect.value = nextValue;
|
zoomSelect.value = nextValue;
|
||||||
}
|
}
|
||||||
localStorage.setItem(zoomKey, nextValue);
|
localStorage.setItem(zoomKey, nextValue);
|
||||||
|
window.requestAnimationFrame(renderPlotLineSvg);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getScene = (sceneId) => root.querySelector(`[data-scene-id='${CSS.escape(String(sceneId))}']`);
|
const getScene = (sceneId) => root.querySelector(`[data-scene-id='${CSS.escape(String(sceneId))}']`);
|
||||||
@ -1351,6 +1353,83 @@
|
|||||||
|
|
||||||
const saveCollapsed = () => localStorage.setItem(collapsedKey, JSON.stringify([...collapsed]));
|
const saveCollapsed = () => localStorage.setItem(collapsedKey, JSON.stringify([...collapsed]));
|
||||||
|
|
||||||
|
const renderPlotLineSvg = () => {
|
||||||
|
root.querySelectorAll("[data-plot-line-svg-board]").forEach((board) => {
|
||||||
|
const svg = board.querySelector("[data-plot-line-svg]");
|
||||||
|
if (!svg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.replaceChildren();
|
||||||
|
const boardRect = board.getBoundingClientRect();
|
||||||
|
const width = Math.max(board.scrollWidth, boardRect.width);
|
||||||
|
const height = Math.max(board.scrollHeight, boardRect.height);
|
||||||
|
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
||||||
|
svg.setAttribute("width", String(width));
|
||||||
|
svg.setAttribute("height", String(height));
|
||||||
|
|
||||||
|
const makeSvg = (name, attributes = {}) => {
|
||||||
|
const element = document.createElementNS("http://www.w3.org/2000/svg", name);
|
||||||
|
Object.entries(attributes).forEach(([key, value]) => element.setAttribute(key, String(value)));
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pointFor = (row, marker) => {
|
||||||
|
const slot = marker.closest("[data-plot-line-slot]");
|
||||||
|
if (!slot) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotRect = slot.getBoundingClientRect();
|
||||||
|
const rowRect = row.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: slotRect.left - boardRect.left + slotRect.width / 2,
|
||||||
|
y: rowRect.top - boardRect.top + rowRect.height / 2,
|
||||||
|
type: (marker.dataset.plotEventType || "Progress").toLowerCase()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawNode = (point, colour) => {
|
||||||
|
const group = makeSvg("g", { class: `plot-event-svg-node plot-event-svg-node-${point.type}` });
|
||||||
|
if (point.type === "twist") {
|
||||||
|
group.append(makeSvg("path", {
|
||||||
|
d: `M ${point.x} ${point.y - 7} L ${point.x + 7} ${point.y} L ${point.x} ${point.y + 7} L ${point.x - 7} ${point.y} Z`,
|
||||||
|
fill: colour
|
||||||
|
}));
|
||||||
|
} else if (point.type === "abandon") {
|
||||||
|
group.append(makeSvg("line", { x1: point.x - 6, y1: point.y - 6, x2: point.x + 6, y2: point.y + 6 }));
|
||||||
|
group.append(makeSvg("line", { x1: point.x + 6, y1: point.y - 6, x2: point.x - 6, y2: point.y + 6 }));
|
||||||
|
} else {
|
||||||
|
group.append(makeSvg("circle", { cx: point.x, cy: point.y, r: 6, fill: colour }));
|
||||||
|
if (point.type === "resolve") {
|
||||||
|
group.append(makeSvg("circle", { cx: point.x, cy: point.y, r: 10, fill: "none", stroke: colour }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
svg.append(group);
|
||||||
|
};
|
||||||
|
|
||||||
|
board.querySelectorAll("[data-plot-line-row]").forEach((row) => {
|
||||||
|
const colour = row.dataset.plotColour || "#2f6f63";
|
||||||
|
const points = [...row.querySelectorAll("[data-plot-event-node]")]
|
||||||
|
.map((marker) => pointFor(row, marker))
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => a.x - b.x);
|
||||||
|
|
||||||
|
if (points.length > 1) {
|
||||||
|
const first = points[0];
|
||||||
|
const last = points[points.length - 1];
|
||||||
|
svg.append(makeSvg("path", {
|
||||||
|
class: "plot-line-svg-path",
|
||||||
|
d: `M ${first.x} ${first.y} L ${last.x} ${last.y}`,
|
||||||
|
stroke: colour
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
points.forEach((point) => drawNode(point, colour));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const setCollapsed = (id, shouldCollapse) => {
|
const setCollapsed = (id, shouldCollapse) => {
|
||||||
document.querySelectorAll(`[data-collapse-body='${CSS.escape(id)}']`).forEach((body) => {
|
document.querySelectorAll(`[data-collapse-body='${CSS.escape(id)}']`).forEach((body) => {
|
||||||
body.classList.toggle("timeline-section-collapsed", shouldCollapse);
|
body.classList.toggle("timeline-section-collapsed", shouldCollapse);
|
||||||
@ -1378,6 +1457,7 @@
|
|||||||
}
|
}
|
||||||
setCollapsed(id, nextCollapsed);
|
setCollapsed(id, nextCollapsed);
|
||||||
saveCollapsed();
|
saveCollapsed();
|
||||||
|
renderPlotLineSvg();
|
||||||
window.dispatchEvent(new Event("plotline:timeline-layout-changed"));
|
window.dispatchEvent(new Event("plotline:timeline-layout-changed"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1394,12 +1474,16 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
|
renderPlotLineSvg();
|
||||||
scrollActiveChapterIntoView("auto");
|
scrollActiveChapterIntoView("auto");
|
||||||
if (hadPendingScrollRestore) {
|
if (hadPendingScrollRestore) {
|
||||||
restoreScrollPosition();
|
restoreScrollPosition();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", renderPlotLineSvg);
|
||||||
|
window.addEventListener("plotline:timeline-layout-changed", renderPlotLineSvg);
|
||||||
|
|
||||||
const saveScrollForSceneSelection = (event) => {
|
const saveScrollForSceneSelection = (event) => {
|
||||||
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
2
PlotLine/wwwroot/js/site.min.js
vendored
2
PlotLine/wwwroot/js/site.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user