Compare commits
	
		
			10 commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1a93571eb4 | |||
| a3311a67fb | |||
| a5f1846491 | |||
| 261ac832b2 | |||
| d6e02406d8 | |||
| 637eaa7f55 | |||
| 258c812383 | |||
| 7a84ef1b8a | |||
| a13fd66f47 | |||
| c909d12d6a | 
					 30 changed files with 1935 additions and 5668 deletions
				
			
		
							
								
								
									
										1
									
								
								.env
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								.env
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1 +0,0 @@
 | 
			
		|||
 | 
			
		||||
							
								
								
									
										21
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1,25 +1,4 @@
 | 
			
		|||
.DS_Store
 | 
			
		||||
node_modules
 | 
			
		||||
/dist
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# local env files
 | 
			
		||||
.env.local
 | 
			
		||||
.env.*.local
 | 
			
		||||
 | 
			
		||||
# Log files
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
pnpm-debug.log*
 | 
			
		||||
 | 
			
		||||
# Editor directories and files
 | 
			
		||||
.idea
 | 
			
		||||
.vscode
 | 
			
		||||
*.suo
 | 
			
		||||
*.ntvs*
 | 
			
		||||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.sw?
 | 
			
		||||
 | 
			
		||||
public/content
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,16 +6,32 @@ build site:
 | 
			
		|||
  image: node:lts
 | 
			
		||||
  stage: build
 | 
			
		||||
  script:
 | 
			
		||||
    - npm ci --cache .npm --prefer-offline
 | 
			
		||||
    - npm run build
 | 
			
		||||
    - yarn install --cache-folder .yarn
 | 
			
		||||
    - yarn build
 | 
			
		||||
    - rm dist/.gitkeep # Necessary because `/public` is empty at the moment
 | 
			
		||||
  cache:
 | 
			
		||||
    key: ${CI_COMMIT_REF_SLUG}
 | 
			
		||||
    paths:
 | 
			
		||||
      - .npm
 | 
			
		||||
      - .yarn
 | 
			
		||||
  artifacts:
 | 
			
		||||
    paths:
 | 
			
		||||
      - dist
 | 
			
		||||
 | 
			
		||||
deploy dev:
 | 
			
		||||
  image: instrumentisto/rsync-ssh
 | 
			
		||||
  stage: deploy
 | 
			
		||||
  only:
 | 
			
		||||
    refs:
 | 
			
		||||
      - develop
 | 
			
		||||
  script:
 | 
			
		||||
    - mkdir ~/.ssh
 | 
			
		||||
    - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
 | 
			
		||||
    - chmod 644 ~/.ssh/known_hosts
 | 
			
		||||
    - eval $(ssh-agent -s)
 | 
			
		||||
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
 | 
			
		||||
    - rsync -vr -e "ssh -p ${SSH_PORT}" dist/ "${DEPLOY_DEST}/dev/"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
deploy site:
 | 
			
		||||
  image: instrumentisto/rsync-ssh
 | 
			
		||||
  stage: deploy
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										76
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										76
									
								
								README.md
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,3 +1,79 @@
 | 
			
		|||
# Line and Surface
 | 
			
		||||
 | 
			
		||||
Web presentation of the Line and Surface project.
 | 
			
		||||
 | 
			
		||||
Live at https://sdbs.cz/las/.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Authoring
 | 
			
		||||
 | 
			
		||||
**(Taken directly from [Digital Garage Anabasis](https://garage.sdbs.cz/tools.inkscape.las.authoring.md) - yet to be tidied up)**
 | 
			
		||||
 | 
			
		||||
### links
 | 
			
		||||
#### anchors
 | 
			
		||||
- object ---> right click ---> create link
 | 
			
		||||
- id without hashtag
 | 
			
		||||
 | 
			
		||||
- anchor <---description <---#anchor_id <---rectangle
 | 
			
		||||
 | 
			
		||||
#### hyperlink
 | 
			
		||||
- object ---> right click ---> create link
 | 
			
		||||
- href >>> url
 | 
			
		||||
 | 
			
		||||
#### intro / start
 | 
			
		||||
- square
 | 
			
		||||
- object properties
 | 
			
		||||
--> id `start`
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### movies
 | 
			
		||||
- image file --> object properties
 | 
			
		||||
- ---> description
 | 
			
		||||
- `down / up /... / up left / right down /....`
 | 
			
		||||
- new line
 | 
			
		||||
- `motion_source/sutr1/files.lst`
 | 
			
		||||
- ---> ! set button !
 | 
			
		||||
- `down
 | 
			
		||||
motion_source/sutr1/files.lst
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#### linux list
 | 
			
		||||
```sh
 | 
			
		||||
ls *.png > files.lst
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### windows list
 | 
			
		||||
The method is the same as for all versions of Windows, starting with Windows 3.1 (from 1985!):
 | 
			
		||||
 | 
			
		||||
1.  Open File Explorer.
 | 
			
		||||
2.  Navigate to the folder under scrutiny.
 | 
			
		||||
3.  Press Ctrl+L
 | 
			
		||||
4.  Type this command (or use copy/paste) and press Enter:  
 | 
			
		||||
    cmd  /c  dir  /b  > "%temp%\\Dir.txt"  & notepad  "%temp%\\Dir.txt"
 | 
			
		||||
	
 | 
			
		||||
### sound
 | 
			
		||||
- circle (ELLIPSE NONONO)
 | 
			
		||||
- object properties ---> desription
 | 
			
		||||
- sound_source/xxx.mp3
 | 
			
		||||
- ---> ! set button !
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Interaction manual
 | 
			
		||||
double click - fullscreen
 | 
			
		||||
click with middle button - grab
 | 
			
		||||
spacebar - anchor //intro
 | 
			
		||||
mouse pointer --> edge - edge scrolling [fullscreen must be on]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### image optimizations
 | 
			
		||||
 | 
			
		||||
#### png
 | 
			
		||||
`parallel --lb --tag optipng -o 5 -i 1 ::: **/*.png`
 | 
			
		||||
 | 
			
		||||
- `parallel` = process in [parallel](https://www.gnu.org/software/parallel/)
 | 
			
		||||
- `-o 5` = ridiculously high optimization
 | 
			
		||||
- `-i 1` = turn on [interlacing](https://blog.codinghorror.com/progressive-image-rendering/)
 | 
			
		||||
 | 
			
		||||
#### jpeg
 | 
			
		||||
`parallel --lb --tag jpegoptim --all-progressive --force ::: **/*.jpg`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
module.exports = {
 | 
			
		||||
  presets: [
 | 
			
		||||
    '@vue/cli-plugin-babel/preset'
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								index.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width,initial-scale=1" />
 | 
			
		||||
    
 | 
			
		||||
    <title>Line and Surface</title>
 | 
			
		||||
  </head>
 | 
			
		||||
 | 
			
		||||
  <body>
 | 
			
		||||
    <noscript>
 | 
			
		||||
      <strong
 | 
			
		||||
        >We're sorry but Line and Surface doesn't work properly without
 | 
			
		||||
        JavaScript enabled. Please enable it to continue.</strong
 | 
			
		||||
      >
 | 
			
		||||
    </noscript>
 | 
			
		||||
 | 
			
		||||
    <div id="app"></div>
 | 
			
		||||
    <script type="module" src="/src/main.ts"></script>
 | 
			
		||||
 | 
			
		||||
    <script
 | 
			
		||||
      data-goatcounter="https://las.goatcounter.com/count"
 | 
			
		||||
      async
 | 
			
		||||
      src="//gc.zgo.at/count.js"
 | 
			
		||||
    ></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										572
									
								
								lint_intro.ts
									
										
									
									
									
								
							
							
						
						
									
										572
									
								
								lint_intro.ts
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,572 +0,0 @@
 | 
			
		|||
#!/usr/bin/env node
 | 
			
		||||
import * as fs from "fs";
 | 
			
		||||
import * as path from "path";
 | 
			
		||||
import { Command } from "@commander-js/extra-typings";
 | 
			
		||||
import { DOMParser, Document, Element } from "@xmldom/xmldom";
 | 
			
		||||
 | 
			
		||||
interface ElementInfo {
 | 
			
		||||
  type: string;
 | 
			
		||||
  element: Element;
 | 
			
		||||
  properties: Record<string, string>;
 | 
			
		||||
  descriptions: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface LintWarning {
 | 
			
		||||
  code: string;
 | 
			
		||||
  message: string;
 | 
			
		||||
  element?: ElementInfo;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface LintContext {
 | 
			
		||||
  filePath: string;
 | 
			
		||||
  fileDir: string;
 | 
			
		||||
  svgDoc: Document;
 | 
			
		||||
  elements: ElementInfo[];
 | 
			
		||||
  warnings: LintWarning[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Setup the command line interface
 | 
			
		||||
const program = new Command()
 | 
			
		||||
  .name("svg-linter")
 | 
			
		||||
  .description("Lints SVG files according to our constraints")
 | 
			
		||||
  .version("0.1.0")
 | 
			
		||||
  .argument("<file>", "Path to the SVG file to lint")
 | 
			
		||||
  .option(
 | 
			
		||||
    "--inspect",
 | 
			
		||||
    "Inspect mode - outputs all SVG elements and their properties"
 | 
			
		||||
  )
 | 
			
		||||
  .parse();
 | 
			
		||||
 | 
			
		||||
const options = program.opts();
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Reads and parses an SVG file
 | 
			
		||||
 * @param filePath Path to the SVG file
 | 
			
		||||
 * @returns The parsed SVG document
 | 
			
		||||
 */
 | 
			
		||||
function parseSvgFile(filePath: string): Document {
 | 
			
		||||
  try {
 | 
			
		||||
    const resolvedPath = path.resolve(filePath);
 | 
			
		||||
    const svgContent = fs.readFileSync(resolvedPath, "utf-8");
 | 
			
		||||
    const parser = new DOMParser();
 | 
			
		||||
    return parser.parseFromString(svgContent, "image/svg+xml");
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error(
 | 
			
		||||
      `Error reading or parsing SVG file: ${
 | 
			
		||||
        error instanceof Error ? error.message : String(error)
 | 
			
		||||
      }`
 | 
			
		||||
    );
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Extracts properties from an SVG element
 | 
			
		||||
 * @param element The SVG element to extract properties from
 | 
			
		||||
 * @returns Record of property name to value
 | 
			
		||||
 */
 | 
			
		||||
function extractElementProperties(element: Element): Record<string, string> {
 | 
			
		||||
  const properties: Record<string, string> = {};
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < element.attributes.length; i++) {
 | 
			
		||||
    const attr = element.attributes[i];
 | 
			
		||||
    properties[attr.name] = attr.value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return properties;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Extracts all elements of a specific type from an SVG document
 | 
			
		||||
 * @param doc The SVG document
 | 
			
		||||
 * @param elementType The element type to extract
 | 
			
		||||
 * @returns Array of elements with their properties
 | 
			
		||||
 */
 | 
			
		||||
/**
 | 
			
		||||
 * Extract desc elements from a parent element
 | 
			
		||||
 * @param element The parent element
 | 
			
		||||
 * @returns Array of text content from desc elements
 | 
			
		||||
 */
 | 
			
		||||
function extractDescElements(element: Element): string[] {
 | 
			
		||||
  const descriptions: string[] = [];
 | 
			
		||||
  const descElements = element.getElementsByTagName("desc");
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < descElements.length; i++) {
 | 
			
		||||
    const descElement = descElements[i];
 | 
			
		||||
    const textContent = descElement.textContent;
 | 
			
		||||
    if (textContent) {
 | 
			
		||||
      descriptions.push(textContent.trim());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return descriptions;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function extractElements(doc: Document, elementType: string): ElementInfo[] {
 | 
			
		||||
  const elements = doc.getElementsByTagName(elementType);
 | 
			
		||||
  const result: ElementInfo[] = [];
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < elements.length; i++) {
 | 
			
		||||
    const element = elements[i];
 | 
			
		||||
    result.push({
 | 
			
		||||
      type: elementType,
 | 
			
		||||
      element: element,
 | 
			
		||||
      properties: extractElementProperties(element),
 | 
			
		||||
      descriptions: extractDescElements(element),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Linting functions
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// Lint: There should be exactly one element with an id "start", and it should be a rect
 | 
			
		||||
function lintStartRect(context: LintContext): void {
 | 
			
		||||
  const startElements = context.elements.filter(
 | 
			
		||||
    (el) => el.properties.id === "start"
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (startElements.length === 0) {
 | 
			
		||||
    context.warnings.push({
 | 
			
		||||
      code: "START_MISSING",
 | 
			
		||||
      message:
 | 
			
		||||
        "No element with id 'start' found. One rect with id='start' is required.",
 | 
			
		||||
    });
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (startElements.length > 1) {
 | 
			
		||||
    context.warnings.push({
 | 
			
		||||
      code: "START_DUPLICATE",
 | 
			
		||||
      message: `Found ${startElements.length} elements with id 'start'. Only one is allowed.`,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const nonRectStarts = startElements.filter((el) => el.type !== "rect");
 | 
			
		||||
  if (nonRectStarts.length > 0) {
 | 
			
		||||
    context.warnings.push({
 | 
			
		||||
      code: "START_NOT_RECT",
 | 
			
		||||
      message: `Element with id 'start' must be a rect, but found: ${nonRectStarts
 | 
			
		||||
        .map((el) => el.type)
 | 
			
		||||
        .join(", ")}.`,
 | 
			
		||||
      element: nonRectStarts[0],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lint: For circles with a description element, the description should point to a relative path to an existing file
 | 
			
		||||
function lintCircleDescriptions(context: LintContext): void {
 | 
			
		||||
  const circlesWithDesc = context.elements.filter(
 | 
			
		||||
    (el) => el.type === "circle" && el.descriptions.length > 0
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  circlesWithDesc.forEach((circle) => {
 | 
			
		||||
    circle.descriptions.forEach((desc) => {
 | 
			
		||||
      // Check if description appears to be a file path
 | 
			
		||||
      if (desc.includes("/") || desc.includes(".")) {
 | 
			
		||||
        // Try to resolve the path relative to the SVG file's directory
 | 
			
		||||
        const descPath = path.resolve(context.fileDir, desc);
 | 
			
		||||
        if (!fs.existsSync(descPath)) {
 | 
			
		||||
          context.warnings.push({
 | 
			
		||||
            code: "CIRCLE_DESC_FILE_MISSING",
 | 
			
		||||
            message: `Circle (id=${circle.properties.id ||
 | 
			
		||||
              "no-id"}) has description pointing to non-existent file: ${desc}`,
 | 
			
		||||
            element: circle,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lint: If there's an ellipse with any description, emit a warning
 | 
			
		||||
function lintEllipseDescriptions(context: LintContext): void {
 | 
			
		||||
  const ellipsesWithDesc = context.elements.filter(
 | 
			
		||||
    (el) => el.type === "ellipse" && el.descriptions.length > 0
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  ellipsesWithDesc.forEach((ellipse) => {
 | 
			
		||||
    // Calculate average radius
 | 
			
		||||
    const rx = parseFloat(ellipse.properties.rx || "0");
 | 
			
		||||
    const ry = parseFloat(ellipse.properties.ry || "0");
 | 
			
		||||
    const avgRadius = (rx + ry) / 2;
 | 
			
		||||
 | 
			
		||||
    context.warnings.push({
 | 
			
		||||
      code: "ELLIPSE_WITH_DESC",
 | 
			
		||||
      message: `Ellipse (id=${ellipse.properties.id ||
 | 
			
		||||
        "no-id"}) has a description. It will be processed as a circle with radius ${avgRadius} (average of rx=${rx} and ry=${ry}).`,
 | 
			
		||||
      element: ellipse,
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lint: Check that hyperlinks starting with "anchor" point to existing anchors and all anchors are referenced
 | 
			
		||||
function lintHyperlinkAnchors(context: LintContext): void {
 | 
			
		||||
  // Find all anchor elements (rect elements with IDs starting with "anchor")
 | 
			
		||||
  const anchorElements = context.elements.filter(
 | 
			
		||||
    (el) => el.type === "rect" && el.properties.id?.startsWith("anchor")
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // Find all hyperlinks (a elements)
 | 
			
		||||
  const aElements = Array.from(context.svgDoc.getElementsByTagName("a"));
 | 
			
		||||
 | 
			
		||||
  // Track anchors that have been referenced
 | 
			
		||||
  const referencedAnchors = new Set<string>();
 | 
			
		||||
 | 
			
		||||
  // Check all hyperlinks for valid anchor references
 | 
			
		||||
  aElements.forEach((aElement) => {
 | 
			
		||||
    const href = aElement.getAttribute("xlink:href");
 | 
			
		||||
    if (!href) return;
 | 
			
		||||
 | 
			
		||||
    if (href.startsWith("anchor")) {
 | 
			
		||||
      // This is an anchor reference - check if the anchor exists
 | 
			
		||||
      const anchorId = href.startsWith("#") ? href.substring(1) : href;
 | 
			
		||||
      const targetAnchor = anchorElements.find(
 | 
			
		||||
        (el) => el.properties.id === anchorId
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (!targetAnchor) {
 | 
			
		||||
        // Hyperlink points to a non-existent anchor
 | 
			
		||||
        context.warnings.push({
 | 
			
		||||
          code: "BROKEN_ANCHOR_LINK",
 | 
			
		||||
          message: `Hyperlink points to non-existent anchor: ${href}`,
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        // Mark this anchor as referenced
 | 
			
		||||
        referencedAnchors.add(anchorId);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Check for unreferenced anchors
 | 
			
		||||
  anchorElements.forEach((anchor) => {
 | 
			
		||||
    if (!referencedAnchors.has(anchor.properties.id)) {
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "UNREFERENCED_ANCHOR",
 | 
			
		||||
        message: `Anchor element with id '${anchor.properties.id}' is not referenced by any hyperlink.`,
 | 
			
		||||
        element: anchor,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lint: Validate that image elements with descriptions follow the proper format for video scrolls
 | 
			
		||||
function lintVideoScrollPaths(context: LintContext): void {
 | 
			
		||||
  const imagesWithDesc = context.elements.filter(
 | 
			
		||||
    (el) => el.type === "image" && el.descriptions.length > 0
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  imagesWithDesc.forEach((image) => {
 | 
			
		||||
    const desc = image.descriptions[0];
 | 
			
		||||
    const descLines = desc.split("\n").filter((line) => line.trim().length > 0);
 | 
			
		||||
 | 
			
		||||
    // Check that the description has at least 2 lines
 | 
			
		||||
    if (descLines.length < 2) {
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "INVALID_SCROLL_DESC_FORMAT",
 | 
			
		||||
        message: `Image (id=${image.properties.id ||
 | 
			
		||||
          "no-id"}) has a description that doesn't follow the required format: first line for direction, second line for file path.`,
 | 
			
		||||
        element: image,
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // First line should be a valid direction
 | 
			
		||||
    const directionLine = descLines[0];
 | 
			
		||||
    const validDirections = [
 | 
			
		||||
      "up",
 | 
			
		||||
      "down",
 | 
			
		||||
      "left",
 | 
			
		||||
      "right",
 | 
			
		||||
      "up left",
 | 
			
		||||
      "up right",
 | 
			
		||||
      "down left",
 | 
			
		||||
      "down right",
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Check if any valid direction is in the direction line
 | 
			
		||||
    const hasValidDirection = validDirections.some((dir) =>
 | 
			
		||||
      directionLine.toLowerCase().includes(dir)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!hasValidDirection) {
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "INVALID_SCROLL_DIRECTION",
 | 
			
		||||
        message: `Image (id=${image.properties.id ||
 | 
			
		||||
          "no-id"}) has a description with invalid scroll direction: "${directionLine}". Valid directions: ${validDirections.join(
 | 
			
		||||
          ", "
 | 
			
		||||
        )}.`,
 | 
			
		||||
        element: image,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Second line should point to an existing file
 | 
			
		||||
    const filePath = descLines[1].replace(/^\//, "");
 | 
			
		||||
    try {
 | 
			
		||||
      const fullPath = path.resolve(context.fileDir, filePath);
 | 
			
		||||
      if (!fs.existsSync(fullPath)) {
 | 
			
		||||
        context.warnings.push({
 | 
			
		||||
          code: "SCROLL_FILE_MISSING",
 | 
			
		||||
          message: `Image (id=${image.properties.id ||
 | 
			
		||||
            "no-id"}) references a non-existent file in description: ${filePath}`,
 | 
			
		||||
          element: image,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // Path resolution error
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "SCROLL_PATH_ERROR",
 | 
			
		||||
        message: `Error checking path for image (id=${image.properties.id ||
 | 
			
		||||
          "no-id"}): ${error instanceof Error ? error.message : String(error)}`,
 | 
			
		||||
        element: image,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Also check if xlink:href exists and points to a valid directory
 | 
			
		||||
    const xlinkHref = image.properties["xlink:href"];
 | 
			
		||||
    if (!xlinkHref) {
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "SCROLL_MISSING_HREF",
 | 
			
		||||
        message: `Image (id=${image.properties.id ||
 | 
			
		||||
          "no-id"}) with scroll description is missing xlink:href attribute`,
 | 
			
		||||
        element: image,
 | 
			
		||||
      });
 | 
			
		||||
    } else if (!xlinkHref.includes("/")) {
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "INVALID_SCROLL_PATH",
 | 
			
		||||
        message: `Image (id=${image.properties.id ||
 | 
			
		||||
          "no-id"}) has an xlink:href that doesn't point to a directory: ${xlinkHref}`,
 | 
			
		||||
        element: image,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lint: Check that all element IDs are unique across the SVG
 | 
			
		||||
function lintIdUniqueness(context: LintContext): void {
 | 
			
		||||
  // Create a map to track IDs and their elements
 | 
			
		||||
  const idMap = new Map<string, ElementInfo[]>();
 | 
			
		||||
 | 
			
		||||
  // Collect all elements with IDs
 | 
			
		||||
  context.elements.forEach((element) => {
 | 
			
		||||
    if (element.properties.id) {
 | 
			
		||||
      const elementsWithId = idMap.get(element.properties.id) || [];
 | 
			
		||||
      elementsWithId.push(element);
 | 
			
		||||
      idMap.set(element.properties.id, elementsWithId);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Check for duplicates
 | 
			
		||||
  idMap.forEach((elements, id) => {
 | 
			
		||||
    if (elements.length > 1) {
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "DUPLICATE_ID",
 | 
			
		||||
        message: `ID '${id}' is used by ${
 | 
			
		||||
          elements.length
 | 
			
		||||
        } elements: ${elements.map((e) => e.type).join(", ")}.`,
 | 
			
		||||
        element: elements[0],
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lint: Check that files referenced in videoscroll file lists exist
 | 
			
		||||
function lintVideoScrollFileContents(context: LintContext): void {
 | 
			
		||||
  // Find all image elements with descriptions that follow the videoscroll format
 | 
			
		||||
  const imagesWithDesc = context.elements.filter(
 | 
			
		||||
    (el) => el.type === "image" && el.descriptions.length > 0
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // Process only images with valid descriptions (at least 2 lines)
 | 
			
		||||
  imagesWithDesc.forEach((image) => {
 | 
			
		||||
    const desc = image.descriptions[0];
 | 
			
		||||
    const descLines = desc.split("\n").filter((line) => line.trim().length > 0);
 | 
			
		||||
 | 
			
		||||
    // Skip if the description doesn't have at least 2 lines
 | 
			
		||||
    if (descLines.length < 2) return;
 | 
			
		||||
 | 
			
		||||
    // Get the file list path from the second line
 | 
			
		||||
    const fileListPath = descLines[1].replace(/^\//, "");
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      // Resolve the full path to the file list
 | 
			
		||||
      const fullFileListPath = path.resolve(context.fileDir, fileListPath);
 | 
			
		||||
 | 
			
		||||
      // Skip if the file list doesn't exist (already checked in lintVideoScrollPaths)
 | 
			
		||||
      if (!fs.existsSync(fullFileListPath)) return;
 | 
			
		||||
 | 
			
		||||
      // Read the file list contents
 | 
			
		||||
      const fileListContent = fs.readFileSync(fullFileListPath, "utf-8");
 | 
			
		||||
      const referencedFiles = fileListContent
 | 
			
		||||
        .split("\n")
 | 
			
		||||
        .map((line) => line.trim())
 | 
			
		||||
        .filter((line) => line.length > 0);
 | 
			
		||||
 | 
			
		||||
      // Get the directory containing the file list
 | 
			
		||||
      const fileListDir = path.dirname(fullFileListPath);
 | 
			
		||||
 | 
			
		||||
      // Check if each referenced file exists
 | 
			
		||||
      const missingFiles: string[] = [];
 | 
			
		||||
 | 
			
		||||
      referencedFiles.forEach((referencedFile) => {
 | 
			
		||||
        const fullFilePath = path.resolve(fileListDir, referencedFile);
 | 
			
		||||
        if (!fs.existsSync(fullFilePath)) {
 | 
			
		||||
          missingFiles.push(referencedFile);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // If there are missing files, emit a warning
 | 
			
		||||
      if (missingFiles.length > 0) {
 | 
			
		||||
        context.warnings.push({
 | 
			
		||||
          code: "SCROLL_MISSING_FILES",
 | 
			
		||||
          message: `Image (id=${image.properties.id ||
 | 
			
		||||
            "no-id"}) references a file list (${fileListPath}) that contains ${
 | 
			
		||||
            missingFiles.length
 | 
			
		||||
          } missing files: ${missingFiles.join(", ")}.`,
 | 
			
		||||
          element: image,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // Error reading or parsing the file list
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "SCROLL_FILE_LIST_ERROR",
 | 
			
		||||
        message: `Error reading file list for image (id=${image.properties.id ||
 | 
			
		||||
          "no-id"}): ${error instanceof Error ? error.message : String(error)}`,
 | 
			
		||||
        element: image,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Run all lint checks
 | 
			
		||||
function runAllLints(context: LintContext): void {
 | 
			
		||||
  lintStartRect(context);
 | 
			
		||||
  lintCircleDescriptions(context);
 | 
			
		||||
  lintEllipseDescriptions(context);
 | 
			
		||||
  lintHyperlinkAnchors(context);
 | 
			
		||||
  lintVideoScrollPaths(context);
 | 
			
		||||
  lintIdUniqueness(context);
 | 
			
		||||
  lintVideoScrollFileContents(context);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Output warnings
 | 
			
		||||
function outputWarnings(context: LintContext): void {
 | 
			
		||||
  if (context.warnings.length === 0) {
 | 
			
		||||
    console.log("✅ SVG file passes all lints.");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  console.log("\n⚠️  Lint Warnings:");
 | 
			
		||||
  console.log("=================\n");
 | 
			
		||||
 | 
			
		||||
  context.warnings.forEach((warning, index) => {
 | 
			
		||||
    console.log(`${index + 1}. [${warning.code}] ${warning.message}`);
 | 
			
		||||
    if (warning.element) {
 | 
			
		||||
      console.log(
 | 
			
		||||
        `   Element: ${warning.element.type}${
 | 
			
		||||
          warning.element.properties.id
 | 
			
		||||
            ? ` (id=${warning.element.properties.id})`
 | 
			
		||||
            : ""
 | 
			
		||||
        }`
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    console.log("");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  console.log(`Total warnings: ${context.warnings.length}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Output inspect details
 | 
			
		||||
function outputInspectDetails(elements: ElementInfo[]): void {
 | 
			
		||||
  console.log("\nElements found in the SVG:");
 | 
			
		||||
  console.log("========================\n");
 | 
			
		||||
 | 
			
		||||
  if (elements.length === 0) {
 | 
			
		||||
    console.log(
 | 
			
		||||
      "No matching elements (image, rect, ellipse, circle) found in the SVG."
 | 
			
		||||
    );
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  elements.forEach((element, index) => {
 | 
			
		||||
    console.log(`${index + 1}. Type: ${element.type}`);
 | 
			
		||||
    console.log("   Properties:");
 | 
			
		||||
    Object.entries(element.properties).forEach(([name, value]) => {
 | 
			
		||||
      if (value.length > 128) {
 | 
			
		||||
        console.log(`     - ${name}: ${value.substring(0, 128)}...`);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log(`     - ${name}: ${value}`);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Output descriptions if any exist
 | 
			
		||||
    if (element.descriptions.length > 0) {
 | 
			
		||||
      console.log("   Descriptions:");
 | 
			
		||||
      element.descriptions.forEach((desc, descIndex) => {
 | 
			
		||||
        if (desc.length > 128) {
 | 
			
		||||
          console.log(`     - ${descIndex + 1}: ${desc.substring(0, 128)}...`);
 | 
			
		||||
        } else {
 | 
			
		||||
          console.log(`     - ${descIndex + 1}: ${desc}`);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log("");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  console.log(`Total elements found: ${elements.length}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Main function to process the SVG file
 | 
			
		||||
 */
 | 
			
		||||
function main() {
 | 
			
		||||
  const options = program.opts();
 | 
			
		||||
  const file = program.args[0];
 | 
			
		||||
  console.log(`Processing SVG file: ${file}`);
 | 
			
		||||
 | 
			
		||||
  const svgDoc = parseSvgFile(file);
 | 
			
		||||
  const fileDir = path.dirname(path.resolve(file));
 | 
			
		||||
 | 
			
		||||
  // Extract elements of interest
 | 
			
		||||
  const elementTypes = ["image", "rect", "ellipse", "circle"];
 | 
			
		||||
  const allElements: ElementInfo[] = [];
 | 
			
		||||
 | 
			
		||||
  elementTypes.forEach((type) => {
 | 
			
		||||
    const elements = extractElements(svgDoc, type);
 | 
			
		||||
    allElements.push(...elements);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Create lint context
 | 
			
		||||
  const context: LintContext = {
 | 
			
		||||
    filePath: file,
 | 
			
		||||
    fileDir: fileDir,
 | 
			
		||||
    svgDoc: svgDoc,
 | 
			
		||||
    elements: allElements,
 | 
			
		||||
    warnings: [],
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Run all lint checks
 | 
			
		||||
  runAllLints(context);
 | 
			
		||||
 | 
			
		||||
  // Output inspection details if requested
 | 
			
		||||
  if (options.inspect) {
 | 
			
		||||
    outputInspectDetails(allElements);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Output warnings
 | 
			
		||||
  outputWarnings(context);
 | 
			
		||||
 | 
			
		||||
  // Exit with appropriate code
 | 
			
		||||
  if (context.warnings.length > 0) {
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
  } else {
 | 
			
		||||
    process.exit(0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Execute the main function
 | 
			
		||||
main();
 | 
			
		||||
							
								
								
									
										48
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										48
									
								
								package.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,32 +1,32 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "line-and-surface",
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "name": "tmp",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
 | 
			
		||||
    "build": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@vue/cli": "^4.5.12",
 | 
			
		||||
    "core-js": "^3.6.5",
 | 
			
		||||
    "fetch-progress": "^1.3.0",
 | 
			
		||||
    "normalize.css": "^8.0.1",
 | 
			
		||||
    "panzoom": "^9.4.1",
 | 
			
		||||
    "stats.js": "^0.17.0",
 | 
			
		||||
    "vue": "^3.0.0",
 | 
			
		||||
    "vuex": "^4.0.0-0"
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "vite build",
 | 
			
		||||
    "preview": "vite preview",
 | 
			
		||||
    "check": "svelte-check --tsconfig ./tsconfig.json"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@commander-js/extra-typings": "^14.0.0",
 | 
			
		||||
    "@sveltejs/vite-plugin-svelte": "^1.0.1",
 | 
			
		||||
    "@tsconfig/svelte": "^3.0.0",
 | 
			
		||||
    "@types/stats.js": "^0.17.0",
 | 
			
		||||
    "@vue/cli-plugin-babel": "~4.5.0",
 | 
			
		||||
    "@vue/cli-plugin-typescript": "~4.5.0",
 | 
			
		||||
    "@vue/cli-plugin-vuex": "~4.5.0",
 | 
			
		||||
    "@vue/cli-service": "~4.5.0",
 | 
			
		||||
    "@vue/compiler-sfc": "^3.0.0",
 | 
			
		||||
    "@xmldom/xmldom": "^0.9.8",
 | 
			
		||||
    "commander": "^14.0.0",
 | 
			
		||||
    "tsx": "^4.20.3",
 | 
			
		||||
    "typescript": "~3.9.3"
 | 
			
		||||
    "svelte": "^3.49.0",
 | 
			
		||||
    "svelte-check": "^2.8.0",
 | 
			
		||||
    "svelte-preprocess": "^4.10.7",
 | 
			
		||||
    "tslib": "^2.4.0",
 | 
			
		||||
    "typescript": "^4.6.4",
 | 
			
		||||
    "vite": "^3.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@sentry/browser": "^7.7.0",
 | 
			
		||||
    "@sentry/tracing": "^7.7.0",
 | 
			
		||||
    "fetch-progress": "^1.3.0",
 | 
			
		||||
    "normalize.css": "^8.0.1",
 | 
			
		||||
    "panzoom": "^9.4.3",
 | 
			
		||||
    "sass": "^1.54.0",
 | 
			
		||||
    "stats.js": "^0.17.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,20 +0,0 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="">
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta charset="utf-8">
 | 
			
		||||
        <meta content="IE=edge" http-equiv="X-UA-Compatible">
 | 
			
		||||
        <meta content="width=device-width,initial-scale=1.0" name="viewport">
 | 
			
		||||
        <link href="<%= BASE_URL %>favicon.ico" rel="icon">
 | 
			
		||||
        <title>Line and Surface</title>
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
        <noscript>
 | 
			
		||||
            <strong>We're sorry but Line and Surface doesn't work properly without JavaScript
 | 
			
		||||
                enabled. Please enable it to continue.</strong>
 | 
			
		||||
        </noscript>
 | 
			
		||||
        <div id="app"></div>
 | 
			
		||||
        <!-- built files will be auto injected -->
 | 
			
		||||
        <script data-goatcounter="https://las.goatcounter.com/count"
 | 
			
		||||
                async src="//gc.zgo.at/count.js"></script>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										31
									
								
								src/App.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/App.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
  import SvgContent from "./components/SVGContent.svelte";
 | 
			
		||||
 | 
			
		||||
  function setBackground(ev: CustomEvent<string>) {
 | 
			
		||||
    document.body.style.background = ev.detail;
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<main>
 | 
			
		||||
  <SvgContent url="content/intro.svg" on:setBackground={setBackground} />
 | 
			
		||||
</main>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
  :global {
 | 
			
		||||
    html,
 | 
			
		||||
    body {
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      background: black;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    html,
 | 
			
		||||
    body,
 | 
			
		||||
    #app,
 | 
			
		||||
    main,
 | 
			
		||||
    main > * {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      cursor: default;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										42
									
								
								src/App.vue
									
										
									
									
									
								
							
							
						
						
									
										42
									
								
								src/App.vue
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,42 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <SVGContent
 | 
			
		||||
    id="root"
 | 
			
		||||
    url="content/intro.svg"
 | 
			
		||||
    @set-background="setBackground"
 | 
			
		||||
  />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from "vue";
 | 
			
		||||
import SVGContent from "@/components/SVGContent.vue";
 | 
			
		||||
import "normalize.css";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: "App",
 | 
			
		||||
  components: {
 | 
			
		||||
    SVGContent,
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    setBackground(background: string) {
 | 
			
		||||
      document.body.style.background = background;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
html,
 | 
			
		||||
body {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  background: black;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html,
 | 
			
		||||
body,
 | 
			
		||||
#app,
 | 
			
		||||
#root {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  cursor: default;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										66
									
								
								src/components/AudioArea.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/components/AudioArea.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,66 @@
 | 
			
		|||
<script lang="ts" context="module">
 | 
			
		||||
  export interface AudioAreaDef {
 | 
			
		||||
    id: string;
 | 
			
		||||
    cx: number;
 | 
			
		||||
    cy: number;
 | 
			
		||||
    radius: number;
 | 
			
		||||
    src: string;
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import type { BoundingBox } from "./SVGContent.svelte";
 | 
			
		||||
 | 
			
		||||
  export let definition: AudioAreaDef;
 | 
			
		||||
  export let bbox: BoundingBox;
 | 
			
		||||
 | 
			
		||||
  let audio: HTMLAudioElement;
 | 
			
		||||
 | 
			
		||||
  console.debug(`[AUDIOAREA] Initializing ${definition.src}...`);
 | 
			
		||||
  // console.debug({definition});
 | 
			
		||||
 | 
			
		||||
  const MIN_SCALE = 0.02;
 | 
			
		||||
  const MIN_VOLUME_MULTIPLIER = 0.33;
 | 
			
		||||
  const vol_x = (1 - MIN_VOLUME_MULTIPLIER) / (1 - MIN_SCALE);
 | 
			
		||||
  const vol_b = 1 - vol_x;
 | 
			
		||||
 | 
			
		||||
  function onBBoxChange() {
 | 
			
		||||
    if (!audio) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const x = bbox.x + bbox.w / 2;
 | 
			
		||||
    const y = bbox.y + bbox.h / 2;
 | 
			
		||||
    const distance = Math.sqrt(
 | 
			
		||||
      Math.pow(x - definition.cx, 2) + Math.pow(y - definition.cy, 2)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (distance < definition.radius) {
 | 
			
		||||
      if (audio.paused) {
 | 
			
		||||
        console.debug(
 | 
			
		||||
          `[AUDIOAREA] Entered audio area "${definition.src}", starting playback...`
 | 
			
		||||
        );
 | 
			
		||||
        audio.play();
 | 
			
		||||
      }
 | 
			
		||||
      const volume = (definition.radius - distance) / definition.radius;
 | 
			
		||||
      audio.volume = volume * (bbox.z < 1 ? bbox.z * vol_x + vol_b : 1);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (!audio.paused) {
 | 
			
		||||
        console.debug(
 | 
			
		||||
          `[AUDIOAREA] Left audio area "${definition.src}", pausing playback...`
 | 
			
		||||
        );
 | 
			
		||||
        audio.pause();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $: {
 | 
			
		||||
    bbox.x;
 | 
			
		||||
    bbox.y;
 | 
			
		||||
    bbox.w;
 | 
			
		||||
    bbox.h;
 | 
			
		||||
    bbox.z;
 | 
			
		||||
    onBBoxChange();
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<audio bind:this={audio} src={definition.src} loop preload="auto" />
 | 
			
		||||
| 
						 | 
				
			
			@ -1,108 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <audio ref="audio" :src="audioSrc" loop preload="auto" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, PropType, ref, watch } from "vue";
 | 
			
		||||
import { BoundingBox } from "@/components/SVGContent.vue";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: "AudioArea",
 | 
			
		||||
  props: {
 | 
			
		||||
    definition: {
 | 
			
		||||
      type: Object as PropType<AudioAreaDef>,
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
    bbox: {
 | 
			
		||||
      type: Object as PropType<BoundingBox>,
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  setup(props) {
 | 
			
		||||
    const audio = ref<HTMLAudioElement | null>(null);
 | 
			
		||||
    const audioSrc = ref<string>(""); // Ref to hold audio source after preloading
 | 
			
		||||
    const isPreloaded = ref<boolean>(false);
 | 
			
		||||
 | 
			
		||||
    console.debug(`[AUDIOAREA] Initializing ${props.definition.src}...`);
 | 
			
		||||
    console.debug(props.definition);
 | 
			
		||||
    
 | 
			
		||||
    // Preload the audio file completely to avoid keeping connections open
 | 
			
		||||
    const preloadAudio = async (src: string) => {
 | 
			
		||||
      console.debug(`[AUDIOAREA] Preloading audio: ${src}`);
 | 
			
		||||
      try {
 | 
			
		||||
        // Fetch the entire audio file
 | 
			
		||||
        const response = await fetch(src);
 | 
			
		||||
        if (!response.ok) throw new Error(`Failed to load audio: ${response.statusText}`);
 | 
			
		||||
        
 | 
			
		||||
        // Convert to blob to ensure full download
 | 
			
		||||
        const blob = await response.blob();
 | 
			
		||||
        
 | 
			
		||||
        // Create a blob URL to use as the audio source
 | 
			
		||||
        const blobUrl = URL.createObjectURL(blob);
 | 
			
		||||
        audioSrc.value = blobUrl;
 | 
			
		||||
        isPreloaded.value = true;
 | 
			
		||||
        console.debug(`[AUDIOAREA] Successfully preloaded audio: ${src}`);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error(`[AUDIOAREA] Error preloading audio: ${error}`);
 | 
			
		||||
        // Fall back to original source if preloading fails
 | 
			
		||||
        audioSrc.value = src;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    // Start preloading when component is created
 | 
			
		||||
    preloadAudio(props.definition.src);
 | 
			
		||||
 | 
			
		||||
    const MIN_SCALE = 0.02;
 | 
			
		||||
    const MIN_VOLUME_MULTIPLIER = 0.33;
 | 
			
		||||
    const vol_x = (1 - MIN_VOLUME_MULTIPLIER) / (1 - MIN_SCALE);
 | 
			
		||||
    const vol_b = 1 - vol_x;
 | 
			
		||||
 | 
			
		||||
    const onBBoxChange = () => {
 | 
			
		||||
      const x = props.bbox.x + props.bbox.w / 2;
 | 
			
		||||
      const y = props.bbox.y + props.bbox.h / 2;
 | 
			
		||||
      const distance = Math.sqrt(
 | 
			
		||||
        Math.pow(x - props.definition.cx, 2) +
 | 
			
		||||
          Math.pow(y - props.definition.cy, 2)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (distance < props.definition.radius) {
 | 
			
		||||
        if (audio.value!.paused) {
 | 
			
		||||
          console.debug(
 | 
			
		||||
            `[AUDIOAREA] Entered audio area "${props.definition.src}", starting playback...`
 | 
			
		||||
          );
 | 
			
		||||
          audio.value!.play();
 | 
			
		||||
        }
 | 
			
		||||
        const volume =
 | 
			
		||||
          (props.definition.radius - distance) / props.definition.radius;
 | 
			
		||||
        audio.value!.volume =
 | 
			
		||||
          volume * (props.bbox.z < 1 ? props.bbox.z * vol_x + vol_b : 1);
 | 
			
		||||
      } else {
 | 
			
		||||
        if (!audio.value!.paused) {
 | 
			
		||||
          console.debug(
 | 
			
		||||
            `[AUDIOAREA] Left audio area "${props.definition.src}", pausing playback...`
 | 
			
		||||
          );
 | 
			
		||||
          audio.value!.pause();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    watch(props.bbox, onBBoxChange, { deep: true });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      audio,
 | 
			
		||||
      audioSrc,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export interface AudioAreaDef {
 | 
			
		||||
  id: string;
 | 
			
		||||
  cx: number;
 | 
			
		||||
  cy: number;
 | 
			
		||||
  radius: number;
 | 
			
		||||
  src: string;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
 | 
			
		||||
<style scoped>
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										639
									
								
								src/components/SVGContent.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										639
									
								
								src/components/SVGContent.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,639 @@
 | 
			
		|||
<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,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  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.05,
 | 
			
		||||
      maxZoom: 3637937,
 | 
			
		||||
      zoomSpeed: 0.05,
 | 
			
		||||
      zoomDoubleClickSpeed: 1,
 | 
			
		||||
      beforeMouseDown: () => {
 | 
			
		||||
        return panning;
 | 
			
		||||
      },
 | 
			
		||||
      beforeWheel: () => {
 | 
			
		||||
        return panning;
 | 
			
		||||
      },
 | 
			
		||||
      onDoubleClick: () => {
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Audio areas
 | 
			
		||||
    console.debug("[SVG] Processing audio areas.");
 | 
			
		||||
    audioAreas = processAudio(svg);
 | 
			
		||||
    console.info(`[SVG] Found ${audioAreas.length} audio areas.`);
 | 
			
		||||
 | 
			
		||||
    // 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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 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}
 | 
			
		||||
  <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;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .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>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,656 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div class="svg-content">
 | 
			
		||||
    <div :class="['loading-screen', { loaded: loadedPercent === 100 }]">
 | 
			
		||||
      <div :style="{ width: `${loadedPercent}%` }" class="loading-bar"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="content" ref="root">
 | 
			
		||||
      <div class="video-scrolls">
 | 
			
		||||
        <VideoScroll
 | 
			
		||||
          v-for="scroll in scrolls"
 | 
			
		||||
          :definition="scroll"
 | 
			
		||||
          :key="scroll.id"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <AudioArea
 | 
			
		||||
      v-for="audio in audioAreas"
 | 
			
		||||
      :definition="audio"
 | 
			
		||||
      :bbox="bbox"
 | 
			
		||||
      :key="audio.id"
 | 
			
		||||
    />
 | 
			
		||||
    <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>
 | 
			
		||||
      <label>
 | 
			
		||||
        <input v-model="showInternal" type="checkbox" />
 | 
			
		||||
        <label>Show internal elements</label>
 | 
			
		||||
      </label>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, onMounted, reactive, ref } from "vue";
 | 
			
		||||
import createPanZoom, { PanZoom } from "panzoom";
 | 
			
		||||
import VideoScroll, {
 | 
			
		||||
  VideoScrollDef,
 | 
			
		||||
  VideoScrollDirection,
 | 
			
		||||
} from "@/components/VideoScroll.vue";
 | 
			
		||||
import AudioArea, { AudioAreaDef } from "@/components/AudioArea.vue";
 | 
			
		||||
import Stats from "stats.js";
 | 
			
		||||
import { rotate } from "@/utils";
 | 
			
		||||
import fetchProgress from "fetch-progress";
 | 
			
		||||
 | 
			
		||||
export interface BoundingBox {
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  w: number;
 | 
			
		||||
  h: number;
 | 
			
		||||
  z: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: "SVGContent",
 | 
			
		||||
  components: { AudioArea, VideoScroll },
 | 
			
		||||
  props: {
 | 
			
		||||
    url: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      showInternal: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    showInternal(value) {
 | 
			
		||||
      Array.from(this.root!.getElementsByClassName("internal")).forEach(
 | 
			
		||||
        (el) => {
 | 
			
		||||
          (el as SVGElement).style.visibility = value ? "visible" : "hidden";
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  setup(props, { emit }) {
 | 
			
		||||
    const root = ref<HTMLDivElement | null>(null);
 | 
			
		||||
    const loadedPercent = ref(0);
 | 
			
		||||
    const panzoom = ref<null | PanZoom>(null);
 | 
			
		||||
    const anchors = ref<SVGRectElement[]>([]);
 | 
			
		||||
    const scrolls = ref<VideoScrollDef[]>([]);
 | 
			
		||||
    const panToAnchor = ref();
 | 
			
		||||
    const audioAreas = ref<AudioAreaDef[]>([]);
 | 
			
		||||
    const bbox: BoundingBox = reactive({
 | 
			
		||||
      x: ref(0),
 | 
			
		||||
      y: ref(0),
 | 
			
		||||
      w: ref(0),
 | 
			
		||||
      h: ref(0),
 | 
			
		||||
      z: ref(1),
 | 
			
		||||
    });
 | 
			
		||||
    const mousePosition = reactive({
 | 
			
		||||
      x: ref(0),
 | 
			
		||||
      y: ref(0),
 | 
			
		||||
    });
 | 
			
		||||
    const panning = ref(false);
 | 
			
		||||
 | 
			
		||||
    onMounted(async () => {
 | 
			
		||||
      const element = (root.value as unknown) as HTMLDivElement;
 | 
			
		||||
      console.info("[SVG] Initializing.");
 | 
			
		||||
 | 
			
		||||
      // Fetch & load SVG
 | 
			
		||||
      console.info(`[SVG] Fetching "${props.url}..."`);
 | 
			
		||||
      const fetchResult = await fetch(props.url).then(
 | 
			
		||||
        fetchProgress({
 | 
			
		||||
          onProgress(progress) {
 | 
			
		||||
            loadedPercent.value = (progress as any).percentage;
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
      const svgParsed = new DOMParser().parseFromString(
 | 
			
		||||
        await fetchResult.text(),
 | 
			
		||||
        "image/svg+xml"
 | 
			
		||||
      ) as Document;
 | 
			
		||||
      console.debug("[SVG] Loaded.");
 | 
			
		||||
      loadedPercent.value = 100;
 | 
			
		||||
      const svg = element.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}`);
 | 
			
		||||
        emit("setBackground", pageColor.value);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // PanZoom
 | 
			
		||||
      const pz = createPanZoom(element, {
 | 
			
		||||
        smoothScroll: false,
 | 
			
		||||
        minZoom: 0.05,
 | 
			
		||||
        maxZoom: 3637937,
 | 
			
		||||
        zoomSpeed: 0.05,
 | 
			
		||||
        zoomDoubleClickSpeed: 1,
 | 
			
		||||
        beforeMouseDown: () => {
 | 
			
		||||
          return panning.value;
 | 
			
		||||
        },
 | 
			
		||||
        beforeWheel: () => {
 | 
			
		||||
          return panning.value;
 | 
			
		||||
        },
 | 
			
		||||
        onDoubleClick: () => {
 | 
			
		||||
          if (!document.fullscreenElement) {
 | 
			
		||||
            console.debug("[SVG] Fullscreen requested.");
 | 
			
		||||
            document.body.requestFullscreen();
 | 
			
		||||
          } else {
 | 
			
		||||
            console.debug("[SVG] Fullscreen exited.");
 | 
			
		||||
            document.exitFullscreen();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return true;
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
      panzoom.value = 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.value = 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.value = 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
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      panToAnchor.value = (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);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Anchors
 | 
			
		||||
      console.debug("[SVG] Processing anchors.");
 | 
			
		||||
      anchors.value = processAnchors(svg);
 | 
			
		||||
      console.info(`[SVG] Found ${anchors.value.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.value.find((a) => a.id == anchorId);
 | 
			
		||||
        if (!anchor) {
 | 
			
		||||
          console.error(`[SVG] Could not find anchor #${anchorId}!`);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        element.addEventListener("click", () => {
 | 
			
		||||
          panToElement(anchor, true);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Audio areas
 | 
			
		||||
      console.debug("[SVG] Processing audio areas.");
 | 
			
		||||
      audioAreas.value = processAudio(svg);
 | 
			
		||||
      console.info(`[SVG] Found ${audioAreas.value.length} audio areas.`);
 | 
			
		||||
 | 
			
		||||
      // Videoscrolls
 | 
			
		||||
      console.debug("[SVG] Processing video scrolls.");
 | 
			
		||||
      scrolls.value = await processScrolls(svg);
 | 
			
		||||
      console.info(`[SVG] Found ${scrolls.value.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.value && 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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      root,
 | 
			
		||||
      loadedPercent,
 | 
			
		||||
      panzoom,
 | 
			
		||||
      anchors,
 | 
			
		||||
      panToAnchor,
 | 
			
		||||
      scrolls,
 | 
			
		||||
      audioAreas,
 | 
			
		||||
      bbox,
 | 
			
		||||
      mousePosition,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function processAnchors(document: XMLDocument): 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function processScrolls(svg: XMLDocument): Promise<VideoScrollDef[]> {
 | 
			
		||||
  const ratio = (svg as any).clientWidth / (svg as any).viewBox.baseVal.width;
 | 
			
		||||
 | 
			
		||||
  return Promise.all(
 | 
			
		||||
    Array.from(svg.getElementsByTagName("image"))
 | 
			
		||||
      .filter((el) =>
 | 
			
		||||
        Array.from(el.children).some((el) => el.tagName == "desc")
 | 
			
		||||
      )
 | 
			
		||||
      .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: XMLDocument): 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 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 SVGRectElement | null;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
 | 
			
		||||
<style>
 | 
			
		||||
.svg-content svg {
 | 
			
		||||
  overflow: visible;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.svg-content svg .internal {
 | 
			
		||||
  visibility: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.devpanel div {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.devpanel label {
 | 
			
		||||
  float: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.devpanel div span {
 | 
			
		||||
  margin: 0 0.5em;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										152
									
								
								src/components/VideoScroll.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								src/components/VideoScroll.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,152 @@
 | 
			
		|||
<script lang="ts" context="module">
 | 
			
		||||
  export enum VideoScrollDirection {
 | 
			
		||||
    RIGHT = "right",
 | 
			
		||||
    LEFT = "left",
 | 
			
		||||
    UP = "up",
 | 
			
		||||
    DOWN = "down",
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface VideoScrollDef {
 | 
			
		||||
    id: string;
 | 
			
		||||
    top: number;
 | 
			
		||||
    left: number;
 | 
			
		||||
    angle: number;
 | 
			
		||||
    width: number;
 | 
			
		||||
    height: number;
 | 
			
		||||
    directions: VideoScrollDirection[];
 | 
			
		||||
    files: string[];
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import { rotate } from "../utils";
 | 
			
		||||
  import { onMount } from "svelte";
 | 
			
		||||
 | 
			
		||||
  export let definition: VideoScrollDef;
 | 
			
		||||
 | 
			
		||||
  let root: HTMLDivElement;
 | 
			
		||||
  let dynamicFiles: { id: string; top: number; left: number; src: string }[] =
 | 
			
		||||
    [];
 | 
			
		||||
  $: {
 | 
			
		||||
    dynamicFiles = definition.files.slice(1).map((src: string, idx: number) => {
 | 
			
		||||
      const id = `${idx}_${src}`;
 | 
			
		||||
      const cy =
 | 
			
		||||
        definition.top +
 | 
			
		||||
        (isVertical ? definition.height * (idx + 1) * verticalDirection : 0);
 | 
			
		||||
      const cx =
 | 
			
		||||
        definition.left +
 | 
			
		||||
        (isHorizontal ? definition.width * (idx + 1) * horizontalDirection : 0);
 | 
			
		||||
      const [left, top] = rotate(
 | 
			
		||||
        cx,
 | 
			
		||||
        cy,
 | 
			
		||||
        definition.left,
 | 
			
		||||
        definition.top,
 | 
			
		||||
        definition.angle
 | 
			
		||||
      );
 | 
			
		||||
      return { id, top, left, src };
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $: isHorizontal = definition.directions.some(
 | 
			
		||||
    (dir: VideoScrollDirection) =>
 | 
			
		||||
      dir === VideoScrollDirection.LEFT || dir === VideoScrollDirection.RIGHT
 | 
			
		||||
  );
 | 
			
		||||
  $: isVertical = definition.directions.some(
 | 
			
		||||
    (dir: VideoScrollDirection) =>
 | 
			
		||||
      dir === VideoScrollDirection.UP || dir === VideoScrollDirection.DOWN
 | 
			
		||||
  );
 | 
			
		||||
  $: horizontalDirection = definition.directions.includes(
 | 
			
		||||
    VideoScrollDirection.RIGHT
 | 
			
		||||
  )
 | 
			
		||||
    ? 1
 | 
			
		||||
    : -1;
 | 
			
		||||
  $: verticalDirection = definition.directions.includes(
 | 
			
		||||
    VideoScrollDirection.DOWN
 | 
			
		||||
  )
 | 
			
		||||
    ? 1
 | 
			
		||||
    : -1;
 | 
			
		||||
 | 
			
		||||
  onMount(() => {
 | 
			
		||||
    const observer = new IntersectionObserver((entries, _) => {
 | 
			
		||||
      entries.forEach((entry) => {
 | 
			
		||||
        const element = entry.target as HTMLImageElement;
 | 
			
		||||
        if (entry.isIntersecting) {
 | 
			
		||||
          element.classList.add("visible");
 | 
			
		||||
          if (!element.src) {
 | 
			
		||||
            console.debug(
 | 
			
		||||
              `[VIDEOSCROLL] Intersected, loading ${element.dataset.src}`
 | 
			
		||||
            );
 | 
			
		||||
            element.src = element.dataset.src!;
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              element.classList.add("displayed");
 | 
			
		||||
            }, 3000);
 | 
			
		||||
            element.onload = () => {
 | 
			
		||||
              element.classList.add("displayed");
 | 
			
		||||
              element.classList.add("loaded");
 | 
			
		||||
              if (isHorizontal) {
 | 
			
		||||
                element.style.height = "auto";
 | 
			
		||||
              } else {
 | 
			
		||||
                element.style.width = "auto";
 | 
			
		||||
              }
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    if (root) {
 | 
			
		||||
      Array.from(root.children).forEach((el) => {
 | 
			
		||||
        observer.observe(el);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if definition.directions.length > 0}
 | 
			
		||||
  <div class="video-scroll" bind:this={root}>
 | 
			
		||||
    <img
 | 
			
		||||
      class="visible displayed loaded"
 | 
			
		||||
      src={definition.files[0]}
 | 
			
		||||
      style:top="{Math.round(definition.top)}px"
 | 
			
		||||
      style:left="{Math.round(definition.left)}px"
 | 
			
		||||
      style:width={isHorizontal ? `${Math.round(definition.width)}px` : "auto"}
 | 
			
		||||
      style:height={isVertical ? `${Math.round(definition.height)}px` : "auto"}
 | 
			
		||||
      style:transform="rotate({definition.angle}deg)"
 | 
			
		||||
      alt=""
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    {#each dynamicFiles as file (file.id)}
 | 
			
		||||
      <img
 | 
			
		||||
        data-src={file.src}
 | 
			
		||||
        style:top="{Math.round(file.top)}px"
 | 
			
		||||
        style:left="{Math.round(file.left)}px"
 | 
			
		||||
        style:width="{Math.round(definition.width)}px"
 | 
			
		||||
        style:height="{Math.round(definition.height)}px"
 | 
			
		||||
        style:transform="rotate({definition.angle}deg)"
 | 
			
		||||
        alt=""
 | 
			
		||||
      />
 | 
			
		||||
    {/each}
 | 
			
		||||
  </div>
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
  .video-scroll img {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    image-rendering: optimizeSpeed;
 | 
			
		||||
    background: grey;
 | 
			
		||||
    visibility: hidden;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transition: opacity 0.5s;
 | 
			
		||||
 | 
			
		||||
    &.visible {
 | 
			
		||||
      visibility: visible !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.displayed {
 | 
			
		||||
      opacity: 1 !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.loaded {
 | 
			
		||||
      background: transparent !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,180 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div class="video-scroll" ref="root" v-if="definition.directions.length > 0">
 | 
			
		||||
    <img
 | 
			
		||||
      class="visible displayed loaded"
 | 
			
		||||
      :src="definition.files[0]"
 | 
			
		||||
      :style="{
 | 
			
		||||
        top: `${Math.round(definition.top)}px`,
 | 
			
		||||
        left: `${Math.round(definition.left)}px`,
 | 
			
		||||
        width: isHorizontal ? `${Math.round(definition.width)}px` : 'auto',
 | 
			
		||||
        height: isVertical ? `${Math.round(definition.height)}px` : 'auto',
 | 
			
		||||
        transform: `rotate(${definition.angle}deg)`,
 | 
			
		||||
      }"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <!--suppress RequiredAttributes -->
 | 
			
		||||
    <img
 | 
			
		||||
      v-for="(file, idx) in dynamicFiles"
 | 
			
		||||
      :key="`${idx}_${file.src}`"
 | 
			
		||||
      :data-src="file.src"
 | 
			
		||||
      :style="{
 | 
			
		||||
        top: `${Math.round(file.top)}px`,
 | 
			
		||||
        left: `${Math.round(file.left)}px`,
 | 
			
		||||
        width: `${Math.round(definition.width)}px`,
 | 
			
		||||
        height: `${Math.round(definition.height)}px`,
 | 
			
		||||
        transform: `rotate(${definition.angle}deg)`,
 | 
			
		||||
      }"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, PropType } from "vue";
 | 
			
		||||
import { rotate } from "@/utils";
 | 
			
		||||
import { queueImageForLoading } from "@/services/ImageLoader";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: "VideoScroll",
 | 
			
		||||
  props: {
 | 
			
		||||
    definition: {
 | 
			
		||||
      type: Object as PropType<VideoScrollDef>,
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    dynamicFiles(): { top: number; left: number; src: string }[] {
 | 
			
		||||
      return this.definition.files.slice(1).map((src: string, idx: number) => {
 | 
			
		||||
        const cy =
 | 
			
		||||
          this.definition.top +
 | 
			
		||||
          (this.isVertical
 | 
			
		||||
            ? this.definition.height * (idx + 1) * this.verticalDirection
 | 
			
		||||
            : 0);
 | 
			
		||||
        const cx =
 | 
			
		||||
          this.definition.left +
 | 
			
		||||
          (this.isHorizontal
 | 
			
		||||
            ? this.definition.width * (idx + 1) * this.horizontalDirection
 | 
			
		||||
            : 0);
 | 
			
		||||
        const [left, top] = rotate(
 | 
			
		||||
          cx,
 | 
			
		||||
          cy,
 | 
			
		||||
          this.definition.left,
 | 
			
		||||
          this.definition.top,
 | 
			
		||||
          this.definition.angle
 | 
			
		||||
        );
 | 
			
		||||
        return { top, left, src };
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    isHorizontal(): boolean {
 | 
			
		||||
      return this.definition.directions.some(
 | 
			
		||||
        (dir: VideoScrollDirection) =>
 | 
			
		||||
          dir === VideoScrollDirection.LEFT ||
 | 
			
		||||
          dir === VideoScrollDirection.RIGHT
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    isVertical(): boolean {
 | 
			
		||||
      return this.definition.directions.some(
 | 
			
		||||
        (dir: VideoScrollDirection) =>
 | 
			
		||||
          dir === VideoScrollDirection.UP || dir === VideoScrollDirection.DOWN
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    horizontalDirection(): number {
 | 
			
		||||
      return this.definition.directions.includes(VideoScrollDirection.RIGHT)
 | 
			
		||||
        ? 1
 | 
			
		||||
        : -1;
 | 
			
		||||
    },
 | 
			
		||||
    verticalDirection(): number {
 | 
			
		||||
      return this.definition.directions.includes(VideoScrollDirection.DOWN)
 | 
			
		||||
        ? 1
 | 
			
		||||
        : -1;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    handleImageLoad(element: HTMLImageElement) {
 | 
			
		||||
      // Setup image display when loaded
 | 
			
		||||
      element.classList.add("displayed");
 | 
			
		||||
      element.classList.add("loaded");
 | 
			
		||||
      
 | 
			
		||||
      // Adjust dimensions based on scroll direction
 | 
			
		||||
      if (this.isHorizontal) {
 | 
			
		||||
        element.style.height = "auto";
 | 
			
		||||
      } else {
 | 
			
		||||
        element.style.width = "auto";
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    const observer = new IntersectionObserver((entries, _) => {
 | 
			
		||||
      entries.forEach((entry) => {
 | 
			
		||||
        const element = entry.target as HTMLImageElement;
 | 
			
		||||
        if (entry.isIntersecting) {
 | 
			
		||||
          element.classList.add("visible");
 | 
			
		||||
          if (!element.src && element.dataset.src) {
 | 
			
		||||
            // Queue the image for loading through the global service
 | 
			
		||||
            const self = this;
 | 
			
		||||
            queueImageForLoading(element, function() {
 | 
			
		||||
              self.handleImageLoad(element);
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            // Add a fallback to show the image after a timeout even if not fully loaded
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              if (!element.classList.contains("loaded")) {
 | 
			
		||||
                element.classList.add("displayed");
 | 
			
		||||
              }
 | 
			
		||||
            }, 3000);
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          element.classList.remove("visible");
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (this.$refs.root) {
 | 
			
		||||
      Array.from((this.$refs.root as Element).children).forEach((el) => {
 | 
			
		||||
        observer.observe(el);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export enum VideoScrollDirection {
 | 
			
		||||
  RIGHT = "right",
 | 
			
		||||
  LEFT = "left",
 | 
			
		||||
  UP = "up",
 | 
			
		||||
  DOWN = "down",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface VideoScrollDef {
 | 
			
		||||
  id: string;
 | 
			
		||||
  top: number;
 | 
			
		||||
  left: number;
 | 
			
		||||
  angle: number;
 | 
			
		||||
  width: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
  directions: VideoScrollDirection[];
 | 
			
		||||
  files: string[];
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
 | 
			
		||||
<style>
 | 
			
		||||
.video-scroll img {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  image-rendering: optimizeSpeed;
 | 
			
		||||
  background: grey;
 | 
			
		||||
  visibility: hidden;
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  transition: opacity 0.5s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.video-scroll img.visible {
 | 
			
		||||
  visibility: visible !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.video-scroll img.displayed {
 | 
			
		||||
  opacity: 1 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.video-scroll img.loaded {
 | 
			
		||||
  background: transparent !important;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										22
									
								
								src/main.ts
									
										
									
									
									
								
							
							
						
						
									
										22
									
								
								src/main.ts
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,5 +1,19 @@
 | 
			
		|||
import { createApp } from 'vue'
 | 
			
		||||
import App from './App.vue'
 | 
			
		||||
import store from './store'
 | 
			
		||||
import App from "./App.svelte";
 | 
			
		||||
import * as Sentry from "@sentry/browser";
 | 
			
		||||
import { BrowserTracing } from "@sentry/tracing";
 | 
			
		||||
 | 
			
		||||
createApp(App).use(store).mount('#app')
 | 
			
		||||
Sentry.init({
 | 
			
		||||
  dsn: "https://8291ffc6948c419591813f3b6ab432d7@o704302.ingest.sentry.io/6603419",
 | 
			
		||||
  integrations: [new BrowserTracing()],
 | 
			
		||||
 | 
			
		||||
  // Set tracesSampleRate to 1.0 to capture 100%
 | 
			
		||||
  // of transactions for performance monitoring.
 | 
			
		||||
  // We recommend adjusting this value in production
 | 
			
		||||
  tracesSampleRate: 0.5,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const app = new App({
 | 
			
		||||
  target: document.getElementById("app"),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default app;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,80 +0,0 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Global image loading queue service to prevent hitting browser connection limits
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// Configuration
 | 
			
		||||
const MAX_CONCURRENT_LOADS = 5;
 | 
			
		||||
 | 
			
		||||
// State
 | 
			
		||||
let activeLoads = 0;
 | 
			
		||||
const imageQueue: Array<{
 | 
			
		||||
  element: HTMLImageElement;
 | 
			
		||||
  onComplete: () => void;
 | 
			
		||||
}> = [];
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Queue an image for loading, respecting the global concurrent loading limit
 | 
			
		||||
 */
 | 
			
		||||
export function queueImageForLoading(
 | 
			
		||||
  element: HTMLImageElement,
 | 
			
		||||
  onComplete?: () => void
 | 
			
		||||
) {
 | 
			
		||||
  if (!element.dataset.src) {
 | 
			
		||||
    console.warn("[ImageLoader] Element has no data-src attribute");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Add to queue
 | 
			
		||||
  imageQueue.push({
 | 
			
		||||
    element,
 | 
			
		||||
    onComplete: onComplete || (() => {}),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Try to process queue
 | 
			
		||||
  processQueue();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Process the next items in the queue if we have capacity
 | 
			
		||||
 */
 | 
			
		||||
function processQueue() {
 | 
			
		||||
  // Load more images if we have capacity and images in the queue
 | 
			
		||||
  while (activeLoads < MAX_CONCURRENT_LOADS && imageQueue.length > 0) {
 | 
			
		||||
    const next = imageQueue.shift();
 | 
			
		||||
    if (next) {
 | 
			
		||||
      loadImage(next.element, next.onComplete);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Internal function to handle the actual image loading
 | 
			
		||||
 */
 | 
			
		||||
function loadImage(element: HTMLImageElement, onComplete: () => void) {
 | 
			
		||||
  // Increment active loads counter
 | 
			
		||||
  activeLoads++;
 | 
			
		||||
 | 
			
		||||
  const src = element.dataset.src;
 | 
			
		||||
  console.debug(`[ImageLoader] Loading ${src}`);
 | 
			
		||||
 | 
			
		||||
  // Start loading the image
 | 
			
		||||
  element.src = src!;
 | 
			
		||||
 | 
			
		||||
  // Handle load completion
 | 
			
		||||
  const handleCompletion = () => {
 | 
			
		||||
    activeLoads--;
 | 
			
		||||
    onComplete();
 | 
			
		||||
    processQueue();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Set handlers
 | 
			
		||||
  element.onload = () => {
 | 
			
		||||
    console.debug(`[ImageLoader] Loaded ${src}`);
 | 
			
		||||
    handleCompletion();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  element.onerror = () => {
 | 
			
		||||
    console.error(`[ImageLoader] Failed to load ${src}`);
 | 
			
		||||
    handleCompletion();
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								src/shims-vue.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								src/shims-vue.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
/* eslint-disable */
 | 
			
		||||
declare module '*.vue' {
 | 
			
		||||
  import type { DefineComponent } from 'vue'
 | 
			
		||||
  const component: DefineComponent<{}, {}, any>
 | 
			
		||||
  export default component
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +0,0 @@
 | 
			
		|||
import { createStore } from 'vuex'
 | 
			
		||||
 | 
			
		||||
export default createStore({
 | 
			
		||||
  state: {
 | 
			
		||||
  },
 | 
			
		||||
  mutations: {
 | 
			
		||||
  },
 | 
			
		||||
  actions: {
 | 
			
		||||
  },
 | 
			
		||||
  modules: {
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										3
									
								
								src/vite-env.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/vite-env.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
/// <reference types="svelte" />
 | 
			
		||||
/// <reference types="vite/client" />
 | 
			
		||||
declare const __COMMIT_HASH__: string
 | 
			
		||||
							
								
								
									
										7
									
								
								svelte.config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								svelte.config.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
import sveltePreprocess from 'svelte-preprocess'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  // Consult https://github.com/sveltejs/svelte-preprocess
 | 
			
		||||
  // for more information about preprocessors
 | 
			
		||||
  preprocess: sveltePreprocess()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,39 +1,21 @@
 | 
			
		|||
{
 | 
			
		||||
  "extends": "@tsconfig/svelte/tsconfig.json",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "target": "esnext",
 | 
			
		||||
    "module": "esnext",
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "jsx": "preserve",
 | 
			
		||||
    "importHelpers": true,
 | 
			
		||||
    "moduleResolution": "node",
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    "esModuleInterop": true,
 | 
			
		||||
    "allowSyntheticDefaultImports": true,
 | 
			
		||||
    "sourceMap": true,
 | 
			
		||||
    "target": "ESNext",
 | 
			
		||||
    "useDefineForClassFields": true,
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    "baseUrl": ".",
 | 
			
		||||
    "types": [
 | 
			
		||||
      "webpack-env"
 | 
			
		||||
    ],
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": [
 | 
			
		||||
        "src/*"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "lib": [
 | 
			
		||||
      "esnext",
 | 
			
		||||
      "dom",
 | 
			
		||||
      "dom.iterable",
 | 
			
		||||
      "scripthost"
 | 
			
		||||
    ]
 | 
			
		||||
    /**
 | 
			
		||||
     * Typecheck JS in `.svelte` and `.js` files by default.
 | 
			
		||||
     * Disable checkJs if you'd like to use dynamic types in JS.
 | 
			
		||||
     * Note that setting allowJs false does not prevent the use
 | 
			
		||||
     * of JS in `.svelte` files.
 | 
			
		||||
     */
 | 
			
		||||
    "allowJs": true,
 | 
			
		||||
    "checkJs": true,
 | 
			
		||||
    "isolatedModules": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": [
 | 
			
		||||
    "src/**/*.ts",
 | 
			
		||||
    "src/**/*.tsx",
 | 
			
		||||
    "src/**/*.vue",
 | 
			
		||||
    "tests/**/*.ts",
 | 
			
		||||
    "tests/**/*.tsx"
 | 
			
		||||
  ],
 | 
			
		||||
  "exclude": [
 | 
			
		||||
    "node_modules"
 | 
			
		||||
  ]
 | 
			
		||||
  "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
 | 
			
		||||
  "references": [{ "path": "./tsconfig.node.json" }]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										8
									
								
								tsconfig.node.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								tsconfig.node.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "composite": true,
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "moduleResolution": "Node"
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["vite.config.ts"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								vite.config.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								vite.config.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
import { defineConfig } from "vite";
 | 
			
		||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
 | 
			
		||||
import cp from "child_process";
 | 
			
		||||
 | 
			
		||||
const commitHash = cp.execSync("git rev-parse --short HEAD").toString();
 | 
			
		||||
 | 
			
		||||
// https://vitejs.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [svelte()],
 | 
			
		||||
  base: "",
 | 
			
		||||
  define: {
 | 
			
		||||
    __COMMIT_HASH__: JSON.stringify(commitHash),
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
module.exports = {
 | 
			
		||||
  // publicPath: process.env.VUE_APP_BASE_URL || '/las/',
 | 
			
		||||
  devServer: {
 | 
			
		||||
    hot: false,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										835
									
								
								yarn.lock
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										835
									
								
								yarn.lock
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,835 @@
 | 
			
		|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
 | 
			
		||||
# yarn lockfile v1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
"@jridgewell/resolve-uri@^3.0.3":
 | 
			
		||||
  version "3.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
 | 
			
		||||
  integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
 | 
			
		||||
 | 
			
		||||
"@jridgewell/sourcemap-codec@^1.4.10":
 | 
			
		||||
  version "1.4.14"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
 | 
			
		||||
  integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
 | 
			
		||||
 | 
			
		||||
"@jridgewell/trace-mapping@^0.3.9":
 | 
			
		||||
  version "0.3.14"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
 | 
			
		||||
  integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@jridgewell/resolve-uri" "^3.0.3"
 | 
			
		||||
    "@jridgewell/sourcemap-codec" "^1.4.10"
 | 
			
		||||
 | 
			
		||||
"@nodelib/fs.scandir@2.1.5":
 | 
			
		||||
  version "2.1.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
 | 
			
		||||
  integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@nodelib/fs.stat" "2.0.5"
 | 
			
		||||
    run-parallel "^1.1.9"
 | 
			
		||||
 | 
			
		||||
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
 | 
			
		||||
  version "2.0.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
 | 
			
		||||
  integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
 | 
			
		||||
 | 
			
		||||
"@nodelib/fs.walk@^1.2.3":
 | 
			
		||||
  version "1.2.8"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
 | 
			
		||||
  integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@nodelib/fs.scandir" "2.1.5"
 | 
			
		||||
    fastq "^1.6.0"
 | 
			
		||||
 | 
			
		||||
"@rollup/pluginutils@^4.2.1":
 | 
			
		||||
  version "4.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
 | 
			
		||||
  integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    estree-walker "^2.0.1"
 | 
			
		||||
    picomatch "^2.2.2"
 | 
			
		||||
 | 
			
		||||
"@sentry/browser@^7.7.0":
 | 
			
		||||
  version "7.7.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.7.0.tgz#7810ee98d4969bd0367e29ac0af6c5779db7e6c4"
 | 
			
		||||
  integrity sha512-oyzpWcsjVZTaf14zAL89Ng1DUHlbjN+V8pl8dR9Y9anphbzL5BK9p0fSK4kPIrO4GukK+XrKnLJDPuE/o7WR3g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@sentry/core" "7.7.0"
 | 
			
		||||
    "@sentry/types" "7.7.0"
 | 
			
		||||
    "@sentry/utils" "7.7.0"
 | 
			
		||||
    tslib "^1.9.3"
 | 
			
		||||
 | 
			
		||||
"@sentry/core@7.7.0":
 | 
			
		||||
  version "7.7.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.7.0.tgz#1a2d477897552d179380f7c54c7d81a4e98ea29a"
 | 
			
		||||
  integrity sha512-Z15ACiuiFINFcK4gbMrnejLn4AVjKBPJOWKrrmpIe8mh+Y9diOuswt5mMUABs+jhpZvqht3PBLLGBL0WMsYMYA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@sentry/hub" "7.7.0"
 | 
			
		||||
    "@sentry/types" "7.7.0"
 | 
			
		||||
    "@sentry/utils" "7.7.0"
 | 
			
		||||
    tslib "^1.9.3"
 | 
			
		||||
 | 
			
		||||
"@sentry/hub@7.7.0":
 | 
			
		||||
  version "7.7.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.7.0.tgz#9ad3471cf5ecaf1a9d3a3a04ca2515ffec9e2c09"
 | 
			
		||||
  integrity sha512-6gydK234+a0nKhBRDdIJ7Dp42CaiW2juTiHegUVDq+482balVzbZyEAmESCmuzKJhx5BhlCElVxs/cci1NjMpg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@sentry/types" "7.7.0"
 | 
			
		||||
    "@sentry/utils" "7.7.0"
 | 
			
		||||
    tslib "^1.9.3"
 | 
			
		||||
 | 
			
		||||
"@sentry/tracing@^7.7.0":
 | 
			
		||||
  version "7.7.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.7.0.tgz#67324b755a28e115289755e44a0b8b467a63d0b2"
 | 
			
		||||
  integrity sha512-HNmvTwemuc21q/K6HXsSp9njkne6N1JQ71TB+QGqYU5VtxsVgYSUhhYqV6WcHz7LK4Hj6TvNFoeu69/rO0ysgw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@sentry/hub" "7.7.0"
 | 
			
		||||
    "@sentry/types" "7.7.0"
 | 
			
		||||
    "@sentry/utils" "7.7.0"
 | 
			
		||||
    tslib "^1.9.3"
 | 
			
		||||
 | 
			
		||||
"@sentry/types@7.7.0":
 | 
			
		||||
  version "7.7.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.7.0.tgz#dd6bd3d119d7efea0e85dbaa4b17de1c22b63c7a"
 | 
			
		||||
  integrity sha512-4x8O7uerSGLnYC10krHl9t8h7xXHn5FextqKYbTCXCnx2hC8D+9lz8wcbQAFo0d97wiUYqI8opmEgFVGx7c5hQ==
 | 
			
		||||
 | 
			
		||||
"@sentry/utils@7.7.0":
 | 
			
		||||
  version "7.7.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.7.0.tgz#013e3097c4268a76de578494c7df999635fb0ad4"
 | 
			
		||||
  integrity sha512-fD+ROSFpeJlK7bEvUT2LOW7QqgjBpXJwVISKZ0P2fuzclRC3KoB2pbZgBM4PXMMTiSzRGWhvfRRjBiBvQJBBJQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@sentry/types" "7.7.0"
 | 
			
		||||
    tslib "^1.9.3"
 | 
			
		||||
 | 
			
		||||
"@sveltejs/vite-plugin-svelte@^1.0.1":
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.1.tgz#7f468f03c933fcdfc60d4773671c73f33b9ef4d6"
 | 
			
		||||
  integrity sha512-PorCgUounn0VXcpeJu+hOweZODKmGuLHsLomwqSj+p26IwjjGffmYQfVHtiTWq+NqaUuuHWWG7vPge6UFw4Aeg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@rollup/pluginutils" "^4.2.1"
 | 
			
		||||
    debug "^4.3.4"
 | 
			
		||||
    deepmerge "^4.2.2"
 | 
			
		||||
    kleur "^4.1.5"
 | 
			
		||||
    magic-string "^0.26.2"
 | 
			
		||||
    svelte-hmr "^0.14.12"
 | 
			
		||||
 | 
			
		||||
"@tsconfig/svelte@^3.0.0":
 | 
			
		||||
  version "3.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@tsconfig/svelte/-/svelte-3.0.0.tgz#b06e059209f04c414de0069f2f0e2796d979fc6f"
 | 
			
		||||
  integrity sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==
 | 
			
		||||
 | 
			
		||||
"@types/node@*":
 | 
			
		||||
  version "18.6.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.1.tgz#828e4785ccca13f44e2fb6852ae0ef11e3e20ba5"
 | 
			
		||||
  integrity sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg==
 | 
			
		||||
 | 
			
		||||
"@types/pug@^2.0.4":
 | 
			
		||||
  version "2.0.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6"
 | 
			
		||||
  integrity sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==
 | 
			
		||||
 | 
			
		||||
"@types/sass@^1.16.0":
 | 
			
		||||
  version "1.43.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.43.1.tgz#86bb0168e9e881d7dade6eba16c9ed6d25dc2f68"
 | 
			
		||||
  integrity sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/node" "*"
 | 
			
		||||
 | 
			
		||||
"@types/stats.js@^0.17.0":
 | 
			
		||||
  version "0.17.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.0.tgz#0ed81d48e03b590c24da85540c1d952077a9fe20"
 | 
			
		||||
  integrity sha512-9w+a7bR8PeB0dCT/HBULU2fMqf6BAzvKbxFboYhmDtDkKPiyXYbjoe2auwsXlEFI7CFNMF1dCv3dFH5Poy9R1w==
 | 
			
		||||
 | 
			
		||||
amator@^1.1.0:
 | 
			
		||||
  version "1.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/amator/-/amator-1.1.0.tgz#08c6b60bc93aec2b61bbfc0c4d677d30323cc0f1"
 | 
			
		||||
  integrity sha512-V5+aH8pe+Z3u/UG3L3pG3BaFQGXAyXHVQDroRwjPHdh08bcUEchAVsU1MCuJSCaU5o60wTK6KaE6te5memzgYw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    bezier-easing "^2.0.3"
 | 
			
		||||
 | 
			
		||||
anymatch@~3.1.2:
 | 
			
		||||
  version "3.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
 | 
			
		||||
  integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    normalize-path "^3.0.0"
 | 
			
		||||
    picomatch "^2.0.4"
 | 
			
		||||
 | 
			
		||||
balanced-match@^1.0.0:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
 | 
			
		||||
  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 | 
			
		||||
 | 
			
		||||
bezier-easing@^2.0.3:
 | 
			
		||||
  version "2.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/bezier-easing/-/bezier-easing-2.1.0.tgz#c04dfe8b926d6ecaca1813d69ff179b7c2025d86"
 | 
			
		||||
  integrity sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==
 | 
			
		||||
 | 
			
		||||
binary-extensions@^2.0.0:
 | 
			
		||||
  version "2.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
 | 
			
		||||
  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
 | 
			
		||||
 | 
			
		||||
brace-expansion@^1.1.7:
 | 
			
		||||
  version "1.1.11"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
 | 
			
		||||
  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    balanced-match "^1.0.0"
 | 
			
		||||
    concat-map "0.0.1"
 | 
			
		||||
 | 
			
		||||
braces@^3.0.2, braces@~3.0.2:
 | 
			
		||||
  version "3.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
 | 
			
		||||
  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    fill-range "^7.0.1"
 | 
			
		||||
 | 
			
		||||
buffer-crc32@^0.2.5:
 | 
			
		||||
  version "0.2.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
 | 
			
		||||
  integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
 | 
			
		||||
 | 
			
		||||
callsites@^3.0.0:
 | 
			
		||||
  version "3.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
 | 
			
		||||
  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 | 
			
		||||
 | 
			
		||||
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1:
 | 
			
		||||
  version "3.5.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
 | 
			
		||||
  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    anymatch "~3.1.2"
 | 
			
		||||
    braces "~3.0.2"
 | 
			
		||||
    glob-parent "~5.1.2"
 | 
			
		||||
    is-binary-path "~2.1.0"
 | 
			
		||||
    is-glob "~4.0.1"
 | 
			
		||||
    normalize-path "~3.0.0"
 | 
			
		||||
    readdirp "~3.6.0"
 | 
			
		||||
  optionalDependencies:
 | 
			
		||||
    fsevents "~2.3.2"
 | 
			
		||||
 | 
			
		||||
concat-map@0.0.1:
 | 
			
		||||
  version "0.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
 | 
			
		||||
  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
 | 
			
		||||
 | 
			
		||||
debug@^4.3.4:
 | 
			
		||||
  version "4.3.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
 | 
			
		||||
  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    ms "2.1.2"
 | 
			
		||||
 | 
			
		||||
deepmerge@^4.2.2:
 | 
			
		||||
  version "4.2.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
 | 
			
		||||
  integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
 | 
			
		||||
 | 
			
		||||
detect-indent@^6.0.0:
 | 
			
		||||
  version "6.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
 | 
			
		||||
  integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==
 | 
			
		||||
 | 
			
		||||
es6-promise@^3.1.2:
 | 
			
		||||
  version "3.3.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
 | 
			
		||||
  integrity sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==
 | 
			
		||||
 | 
			
		||||
esbuild-android-64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.50.tgz#a46fc80fa2007690e647680d837483a750a3097f"
 | 
			
		||||
  integrity sha512-H7iUEm7gUJHzidsBlFPGF6FTExazcgXL/46xxLo6i6bMtPim6ZmXyTccS8yOMpy6HAC6dPZ/JCQqrkkin69n6Q==
 | 
			
		||||
 | 
			
		||||
esbuild-android-arm64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.50.tgz#bdda7851fa7f5f770d6ff0ad593a8945d3a0fcdd"
 | 
			
		||||
  integrity sha512-NFaoqEwa+OYfoYVpQWDMdKII7wZZkAjtJFo1WdnBeCYlYikvUhTnf2aPwPu5qEAw/ie1NYK0yn3cafwP+kP+OQ==
 | 
			
		||||
 | 
			
		||||
esbuild-darwin-64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.50.tgz#f0535435f9760766f30db14a991ee5ca94c022a4"
 | 
			
		||||
  integrity sha512-gDQsCvGnZiJv9cfdO48QqxkRV8oKAXgR2CGp7TdIpccwFdJMHf8hyIJhMW/05b/HJjET/26Us27Jx91BFfEVSA==
 | 
			
		||||
 | 
			
		||||
esbuild-darwin-arm64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.50.tgz#76a41a40e8947a15ae62970e9ed2853883c4b16c"
 | 
			
		||||
  integrity sha512-36nNs5OjKIb/Q50Sgp8+rYW/PqirRiFN0NFc9hEvgPzNJxeJedktXwzfJSln4EcRFRh5Vz4IlqFRScp+aiBBzA==
 | 
			
		||||
 | 
			
		||||
esbuild-freebsd-64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.50.tgz#2ed6633c17ed42c20a1bd68e82c4bbc75ea4fb57"
 | 
			
		||||
  integrity sha512-/1pHHCUem8e/R86/uR+4v5diI2CtBdiWKiqGuPa9b/0x3Nwdh5AOH7lj+8823C6uX1e0ufwkSLkS+aFZiBCWxA==
 | 
			
		||||
 | 
			
		||||
esbuild-freebsd-arm64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.50.tgz#cb115f4cdafe9cdbe58875ba482fccc54d32aa43"
 | 
			
		||||
  integrity sha512-iKwUVMQztnPZe5pUYHdMkRc9aSpvoV1mkuHlCoPtxZA3V+Kg/ptpzkcSY+fKd0kuom+l6Rc93k0UPVkP7xoqrw==
 | 
			
		||||
 | 
			
		||||
esbuild-linux-32@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.50.tgz#fe2b724994dcf1d4e48dc4832ff008ad7d00bcfd"
 | 
			
		||||
  integrity sha512-sWUwvf3uz7dFOpLzYuih+WQ7dRycrBWHCdoXJ4I4XdMxEHCECd8b7a9N9u7FzT6XR2gHPk9EzvchQUtiEMRwqw==
 | 
			
		||||
 | 
			
		||||
esbuild-linux-64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.50.tgz#7851ab5151df9501a2187bd4909c594ad232b623"
 | 
			
		||||
  integrity sha512-u0PQxPhaeI629t4Y3EEcQ0wmWG+tC/LpP2K7yDFvwuPq0jSQ8SIN+ARNYfRjGW15O2we3XJvklbGV0wRuUCPig==
 | 
			
		||||
 | 
			
		||||
esbuild-linux-arm64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.50.tgz#76a76afef484a0512f1fbbcc762edd705dee8892"
 | 
			
		||||
  integrity sha512-ZyfoNgsTftD7Rp5S7La5auomKdNeB3Ck+kSKXC4pp96VnHyYGjHHXWIlcbH8i+efRn9brszo1/Thl1qn8RqmhQ==
 | 
			
		||||
 | 
			
		||||
esbuild-linux-arm@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.50.tgz#6d7a8c0712091b0c3a668dd5d8b5c924adbaeb12"
 | 
			
		||||
  integrity sha512-VALZq13bhmFJYFE/mLEb+9A0w5vo8z+YDVOWeaf9vOTrSC31RohRIwtxXBnVJ7YKLYfEMzcgFYf+OFln3Y0cWg==
 | 
			
		||||
 | 
			
		||||
esbuild-linux-mips64le@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.50.tgz#43426909c1884c5dc6b40765673a08a7ec1d2064"
 | 
			
		||||
  integrity sha512-ygo31Vxn/WrmjKCHkBoutOlFG5yM9J2UhzHb0oWD9O61dGg+Hzjz9hjf5cmM7FBhAzdpOdEWHIrVOg2YAi6rTw==
 | 
			
		||||
 | 
			
		||||
esbuild-linux-ppc64le@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.50.tgz#c754ea3da1dd180c6e9b6b508dc18ce983d92b11"
 | 
			
		||||
  integrity sha512-xWCKU5UaiTUT6Wz/O7GKP9KWdfbsb7vhfgQzRfX4ahh5NZV4ozZ4+SdzYG8WxetsLy84UzLX3Pi++xpVn1OkFQ==
 | 
			
		||||
 | 
			
		||||
esbuild-linux-riscv64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.50.tgz#f3b2dd3c4c2b91bf191d3b98a9819c8aa6f5ad7f"
 | 
			
		||||
  integrity sha512-0+dsneSEihZTopoO9B6Z6K4j3uI7EdxBP7YSF5rTwUgCID+wHD3vM1gGT0m+pjCW+NOacU9kH/WE9N686FHAJg==
 | 
			
		||||
 | 
			
		||||
esbuild-linux-s390x@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.50.tgz#3dfbc4578b2a81995caabb79df2b628ea86a5390"
 | 
			
		||||
  integrity sha512-tVjqcu8o0P9H4StwbIhL1sQYm5mWATlodKB6dpEZFkcyTI8kfIGWiWcrGmkNGH2i1kBUOsdlBafPxR3nzp3TDA==
 | 
			
		||||
 | 
			
		||||
esbuild-netbsd-64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.50.tgz#17dbf51eaa48d983e794b588d195415410ef8c85"
 | 
			
		||||
  integrity sha512-0R/glfqAQ2q6MHDf7YJw/TulibugjizBxyPvZIcorH0Mb7vSimdHy0XF5uCba5CKt+r4wjax1mvO9lZ4jiAhEg==
 | 
			
		||||
 | 
			
		||||
esbuild-openbsd-64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.50.tgz#cf6b1a50c8cf67b0725aaa4bce9773976168c50e"
 | 
			
		||||
  integrity sha512-7PAtmrR5mDOFubXIkuxYQ4bdNS6XCK8AIIHUiZxq1kL8cFIH5731jPcXQ4JNy/wbj1C9sZ8rzD8BIM80Tqk29w==
 | 
			
		||||
 | 
			
		||||
esbuild-sunos-64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.50.tgz#f705ae0dd914c3b45dc43319c4f532216c3d841f"
 | 
			
		||||
  integrity sha512-gBxNY/wyptvD7PkHIYcq7se6SQEXcSC8Y7mE0FJB+CGgssEWf6vBPfTTZ2b6BWKnmaP6P6qb7s/KRIV5T2PxsQ==
 | 
			
		||||
 | 
			
		||||
esbuild-windows-32@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.50.tgz#6364905a99c1e6c1e2fe7bfccebd958131b1cd6c"
 | 
			
		||||
  integrity sha512-MOOe6J9cqe/iW1qbIVYSAqzJFh0p2LBLhVUIWdMVnNUNjvg2/4QNX4oT4IzgDeldU+Bym9/Tn6+DxvUHJXL5Zw==
 | 
			
		||||
 | 
			
		||||
esbuild-windows-64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.50.tgz#56603cb6367e30d14098deb77de6aa18d76dd89b"
 | 
			
		||||
  integrity sha512-r/qE5Ex3w1jjGv/JlpPoWB365ldkppUlnizhMxJgojp907ZF1PgLTuW207kgzZcSCXyquL9qJkMsY+MRtaZ5yQ==
 | 
			
		||||
 | 
			
		||||
esbuild-windows-arm64@0.14.50:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.50.tgz#e7ddde6a97194051a5a4ac05f4f5900e922a7ea5"
 | 
			
		||||
  integrity sha512-EMS4lQnsIe12ZyAinOINx7eq2mjpDdhGZZWDwPZE/yUTN9cnc2Ze/xUTYIAyaJqrqQda3LnDpADKpvLvol6ENQ==
 | 
			
		||||
 | 
			
		||||
esbuild@^0.14.47:
 | 
			
		||||
  version "0.14.50"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.50.tgz#7a665392c8df94bf6e1ae1e999966a5ee62c6cbc"
 | 
			
		||||
  integrity sha512-SbC3k35Ih2IC6trhbMYW7hYeGdjPKf9atTKwBUHqMCYFZZ9z8zhuvfnZihsnJypl74FjiAKjBRqFkBkAd0rS/w==
 | 
			
		||||
  optionalDependencies:
 | 
			
		||||
    esbuild-android-64 "0.14.50"
 | 
			
		||||
    esbuild-android-arm64 "0.14.50"
 | 
			
		||||
    esbuild-darwin-64 "0.14.50"
 | 
			
		||||
    esbuild-darwin-arm64 "0.14.50"
 | 
			
		||||
    esbuild-freebsd-64 "0.14.50"
 | 
			
		||||
    esbuild-freebsd-arm64 "0.14.50"
 | 
			
		||||
    esbuild-linux-32 "0.14.50"
 | 
			
		||||
    esbuild-linux-64 "0.14.50"
 | 
			
		||||
    esbuild-linux-arm "0.14.50"
 | 
			
		||||
    esbuild-linux-arm64 "0.14.50"
 | 
			
		||||
    esbuild-linux-mips64le "0.14.50"
 | 
			
		||||
    esbuild-linux-ppc64le "0.14.50"
 | 
			
		||||
    esbuild-linux-riscv64 "0.14.50"
 | 
			
		||||
    esbuild-linux-s390x "0.14.50"
 | 
			
		||||
    esbuild-netbsd-64 "0.14.50"
 | 
			
		||||
    esbuild-openbsd-64 "0.14.50"
 | 
			
		||||
    esbuild-sunos-64 "0.14.50"
 | 
			
		||||
    esbuild-windows-32 "0.14.50"
 | 
			
		||||
    esbuild-windows-64 "0.14.50"
 | 
			
		||||
    esbuild-windows-arm64 "0.14.50"
 | 
			
		||||
 | 
			
		||||
estree-walker@^2.0.1:
 | 
			
		||||
  version "2.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
 | 
			
		||||
  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
 | 
			
		||||
 | 
			
		||||
fast-glob@^3.2.7:
 | 
			
		||||
  version "3.2.11"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
 | 
			
		||||
  integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@nodelib/fs.stat" "^2.0.2"
 | 
			
		||||
    "@nodelib/fs.walk" "^1.2.3"
 | 
			
		||||
    glob-parent "^5.1.2"
 | 
			
		||||
    merge2 "^1.3.0"
 | 
			
		||||
    micromatch "^4.0.4"
 | 
			
		||||
 | 
			
		||||
fastq@^1.6.0:
 | 
			
		||||
  version "1.13.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
 | 
			
		||||
  integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    reusify "^1.0.4"
 | 
			
		||||
 | 
			
		||||
fetch-progress@^1.3.0:
 | 
			
		||||
  version "1.3.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/fetch-progress/-/fetch-progress-1.3.0.tgz#5a9992743fc08b0480d4088edc155c85eb44f6eb"
 | 
			
		||||
  integrity sha512-BCeKkVRx0x4mk/ykGGJ9FA2oJgrSp/lQgMiy2Ub+S2SMipt+po2uULUBM3OMOM/5XiwPpM4QyYmbYv/e98NWng==
 | 
			
		||||
 | 
			
		||||
fill-range@^7.0.1:
 | 
			
		||||
  version "7.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
 | 
			
		||||
  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    to-regex-range "^5.0.1"
 | 
			
		||||
 | 
			
		||||
fs.realpath@^1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
 | 
			
		||||
  integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
 | 
			
		||||
 | 
			
		||||
fsevents@~2.3.2:
 | 
			
		||||
  version "2.3.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
 | 
			
		||||
  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
 | 
			
		||||
 | 
			
		||||
function-bind@^1.1.1:
 | 
			
		||||
  version "1.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
 | 
			
		||||
  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
 | 
			
		||||
 | 
			
		||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
 | 
			
		||||
  version "5.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
 | 
			
		||||
  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    is-glob "^4.0.1"
 | 
			
		||||
 | 
			
		||||
glob@^7.1.3:
 | 
			
		||||
  version "7.2.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
 | 
			
		||||
  integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    fs.realpath "^1.0.0"
 | 
			
		||||
    inflight "^1.0.4"
 | 
			
		||||
    inherits "2"
 | 
			
		||||
    minimatch "^3.1.1"
 | 
			
		||||
    once "^1.3.0"
 | 
			
		||||
    path-is-absolute "^1.0.0"
 | 
			
		||||
 | 
			
		||||
graceful-fs@^4.1.3:
 | 
			
		||||
  version "4.2.10"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
 | 
			
		||||
  integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
 | 
			
		||||
 | 
			
		||||
has@^1.0.3:
 | 
			
		||||
  version "1.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
 | 
			
		||||
  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    function-bind "^1.1.1"
 | 
			
		||||
 | 
			
		||||
immutable@^4.0.0:
 | 
			
		||||
  version "4.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
 | 
			
		||||
  integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==
 | 
			
		||||
 | 
			
		||||
import-fresh@^3.2.1:
 | 
			
		||||
  version "3.3.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
 | 
			
		||||
  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    parent-module "^1.0.0"
 | 
			
		||||
    resolve-from "^4.0.0"
 | 
			
		||||
 | 
			
		||||
inflight@^1.0.4:
 | 
			
		||||
  version "1.0.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
 | 
			
		||||
  integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    once "^1.3.0"
 | 
			
		||||
    wrappy "1"
 | 
			
		||||
 | 
			
		||||
inherits@2:
 | 
			
		||||
  version "2.0.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
 | 
			
		||||
  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
 | 
			
		||||
 | 
			
		||||
is-binary-path@~2.1.0:
 | 
			
		||||
  version "2.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
 | 
			
		||||
  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    binary-extensions "^2.0.0"
 | 
			
		||||
 | 
			
		||||
is-core-module@^2.9.0:
 | 
			
		||||
  version "2.9.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
 | 
			
		||||
  integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    has "^1.0.3"
 | 
			
		||||
 | 
			
		||||
is-extglob@^2.1.1:
 | 
			
		||||
  version "2.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
 | 
			
		||||
  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
 | 
			
		||||
 | 
			
		||||
is-glob@^4.0.1, is-glob@~4.0.1:
 | 
			
		||||
  version "4.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
 | 
			
		||||
  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    is-extglob "^2.1.1"
 | 
			
		||||
 | 
			
		||||
is-number@^7.0.0:
 | 
			
		||||
  version "7.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
 | 
			
		||||
  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
 | 
			
		||||
 | 
			
		||||
kleur@^4.1.5:
 | 
			
		||||
  version "4.1.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
 | 
			
		||||
  integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
 | 
			
		||||
 | 
			
		||||
magic-string@^0.25.7:
 | 
			
		||||
  version "0.25.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
 | 
			
		||||
  integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    sourcemap-codec "^1.4.8"
 | 
			
		||||
 | 
			
		||||
magic-string@^0.26.2:
 | 
			
		||||
  version "0.26.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.2.tgz#5331700e4158cd6befda738bb6b0c7b93c0d4432"
 | 
			
		||||
  integrity sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    sourcemap-codec "^1.4.8"
 | 
			
		||||
 | 
			
		||||
merge2@^1.3.0:
 | 
			
		||||
  version "1.4.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
 | 
			
		||||
  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 | 
			
		||||
 | 
			
		||||
micromatch@^4.0.4:
 | 
			
		||||
  version "4.0.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
 | 
			
		||||
  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    braces "^3.0.2"
 | 
			
		||||
    picomatch "^2.3.1"
 | 
			
		||||
 | 
			
		||||
min-indent@^1.0.0:
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
 | 
			
		||||
  integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
 | 
			
		||||
 | 
			
		||||
minimatch@^3.1.1:
 | 
			
		||||
  version "3.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
 | 
			
		||||
  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    brace-expansion "^1.1.7"
 | 
			
		||||
 | 
			
		||||
minimist@^1.2.0, minimist@^1.2.6:
 | 
			
		||||
  version "1.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
 | 
			
		||||
  integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
 | 
			
		||||
 | 
			
		||||
mkdirp@^0.5.1:
 | 
			
		||||
  version "0.5.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
 | 
			
		||||
  integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    minimist "^1.2.6"
 | 
			
		||||
 | 
			
		||||
mri@^1.1.0:
 | 
			
		||||
  version "1.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
 | 
			
		||||
  integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
 | 
			
		||||
 | 
			
		||||
ms@2.1.2:
 | 
			
		||||
  version "2.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
 | 
			
		||||
  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 | 
			
		||||
 | 
			
		||||
nanoid@^3.3.4:
 | 
			
		||||
  version "3.3.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
 | 
			
		||||
  integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
 | 
			
		||||
 | 
			
		||||
ngraph.events@^1.2.2:
 | 
			
		||||
  version "1.2.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ngraph.events/-/ngraph.events-1.2.2.tgz#3ceb92d676a04a4e7ce60a09fa8e17a4f0346d7f"
 | 
			
		||||
  integrity sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ==
 | 
			
		||||
 | 
			
		||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
 | 
			
		||||
  version "3.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
 | 
			
		||||
  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
 | 
			
		||||
 | 
			
		||||
normalize.css@^8.0.1:
 | 
			
		||||
  version "8.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
 | 
			
		||||
  integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
 | 
			
		||||
 | 
			
		||||
once@^1.3.0:
 | 
			
		||||
  version "1.4.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
 | 
			
		||||
  integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    wrappy "1"
 | 
			
		||||
 | 
			
		||||
panzoom@^9.4.3:
 | 
			
		||||
  version "9.4.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/panzoom/-/panzoom-9.4.3.tgz#195c4031bb643f2e6c42f1de0ca87cc10e224042"
 | 
			
		||||
  integrity sha512-xaxCpElcRbQsUtIdwlrZA90P90+BHip4Vda2BC8MEb4tkI05PmR6cKECdqUCZ85ZvBHjpI9htJrZBxV5Gp/q/w==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    amator "^1.1.0"
 | 
			
		||||
    ngraph.events "^1.2.2"
 | 
			
		||||
    wheel "^1.0.0"
 | 
			
		||||
 | 
			
		||||
parent-module@^1.0.0:
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
 | 
			
		||||
  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    callsites "^3.0.0"
 | 
			
		||||
 | 
			
		||||
path-is-absolute@^1.0.0:
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
 | 
			
		||||
  integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
 | 
			
		||||
 | 
			
		||||
path-parse@^1.0.7:
 | 
			
		||||
  version "1.0.7"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
 | 
			
		||||
  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
 | 
			
		||||
 | 
			
		||||
picocolors@^1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
 | 
			
		||||
  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
 | 
			
		||||
 | 
			
		||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
 | 
			
		||||
  version "2.3.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
 | 
			
		||||
  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
 | 
			
		||||
 | 
			
		||||
postcss@^8.4.14:
 | 
			
		||||
  version "8.4.14"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
 | 
			
		||||
  integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    nanoid "^3.3.4"
 | 
			
		||||
    picocolors "^1.0.0"
 | 
			
		||||
    source-map-js "^1.0.2"
 | 
			
		||||
 | 
			
		||||
queue-microtask@^1.2.2:
 | 
			
		||||
  version "1.2.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
 | 
			
		||||
  integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
 | 
			
		||||
 | 
			
		||||
readdirp@~3.6.0:
 | 
			
		||||
  version "3.6.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
 | 
			
		||||
  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    picomatch "^2.2.1"
 | 
			
		||||
 | 
			
		||||
resolve-from@^4.0.0:
 | 
			
		||||
  version "4.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
 | 
			
		||||
  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 | 
			
		||||
 | 
			
		||||
resolve@^1.22.1:
 | 
			
		||||
  version "1.22.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
 | 
			
		||||
  integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    is-core-module "^2.9.0"
 | 
			
		||||
    path-parse "^1.0.7"
 | 
			
		||||
    supports-preserve-symlinks-flag "^1.0.0"
 | 
			
		||||
 | 
			
		||||
reusify@^1.0.4:
 | 
			
		||||
  version "1.0.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
 | 
			
		||||
  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
 | 
			
		||||
 | 
			
		||||
rimraf@^2.5.2:
 | 
			
		||||
  version "2.7.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
 | 
			
		||||
  integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    glob "^7.1.3"
 | 
			
		||||
 | 
			
		||||
rollup@^2.75.6:
 | 
			
		||||
  version "2.77.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.1.tgz#63463ebdbc04232fc42630ec72d137cd4400975d"
 | 
			
		||||
  integrity sha512-GhutNJrvTYD6s1moo+kyq7lD9DeR5HDyXo4bDFlDSkepC9kVKY+KK/NSZFzCmeXeia3kEzVuToQmHRdugyZHxw==
 | 
			
		||||
  optionalDependencies:
 | 
			
		||||
    fsevents "~2.3.2"
 | 
			
		||||
 | 
			
		||||
run-parallel@^1.1.9:
 | 
			
		||||
  version "1.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
 | 
			
		||||
  integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    queue-microtask "^1.2.2"
 | 
			
		||||
 | 
			
		||||
sade@^1.7.4:
 | 
			
		||||
  version "1.8.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
 | 
			
		||||
  integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    mri "^1.1.0"
 | 
			
		||||
 | 
			
		||||
sander@^0.5.0:
 | 
			
		||||
  version "0.5.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sander/-/sander-0.5.1.tgz#741e245e231f07cafb6fdf0f133adfa216a502ad"
 | 
			
		||||
  integrity sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    es6-promise "^3.1.2"
 | 
			
		||||
    graceful-fs "^4.1.3"
 | 
			
		||||
    mkdirp "^0.5.1"
 | 
			
		||||
    rimraf "^2.5.2"
 | 
			
		||||
 | 
			
		||||
sass@^1.54.0:
 | 
			
		||||
  version "1.54.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.0.tgz#24873673265e2a4fe3d3a997f714971db2fba1f4"
 | 
			
		||||
  integrity sha512-C4zp79GCXZfK0yoHZg+GxF818/aclhp9F48XBu/+bm9vXEVAYov9iU3FBVRMq3Hx3OA4jfKL+p2K9180mEh0xQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    chokidar ">=3.0.0 <4.0.0"
 | 
			
		||||
    immutable "^4.0.0"
 | 
			
		||||
    source-map-js ">=0.6.2 <2.0.0"
 | 
			
		||||
 | 
			
		||||
sorcery@^0.10.0:
 | 
			
		||||
  version "0.10.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.10.0.tgz#8ae90ad7d7cb05fc59f1ab0c637845d5c15a52b7"
 | 
			
		||||
  integrity sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    buffer-crc32 "^0.2.5"
 | 
			
		||||
    minimist "^1.2.0"
 | 
			
		||||
    sander "^0.5.0"
 | 
			
		||||
    sourcemap-codec "^1.3.0"
 | 
			
		||||
 | 
			
		||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
 | 
			
		||||
  integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
 | 
			
		||||
 | 
			
		||||
sourcemap-codec@^1.3.0, sourcemap-codec@^1.4.8:
 | 
			
		||||
  version "1.4.8"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
 | 
			
		||||
  integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
 | 
			
		||||
 | 
			
		||||
stats.js@^0.17.0:
 | 
			
		||||
  version "0.17.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/stats.js/-/stats.js-0.17.0.tgz#b1c3dc46d94498b578b7fd3985b81ace7131cc7d"
 | 
			
		||||
  integrity sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==
 | 
			
		||||
 | 
			
		||||
strip-indent@^3.0.0:
 | 
			
		||||
  version "3.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
 | 
			
		||||
  integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    min-indent "^1.0.0"
 | 
			
		||||
 | 
			
		||||
supports-preserve-symlinks-flag@^1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
 | 
			
		||||
  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 | 
			
		||||
 | 
			
		||||
svelte-check@^2.8.0:
 | 
			
		||||
  version "2.8.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-2.8.0.tgz#cfe1354e72545839c47f0f022c2c007454cd4095"
 | 
			
		||||
  integrity sha512-HRL66BxffMAZusqe5I5k26mRWQ+BobGd9Rxm3onh7ZVu0nTk8YTKJ9vu3LVPjUGLU9IX7zS+jmwPVhJYdXJ8vg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@jridgewell/trace-mapping" "^0.3.9"
 | 
			
		||||
    chokidar "^3.4.1"
 | 
			
		||||
    fast-glob "^3.2.7"
 | 
			
		||||
    import-fresh "^3.2.1"
 | 
			
		||||
    picocolors "^1.0.0"
 | 
			
		||||
    sade "^1.7.4"
 | 
			
		||||
    svelte-preprocess "^4.0.0"
 | 
			
		||||
    typescript "*"
 | 
			
		||||
 | 
			
		||||
svelte-hmr@^0.14.12:
 | 
			
		||||
  version "0.14.12"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.14.12.tgz#a127aec02f1896500b10148b2d4d21ddde39973f"
 | 
			
		||||
  integrity sha512-4QSW/VvXuqVcFZ+RhxiR8/newmwOCTlbYIezvkeN6302YFRE8cXy0naamHcjz8Y9Ce3ITTZtrHrIL0AGfyo61w==
 | 
			
		||||
 | 
			
		||||
svelte-preprocess@^4.0.0, svelte-preprocess@^4.10.7:
 | 
			
		||||
  version "4.10.7"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-4.10.7.tgz#3626de472f51ffe20c9bc71eff5a3da66797c362"
 | 
			
		||||
  integrity sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/pug" "^2.0.4"
 | 
			
		||||
    "@types/sass" "^1.16.0"
 | 
			
		||||
    detect-indent "^6.0.0"
 | 
			
		||||
    magic-string "^0.25.7"
 | 
			
		||||
    sorcery "^0.10.0"
 | 
			
		||||
    strip-indent "^3.0.0"
 | 
			
		||||
 | 
			
		||||
svelte@^3.49.0:
 | 
			
		||||
  version "3.49.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029"
 | 
			
		||||
  integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==
 | 
			
		||||
 | 
			
		||||
to-regex-range@^5.0.1:
 | 
			
		||||
  version "5.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
 | 
			
		||||
  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    is-number "^7.0.0"
 | 
			
		||||
 | 
			
		||||
tslib@^1.9.3:
 | 
			
		||||
  version "1.14.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
 | 
			
		||||
  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 | 
			
		||||
 | 
			
		||||
tslib@^2.4.0:
 | 
			
		||||
  version "2.4.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
 | 
			
		||||
  integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
 | 
			
		||||
 | 
			
		||||
typescript@*, typescript@^4.6.4:
 | 
			
		||||
  version "4.7.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
 | 
			
		||||
  integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
 | 
			
		||||
 | 
			
		||||
vite@^3.0.0:
 | 
			
		||||
  version "3.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.3.tgz#c7b2ed9505a36a04be1d5d23aea4ea6fc028043f"
 | 
			
		||||
  integrity sha512-sDIpIcl3mv1NUaSzZwiXGEy1ZoWwwC2vkxUHY6yiDacR6zf//ZFuBJrozO62gedpE43pmxnLATNR5IYUdAEkMQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    esbuild "^0.14.47"
 | 
			
		||||
    postcss "^8.4.14"
 | 
			
		||||
    resolve "^1.22.1"
 | 
			
		||||
    rollup "^2.75.6"
 | 
			
		||||
  optionalDependencies:
 | 
			
		||||
    fsevents "~2.3.2"
 | 
			
		||||
 | 
			
		||||
wheel@^1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/wheel/-/wheel-1.0.0.tgz#6cf46e06a854181adb8649228077f8b0d5c574ce"
 | 
			
		||||
  integrity sha512-XiCMHibOiqalCQ+BaNSwRoZ9FDTAvOsXxGHXChBugewDj7HC8VBIER71dEOiRH1fSdLbRCQzngKTSiZ06ZQzeA==
 | 
			
		||||
 | 
			
		||||
wrappy@1:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
 | 
			
		||||
  integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue