830 lines
24 KiB
Svelte
830 lines
24 KiB
Svelte
<script lang="ts" context="module">
|
|
export interface BoundingBox {
|
|
x: number;
|
|
y: number;
|
|
w: number;
|
|
h: number;
|
|
z: number;
|
|
}
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import createPanZoom, { type PanZoom } from "panzoom";
|
|
import Stats from "stats.js";
|
|
import { rotate } from "../utils";
|
|
import fetchProgress from "fetch-progress";
|
|
import { createEventDispatcher, onMount } from "svelte";
|
|
import VideoScroll, {
|
|
VideoScrollDirection,
|
|
type VideoScrollDef,
|
|
} from "./VideoScroll.svelte";
|
|
import AudioArea, { type AudioAreaDef } from "./AudioArea.svelte";
|
|
const dispatch = createEventDispatcher();
|
|
|
|
export let url: string;
|
|
let showInternal = false;
|
|
$: {
|
|
if (root) {
|
|
Array.from(root.getElementsByClassName("internal")).forEach((el) => {
|
|
(el as SVGElement).style.visibility = showInternal
|
|
? "visible"
|
|
: "hidden";
|
|
});
|
|
}
|
|
}
|
|
|
|
let root: HTMLDivElement;
|
|
let anchors: SVGRectElement[] = [];
|
|
let scrolls = [];
|
|
let audioAreas = [];
|
|
let loadedPercent = 0;
|
|
let panzoom: PanZoom;
|
|
let panning = false;
|
|
let bbox: BoundingBox = {
|
|
x: 0,
|
|
y: 0,
|
|
w: 0,
|
|
h: 0,
|
|
z: 0,
|
|
};
|
|
let mousePosition = {
|
|
x: 0,
|
|
y: 0,
|
|
};
|
|
let panToStart: undefined | (() => void);
|
|
|
|
onMount(async () => {
|
|
console.info("[SVG] Initializing.");
|
|
|
|
// Fetch & load SVG
|
|
console.info(`[SVG] Fetching "${url}..."`);
|
|
const fetchResult = await fetch(url).then(
|
|
fetchProgress({
|
|
onProgress(progress) {
|
|
loadedPercent = (progress.transferred / progress.total) * 100;
|
|
},
|
|
})
|
|
);
|
|
if (!fetchResult.ok) {
|
|
alert(fetchResult.status);
|
|
throw new Error("Failed to load.");
|
|
}
|
|
const svgParsed = new DOMParser().parseFromString(
|
|
await fetchResult.text(),
|
|
"image/svg+xml"
|
|
) as Document;
|
|
console.debug("[SVG] Loaded.");
|
|
loadedPercent = 100;
|
|
|
|
// Prevent 404s due to relative image paths
|
|
const imageElements = Array.from(
|
|
svgParsed.getElementsByTagName("image")
|
|
).filter((el) =>
|
|
Array.from(el.children).some((el) => el.tagName == "desc")
|
|
);
|
|
imageElements.forEach((el) => {
|
|
el.remove();
|
|
});
|
|
|
|
const svg = root.appendChild(svgParsed.firstElementChild as Element) as any;
|
|
|
|
// Set document background
|
|
const pageColor = svg
|
|
.getElementById("base")
|
|
?.attributes.getNamedItem("pagecolor");
|
|
if (pageColor) {
|
|
console.debug(`[SVG] Found pageColor attribute: ${pageColor.value}`);
|
|
dispatch("setBackground", pageColor.value);
|
|
}
|
|
|
|
// PanZoom
|
|
const pz = createPanZoom(root, {
|
|
smoothScroll: false,
|
|
minZoom: 0.1,
|
|
maxZoom: 2,
|
|
zoomSpeed: 0.05,
|
|
zoomDoubleClickSpeed: 1,
|
|
beforeMouseDown: () => {
|
|
return panning;
|
|
},
|
|
beforeWheel: () => {
|
|
return panning;
|
|
},
|
|
onTouch: function () {
|
|
return false; // tells the library to not preventDefault.
|
|
},
|
|
onDoubleClick: () => {
|
|
return; // TODO WHY BLACK?!
|
|
if (!document.fullscreenElement) {
|
|
console.debug("[SVG] Fullscreen requested.");
|
|
document.body.requestFullscreen();
|
|
} else {
|
|
console.debug("[SVG] Fullscreen exited.");
|
|
document.exitFullscreen();
|
|
}
|
|
|
|
return true;
|
|
},
|
|
});
|
|
panzoom = pz;
|
|
|
|
// Calculate SVG-unit bounding box, update transform
|
|
pz.on("transform", function (_) {
|
|
const transform = pz.getTransform();
|
|
const currentRatio =
|
|
(svg.clientWidth * transform.scale) / svg.viewBox.baseVal.width;
|
|
|
|
bbox.x = (transform.x / currentRatio) * -1;
|
|
bbox.y = (transform.y / currentRatio) * -1;
|
|
bbox.w = window.innerWidth / currentRatio;
|
|
bbox.h = window.innerHeight / currentRatio;
|
|
bbox.z = transform.scale;
|
|
|
|
window.location.hash = `${Math.round(bbox.x + bbox.w / 2)},${Math.round(
|
|
bbox.y + bbox.h / 2
|
|
)},${Math.round(transform.scale * 1000) / 1000}z`;
|
|
});
|
|
|
|
function panToElement(target: SVGRectElement, smooth: boolean) {
|
|
console.debug(`[SVG] Panning to element: #${target.id}`);
|
|
const transform = pz.getTransform();
|
|
const currentRatio =
|
|
(svg.clientWidth * transform.scale) / svg.viewBox.baseVal.width;
|
|
const ratio = svg.clientWidth / svg.viewBox.baseVal.width;
|
|
const targetScale =
|
|
window.innerWidth / (target.width.baseVal.value * ratio);
|
|
|
|
const svgTargetX =
|
|
(target.x.baseVal.value + target.width.baseVal.value / 2) *
|
|
currentRatio;
|
|
const svgTargetY =
|
|
(target.y.baseVal.value + target.height.baseVal.value / 2) *
|
|
currentRatio;
|
|
|
|
if (smooth) {
|
|
panning = true;
|
|
|
|
pz.smoothMoveTo(
|
|
svgTargetX * -1 + window.innerWidth / 2,
|
|
svgTargetY * -1 + window.innerHeight / 2
|
|
);
|
|
|
|
setTimeout(() => {
|
|
const finalTransform = pz.getTransform();
|
|
pz.smoothZoomAbs(
|
|
svgTargetX + finalTransform.x,
|
|
svgTargetY + finalTransform.y,
|
|
targetScale
|
|
);
|
|
setTimeout(() => {
|
|
panning = false;
|
|
}, 400);
|
|
}, 400 * 4);
|
|
} else {
|
|
pz.moveTo(
|
|
svgTargetX * -1 + window.innerWidth / 2,
|
|
svgTargetY * -1 + window.innerHeight / 2
|
|
);
|
|
pz.zoomAbs(window.innerWidth / 2, window.innerHeight / 2, targetScale);
|
|
}
|
|
}
|
|
|
|
function panToAnchor(anchor: SVGRectElement) {
|
|
panToElement(anchor, true);
|
|
}
|
|
|
|
// Process start element
|
|
const start = processStart(svg);
|
|
if (start) {
|
|
console.info("[SVG] Found start element.");
|
|
window.addEventListener("keydown", (ev) => {
|
|
if (ev.key === " ") {
|
|
panToElement(start, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Pan to start element or location in hash
|
|
const locationMatch = window.location.href.match(
|
|
/#([\-0-9.]+),([\-0-9.]+),([0-9.]+)z/
|
|
);
|
|
if (locationMatch) {
|
|
console.debug(`[SVGCONTENT] Got a location match: ${locationMatch}`);
|
|
const [_, x, y, z] = locationMatch;
|
|
|
|
const transform = pz.getTransform();
|
|
const currentRatio =
|
|
(svg.clientWidth * transform.scale) / svg.viewBox.baseVal.width;
|
|
pz.moveTo(
|
|
parseFloat(x) * currentRatio * -1 + window.innerWidth / 2,
|
|
parseFloat(y) * currentRatio * -1 + window.innerHeight / 2
|
|
);
|
|
pz.zoomAbs(window.innerWidth / 2, window.innerHeight / 2, parseFloat(z));
|
|
} else if (start) {
|
|
console.debug(`[SVGCONTENT] Panning to start anchor.`);
|
|
panToElement(start, false);
|
|
}
|
|
if (start) {
|
|
panToStart = () => panToElement(start, true);
|
|
}
|
|
|
|
// Anchors
|
|
console.debug("[SVG] Processing anchors.");
|
|
anchors = processAnchors(svg);
|
|
console.info(`[SVG] Found ${anchors.length} anchors.`);
|
|
|
|
// Links
|
|
console.debug("[SVG] Processing hyperlinks.");
|
|
const { anchor, hyper } = processHyperlinks(svg);
|
|
console.info(
|
|
`[SVG] Found ${anchor.length} anchor links and ${hyper.length} hyperlinks.`
|
|
);
|
|
anchor.forEach(([anchorId, element]) => {
|
|
const anchor = anchors.find((a) => a.id == anchorId);
|
|
if (!anchor) {
|
|
console.error(`[SVG] Could not find anchor #${anchorId}!`);
|
|
return;
|
|
}
|
|
element.addEventListener("click", () => {
|
|
panToElement(anchor, true);
|
|
});
|
|
});
|
|
|
|
// Images
|
|
console.debug("[SVG] Processing images.");
|
|
const images = processImages(svg);
|
|
console.info(`[SVG] Found ${images.length} images.`);
|
|
|
|
// Videos
|
|
console.debug("[SVG] Processing images as videos.");
|
|
processVideos(svg, (el) => root.appendChild(el));
|
|
// console.info(`[SVG] Found ${audioAreas.length} audio areas.`);
|
|
|
|
// Audio areas
|
|
console.debug("[SVG] Processing audio areas.");
|
|
audioAreas = processAudio(svg);
|
|
console.info(`[SVG] Found ${audioAreas.length} audio areas.`);
|
|
|
|
// Audio areas
|
|
console.debug("[SVG] Processing audio players.");
|
|
const audioPlayers = processAudioPlayers(svg);
|
|
// console.info(`[SVG] Found ${audioAreas.length} audio areas.`);
|
|
audioPlayers.forEach((el) => root.appendChild(el));
|
|
|
|
// Videoscrolls
|
|
console.debug("[SVG] Processing video scrolls.");
|
|
scrolls = await processScrolls(svg, imageElements);
|
|
console.info(`[SVG] Found ${scrolls.length} video scrolls.`);
|
|
|
|
// Debug Stats
|
|
let stats: Stats | undefined;
|
|
if (window.location.search.includes("debug")) {
|
|
console.info("[SVG] DEBUG mode active, turning on stats & dev panel.");
|
|
stats = new Stats();
|
|
document.body.appendChild(stats.dom);
|
|
|
|
Array.from(document.body.getElementsByClassName("dev")).forEach((el) => {
|
|
(el as HTMLElement).style.display = "block";
|
|
});
|
|
}
|
|
|
|
// Animations: FPS Counter, Edge scrolling
|
|
let mouse: MouseEvent | undefined;
|
|
window.addEventListener("mousemove", (ev) => {
|
|
mouse = ev;
|
|
const transform = pz.getTransform();
|
|
const currentRatio =
|
|
(svg.clientWidth * transform.scale) / svg.viewBox.baseVal.width;
|
|
mousePosition.x = (mouse.clientX - transform.x) / currentRatio;
|
|
mousePosition.y = (mouse.clientY - transform.y) / currentRatio;
|
|
});
|
|
|
|
let gamePadZoomSpeed = 10;
|
|
|
|
function animate() {
|
|
if (stats) {
|
|
stats.begin();
|
|
}
|
|
|
|
// Edge scrolling
|
|
const MOVE_EDGE_X = window.innerWidth * 0.25;
|
|
const MOVE_EDGE_Y = window.innerHeight * 0.25;
|
|
const MAX_SPEED = 20;
|
|
|
|
if (mouse && !panning && document.fullscreenElement) {
|
|
let horizontalShift: number;
|
|
let verticalShift: number;
|
|
|
|
const transform = pz.getTransform();
|
|
if (
|
|
mouse.clientX < MOVE_EDGE_X ||
|
|
mouse.clientX > window.innerWidth - MOVE_EDGE_X
|
|
) {
|
|
const horizontalEdgeDistance =
|
|
mouse.clientX < window.innerWidth / 2
|
|
? 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;
|
|
horizontalShift = horizontalRatio * direction * MAX_SPEED;
|
|
} else {
|
|
horizontalShift = 0;
|
|
}
|
|
|
|
if (
|
|
mouse.clientY < MOVE_EDGE_Y ||
|
|
mouse.clientY > window.innerHeight - MOVE_EDGE_Y
|
|
) {
|
|
const verticalEdgeDistance =
|
|
mouse.clientY < window.innerHeight / 2
|
|
? 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;
|
|
verticalShift = verticalRatio * direction * MAX_SPEED;
|
|
} else {
|
|
verticalShift = 0;
|
|
}
|
|
|
|
if (horizontalShift || verticalShift) {
|
|
pz.moveTo(
|
|
transform!.x + horizontalShift,
|
|
transform!.y + verticalShift
|
|
);
|
|
}
|
|
}
|
|
|
|
if (navigator.getGamepads) {
|
|
var gamepads = navigator.getGamepads();
|
|
var gp = gamepads[0];
|
|
|
|
if (gp) {
|
|
if (gp.buttons[7].pressed) {
|
|
gamePadZoomSpeed += 0.1;
|
|
}
|
|
if (gp.buttons[5].pressed) {
|
|
gamePadZoomSpeed -= 0.1;
|
|
}
|
|
if (gamePadZoomSpeed < 1) {
|
|
gamePadZoomSpeed = 1;
|
|
}
|
|
if (gamePadZoomSpeed > 30) {
|
|
gamePadZoomSpeed = 30;
|
|
}
|
|
|
|
const transform = pz.getTransform();
|
|
|
|
const horizontalShift = gp.axes[0] * -1 * gamePadZoomSpeed;
|
|
const verticalShift = gp.axes[1] * -1 * gamePadZoomSpeed;
|
|
|
|
if (horizontalShift || verticalShift) {
|
|
pz.moveTo(
|
|
transform!.x + horizontalShift,
|
|
transform!.y + verticalShift
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (stats) {
|
|
stats.end();
|
|
}
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
requestAnimationFrame(animate);
|
|
});
|
|
|
|
function processAnchors(document: SVGElement): SVGRectElement[] {
|
|
const result: SVGRectElement[] = [];
|
|
Array.from(document.getElementsByTagName("rect"))
|
|
.filter((el) => el.id.startsWith("anchor"))
|
|
.forEach((anchor) => {
|
|
console.debug(`[SVG/ANCHORS] Found anchor #${anchor.id}.`);
|
|
anchor.classList.add("internal");
|
|
result.push(anchor);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function processImages(svgDocument: SVGElement): SVGImageElement[] {
|
|
const result: SVGImageElement[] = [];
|
|
Array.from(svgDocument.getElementsByTagName("image")).forEach((image) => {
|
|
const href = image.getAttribute("xlink:href");
|
|
console.debug(`[SVG/IMAGES] Found image #${image.id} (${href}).`);
|
|
const origPath = href.split("/");
|
|
let newHref = `content/images/${origPath[origPath.length - 1]}`;
|
|
newHref = newHref.replace(/jpg$/, "webp");
|
|
image.setAttribute("xlink:href", newHref);
|
|
result.push(image);
|
|
|
|
const placeholderRect = document.createElementNS(
|
|
"http://www.w3.org/2000/svg",
|
|
"rect"
|
|
);
|
|
placeholderRect.setAttribute("fill", "lightgray");
|
|
["x", "y", "width", "height"].forEach((attr) => {
|
|
placeholderRect.setAttribute(attr, image.getAttribute(attr));
|
|
});
|
|
image.parentElement.prepend(placeholderRect);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function processVideos(
|
|
svgDocument: SVGElement,
|
|
callback: (el: HTMLDivElement) => void
|
|
) {
|
|
const ratio =
|
|
svgDocument.clientWidth / (svgDocument as any).viewBox.baseVal.width;
|
|
|
|
Array.from(svgDocument.getElementsByTagName("image")).forEach((el) => {
|
|
const href = el.getAttribute("xlink:href");
|
|
const origPath = href.split("/");
|
|
const basename = origPath[origPath.length - 1];
|
|
const videoUrl = `content/video/${basename.replace(
|
|
/.[a-zA-Z0-9]+$/,
|
|
".mp4"
|
|
)}`;
|
|
|
|
fetch(videoUrl, {
|
|
method: "HEAD",
|
|
cache: "no-store",
|
|
}).then((videoExistsFetch) => {
|
|
if (videoExistsFetch.status !== 200) {
|
|
return;
|
|
}
|
|
console.debug(
|
|
`[SVG/VIDEOS] Found image with video: #${el.id} (${href} / ${videoUrl}).`
|
|
);
|
|
|
|
let x = el.x.baseVal.value;
|
|
let y = el.y.baseVal.value;
|
|
let w = el.width.baseVal.value;
|
|
let h = el.height.baseVal.value;
|
|
let angle = 0;
|
|
|
|
const transform = el.attributes.getNamedItem("transform");
|
|
const rotateResult = /rotate\((-?[0-9.]+)\)/.exec(
|
|
transform?.value || ""
|
|
);
|
|
if (rotateResult) {
|
|
angle = parseFloat(rotateResult[1]);
|
|
const [ncx, ncy] = rotate(x + w / 2, y + h / 2, 0, 0, angle);
|
|
x = ncx - w / 2;
|
|
y = ncy - h / 2;
|
|
}
|
|
|
|
const playEl = document.createElement("div");
|
|
playEl.classList.add("videoOverlay");
|
|
playEl.style.position = "absolute";
|
|
playEl.style.top = `${y * ratio}px`;
|
|
playEl.style.left = `${x * ratio}px`;
|
|
playEl.style.width = `${w * ratio}px`;
|
|
playEl.style.height = `${h * ratio}px`;
|
|
playEl.addEventListener("click", () => {
|
|
dispatch("video", videoUrl);
|
|
});
|
|
callback(playEl);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function processScrolls(
|
|
svg: SVGElement,
|
|
images: SVGImageElement[]
|
|
): Promise<VideoScrollDef[]> {
|
|
const ratio = svg.clientWidth / (svg as any).viewBox.baseVal.width;
|
|
|
|
return Promise.all(
|
|
images.map(async (el) => {
|
|
const descNode = Array.from(el.children).find(
|
|
(el) => el.tagName == "desc"
|
|
);
|
|
console.debug(
|
|
`[SVG/VIDEOSCROLLS] Found video scroll #${el.id}: ${descNode?.textContent}`
|
|
);
|
|
const [directionString, filesURL] = descNode!.textContent!.split("\n");
|
|
|
|
const directions: VideoScrollDirection[] = directionString
|
|
.split(" ")
|
|
.map((direction) => {
|
|
if (
|
|
!Object.values(VideoScrollDirection).includes(
|
|
direction as VideoScrollDirection
|
|
)
|
|
) {
|
|
console.error(
|
|
`Unknown direction definition: "${direction}" (in #${el.id})`
|
|
);
|
|
return false;
|
|
}
|
|
return direction as VideoScrollDirection;
|
|
})
|
|
.filter((d) => Boolean(d)) as VideoScrollDirection[];
|
|
|
|
console.debug(`[SVG/VIDEOSCROLLS] Fetching ${filesURL}...`);
|
|
const fileFetch = await fetch(`content/${filesURL}`);
|
|
const preURL = fileFetch.url.replace(/\/files.lst$/, "");
|
|
const files = (await fileFetch.text())
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((str) => `${preURL}/${str}`);
|
|
|
|
let x = el.x.baseVal.value;
|
|
let y = el.y.baseVal.value;
|
|
let w = el.width.baseVal.value;
|
|
let h = el.height.baseVal.value;
|
|
let angle = 0;
|
|
|
|
const transform = el.attributes.getNamedItem("transform");
|
|
const rotateResult = /rotate\((-?[0-9.]+)\)/.exec(
|
|
transform?.value || ""
|
|
);
|
|
if (rotateResult) {
|
|
angle = parseFloat(rotateResult[1]);
|
|
const [ncx, ncy] = rotate(x + w / 2, y + h / 2, 0, 0, angle);
|
|
x = ncx - w / 2;
|
|
y = ncy - h / 2;
|
|
}
|
|
|
|
return {
|
|
id: el.id,
|
|
top: y * ratio,
|
|
left: x * ratio,
|
|
angle,
|
|
width: w * ratio,
|
|
height: h * ratio,
|
|
directions,
|
|
files,
|
|
};
|
|
})
|
|
);
|
|
}
|
|
|
|
function processAudio(svg: SVGElement): AudioAreaDef[] {
|
|
const circles: (SVGCircleElement | SVGEllipseElement)[] = Array.from(
|
|
svg.getElementsByTagName("circle")
|
|
);
|
|
const ellipses: (SVGCircleElement | SVGEllipseElement)[] = Array.from(
|
|
svg.getElementsByTagName("ellipse")
|
|
);
|
|
return circles
|
|
.concat(ellipses)
|
|
.filter((el) =>
|
|
Array.from(el.children).some((el) => el.tagName == "desc")
|
|
)
|
|
.map((el) => {
|
|
const descNode = Array.from(el.children).find(
|
|
(el) => el.tagName == "desc"
|
|
);
|
|
console.debug(
|
|
`[SVG/AUDIOAREAS] Found audio area #${el.id}: ${descNode?.textContent}`
|
|
);
|
|
const audioSrc = descNode!.textContent!.trim();
|
|
|
|
const radius = el.hasAttribute("r")
|
|
? (el as SVGCircleElement).r.baseVal.value
|
|
: ((el as SVGEllipseElement).rx.baseVal.value +
|
|
(el as SVGEllipseElement).ry.baseVal.value) /
|
|
2;
|
|
|
|
el.classList.add("internal");
|
|
|
|
return {
|
|
id: el.id,
|
|
cx: el.cx.baseVal.value,
|
|
cy: el.cy.baseVal.value,
|
|
radius,
|
|
src: `content/${audioSrc}`,
|
|
};
|
|
});
|
|
}
|
|
|
|
function processAudioPlayers(svg: SVGElement): HTMLAudioElement[] {
|
|
const ratio = svg.clientWidth / (svg as any).viewBox.baseVal.width;
|
|
const result = [];
|
|
|
|
Array.from(svg.getElementsByTagName("rect")).forEach((el) => {
|
|
if (el.id.includes("audio")) {
|
|
const descNode = Array.from(el.children).find(
|
|
(el) => el.tagName == "desc"
|
|
);
|
|
console.debug(
|
|
`[SVG/AUDIOPLAYERS] Found audio player #${el.id}: ${descNode?.textContent}`
|
|
);
|
|
|
|
let x = el.x.baseVal.value;
|
|
let y = el.y.baseVal.value;
|
|
let w = el.width.baseVal.value;
|
|
let h = el.height.baseVal.value;
|
|
let angle = 0;
|
|
|
|
const transform = el.attributes.getNamedItem("transform");
|
|
const rotateResult = /rotate\((-?[0-9.]+)\)/.exec(
|
|
transform?.value || ""
|
|
);
|
|
if (rotateResult) {
|
|
angle = parseFloat(rotateResult[1]);
|
|
const [ncx, ncy] = rotate(x + w / 2, y + h / 2, 0, 0, angle);
|
|
x = ncx - w / 2;
|
|
y = ncy - h / 2;
|
|
}
|
|
|
|
const audioEl = document.createElement("audio");
|
|
audioEl.classList.add("audioPlayer");
|
|
audioEl.controls = true;
|
|
audioEl.src = `content/${descNode.textContent}`;
|
|
audioEl.style.position = "absolute";
|
|
audioEl.style.top = `${y * ratio}px`;
|
|
audioEl.style.left = `${x * ratio}px`;
|
|
audioEl.style.width = `${w * ratio}px`;
|
|
audioEl.style.height = `${h * ratio}px`;
|
|
console.log({ audioEl });
|
|
result.push(audioEl);
|
|
|
|
el.classList.add("internal");
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
function processHyperlinks(svg: XMLDocument): {
|
|
anchor: [string, SVGAElement][];
|
|
hyper: SVGAElement[];
|
|
} {
|
|
const anchor: [string, SVGAElement][] = [];
|
|
const hyper: SVGAElement[] = [];
|
|
Array.from(svg.getElementsByTagName("a")).forEach((el) => {
|
|
if (el.getAttribute("xlink:href")?.startsWith("anchor")) {
|
|
anchor.push([
|
|
el.getAttribute("xlink:href") as string,
|
|
el as unknown as SVGAElement,
|
|
]);
|
|
el.setAttribute("xlink:href", "#");
|
|
} else {
|
|
el.setAttribute("target", "_blank");
|
|
hyper.push(el as unknown as SVGAElement);
|
|
}
|
|
});
|
|
return { anchor, hyper };
|
|
}
|
|
|
|
function processStart(svg: XMLDocument): SVGRectElement | null {
|
|
const start = svg.getElementById("start");
|
|
if (start) {
|
|
start.classList.add("internal");
|
|
}
|
|
return start as unknown as SVGRectElement | null;
|
|
}
|
|
</script>
|
|
|
|
<div class="svg-content">
|
|
<div class="loading-screen" class:loaded={loadedPercent === 100}>
|
|
<div style="width: {loadedPercent}%" class="loading-bar" />
|
|
</div>
|
|
<div class="content" bind:this={root}>
|
|
<div class="video-scrolls">
|
|
{#each scrolls as scroll (scroll.id)}
|
|
<VideoScroll definition={scroll} />
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{#each audioAreas as audio (audio.id)}
|
|
<AudioArea definition={audio} {bbox} />
|
|
{/each}
|
|
{#if panToStart !== undefined}
|
|
<button on:click={() => panToStart()}>zpět na začátek</button>
|
|
{/if}
|
|
<div class="dev devpanel">
|
|
<div>
|
|
<span>Current viewport position:</span>
|
|
<span>{Math.round(bbox.x)}x{Math.round(bbox.y)}</span>
|
|
</div>
|
|
<div>
|
|
<span>Current cursor position:</span>
|
|
<span>{Math.round(mousePosition.x)}x{Math.round(mousePosition.y)}</span>
|
|
</div>
|
|
<div>
|
|
<span>Zoom level:</span><span>{Math.round(bbox.z * 1000) / 1000}</span>
|
|
</div>
|
|
<div>
|
|
<label>
|
|
<input bind:checked={showInternal} type="checkbox" />
|
|
Show internal elements
|
|
</label>
|
|
</div>
|
|
<div>
|
|
<!-- svelte-ignore missing-declaration -->
|
|
<p class="version">
|
|
Version - {__COMMIT_HASH__}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style lang="scss">
|
|
:global {
|
|
.svg-content svg {
|
|
overflow: visible;
|
|
|
|
.internal {
|
|
visibility: hidden;
|
|
}
|
|
}
|
|
|
|
audio {
|
|
scale: 2;
|
|
transform: translate(25%, -25%);
|
|
}
|
|
}
|
|
|
|
.loading-screen {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background: black;
|
|
transition: opacity 0.5s;
|
|
opacity: 1;
|
|
}
|
|
|
|
.loading-screen.loaded {
|
|
opacity: 0 !important;
|
|
}
|
|
|
|
.loading-bar {
|
|
height: 6px;
|
|
background: white;
|
|
margin: calc(50vh - 3px) auto;
|
|
transition: width 0.2s;
|
|
}
|
|
|
|
button {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 50%;
|
|
font-size: 1.5rem;
|
|
transform: translateX(-50%);
|
|
padding: 1rem 2rem;
|
|
margin-bottom: 10px;
|
|
background: #fefefe;
|
|
border-radius: 1rem;
|
|
cursor: pointer;
|
|
border: 1px solid rgba(0, 0, 0, 0.5);
|
|
box-shadow: 0 6px 3px rgba(0, 0, 0, 0.33);
|
|
}
|
|
|
|
:global(.videoOverlay) {
|
|
background: url("play.png");
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
background-size: 66%;
|
|
opacity: 0.25;
|
|
background-color: rgba(0, 0, 0, 1);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.dev {
|
|
display: none;
|
|
}
|
|
|
|
.devpanel {
|
|
position: fixed;
|
|
top: 0;
|
|
right: 0;
|
|
z-index: 999;
|
|
|
|
color: white;
|
|
background: #000000aa;
|
|
border: 2px solid white;
|
|
font-family: monospace;
|
|
padding: 1em 2em;
|
|
|
|
.version {
|
|
text-align: center;
|
|
font-weight: bold;
|
|
width: 100%;
|
|
margin: 0.5em;
|
|
}
|
|
}
|
|
|
|
.devpanel div {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.devpanel label {
|
|
float: right;
|
|
}
|
|
|
|
.devpanel div span {
|
|
margin: 0 0.5em;
|
|
}
|
|
</style>
|