autoformat + npm update

This commit is contained in:
Tomáš Mládek 2021-04-08 20:43:21 +02:00
parent 66852229bd
commit c104a70dbe
3 changed files with 15262 additions and 5901 deletions

20682
app/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@
"build": "vue-cli-service build" "build": "vue-cli-service build"
}, },
"dependencies": { "dependencies": {
"@vue/cli": "^4.5.12",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"fetch-progress": "^1.3.0", "fetch-progress": "^1.3.0",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",

View file

@ -1,14 +1,14 @@
<template> <template>
<div class="svg-content"> <div class="svg-content">
<div :class="['loading-screen', {loaded: loadedPercent === 100}]"> <div :class="['loading-screen', { loaded: loadedPercent === 100 }]">
<div :style="{width: `${loadedPercent}%`}" class="loading-bar"></div> <div :style="{ width: `${loadedPercent}%` }" class="loading-bar"></div>
</div> </div>
<div class="content" ref="root"> <div class="content" ref="root">
<div class="video-scrolls"> <div class="video-scrolls">
<VideoScroll v-for="scroll in scrolls" :definition="scroll"/> <VideoScroll v-for="scroll in scrolls" :definition="scroll" />
</div> </div>
</div> </div>
<AudioArea v-for="audio in audioAreas" :definition="audio" :bbox="bbox"/> <AudioArea v-for="audio in audioAreas" :definition="audio" :bbox="bbox" />
<div class="dev devpanel"> <div class="dev devpanel">
<div> <div>
<span>Current viewport position:</span> <span>Current viewport position:</span>
@ -16,13 +16,18 @@
</div> </div>
<div> <div>
<span>Current cursor position:</span> <span>Current cursor position:</span>
<span>{{ Math.round(mousePosition.x) }}x{{ Math.round(mousePosition.y) }}</span> <span
>{{ Math.round(mousePosition.x) }}x{{
Math.round(mousePosition.y)
}}</span
>
</div> </div>
<div> <div>
<span>Zoom level:</span><span>{{ (Math.round(bbox.z * 1000) / 1000) }}</span> <span>Zoom level:</span
><span>{{ Math.round(bbox.z * 1000) / 1000 }}</span>
</div> </div>
<label> <label>
<input v-model="showInternal" type="checkbox"> <input v-model="showInternal" type="checkbox" />
<label>Show internal elements</label> <label>Show internal elements</label>
</label> </label>
</div> </div>
@ -30,46 +35,49 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent, onMounted, reactive, ref} from "vue"; import { defineComponent, onMounted, reactive, ref } from "vue";
import createPanZoom, {PanZoom} from "panzoom"; import createPanZoom, { PanZoom } from "panzoom";
import VideoScroll, {VideoScrollDef, VideoScrollDirection} from "@/components/VideoScroll.vue"; import VideoScroll, {
import AudioArea, {AudioAreaDef} from "@/components/AudioArea.vue"; VideoScrollDef,
VideoScrollDirection,
} from "@/components/VideoScroll.vue";
import AudioArea, { AudioAreaDef } from "@/components/AudioArea.vue";
import Stats from "stats.js"; import Stats from "stats.js";
import {rotate} from "@/utils"; import { rotate } from "@/utils";
import fetchProgress from "fetch-progress"; import fetchProgress from "fetch-progress";
export interface BoundingBox { export interface BoundingBox {
x: number, x: number;
y: number, y: number;
w: number, w: number;
h: number, h: number;
z: number z: number;
} }
export default defineComponent({ export default defineComponent({
name: "SVGContent", name: "SVGContent",
components: {AudioArea, VideoScroll}, components: { AudioArea, VideoScroll },
props: { props: {
url: { url: {
type: String, type: String,
required: true required: true,
} },
}, },
data() { data() {
return { return {
showInternal: false showInternal: false,
}; };
}, },
watch: { watch: {
showInternal(value) { showInternal(value) {
Array.from(this.root!.getElementsByClassName("internal")).forEach((el) => { Array.from(this.root!.getElementsByClassName("internal")).forEach(
(el) => {
(el as SVGElement).style.visibility = value ? "visible" : "hidden"; (el as SVGElement).style.visibility = value ? "visible" : "hidden";
});
} }
);
}, },
setup(props, {emit}) { },
setup(props, { emit }) {
const root = ref<HTMLDivElement | null>(null); const root = ref<HTMLDivElement | null>(null);
const loadedPercent = ref(0); const loadedPercent = ref(0);
const panzoom = ref<null | PanZoom>(null); const panzoom = ref<null | PanZoom>(null);
@ -82,16 +90,16 @@ export default defineComponent({
y: ref(0), y: ref(0),
w: ref(0), w: ref(0),
h: ref(0), h: ref(0),
z: ref(1) z: ref(1),
}); });
const mousePosition = reactive({ const mousePosition = reactive({
x: ref(0), x: ref(0),
y: ref(0) y: ref(0),
}); });
const panning = ref(false); const panning = ref(false);
onMounted(async () => { onMounted(async () => {
const element = root.value as unknown as HTMLDivElement; const element = (root.value as unknown) as HTMLDivElement;
console.info("[SVG] Initializing."); console.info("[SVG] Initializing.");
// Fetch & load SVG // Fetch & load SVG
@ -103,13 +111,20 @@ export default defineComponent({
}, },
}) })
); );
const svgParsed = new DOMParser().parseFromString(await fetchResult.text(), "image/svg+xml") as Document; const svgParsed = new DOMParser().parseFromString(
await fetchResult.text(),
"image/svg+xml"
) as Document;
console.debug("[SVG] Loaded."); console.debug("[SVG] Loaded.");
loadedPercent.value = 100; loadedPercent.value = 100;
const svg = element.appendChild(svgParsed.firstElementChild as Element) as any; const svg = element.appendChild(
svgParsed.firstElementChild as Element
) as any;
// Set document background // Set document background
const pageColor = svg.getElementById("base")?.attributes.getNamedItem("pagecolor"); const pageColor = svg
.getElementById("base")
?.attributes.getNamedItem("pagecolor");
if (pageColor) { if (pageColor) {
console.debug(`[SVG] Found pageColor attribute: ${pageColor.value}`); console.debug(`[SVG] Found pageColor attribute: ${pageColor.value}`);
emit("setBackground", pageColor.value); emit("setBackground", pageColor.value);
@ -138,48 +153,56 @@ export default defineComponent({
} }
return true; return true;
} },
}); });
panzoom.value = pz; panzoom.value = pz;
// Calculate SVG-unit bounding box, update transform // Calculate SVG-unit bounding box, update transform
pz.on("transform", function (_) { pz.on("transform", function (_) {
const transform = pz.getTransform(); const transform = pz.getTransform();
const currentRatio = svg.clientWidth * transform.scale / svg.viewBox.baseVal.width; const currentRatio =
(svg.clientWidth * transform.scale) / svg.viewBox.baseVal.width;
bbox.x = transform.x / currentRatio * -1; bbox.x = (transform.x / currentRatio) * -1;
bbox.y = transform.y / currentRatio * -1; bbox.y = (transform.y / currentRatio) * -1;
bbox.w = window.innerWidth / currentRatio; bbox.w = window.innerWidth / currentRatio;
bbox.h = window.innerHeight / currentRatio; bbox.h = window.innerHeight / currentRatio;
bbox.z = transform.scale; bbox.z = transform.scale;
window.location.hash = window.location.hash = `${Math.round(bbox.x + bbox.w / 2)},${Math.round(
`${Math.round(bbox.x + bbox.w / 2)},${Math.round(bbox.y + bbox.h / 2)},${Math.round(transform.scale * 1000) / 1000}z`; bbox.y + bbox.h / 2
)},${Math.round(transform.scale * 1000) / 1000}z`;
}); });
function panToElement(target: SVGRectElement, smooth: boolean) { function panToElement(target: SVGRectElement, smooth: boolean) {
console.debug(`[SVG] Panning to element: #${target.id}`); console.debug(`[SVG] Panning to element: #${target.id}`);
const transform = pz.getTransform(); const transform = pz.getTransform();
const currentRatio = svg.clientWidth * transform.scale / svg.viewBox.baseVal.width; const currentRatio =
(svg.clientWidth * transform.scale) / svg.viewBox.baseVal.width;
const ratio = svg.clientWidth / svg.viewBox.baseVal.width; const ratio = svg.clientWidth / svg.viewBox.baseVal.width;
const targetScale = window.innerWidth / (target.width.baseVal.value * ratio); const targetScale =
window.innerWidth / (target.width.baseVal.value * ratio);
const svgTargetX = (target.x.baseVal.value + target.width.baseVal.value / 2) * currentRatio; const svgTargetX =
const svgTargetY = (target.y.baseVal.value + target.height.baseVal.value / 2) * currentRatio; (target.x.baseVal.value + target.width.baseVal.value / 2) *
currentRatio;
const svgTargetY =
(target.y.baseVal.value + target.height.baseVal.value / 2) *
currentRatio;
if (smooth) { if (smooth) {
panning.value = true; panning.value = true;
pz.smoothMoveTo( pz.smoothMoveTo(
svgTargetX * -1 + window.innerWidth / 2, svgTargetX * -1 + window.innerWidth / 2,
svgTargetY * -1 + window.innerHeight / 2, svgTargetY * -1 + window.innerHeight / 2
); );
setTimeout(() => { setTimeout(() => {
const finalTransform = pz.getTransform(); const finalTransform = pz.getTransform();
pz.smoothZoomAbs( pz.smoothZoomAbs(
(svgTargetX + finalTransform.x), svgTargetX + finalTransform.x,
(svgTargetY + finalTransform.y), svgTargetY + finalTransform.y,
targetScale targetScale
); );
setTimeout(() => { setTimeout(() => {
@ -189,9 +212,13 @@ export default defineComponent({
} else { } else {
pz.moveTo( pz.moveTo(
svgTargetX * -1 + window.innerWidth / 2, svgTargetX * -1 + window.innerWidth / 2,
svgTargetY * -1 + window.innerHeight / 2, svgTargetY * -1 + window.innerHeight / 2
);
pz.zoomAbs(
window.innerWidth / 2,
window.innerHeight / 2,
targetScale
); );
pz.zoomAbs(window.innerWidth / 2, window.innerHeight / 2, targetScale);
} }
} }
@ -211,18 +238,25 @@ export default defineComponent({
} }
// Pan to start element or location in hash // Pan to start element or location in hash
const locationMatch = window.location.href.match(/#([\-0-9.]+),([\-0-9.]+),([0-9.]+)z/); const locationMatch = window.location.href.match(
/#([\-0-9.]+),([\-0-9.]+),([0-9.]+)z/
);
if (locationMatch) { if (locationMatch) {
console.debug(`[SVGCONTENT] Got a location match: ${locationMatch}`); console.debug(`[SVGCONTENT] Got a location match: ${locationMatch}`);
const [_, x, y, z] = locationMatch; const [_, x, y, z] = locationMatch;
const transform = pz.getTransform(); const transform = pz.getTransform();
const currentRatio = svg.clientWidth * transform.scale / svg.viewBox.baseVal.width; const currentRatio =
(svg.clientWidth * transform.scale) / svg.viewBox.baseVal.width;
pz.moveTo( pz.moveTo(
(parseFloat(x) * currentRatio * -1 + window.innerWidth / 2), parseFloat(x) * currentRatio * -1 + window.innerWidth / 2,
(parseFloat(y) * currentRatio * -1 + window.innerHeight / 2) parseFloat(y) * currentRatio * -1 + window.innerHeight / 2
);
pz.zoomAbs(
window.innerWidth / 2,
window.innerHeight / 2,
parseFloat(z)
); );
pz.zoomAbs(window.innerWidth / 2, window.innerHeight / 2, parseFloat(z));
} else if (start) { } else if (start) {
console.debug(`[SVGCONTENT] Panning to start anchor.`); console.debug(`[SVGCONTENT] Panning to start anchor.`);
panToElement(start, false); panToElement(start, false);
@ -235,8 +269,10 @@ export default defineComponent({
// Links // Links
console.debug("[SVG] Processing hyperlinks."); console.debug("[SVG] Processing hyperlinks.");
const {anchor, hyper} = processHyperlinks(svg); const { anchor, hyper } = processHyperlinks(svg);
console.info(`[SVG] Found ${anchor.length} anchor links and ${hyper.length} hyperlinks.`); console.info(
`[SVG] Found ${anchor.length} anchor links and ${hyper.length} hyperlinks.`
);
anchor.forEach(([anchorId, element]) => { anchor.forEach(([anchorId, element]) => {
const anchor = anchors.value.find((a) => a.id == anchorId); const anchor = anchors.value.find((a) => a.id == anchorId);
if (!anchor) { if (!anchor) {
@ -265,9 +301,11 @@ export default defineComponent({
stats = new Stats(); stats = new Stats();
document.body.appendChild(stats.dom); document.body.appendChild(stats.dom);
Array.from(document.body.getElementsByClassName("dev")).forEach((el) => { Array.from(document.body.getElementsByClassName("dev")).forEach(
(el) => {
(el as HTMLElement).style.display = "block"; (el as HTMLElement).style.display = "block";
}); }
);
} }
// Animations: FPS Counter, Edge scrolling // Animations: FPS Counter, Edge scrolling
@ -275,7 +313,8 @@ export default defineComponent({
window.addEventListener("mousemove", (ev) => { window.addEventListener("mousemove", (ev) => {
mouse = ev; mouse = ev;
const transform = pz.getTransform(); const transform = pz.getTransform();
const currentRatio = svg.clientWidth * transform.scale / svg.viewBox.baseVal.width; const currentRatio =
(svg.clientWidth * transform.scale) / svg.viewBox.baseVal.width;
mousePosition.x = (mouse.clientX - transform.x) / currentRatio; mousePosition.x = (mouse.clientX - transform.x) / currentRatio;
mousePosition.y = (mouse.clientY - transform.y) / currentRatio; mousePosition.y = (mouse.clientY - transform.y) / currentRatio;
}); });
@ -286,8 +325,8 @@ export default defineComponent({
} }
// Edge scrolling // Edge scrolling
const MOVE_EDGE_X = window.innerWidth * .25; const MOVE_EDGE_X = window.innerWidth * 0.25;
const MOVE_EDGE_Y = window.innerHeight * .25; const MOVE_EDGE_Y = window.innerHeight * 0.25;
const MAX_SPEED = 20; const MAX_SPEED = 20;
if (mouse && !panning.value && document.fullscreenElement) { if (mouse && !panning.value && document.fullscreenElement) {
@ -295,20 +334,32 @@ export default defineComponent({
let verticalShift: number; let verticalShift: number;
const transform = pz.getTransform(); const transform = pz.getTransform();
if (mouse.clientX < MOVE_EDGE_X || mouse.clientX > window.innerWidth - MOVE_EDGE_X) { if (
mouse.clientX < MOVE_EDGE_X ||
mouse.clientX > window.innerWidth - MOVE_EDGE_X
) {
const horizontalEdgeDistance = const horizontalEdgeDistance =
(mouse.clientX < window.innerWidth / 2) ? mouse.clientX : (mouse.clientX - window.innerWidth); mouse.clientX < window.innerWidth / 2
const horizontalRatio = (MOVE_EDGE_X - Math.abs(horizontalEdgeDistance)) / MOVE_EDGE_X; ? mouse.clientX
: mouse.clientX - window.innerWidth;
const horizontalRatio =
(MOVE_EDGE_X - Math.abs(horizontalEdgeDistance)) / MOVE_EDGE_X;
const direction = mouse.clientX < MOVE_EDGE_X ? 1 : -1; const direction = mouse.clientX < MOVE_EDGE_X ? 1 : -1;
horizontalShift = horizontalRatio * direction * MAX_SPEED; horizontalShift = horizontalRatio * direction * MAX_SPEED;
} else { } else {
horizontalShift = 0; horizontalShift = 0;
} }
if (mouse.clientY < MOVE_EDGE_Y || mouse.clientY > window.innerHeight - MOVE_EDGE_Y) { if (
mouse.clientY < MOVE_EDGE_Y ||
mouse.clientY > window.innerHeight - MOVE_EDGE_Y
) {
const verticalEdgeDistance = const verticalEdgeDistance =
(mouse.clientY < window.innerHeight / 2) ? mouse.clientY : (mouse.clientY - window.innerHeight); mouse.clientY < window.innerHeight / 2
const verticalRatio = (MOVE_EDGE_Y - Math.abs(verticalEdgeDistance)) / MOVE_EDGE_Y; ? mouse.clientY
: mouse.clientY - window.innerHeight;
const verticalRatio =
(MOVE_EDGE_Y - Math.abs(verticalEdgeDistance)) / MOVE_EDGE_Y;
const direction = mouse.clientY < MOVE_EDGE_Y ? 1 : -1; const direction = mouse.clientY < MOVE_EDGE_Y ? 1 : -1;
verticalShift = verticalRatio * direction * MAX_SPEED; verticalShift = verticalRatio * direction * MAX_SPEED;
} else { } else {
@ -316,7 +367,10 @@ export default defineComponent({
} }
if (horizontalShift || verticalShift) { if (horizontalShift || verticalShift) {
pz.moveTo(transform!.x + horizontalShift, transform!.y + verticalShift); pz.moveTo(
transform!.x + horizontalShift,
transform!.y + verticalShift
);
} }
} }
@ -330,7 +384,6 @@ export default defineComponent({
requestAnimationFrame(animate); requestAnimationFrame(animate);
}); });
return { return {
root, root,
loadedPercent, loadedPercent,
@ -340,7 +393,7 @@ export default defineComponent({
scrolls, scrolls,
audioAreas, audioAreas,
bbox, bbox,
mousePosition mousePosition,
}; };
}, },
}); });
@ -362,24 +415,38 @@ async function processScrolls(svg: XMLDocument): Promise<VideoScrollDef[]> {
return Promise.all( return Promise.all(
Array.from(svg.getElementsByTagName("image")) Array.from(svg.getElementsByTagName("image"))
.filter((el) => Array.from(el.children).some((el) => el.tagName == "desc")) .filter((el) =>
Array.from(el.children).some((el) => el.tagName == "desc")
)
.map(async (el) => { .map(async (el) => {
const descNode = Array.from(el.children).find((el) => el.tagName == "desc"); const descNode = Array.from(el.children).find(
console.debug(`[SVG/VIDEOSCROLLS] Found video scroll #${el.id}: ${descNode?.textContent}`); (el) => el.tagName == "desc"
);
console.debug(
`[SVG/VIDEOSCROLLS] Found video scroll #${el.id}: ${descNode?.textContent}`
);
const [directionString, filesURL] = descNode!.textContent!.split("\n"); const [directionString, filesURL] = descNode!.textContent!.split("\n");
const directions: VideoScrollDirection[] = directionString.split(" ").map((direction) => { const directions: VideoScrollDirection[] = directionString
if (!Object.values(VideoScrollDirection).includes(direction as VideoScrollDirection)) { .split(" ")
.map((direction) => {
if (
!Object.values(VideoScrollDirection).includes(
direction as VideoScrollDirection
)
) {
throw new Error(`Unknown direction string: "${direction}"`); throw new Error(`Unknown direction string: "${direction}"`);
} }
return direction as VideoScrollDirection; return direction as VideoScrollDirection;
}); });
console.debug(`[SVG/VIDEOSCROLLS] Fetching ${filesURL}...`); console.debug(`[SVG/VIDEOSCROLLS] Fetching ${filesURL}...`);
const fileFetch = await fetch(`content/${filesURL}`); const fileFetch = await fetch(`content/${filesURL}`);
const preURL = fileFetch.url.replace(/\/files.lst$/, ""); const preURL = fileFetch.url.replace(/\/files.lst$/, "");
const files = (await fileFetch.text()).split("\n").filter(Boolean).map((str) => `${preURL}/${str}`); const files = (await fileFetch.text())
.split("\n")
.filter(Boolean)
.map((str) => `${preURL}/${str}`);
let x = el.x.baseVal.value; let x = el.x.baseVal.value;
let y = el.y.baseVal.value; let y = el.y.baseVal.value;
@ -388,7 +455,9 @@ async function processScrolls(svg: XMLDocument): Promise<VideoScrollDef[]> {
let angle = 0; let angle = 0;
const transform = el.attributes.getNamedItem("transform"); const transform = el.attributes.getNamedItem("transform");
const rotateResult = /rotate\((-?[0-9.]+)\)/.exec(transform?.value || ""); const rotateResult = /rotate\((-?[0-9.]+)\)/.exec(
transform?.value || ""
);
if (rotateResult) { if (rotateResult) {
angle = parseFloat(rotateResult[1]); angle = parseFloat(rotateResult[1]);
const [ncx, ncy] = rotate(x + w / 2, y + h / 2, 0, 0, angle); const [ncx, ncy] = rotate(x + w / 2, y + h / 2, 0, 0, angle);
@ -403,25 +472,36 @@ async function processScrolls(svg: XMLDocument): Promise<VideoScrollDef[]> {
width: w * ratio, width: w * ratio,
height: h * ratio, height: h * ratio,
directions, directions,
files files,
}; };
}) })
); );
} }
function processAudio(svg: XMLDocument): AudioAreaDef[] { function processAudio(svg: XMLDocument): AudioAreaDef[] {
const circles: (SVGCircleElement | SVGEllipseElement)[] = Array.from(svg.getElementsByTagName("circle")); const circles: (SVGCircleElement | SVGEllipseElement)[] = Array.from(
const ellipses: (SVGCircleElement | SVGEllipseElement)[] = Array.from(svg.getElementsByTagName("ellipse")); svg.getElementsByTagName("circle")
return circles.concat(ellipses) );
const ellipses: (SVGCircleElement | SVGEllipseElement)[] = Array.from(
svg.getElementsByTagName("ellipse")
);
return circles
.concat(ellipses)
.filter((el) => Array.from(el.children).some((el) => el.tagName == "desc")) .filter((el) => Array.from(el.children).some((el) => el.tagName == "desc"))
.map((el) => { .map((el) => {
const descNode = Array.from(el.children).find((el) => el.tagName == "desc"); const descNode = Array.from(el.children).find(
console.debug(`[SVG/AUDIOAREAS] Found audio area #${el.id}: ${descNode?.textContent}`); (el) => el.tagName == "desc"
);
console.debug(
`[SVG/AUDIOAREAS] Found audio area #${el.id}: ${descNode?.textContent}`
);
const audioSrc = descNode!.textContent!.trim(); const audioSrc = descNode!.textContent!.trim();
const radius = el.hasAttribute("r") ? const radius = el.hasAttribute("r")
(el as SVGCircleElement).r.baseVal.value : ? (el as SVGCircleElement).r.baseVal.value
((el as SVGEllipseElement).rx.baseVal.value + (el as SVGEllipseElement).ry.baseVal.value) / 2; : ((el as SVGEllipseElement).rx.baseVal.value +
(el as SVGEllipseElement).ry.baseVal.value) /
2;
el.classList.add("internal"); el.classList.add("internal");
@ -434,19 +514,24 @@ function processAudio(svg: XMLDocument): AudioAreaDef[] {
}); });
} }
function processHyperlinks(svg: XMLDocument): { anchor: [string, SVGAElement][], hyper: SVGAElement[] } { function processHyperlinks(
svg: XMLDocument
): { anchor: [string, SVGAElement][]; hyper: SVGAElement[] } {
const anchor: [string, SVGAElement][] = []; const anchor: [string, SVGAElement][] = [];
const hyper: SVGAElement[] = []; const hyper: SVGAElement[] = [];
Array.from(svg.getElementsByTagName("a")).forEach((el) => { Array.from(svg.getElementsByTagName("a")).forEach((el) => {
if (el.getAttribute("xlink:href")?.startsWith("anchor")) { if (el.getAttribute("xlink:href")?.startsWith("anchor")) {
anchor.push([el.getAttribute("xlink:href") as string, el as unknown as SVGAElement]); anchor.push([
el.getAttribute("xlink:href") as string,
(el as unknown) as SVGAElement,
]);
el.setAttribute("xlink:href", "#"); el.setAttribute("xlink:href", "#");
} else { } else {
el.setAttribute("target", "_blank"); el.setAttribute("target", "_blank");
hyper.push(el as unknown as SVGAElement); hyper.push((el as unknown) as SVGAElement);
} }
}); });
return {anchor, hyper}; return { anchor, hyper };
} }
function processStart(svg: XMLDocument): SVGRectElement | null { function processStart(svg: XMLDocument): SVGRectElement | null {
@ -454,9 +539,8 @@ function processStart(svg: XMLDocument): SVGRectElement | null {
if (start) { if (start) {
start.classList.add("internal"); start.classList.add("internal");
} }
return start as (SVGRectElement | null); return start as SVGRectElement | null;
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
@ -518,6 +602,6 @@ function processStart(svg: XMLDocument): SVGRectElement | null {
} }
.devpanel div span { .devpanel div span {
margin: 0 .5em; margin: 0 0.5em;
} }
</style> </style>