2020-07-28 12:46:10 +00:00
|
|
|
// Copyright 2020 the V8 project authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
|
|
// found in the LICENSE file.
|
|
|
|
|
2020-08-13 09:14:29 +00:00
|
|
|
import {
|
|
|
|
defineCustomElement, V8CustomElement,
|
2020-08-20 04:17:50 +00:00
|
|
|
typeToColor, CSSColor
|
2020-08-13 09:14:29 +00:00
|
|
|
} from '../helper.mjs';
|
|
|
|
import { kChunkWidth, kChunkHeight } from '../map-processor.mjs';
|
2020-08-13 12:55:19 +00:00
|
|
|
import { SelectionEvent, FocusEvent, SelectTimeEvent } from '../events.mjs';
|
2020-07-28 12:46:10 +00:00
|
|
|
|
|
|
|
defineCustomElement('./timeline/timeline-track', (templateText) =>
|
|
|
|
class TimelineTrack extends V8CustomElement {
|
|
|
|
#timeline;
|
|
|
|
#nofChunks = 400;
|
|
|
|
#chunks;
|
|
|
|
#selectedEntry;
|
2020-08-13 09:14:29 +00:00
|
|
|
#timeToPixel;
|
|
|
|
#timeSelection = { start: 0, end: Infinity };
|
2020-07-28 12:46:10 +00:00
|
|
|
constructor() {
|
|
|
|
super(templateText);
|
2020-08-13 09:14:29 +00:00
|
|
|
this.timeline.addEventListener("mousedown",
|
|
|
|
e => this.handleTimeRangeSelectionStart(e));
|
|
|
|
this.timeline.addEventListener("mouseup",
|
|
|
|
e => this.handleTimeRangeSelectionEnd(e));
|
2020-08-06 04:49:39 +00:00
|
|
|
this.timeline.addEventListener("scroll",
|
|
|
|
e => this.handleTimelineScroll(e));
|
2020-07-28 12:46:10 +00:00
|
|
|
this.backgroundCanvas = document.createElement('canvas');
|
|
|
|
this.isLocked = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
get timelineCanvas() {
|
|
|
|
return this.$('#timelineCanvas');
|
|
|
|
}
|
|
|
|
|
|
|
|
get timelineChunks() {
|
|
|
|
return this.$('#timelineChunks');
|
|
|
|
}
|
|
|
|
|
|
|
|
get timeline() {
|
|
|
|
return this.$('#timeline');
|
|
|
|
}
|
|
|
|
|
2020-08-20 04:17:50 +00:00
|
|
|
get timelineLegend() {
|
|
|
|
return this.$('#legend');
|
2020-07-28 12:46:10 +00:00
|
|
|
}
|
|
|
|
|
2020-08-20 04:17:50 +00:00
|
|
|
get timelineLegendContent() {
|
|
|
|
return this.$('#legendContent');
|
|
|
|
}
|
2020-07-28 12:46:10 +00:00
|
|
|
set data(value) {
|
|
|
|
this.#timeline = value;
|
|
|
|
this.updateChunks();
|
|
|
|
this.updateTimeline();
|
2020-08-20 04:17:50 +00:00
|
|
|
this.updateLegend();
|
2020-07-28 12:46:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
get data() {
|
|
|
|
return this.#timeline;
|
|
|
|
}
|
|
|
|
|
2020-08-13 09:14:29 +00:00
|
|
|
set nofChunks(count) {
|
2020-07-28 12:46:10 +00:00
|
|
|
this.#nofChunks = count;
|
|
|
|
this.updateChunks();
|
|
|
|
this.updateTimeline();
|
|
|
|
}
|
2020-08-13 09:14:29 +00:00
|
|
|
get nofChunks() {
|
2020-07-28 12:46:10 +00:00
|
|
|
return this.#nofChunks;
|
|
|
|
}
|
|
|
|
updateChunks() {
|
|
|
|
this.#chunks = this.data.chunks(this.nofChunks);
|
|
|
|
}
|
2020-08-13 09:14:29 +00:00
|
|
|
get chunks() {
|
2020-07-28 12:46:10 +00:00
|
|
|
return this.#chunks;
|
|
|
|
}
|
2020-08-13 09:14:29 +00:00
|
|
|
set selectedEntry(value) {
|
2020-07-28 12:46:10 +00:00
|
|
|
this.#selectedEntry = value;
|
2020-08-13 09:14:29 +00:00
|
|
|
if (value.edge) this.redraw();
|
2020-07-28 12:46:10 +00:00
|
|
|
}
|
2020-08-13 09:14:29 +00:00
|
|
|
get selectedEntry() {
|
2020-07-28 12:46:10 +00:00
|
|
|
return this.#selectedEntry;
|
|
|
|
}
|
|
|
|
|
2020-08-13 09:14:29 +00:00
|
|
|
set scrollLeft(offset) {
|
2020-08-06 04:49:39 +00:00
|
|
|
this.timeline.scrollLeft = offset;
|
|
|
|
}
|
2020-08-20 04:17:50 +00:00
|
|
|
//TODO(zcankara) Carry to the Model, nothing UI related
|
|
|
|
updateLegend() {
|
|
|
|
const uniqueTypes = new Map();
|
2020-07-28 12:46:10 +00:00
|
|
|
for (const entry of this.data.all) {
|
2020-08-20 04:17:50 +00:00
|
|
|
if (!uniqueTypes.has(entry.type)) {
|
|
|
|
uniqueTypes.set(entry.type, [entry]);
|
2020-07-28 12:46:10 +00:00
|
|
|
} else {
|
2020-08-20 04:17:50 +00:00
|
|
|
uniqueTypes.get(entry.type).push(entry);
|
2020-07-28 12:46:10 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-20 04:17:50 +00:00
|
|
|
this.renderLegend(uniqueTypes);
|
2020-07-28 12:46:10 +00:00
|
|
|
}
|
|
|
|
|
2020-08-20 04:17:50 +00:00
|
|
|
renderLegend(uniqueTypes) {
|
|
|
|
let timelineLegend = this.timelineLegend;
|
2020-07-28 12:46:10 +00:00
|
|
|
let timelineLegendContent = this.timelineLegendContent;
|
|
|
|
this.removeAllChildren(timelineLegendContent);
|
2020-08-20 04:17:50 +00:00
|
|
|
let row = this.tr();
|
|
|
|
row.entries = this.data.all;
|
|
|
|
row.addEventListener('dblclick', e => this.handleEntryTypeDblClick(e));
|
|
|
|
row.appendChild(this.td(""));
|
|
|
|
let td = this.td("All");
|
|
|
|
row.appendChild(td);
|
|
|
|
row.appendChild(this.td(this.data.all.length));
|
|
|
|
row.appendChild(this.td("100%"));
|
|
|
|
timelineLegendContent.appendChild(row);
|
2020-07-28 12:46:10 +00:00
|
|
|
let colorIterator = 0;
|
2020-08-20 04:17:50 +00:00
|
|
|
uniqueTypes.forEach((entries, type) => {
|
|
|
|
let row = this.tr();
|
|
|
|
row.entries = entries;
|
|
|
|
row.addEventListener('dblclick', e => this.handleEntryTypeDblClick(e));
|
|
|
|
let color = typeToColor(type);
|
|
|
|
if (color !== null) {
|
|
|
|
let div = this.div(["colorbox"]);
|
|
|
|
div.style.backgroundColor = color;
|
|
|
|
row.appendChild(this.td(div));
|
|
|
|
} else {
|
|
|
|
row.appendChild(this.td(""));
|
|
|
|
}
|
|
|
|
let td = this.td(type);
|
|
|
|
row.appendChild(td);
|
|
|
|
row.appendChild(this.td(entries.length));
|
|
|
|
let percent = (entries.length / this.data.all.length) * 100;
|
|
|
|
row.appendChild(this.td(percent.toFixed(1) + "%"));
|
|
|
|
timelineLegendContent.appendChild(row);
|
2020-07-28 12:46:10 +00:00
|
|
|
colorIterator += 1;
|
|
|
|
});
|
2020-08-20 04:17:50 +00:00
|
|
|
timelineLegend.appendChild(timelineLegendContent);
|
2020-07-28 12:46:10 +00:00
|
|
|
}
|
|
|
|
|
2020-08-13 09:14:29 +00:00
|
|
|
handleEntryTypeDblClick(e) {
|
2020-08-20 04:17:50 +00:00
|
|
|
this.dispatchEvent(new SelectionEvent(e.target.parentNode.entries));
|
2020-08-04 09:16:41 +00:00
|
|
|
}
|
|
|
|
|
2020-08-06 04:49:39 +00:00
|
|
|
timelineIndicatorMove(offset) {
|
2020-07-28 12:46:10 +00:00
|
|
|
this.timeline.scrollLeft += offset;
|
|
|
|
}
|
|
|
|
|
2020-08-13 09:14:29 +00:00
|
|
|
handleTimeRangeSelectionStart(e) {
|
|
|
|
this.#timeSelection.start = this.positionToTime(e.clientX);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleTimeRangeSelectionEnd(e) {
|
|
|
|
this.#timeSelection.end = this.positionToTime(e.clientX);
|
|
|
|
this.dispatchEvent(new SelectTimeEvent(
|
|
|
|
Math.min(this.#timeSelection.start, this.#timeSelection.end),
|
|
|
|
Math.max(this.#timeSelection.start, this.#timeSelection.end)));
|
|
|
|
}
|
|
|
|
|
|
|
|
positionToTime(posX) {
|
|
|
|
let rect = this.timeline.getBoundingClientRect();
|
|
|
|
let posClickedX = posX - rect.left + this.timeline.scrollLeft;
|
|
|
|
let selectedTime = posClickedX / this.#timeToPixel;
|
|
|
|
return selectedTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
handleTimelineScroll(e) {
|
2020-08-06 04:49:39 +00:00
|
|
|
let horizontal = e.currentTarget.scrollLeft;
|
|
|
|
this.dispatchEvent(new CustomEvent(
|
2020-08-13 09:14:29 +00:00
|
|
|
'scrolltrack', {
|
|
|
|
bubbles: true, composed: true,
|
|
|
|
detail: horizontal
|
|
|
|
}));
|
2020-08-06 04:49:39 +00:00
|
|
|
}
|
|
|
|
|
2020-07-28 12:46:10 +00:00
|
|
|
asyncSetTimelineChunkBackground(backgroundTodo) {
|
|
|
|
const kIncrement = 100;
|
|
|
|
let start = 0;
|
|
|
|
let delay = 1;
|
|
|
|
while (start < backgroundTodo.length) {
|
|
|
|
let end = Math.min(start + kIncrement, backgroundTodo.length);
|
|
|
|
setTimeout((from, to) => {
|
|
|
|
for (let i = from; i < to; i++) {
|
|
|
|
let [chunk, node] = backgroundTodo[i];
|
|
|
|
this.setTimelineChunkBackground(chunk, node);
|
|
|
|
}
|
|
|
|
}, delay++, start, end);
|
|
|
|
start = end;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
setTimelineChunkBackground(chunk, node) {
|
|
|
|
// Render the types of transitions as bar charts
|
|
|
|
const kHeight = chunk.height;
|
|
|
|
const kWidth = 1;
|
|
|
|
this.backgroundCanvas.width = kWidth;
|
|
|
|
this.backgroundCanvas.height = kHeight;
|
|
|
|
let ctx = this.backgroundCanvas.getContext('2d');
|
|
|
|
ctx.clearRect(0, 0, kWidth, kHeight);
|
|
|
|
let y = 0;
|
|
|
|
let total = chunk.size();
|
|
|
|
let type, count;
|
|
|
|
if (true) {
|
|
|
|
chunk.getBreakdown(map => map.type).forEach(([type, count]) => {
|
2020-08-20 04:17:50 +00:00
|
|
|
ctx.fillStyle = typeToColor(type);
|
2020-07-28 12:46:10 +00:00
|
|
|
let height = count / total * kHeight;
|
|
|
|
ctx.fillRect(0, y, kWidth, y + height);
|
|
|
|
y += height;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
chunk.items.forEach(map => {
|
2020-08-20 04:17:50 +00:00
|
|
|
ctx.fillStyle = typeToColor(map.type);
|
2020-07-28 12:46:10 +00:00
|
|
|
let y = chunk.yOffset(map);
|
|
|
|
ctx.fillRect(0, y, kWidth, y + 1);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let imageData = this.backgroundCanvas.toDataURL('image/webp', 0.2);
|
|
|
|
node.style.backgroundImage = 'url(' + imageData + ')';
|
|
|
|
}
|
|
|
|
|
|
|
|
updateTimeline() {
|
|
|
|
let chunksNode = this.timelineChunks;
|
|
|
|
this.removeAllChildren(chunksNode);
|
|
|
|
let chunks = this.chunks;
|
|
|
|
let max = chunks.max(each => each.size());
|
|
|
|
let start = this.data.startTime;
|
|
|
|
let end = this.data.endTime;
|
|
|
|
let duration = end - start;
|
2020-08-13 09:14:29 +00:00
|
|
|
this.#timeToPixel = chunks.length * kChunkWidth / duration;
|
2020-07-28 12:46:10 +00:00
|
|
|
let addTimestamp = (time, name) => {
|
|
|
|
let timeNode = this.div('timestamp');
|
|
|
|
timeNode.innerText = name;
|
2020-08-13 09:14:29 +00:00
|
|
|
timeNode.style.left = ((time - start) * this.#timeToPixel) + 'px';
|
2020-07-28 12:46:10 +00:00
|
|
|
chunksNode.appendChild(timeNode);
|
|
|
|
};
|
|
|
|
let backgroundTodo = [];
|
|
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
|
|
let chunk = chunks[i];
|
|
|
|
let height = (chunk.size() / max * kChunkHeight);
|
|
|
|
chunk.height = height;
|
|
|
|
if (chunk.isEmpty()) continue;
|
|
|
|
let node = this.div();
|
|
|
|
node.className = 'chunk';
|
2020-08-14 09:51:49 +00:00
|
|
|
node.style.left = (chunks[i].start * this.#timeToPixel) + 'px';
|
2020-07-28 12:46:10 +00:00
|
|
|
node.style.height = height + 'px';
|
|
|
|
node.chunk = chunk;
|
|
|
|
node.addEventListener('mousemove', e => this.handleChunkMouseMove(e));
|
|
|
|
node.addEventListener('click', e => this.handleChunkClick(e));
|
|
|
|
node.addEventListener('dblclick', e => this.handleChunkDoubleClick(e));
|
|
|
|
backgroundTodo.push([chunk, node])
|
|
|
|
chunksNode.appendChild(node);
|
|
|
|
}
|
|
|
|
this.asyncSetTimelineChunkBackground(backgroundTodo)
|
|
|
|
|
|
|
|
// Put a time marker roughly every 20 chunks.
|
|
|
|
let expected = duration / chunks.length * 20;
|
|
|
|
let interval = (10 ** Math.floor(Math.log10(expected)));
|
|
|
|
let correction = Math.log10(expected / interval);
|
|
|
|
correction = (correction < 0.33) ? 1 : (correction < 0.75) ? 2.5 : 5;
|
|
|
|
interval *= correction;
|
|
|
|
|
|
|
|
let time = start;
|
|
|
|
while (time < end) {
|
|
|
|
addTimestamp(time, ((time - start) / 1000) + ' ms');
|
|
|
|
time += interval;
|
|
|
|
}
|
|
|
|
this.drawOverview();
|
|
|
|
this.redraw();
|
|
|
|
}
|
|
|
|
|
|
|
|
handleChunkMouseMove(event) {
|
|
|
|
if (this.isLocked) return false;
|
|
|
|
let chunk = event.target.chunk;
|
|
|
|
if (!chunk) return;
|
|
|
|
// topmost map (at chunk.height) == map #0.
|
|
|
|
let relativeIndex =
|
2020-08-13 09:14:29 +00:00
|
|
|
Math.round(event.layerY / event.target.offsetHeight * chunk.size());
|
2020-07-28 12:46:10 +00:00
|
|
|
let map = chunk.at(relativeIndex);
|
2020-08-13 12:55:19 +00:00
|
|
|
this.dispatchEvent(new FocusEvent(map));
|
2020-07-28 12:46:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
handleChunkClick(event) {
|
|
|
|
this.isLocked = !this.isLocked;
|
|
|
|
}
|
|
|
|
|
|
|
|
handleChunkDoubleClick(event) {
|
|
|
|
this.isLocked = true;
|
|
|
|
let chunk = event.target.chunk;
|
|
|
|
if (!chunk) return;
|
2020-08-13 12:55:19 +00:00
|
|
|
let maps = chunk.items;
|
2020-08-06 14:41:38 +00:00
|
|
|
this.dispatchEvent(new SelectionEvent(maps));
|
2020-07-28 12:46:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
drawOverview() {
|
|
|
|
const height = 50;
|
|
|
|
const kFactor = 2;
|
|
|
|
let canvas = this.backgroundCanvas;
|
|
|
|
canvas.height = height;
|
|
|
|
canvas.width = window.innerWidth;
|
|
|
|
let ctx = canvas.getContext('2d');
|
|
|
|
let chunks = this.data.chunkSizes(canvas.width * kFactor);
|
|
|
|
let max = chunks.max();
|
|
|
|
ctx.clearRect(0, 0, canvas.width, height);
|
|
|
|
ctx.fillStyle = CSSColor.onBackgroundColor;
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.moveTo(0, height);
|
|
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
|
|
ctx.lineTo(i / kFactor, height - chunks[i] / max * height);
|
|
|
|
}
|
|
|
|
ctx.lineTo(chunks.length, height);
|
|
|
|
ctx.strokeStyle = CSSColor.onBackgroundColor;
|
|
|
|
ctx.stroke();
|
|
|
|
ctx.closePath();
|
|
|
|
ctx.fill();
|
|
|
|
let imageData = canvas.toDataURL('image/webp', 0.2);
|
|
|
|
this.dispatchEvent(new CustomEvent(
|
2020-08-13 12:55:19 +00:00
|
|
|
'overviewupdate', {
|
|
|
|
bubbles: true, composed: true,
|
|
|
|
detail: imageData
|
|
|
|
}));
|
2020-07-28 12:46:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
redraw() {
|
|
|
|
let canvas = this.timelineCanvas;
|
|
|
|
canvas.width = (this.chunks.length + 1) * kChunkWidth;
|
|
|
|
canvas.height = kChunkHeight;
|
|
|
|
let ctx = canvas.getContext('2d');
|
|
|
|
ctx.clearRect(0, 0, canvas.width, kChunkHeight);
|
|
|
|
if (!this.selectedEntry || !this.selectedEntry.edge) return;
|
|
|
|
this.drawEdges(ctx);
|
|
|
|
}
|
|
|
|
setMapStyle(map, ctx) {
|
|
|
|
ctx.fillStyle = map.edge && map.edge.from ?
|
|
|
|
CSSColor.onBackgroundColor : CSSColor.onPrimaryColor;
|
|
|
|
}
|
|
|
|
|
|
|
|
setEdgeStyle(edge, ctx) {
|
2020-08-20 04:17:50 +00:00
|
|
|
let color = typeToColor(edge.type);
|
2020-07-28 12:46:10 +00:00
|
|
|
ctx.strokeStyle = color;
|
|
|
|
ctx.fillStyle = color;
|
|
|
|
}
|
|
|
|
|
|
|
|
markMap(ctx, map) {
|
|
|
|
let [x, y] = map.position(this.chunks);
|
|
|
|
ctx.beginPath();
|
|
|
|
this.setMapStyle(map, ctx);
|
|
|
|
ctx.arc(x, y, 3, 0, 2 * Math.PI);
|
|
|
|
ctx.fill();
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.fillStyle = CSSColor.onBackgroundColor;
|
|
|
|
ctx.arc(x, y, 2, 0, 2 * Math.PI);
|
|
|
|
ctx.fill();
|
|
|
|
}
|
|
|
|
|
|
|
|
markSelectedMap(ctx, map) {
|
|
|
|
let [x, y] = map.position(this.chunks);
|
|
|
|
ctx.beginPath();
|
|
|
|
this.setMapStyle(map, ctx);
|
|
|
|
ctx.arc(x, y, 6, 0, 2 * Math.PI);
|
|
|
|
ctx.strokeStyle = CSSColor.onBackgroundColor;
|
|
|
|
ctx.stroke();
|
|
|
|
}
|
|
|
|
|
|
|
|
drawEdges(ctx) {
|
|
|
|
// Draw the trace of maps in reverse order to make sure the outgoing
|
|
|
|
// transitions of previous maps aren't drawn over.
|
|
|
|
const kMaxOutgoingEdges = 100;
|
|
|
|
let nofEdges = 0;
|
|
|
|
let stack = [];
|
|
|
|
let current = this.selectedEntry;
|
|
|
|
while (current && nofEdges < kMaxOutgoingEdges) {
|
|
|
|
nofEdges += current.children.length;
|
|
|
|
stack.push(current);
|
|
|
|
current = current.parent();
|
|
|
|
}
|
|
|
|
ctx.save();
|
|
|
|
this.drawOutgoingEdges(ctx, this.selectedEntry, 3);
|
|
|
|
ctx.restore();
|
|
|
|
|
|
|
|
let labelOffset = 15;
|
|
|
|
let xPrev = 0;
|
|
|
|
while (current = stack.pop()) {
|
|
|
|
if (current.edge) {
|
|
|
|
this.setEdgeStyle(current.edge, ctx);
|
|
|
|
let [xTo, yTo] = this.drawEdge(ctx, current.edge, true, labelOffset);
|
|
|
|
if (xTo == xPrev) {
|
|
|
|
labelOffset += 8;
|
|
|
|
} else {
|
|
|
|
labelOffset = 15
|
|
|
|
}
|
|
|
|
xPrev = xTo;
|
|
|
|
}
|
|
|
|
this.markMap(ctx, current);
|
|
|
|
current = current.parent();
|
|
|
|
ctx.save();
|
|
|
|
// this.drawOutgoingEdges(ctx, current, 1);
|
|
|
|
ctx.restore();
|
|
|
|
}
|
|
|
|
// Mark selected map
|
|
|
|
this.markSelectedMap(ctx, this.selectedEntry);
|
|
|
|
}
|
|
|
|
|
|
|
|
drawEdge(ctx, edge, showLabel = true, labelOffset = 20) {
|
|
|
|
if (!edge.from || !edge.to) return [-1, -1];
|
|
|
|
let [xFrom, yFrom] = edge.from.position(this.chunks);
|
|
|
|
let [xTo, yTo] = edge.to.position(this.chunks);
|
|
|
|
let sameChunk = xTo == xFrom;
|
|
|
|
if (sameChunk) labelOffset += 8;
|
|
|
|
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.moveTo(xFrom, yFrom);
|
|
|
|
let offsetX = 20;
|
|
|
|
let offsetY = 20;
|
|
|
|
let midX = xFrom + (xTo - xFrom) / 2;
|
|
|
|
let midY = (yFrom + yTo) / 2 - 100;
|
|
|
|
if (!sameChunk) {
|
|
|
|
ctx.quadraticCurveTo(midX, midY, xTo, yTo);
|
|
|
|
} else {
|
|
|
|
ctx.lineTo(xTo, yTo);
|
|
|
|
}
|
|
|
|
if (!showLabel) {
|
|
|
|
ctx.strokeStyle = CSSColor.onBackgroundColor;
|
|
|
|
ctx.stroke();
|
|
|
|
} else {
|
|
|
|
let centerX, centerY;
|
|
|
|
if (!sameChunk) {
|
|
|
|
centerX = (xFrom / 2 + midX + xTo / 2) / 2;
|
|
|
|
centerY = (yFrom / 2 + midY + yTo / 2) / 2;
|
|
|
|
} else {
|
|
|
|
centerX = xTo;
|
|
|
|
centerY = yTo;
|
|
|
|
}
|
|
|
|
ctx.strokeStyle = CSSColor.onBackgroundColor;
|
|
|
|
ctx.moveTo(centerX, centerY);
|
|
|
|
ctx.lineTo(centerX + offsetX, centerY - labelOffset);
|
|
|
|
ctx.stroke();
|
|
|
|
ctx.textAlign = 'left';
|
|
|
|
ctx.fillStyle = CSSColor.onBackgroundColor;
|
|
|
|
ctx.fillText(
|
2020-08-13 09:14:29 +00:00
|
|
|
edge.toString(), centerX + offsetX + 2, centerY - labelOffset);
|
2020-07-28 12:46:10 +00:00
|
|
|
}
|
|
|
|
return [xTo, yTo];
|
|
|
|
}
|
|
|
|
|
|
|
|
drawOutgoingEdges(ctx, map, max = 10, depth = 0) {
|
|
|
|
if (!map) return;
|
|
|
|
if (depth >= max) return;
|
|
|
|
ctx.globalAlpha = 0.5 - depth * (0.3 / max);
|
|
|
|
ctx.strokeStyle = CSSColor.timelineBackgroundColor;
|
|
|
|
|
|
|
|
const limit = Math.min(map.children.length, 100)
|
|
|
|
for (let i = 0; i < limit; i++) {
|
|
|
|
let edge = map.children[i];
|
|
|
|
this.drawEdge(ctx, edge, true);
|
|
|
|
this.drawOutgoingEdges(ctx, edge.to, max, depth + 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|